Commit 1731bab7 authored by Nick Thomas's avatar Nick Thomas

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2017-06-07

parents 918210a8 40588ddd
......@@ -117,6 +117,14 @@
}
}
.user-settings-pipeline-quota {
margin-top: $gl-padding;
.pipeline-quota {
border-top: none;
}
}
table.pipeline-project-metrics tr td {
padding: $gl-padding;
}
......
......@@ -56,7 +56,7 @@
&.expanded {
max-height: none;
overflow-y: hidden;
overflow-y: visible;
animation: expandMaxHeight 300ms ease-in;
}
......
class Profiles::PipelineQuotaController < Profiles::ApplicationController
def index
@namespace = current_user.namespace
@projects = @namespace.projects.with_shared_runners_limit_enabled.page(params[:page])
end
end
module EE
module GroupsHelper
def group_shared_runner_limits_quota(group)
used = group.shared_runners_minutes.to_i
module NamespaceHelper
def namespace_shared_runner_limits_quota(namespace)
used = namespace.shared_runners_minutes.to_i
if group.shared_runners_minutes_limit_enabled?
limit = group.actual_shared_runners_minutes_limit
status = group.shared_runners_minutes_used? ? 'over_quota' : 'under_quota'
if namespace.shared_runners_minutes_limit_enabled?
limit = namespace.actual_shared_runners_minutes_limit
status = namespace.shared_runners_minutes_used? ? 'over_quota' : 'under_quota'
else
limit = 'Unlimited'
status = 'disabled'
......@@ -16,14 +16,14 @@ module EE
end
end
def group_shared_runner_limits_percent_used(group)
return 0 unless group.shared_runners_minutes_limit_enabled?
def namespace_shared_runner_limits_percent_used(namespace)
return 0 unless namespace.shared_runners_minutes_limit_enabled?
100 * group.shared_runners_minutes.to_i / group.actual_shared_runners_minutes_limit
100 * namespace.shared_runners_minutes.to_i / namespace.actual_shared_runners_minutes_limit
end
def group_shared_runner_limits_progress_bar(group)
percent = [group_shared_runner_limits_percent_used(group), 100].min
def namespace_shared_runner_limits_progress_bar(namespace)
percent = [namespace_shared_runner_limits_percent_used(namespace), 100].min
status =
if percent == 100
......
class Geo::BaseRegistry < ActiveRecord::Base
self.abstract_class = true
if Gitlab::Geo.configured? && (Gitlab::Geo.secondary? || Rails.env.test?)
if Gitlab::Geo.secondary_role_enabled?
establish_connection Rails.configuration.geo_database
end
end
......@@ -21,9 +21,7 @@ class GeoNode < ActiveRecord::Base
validates :encrypted_secret_access_key, presence: true
after_initialize :build_dependents
after_save :refresh_bulk_notify_worker_status
after_save :expire_cache!
after_destroy :refresh_bulk_notify_worker_status
after_destroy :expire_cache!
before_validation :update_dependents_attributes
......@@ -130,10 +128,6 @@ class GeoNode < ActiveRecord::Base
{ protocol: schema, host: host, port: port, script_name: relative_url }
end
def refresh_bulk_notify_worker_status
Gitlab::Geo.configure_cron_jobs!
end
def build_dependents
unless persisted?
self.build_geo_node_key if geo_node_key.nil?
......
......@@ -41,6 +41,7 @@ module Issues
'Due Date' => -> (issue) { issue.due_date&.to_s(:csv) },
'Created At (UTC)' => -> (issue) { issue.created_at&.to_s(:csv) },
'Updated At (UTC)' => -> (issue) { issue.updated_at&.to_s(:csv) },
'Closed At (UTC)' => -> (issue) { issue.closed_at&.to_s(:csv) },
'Milestone' => -> (issue) { issue.milestone&.title },
'Labels' => -> (issue) { @labels[issue.id].sort.join(',').presence }
}
......
......@@ -9,44 +9,5 @@
%strong= @group.name
group
.pipeline-quota.container-fluid
.row
.col-sm-6
%strong
- last_reset = @group.shared_runners_seconds_last_reset
- if last_reset
Usage since
= last_reset.strftime('%b %d, %Y')
- else
Current period usage
%div
= group_shared_runner_limits_quota(@group)
minutes
.col-sm-6.right
- if @group.shared_runners_minutes_limit_enabled?
#{group_shared_runner_limits_percent_used(@group)}% used
- else
Unlimited
= group_shared_runner_limits_progress_bar(@group)
%table.table.pipeline-project-metrics
%thead
%tr
%th Project
%th Minutes
%tbody
- @projects.each do |project|
%tr
%td
.avatar-container.s20.hidden-xs
= project_icon(project, alt: '', class: 'avatar project-avatar s20')
%strong= link_to project.name, project
%td
= project.shared_runners_minutes
- if @projects.blank?
%tr
%td{ colspan: 2 }
.nothing-here-block This group has no projects which use shared runners
= paginate @projects, theme: "gitlab"
= render "namespaces/pipelines_quota/list",
locals: { namespace: @group, projects: @projects }
......@@ -51,3 +51,7 @@
= link_to audit_log_profile_path, title: 'Authentication log' do
%span
Authentication log
= nav_link(path: 'profiles#pipeline_quota') do
= link_to profile_pipeline_quota_path, title: 'Pipeline quota' do
%span
Pipeline quota
......@@ -3,5 +3,5 @@
%li
%span.light Pipeline minutes quota:
%strong
= group_shared_runner_limits_quota(namespace)
= namespace_shared_runner_limits_quota(namespace)
= link_to icon('question-circle'), help_page_path("user/admin_area/settings/continuous_integration", anchor: "shared-runners-build-minutes-quota"), target: '_blank'
- namespace = locals.fetch(:namespace)
- projects = locals.fetch(:projects)
.pipeline-quota.container-fluid
.row
.col-sm-6
%strong
- last_reset = namespace.shared_runners_seconds_last_reset
- if last_reset
Usage since
= last_reset.strftime('%b %d, %Y')
- else
Current period usage
%div
= namespace_shared_runner_limits_quota(namespace)
minutes
.col-sm-6.right
- if namespace.shared_runners_minutes_limit_enabled?
#{namespace_shared_runner_limits_percent_used(namespace)}% used
- else
Unlimited
= namespace_shared_runner_limits_progress_bar(namespace)
%table.table.pipeline-project-metrics
%thead
%tr
%th Project
%th Minutes
%tbody
- projects.each do |project|
%tr
%td
.avatar-container.s20.hidden-xs
= project_icon(project, alt: '', class: 'avatar project-avatar s20')
%strong= link_to project.name, project
%td
= project.shared_runners_minutes
- if projects.blank?
%tr
%td{ colspan: 2 }
.nothing-here-block This group has no projects which use shared runners
= paginate projects, theme: "gitlab"
- page_title 'Personal pipelines quota'
= render 'profiles/head'
.user-settings-pipeline-quota.row
.profile-settings-sidebar.col-lg-3
%h4
Personal pipelines quota
= link_to icon('question-circle'), help_page_path("user/admin_area/settings/continuous_integration", anchor: "shared-runners-build-minutes-quota"), target: '_blank'
%p.light
Monthly build minutes usage across shared Runners
.col-lg-9
= render "namespaces/pipelines_quota/list",
locals: { namespace: @namespace, projects: @projects }
.row
= form_errors(@project)
.row.prepend-top-default.append-bottom-default
= form_for @project, url: namespace_project_mirror_path(@project.namespace, @project) do |f|
.col-lg-3
%h4.prepend-top-0
- expanded = Rails.env.test?
%section.settings.project-mirror-settings
.settings-header
%h4
Pull from a remote repository
%p.light
%button.btn.js-settings-toggle
= expanded ? 'Close' : 'Expand'
%p
Set up your project to automatically have its branches, tags, and commits
updated from an upstream repository every hour.
= link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pulling-from-a-remote-repository'), target: '_blank'
.col-lg-9
%h5.prepend-top-0
.settings-content.no-animate{ class: ('expanded' if expanded) }
= form_for @project, url: namespace_project_mirror_path(@project.namespace, @project) do |f|
%div
= form_errors(@project)
%h5
Set up mirror repository
= render "shared/mirror_update_button"
- if @project.mirror_last_update_failed?
......@@ -43,16 +46,22 @@
They need to have at least master access to this project.
- if @project.builds_enabled?
= render "shared/mirror_trigger_builds_setting", f: f
.col-sm-12
%hr
.col-lg-3
%h4.prepend-top-0
= f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror'
%section.settings
.settings-header
%h4
Push to a remote repository
%p.light
%button.btn.js-settings-toggle
= expanded ? 'Close' : 'Expand'
%p
Set up the remote repository that you want to update with the content of the current repository
every time someone pushes to it.
= link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pushing-to-a-remote-repository'), target: '_blank'
.col-lg-9
.settings-content.no-animate{ class: ('expanded' if expanded) }
= form_for @project, url: namespace_project_mirror_path(@project.namespace, @project) do |f|
%div
= form_errors(@project)
= render "shared/remote_mirror_update_button", remote_mirror: @remote_mirror
- if @remote_mirror.last_error.present?
.panel.panel-danger
......@@ -76,4 +85,3 @@
= rm_form.text_field :url, class: "form-control", placeholder: 'https://username:password@gitlab.company.com/group/project.git'
= render "projects/mirrors/instructions"
= f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror'
%hr
......@@ -2,14 +2,7 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('protected_tags')
<<<<<<< HEAD
.row.prepend-top-default.append-bottom-default.js-protected-tags-container{ data: { "groups-autocomplete" => "#{autocomplete_project_groups_path(format: :json)}", "users-autocomplete" => "#{autocomplete_users_path(format: :json)}" } }
.col-lg-3
%h4.prepend-top-0
Protected Tags
%p.prepend-top-20
=======
%section.settings
%section.settings.js-protected-tags-container{ data: { "groups-autocomplete" => "#{autocomplete_project_groups_path(format: :json)}", "users-autocomplete" => "#{autocomplete_users_path(format: :json)}" } }
.settings-header
%h4
Protected Tags
......@@ -19,20 +12,14 @@
Limit access to creating and updating tags.
.settings-content.no-animate{ class: ('expanded' if expanded) }
%p
>>>>>>> ce/master
By default, protected tags are designed to:
%ul
%li Prevent tag creation by everybody except Masters
%li Prevent <strong>anyone</strong> from updating the tag
%li Prevent <strong>anyone</strong> from deleting the tag
<<<<<<< HEAD
%p.append-bottom-0 Read more about #{link_to "protected tags", help_page_path("user/project/protected_tags"), class: "underlined-link"}.
.col-lg-9
=======
%p Read more about #{link_to "protected tags", help_page_path("user/project/protected_tags"), class: "underlined-link"}.
>>>>>>> ce/master
- if can? current_user, :admin_project, @project
= render 'projects/protected_tags/create_protected_tag'
......
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
- expanded = Rails.env.test?
%section.settings
.settings-header
%h4
Push Rules
%p.light
%button.btn.js-settings-toggle
= expanded ? 'Close' : 'Expand'
%p
Push Rules outline what is accepted for this project.
.col-lg-9
%h5.prepend-top-0
.settings-content.no-animate{ class: ('expanded' if expanded) }
%h5
Add new push rule
= form_for [@project.namespace.becomes(Namespace), @project, @push_rule] do |f|
= form_errors(@push_rule)
......
......@@ -22,7 +22,7 @@ class GeoFileDownloadDispatchWorker
# files, excluding ones in progress.
# 5. Quit when we have scheduled all downloads or exceeded an hour.
def perform
return unless Gitlab::Geo.configured?
return unless Gitlab::Geo.secondary_role_enabled?
return unless Gitlab::Geo.secondary?
@start_time = Time.now
......@@ -153,7 +153,7 @@ class GeoFileDownloadDispatchWorker
def node_enabled?
# Only check every minute to avoid polling the DB excessively
unless @last_enabled_check.present? && (Time.now - @last_enabled_check > 1.minute)
unless @last_enabled_check.present? && @last_enabled_check > 1.minute.ago
@last_enabled_check = Time.now
@current_node_enabled = nil
end
......
......@@ -7,7 +7,7 @@ class GeoRepositorySyncWorker
LAST_SYNC_INTERVAL = 24.hours
def perform
return unless Gitlab::Geo.configured?
return unless Gitlab::Geo.secondary_role_enabled?
return unless Gitlab::Geo.primary_node.present?
start_time = Time.now
......@@ -20,7 +20,7 @@ class GeoRepositorySyncWorker
project_ids.each do |project_id|
begin
break if over_time?(start_time)
break unless Gitlab::Geo.current_node_enabled?
break unless node_enabled?
# We try to obtain a lease here for the entire sync process because we
# want to sync the repositories continuously at a controlled rate
......@@ -73,6 +73,16 @@ class GeoRepositorySyncWorker
Time.now - start_time >= RUN_TIME
end
def node_enabled?
# Only check every minute to avoid polling the DB excessively
unless @last_enabled_check.present? && @last_enabled_check > 1.minute.ago
@last_enabled_check = Time.now
@current_node_enabled = nil
end
@current_node_enabled ||= Gitlab::Geo.current_node_enabled?
end
def try_obtain_lease
lease = Gitlab::ExclusiveLease.new(lease_key, timeout: lease_timeout).try_obtain
......
---
title: Geo - Properly set tracking database connection and cron jobs on secondary nodes
merge_request:
author:
---
title: Allow to view Personal pipelines quota
merge_request:
author:
---
title: Add closed_at field to issue CSV export
merge_request:
author:
......@@ -622,6 +622,12 @@ production: &base
# host: localhost
# port: 3808
## GitLab Geo settings (EE-only)
geo_primary_role:
enabled: false
geo_secondary_role:
enabled: false
#
# 5. Extra customization
# ==========================
......@@ -705,6 +711,10 @@ test:
user_filter: ''
group_base: 'ou=groups,dc=example,dc=com'
admin_group: ''
geo_primary_role:
enabled: true
geo_secondary_role:
enabled: true
staging:
<<: *base
......@@ -342,6 +342,10 @@ Settings.pages['external_https'] ||= false unless Settings.pages['external_http
# Geo
#
Settings.gitlab['geo_status_timeout'] ||= 10
Settings['geo_primary_role'] ||= Settingslogic.new({})
Settings.geo_primary_role['enabled'] = false if Settings.geo_primary_role['enabled'].nil?
Settings['geo_secondary_role'] ||= Settingslogic.new({})
Settings.geo_secondary_role['enabled'] = false if Settings.geo_secondary_role['enabled'].nil?
#
# Git LFS
......
if File.exist?(Rails.root.join('config/database_geo.yml'))
if Gitlab::Geo.secondary_role_enabled?
Rails.application.configure do
config.geo_database = config_for(:database_geo)
end
......
......@@ -47,5 +47,9 @@ resource :profile, only: [:show, :update] do
end
resources :u2f_registrations, only: [:destroy]
## EE-specific
resources :pipeline_quota, only: [:index]
## EE-specific
end
end
......@@ -31,6 +31,7 @@ scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) d
get :contributed, as: :contributed_projects
get :snippets
get :exists
get :pipelines_quota
get '/', to: redirect('/%{username}'), as: nil
end
......
......@@ -7,7 +7,7 @@ feature tests with Capybara for e2e (end-to-end) integration testing.
Unit and feature tests need to be written for all new features.
Most of the time, you should use rspec for your feature tests.
There are cases where the behaviour you are testing is not worth the time spent running the full application,
for example, if you are testing styling, animation or small actions that don't involve the backend,
for example, if you are testing styling, animation, edge cases or small actions that don't involve the backend,
you should write an integration test using Jasmine.
![Testing priority triangle](img/testing_triangle.png)
......
......@@ -35,7 +35,6 @@ module API
get 'status' do
authenticate_by_gitlab_geo_node_token!
require_node_to_be_secondary!
require_node_to_have_tracking_db!
present GeoNodeStatus.new(id: Gitlab::Geo.current_node.id), with: Entities::GeoNodeStatus
end
......@@ -110,10 +109,6 @@ module API
def require_node_to_be_secondary!
forbidden! 'Geo node is not secondary node.' unless Gitlab::Geo.current_node&.secondary?
end
def require_node_to_have_tracking_db!
not_found! 'Geo node does not have its tracking database enabled.' unless Gitlab::Geo.configured?
end
end
end
end
......@@ -42,8 +42,12 @@ module Gitlab
Gitlab::Geo.current_node.reload.enabled?
end
def self.configured?
Rails.configuration.respond_to?(:geo_database)
def self.primary_role_enabled?
Gitlab.config.geo_primary_role['enabled']
end
def self.secondary_role_enabled?
Gitlab.config.geo_secondary_role['enabled']
end
def self.license_allows?
......@@ -94,9 +98,9 @@ module Gitlab
end
def self.configure_cron_jobs!
if self.primary?
if self.primary_role_enabled?
self.configure_primary_jobs!
elsif self.secondary?
elsif self.secondary_role_enabled?
self.configure_secondary_jobs!
else
self.disable_all_jobs!
......
......@@ -3,7 +3,8 @@ module Gitlab
class HealthCheck
def self.perform_checks
return '' unless Gitlab::Geo.secondary?
return 'The Geo database configuration file is missing.' unless Gitlab::Geo.configured?
return 'The Geo secondary role is disabled.' unless Gitlab::Geo.secondary_role_enabled?
return 'The Geo database configuration file is missing.' unless self.geo_database_configured?
return 'The Geo node has a database that is not configured for streaming replication with the primary node.' unless self.database_secondary?
database_version = self.get_database_version.to_i
......@@ -57,6 +58,10 @@ module Gitlab
.first
.fetch('pg_is_in_recovery') == 't'
end
def self.geo_database_configured?
Rails.configuration.respond_to?(:geo_database)
end
end
end
end
require 'spec_helper'
feature 'Profile > Pipeline Quota', feature: true do
let(:user) { create(:user) }
let(:namespace) { create(:namespace, owner: user) }
let!(:project) { create(:empty_project, namespace: namespace, shared_runners_enabled: true) }
before do
login_with(user)
end
it 'is linked within the profile page' do
visit profile_path
page.within('.layout-nav') do
expect(page).to have_selector(:link_or_button, 'Pipeline quota')
end
end
context 'with no quota' do
let(:namespace) { create(:namespace, :with_build_minutes, owner: user) }
it 'shows correct group quota info' do
visit profile_pipeline_quota_path
page.within('.pipeline-quota') do
expect(page).to have_content("400 / Unlimited minutes")
expect(page).to have_selector('.progress-bar-success')
end
end
end
context 'with no projects using shared runners' do
let(:namespace) { create(:namespace, :with_not_used_build_minutes_limit, owner: user) }
let!(:project) { create(:empty_project, namespace: namespace, shared_runners_enabled: false) }
it 'shows correct group quota info' do
visit profile_pipeline_quota_path
page.within('.pipeline-quota') do
expect(page).to have_content("300 / Unlimited minutes")
expect(page).to have_selector('.progress-bar-success')
end
page.within('.pipeline-project-metrics') do
expect(page).to have_content('This group has no projects which use shared runners')
end
end
end
context 'minutes under quota' do
let(:namespace) { create(:namespace, :with_not_used_build_minutes_limit, owner: user) }
it 'shows correct group quota info' do
visit profile_pipeline_quota_path
page.within('.pipeline-quota') do
expect(page).to have_content("300 / 500 minutes")
expect(page).to have_content("60% used")
expect(page).to have_selector('.progress-bar-success')
end
end
end
context 'minutes over quota' do
let(:namespace) { create(:namespace, :with_used_build_minutes_limit, owner: user) }
let!(:other_project) { create(:empty_project, namespace: namespace, shared_runners_enabled: false) }
it 'shows correct group quota and projects info' do
visit profile_pipeline_quota_path
page.within('.pipeline-quota') do
expect(page).to have_content("1000 / 500 minutes")
expect(page).to have_content("200% used")
expect(page).to have_selector('.progress-bar-danger')
end
page.within('.pipeline-project-metrics') do
expect(page).to have_content(project.name)
expect(page).not_to have_content(other_project.name)
end
end
end
end
......@@ -41,7 +41,7 @@ describe 'Project settings > [EE] repository', feature: true do
end
it 'sets mirror user' do
page.within('.edit_project') do
page.within('.project-mirror-settings') do
select2(user2.id, from: '#project_mirror_user_id')
click_button('Save changes')
......
......@@ -16,6 +16,13 @@ describe Gitlab::Geo::HealthCheck do
expect(subject.perform_checks).to be_blank
end
it 'returns an error when secondary role is disabled' do
allow(Gitlab::Geo).to receive(:secondary?) { true }
allow(Gitlab::Geo).to receive(:secondary_role_enabled?).and_return(false)
expect(subject.perform_checks).not_to be_blank
end
it 'returns an error when database is not configured for streaming replication' do
allow(Gitlab::Geo).to receive(:secondary?) { true }
allow(Gitlab::Database).to receive(:postgresql?) { true }
......
......@@ -127,7 +127,9 @@ describe Gitlab::Geo, lib: true do
end
it 'activates cron jobs for primary' do
allow(described_class).to receive(:primary?).and_return(true)
allow(described_class).to receive(:primary_role_enabled?).and_return(true)
allow(described_class).to receive(:secondary_role_enabled?).and_return(false)
described_class.configure_cron_jobs!
expect(described_class.bulk_notify_job).to be_enabled
......@@ -136,7 +138,9 @@ describe Gitlab::Geo, lib: true do
end
it 'activates cron jobs for secondary' do
allow(described_class).to receive(:secondary?).and_return(true)
allow(described_class).to receive(:primary_role_enabled?).and_return(false)
allow(described_class).to receive(:secondary_role_enabled?).and_return(true)
described_class.configure_cron_jobs!
expect(described_class.bulk_notify_job).not_to be_enabled
......@@ -145,6 +149,9 @@ describe Gitlab::Geo, lib: true do
end
it 'deactivates all jobs when Geo is not active' do
allow(described_class).to receive(:primary_role_enabled?).and_return(false)
allow(described_class).to receive(:secondary_role_enabled?).and_return(false)
described_class.configure_cron_jobs!
expect(described_class.bulk_notify_job).not_to be_enabled
......
......@@ -307,14 +307,6 @@ describe API::Geo, api: true do
expect(response).to have_http_status(200)
expect(response).to match_response_schema('geo_node_status')
end
it 'responds with a 404 when the tracking database is disabled' do
allow(Gitlab::Geo).to receive(:configured?).and_return(false)
get api('/geo/status'), nil, request.headers
expect(response).to have_http_status(404)
end
end
context 'when requesting primary node with valid auth header' do
......
......@@ -39,6 +39,7 @@ describe Issues::ExportCsvService, services: true do
due_date: DateTime.new(2014, 3, 2),
created_at: DateTime.new(2015, 4, 3, 2, 1, 0),
updated_at: DateTime.new(2016, 5, 4, 3, 2, 1),
closed_at: DateTime.new(2017, 6, 5, 4, 3, 2),
labels: [feature_label, idea_label])
end
......@@ -101,6 +102,10 @@ describe Issues::ExportCsvService, services: true do
specify 'updated_at' do
expect(csv[0]['Updated At (UTC)']).to eq '2016-05-04 03:02:01'
end
specify 'closed_at' do
expect(csv[0]['Closed At (UTC)']).to eq '2017-06-05 04:03:02'
end
end
context 'with minimal details' do
......
......@@ -13,10 +13,10 @@ describe GeoFileDownloadDispatchWorker do
subject { described_class.new }
describe '#perform' do
it 'does not schedule anything when tracking DB is not available' do
it 'does not schedule anything when secondary role is disabled' do
create(:lfs_object, :with_file)
allow(Rails.configuration).to receive(:respond_to?).with(:geo_database) { false }
allow(Gitlab::Geo).to receive(:secondary_role_enabled?) { false }
expect(GeoFileDownloadWorker).not_to receive(:perform_async)
......
......@@ -52,8 +52,8 @@ describe GeoRepositorySyncWorker do
subject.perform
end
it 'does not perform Geo::RepositorySyncService when tracking DB is not available' do
allow(Rails.configuration).to receive(:respond_to?).with(:geo_database) { false }
it 'does not perform Geo::RepositorySyncService when secondary role is disabled' do
allow(Gitlab::Geo).to receive(:secondary_role_enabled?) { false }
expect(Geo::RepositorySyncService).not_to receive(:new)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment