summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_actions.vue3
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_artifacts.vue6
-rw-r--r--app/helpers/groups_helper.rb2
-rw-r--r--app/helpers/sorting_helper.rb54
-rw-r--r--app/views/projects/branches/_branch.html.haml33
-rw-r--r--app/views/projects/branches/_delete_protected_modal.html.haml34
-rw-r--r--app/views/projects/branches/index.html.haml24
-rw-r--r--app/views/projects/buttons/_download.html.haml5
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml8
-rw-r--r--app/views/projects/tree/_old_tree_header.html.haml8
-rw-r--r--app/views/shared/boards/_show.html.haml15
-rw-r--r--changelogs/unreleased/36549-circuit-breaker-handles-missing-storages.yml5
-rw-r--r--doc/development/ux_guide/basics.md2
-rw-r--r--lib/gitlab/data_builder/push.rb7
-rw-r--r--lib/gitlab/git/storage.rb1
-rw-r--r--lib/gitlab/git/storage/circuit_breaker.rb30
-rw-r--r--lib/gitlab/git/storage/health.rb2
-rw-r--r--lib/gitlab/git/storage/null_circuit_breaker.rb47
-rw-r--r--lib/gitlab/health_checks/fs_shards_check.rb2
-rw-r--r--spec/javascripts/pipelines/pipelines_artifacts_spec.js2
-rw-r--r--spec/lib/gitlab/git/storage/circuit_breaker_spec.rb13
-rw-r--r--spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb77
-rw-r--r--spec/lib/gitlab/health_checks/fs_shards_check_spec.rb18
-rw-r--r--spec/support/stub_configuration.rb2
24 files changed, 272 insertions, 128 deletions
diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
index 01dfe51cc17..c4c63a52358 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
@@ -69,8 +69,7 @@
@click="onClickAction(action.path)"
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)">
- <span v-html="playIconSvg"></span>
- <span>{{action.name}}</span>
+ {{action.name}}
</button>
</li>
</ul>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
index b19bd509a00..751a20991af 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
@@ -39,11 +39,7 @@
rel="nofollow"
download
:href="artifact.path">
- <i
- class="fa fa-download"
- aria-hidden="true">
- </i>
- <span>Download {{artifact.name}} artifacts</span>
+ Download {{artifact.name}} artifacts
</a>
</li>
</ul>
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index e8efe8fab27..82bceddf1f0 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -125,7 +125,7 @@ module GroupsHelper
end
def default_help
- s_("GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner.")
+ s_("GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually.")
end
def ancestor_locked_but_you_can_override(group)
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index b408ec0c6a4..c4a73bedbcd 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -79,111 +79,111 @@ module SortingHelper
end
def sort_title_priority
- 'Priority'
+ s_('SortOptions|Priority')
end
def sort_title_label_priority
- 'Label priority'
+ s_('SortOptions|Label priority')
end
def sort_title_oldest_updated
- 'Oldest updated'
+ s_('SortOptions|Oldest updated')
end
def sort_title_recently_updated
- 'Last updated'
+ s_('SortOptions|Last updated')
end
def sort_title_oldest_activity
- 'Oldest updated'
+ s_('SortOptions|Oldest updated')
end
def sort_title_latest_activity
- 'Last updated'
+ s_('SortOptions|Last updated')
end
def sort_title_oldest_created
- 'Oldest created'
+ s_('SortOptions|Oldest created')
end
def sort_title_recently_created
- 'Last created'
+ s_('SortOptions|Last created')
end
def sort_title_milestone_soon
- 'Milestone due soon'
+ s_('SortOptions|Milestone due soon')
end
def sort_title_milestone_later
- 'Milestone due later'
+ s_('SortOptions|Milestone due later')
end
def sort_title_due_date_soon
- 'Due soon'
+ s_('SortOptions|Due soon')
end
def sort_title_due_date_later
- 'Due later'
+ s_('SortOptions|Due later')
end
def sort_title_start_date_soon
- 'Start soon'
+ s_('SortOptions|Start soon')
end
def sort_title_start_date_later
- 'Start later'
+ s_('SortOptions|Start later')
end
def sort_title_name
- 'Name'
+ s_('SortOptions|Name')
end
def sort_title_largest_repo
- 'Largest repository'
+ s_('SortOptions|Largest repository')
end
def sort_title_largest_group
- 'Largest group'
+ s_('SortOptions|Largest group')
end
def sort_title_recently_signin
- 'Recent sign in'
+ s_('SortOptions|Recent sign in')
end
def sort_title_oldest_signin
- 'Oldest sign in'
+ s_('SortOptions|Oldest sign in')
end
def sort_title_downvotes
- 'Least popular'
+ s_('SortOptions|Least popular')
end
def sort_title_upvotes
- 'Most popular'
+ s_('SortOptions|Most popular')
end
def sort_title_last_joined
- 'Last joined'
+ s_('SortOptions|Last joined')
end
def sort_title_oldest_joined
- 'Oldest joined'
+ s_('SortOptions|Oldest joined')
end
def sort_title_access_level_asc
- 'Access level, ascending'
+ s_('SortOptions|Access level, ascending')
end
def sort_title_access_level_desc
- 'Access level, descending'
+ s_('SortOptions|Access level, descending')
end
def sort_title_name_asc
- 'Name, ascending'
+ s_('SortOptions|Name, ascending')
end
def sort_title_name_desc
- 'Name, descending'
+ s_('SortOptions|Name, descending')
end
def sort_value_last_joined
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 05c1d2b383c..49101d1efa4 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -13,20 +13,23 @@
- if branch.name == @repository.root_ref
%span.label.label-primary default
- elsif @repository.merged_to_root_ref? branch.name
- %span.label.label-info.has-tooltip{ title: "Merged into #{@repository.root_ref}" }
- merged
+ %span.label.label-info.has-tooltip{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }
+ = s_('Branches|merged')
- if protected_branch?(@project, branch)
%span.label.label-success
- protected
+ = s_('Branches|protected')
.controls.hidden-xs<
- if merge_project && create_mr_button?(@repository.root_ref, branch.name)
= link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do
- Merge request
+ = _('Merge request')
- if branch.name != @repository.root_ref
- = link_to project_compare_index_path(@project, from: @repository.root_ref, to: branch.name), class: "btn btn-default #{'prepend-left-10' unless merge_project}", method: :post, title: "Compare" do
- Compare
+ = link_to project_compare_index_path(@project, from: @repository.root_ref, to: branch.name),
+ class: "btn btn-default #{'prepend-left-10' unless merge_project}",
+ method: :post,
+ title: s_('Branches|Compare') do
+ = s_('Branches|Compare')
= render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name]
@@ -34,12 +37,12 @@
- if branch.name == @project.repository.root_ref
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
disabled: true,
- title: "The default branch cannot be deleted" }
+ title: s_('Branches|The default branch cannot be deleted') }
= icon("trash-o")
- elsif protected_branch?(@project, branch)
- if can?(current_user, :delete_protected_branch, @project)
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip",
- title: "Delete protected branch",
+ title: s_('Branches|Delete protected branch'),
data: { toggle: "modal",
target: "#modal-delete-branch",
delete_path: project_branch_path(@project, branch.name),
@@ -49,20 +52,22 @@
- else
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
disabled: true,
- title: "Only a project master or owner can delete a protected branch" }
+ title: s_('Branches|Only a project master or owner can delete a protected branch') }
= icon("trash-o")
- else
= link_to project_branch_path(@project, branch.name),
class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip",
- title: "Delete branch",
+ title: s_('Branches|Delete branch'),
method: :delete,
- data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?" },
+ data: { confirm: s_("Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?") % { branch_name: branch.name } },
remote: true,
- "aria-label" => "Delete branch" do
+ 'aria-label' => s_('Branches|Delete branch') do
= icon("trash-o")
- if branch.name != @repository.root_ref
- .divergence-graph{ title: "#{number_commits_behind} commits behind #{@repository.root_ref}, #{number_commits_ahead} commits ahead" }
+ .divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: number_commits_behind,
+ default_branch: @repository.root_ref,
+ number_commits_ahead: number_commits_ahead } }
.graph-side
.bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" }
%span.count.count-behind= number_commits_behind
@@ -76,4 +81,4 @@
= render 'projects/branches/commit', commit: commit, project: @project
- else
%p
- Cant find HEAD commit for this branch
+ = s_('Branches|Cant find HEAD commit for this branch')
diff --git a/app/views/projects/branches/_delete_protected_modal.html.haml b/app/views/projects/branches/_delete_protected_modal.html.haml
index f00a0ee6925..e0008e322a0 100644
--- a/app/views/projects/branches/_delete_protected_modal.html.haml
+++ b/app/views/projects/branches/_delete_protected_modal.html.haml
@@ -4,36 +4,38 @@
.modal-header
%button.close{ data: { dismiss: 'modal' } } ×
%h3.page-title
- Delete protected branch
- = surround "'", "'?" do
+ - title_branch_name = capture do
%span.js-branch-name.ref-name>[branch name]
+ = s_("Branches|Delete protected branch '%{branch_name}'?").html_safe % { branch_name: title_branch_name }
.modal-body
%p
- You’re about to permanently delete the protected branch
- = succeed '.' do
- %strong.js-branch-name.ref-name [branch name]
+ - branch_name = capture do
+ %strong.js-branch-name.ref-name>[branch name]
+ = s_('Branches|You’re about to permanently delete the protected branch %{branch_name}.').html_safe % { branch_name: branch_name }
%p.js-not-merged
- default_branch = capture do
%span.ref-name= @repository.root_ref
- = s_("Branches|This branch hasn’t been merged into %{default_branch}.").html_safe % { default_branch: default_branch }
- = s_("Branches|To avoid data loss, consider merging this branch before deleting it.")
+ = s_('Branches|This branch hasn’t been merged into %{default_branch}.').html_safe % { default_branch: default_branch }
+ = s_('Branches|To avoid data loss, consider merging this branch before deleting it.')
%p
- Once you confirm and press
- = succeed ',' do
- %strong Delete protected branch
- it cannot be undone or recovered.
+ - delete_protected_branch = capture do
+ %strong
+ = s_('Branches|Delete protected branch')
+ = s_('Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered.').html_safe % { delete_protected_branch: delete_protected_branch }
%p
- %strong To confirm, type
- %kbd.js-branch-name [branch name]
+ - branch_name_confirmation = capture do
+ %kbd.js-branch-name [branch name]
+ %strong
+ = s_('Branches|To confirm, type %{branch_name_confirmation}:').html_safe % { branch_name_confirmation: branch_name_confirmation }
.form-group
= text_field_tag 'delete_branch_input', '', class: 'form-control js-delete-branch-input'
.modal-footer
%button.btn{ data: { dismiss: 'modal' } } Cancel
- = link_to 'Delete protected branch', '',
+ = link_to s_('Branches|Delete protected branch'), '',
class: "btn btn-danger js-delete-branch",
- title: 'Delete branch',
+ title: s_('Branches|Delete branch'),
method: :delete,
- "aria-label" => "Delete"
+ 'aria-label' => s_('Branches|Delete branch')
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 73583c6bbc2..ea6e7e9db6c 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -1,17 +1,17 @@
- @no_container = true
-- page_title "Branches"
+- page_title _('Branches')
= render "projects/commits/head"
%div{ class: container_class }
.top-area.adjust
- if can?(current_user, :admin_project, @project)
.nav-text
- Protected branches can be managed in
- = link_to 'project settings', project_protected_branches_path(@project)
+ - project_settings_link = link_to s_('Branches|project settings'), project_protected_branches_path(@project)
+ = s_('Branches|Protected branches can be managed in %{project_settings_link}').html_safe % { project_settings_link: project_settings_link }
.nav-controls
= form_tag(filter_branches_path, method: :get) do
- = search_field_tag :search, params[:search], { placeholder: 'Filter by branch name', id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false }
+ = search_field_tag :search, params[:search], { placeholder: s_('Branches|Filter by branch name'), id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false }
.dropdown.inline>
%button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
@@ -20,16 +20,21 @@
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
%li.dropdown-header
- Sort by
+ = s_('Branches|Sort by')
- branches_sort_options_hash.each do |value, title|
%li
= link_to title, filter_branches_path(sort: value), class: ("is-active" if @sort == value)
- if can? current_user, :push_code, @project
- = link_to project_merged_branches_path(@project), class: 'btn btn-inverted btn-remove has-tooltip', title: "Delete all branches that are merged into '#{@project.repository.root_ref}'", method: :delete, data: { confirm: "Deleting the merged branches cannot be undone. Are you sure?", container: 'body' } do
- Delete merged branches
+ = link_to project_merged_branches_path(@project),
+ class: 'btn btn-inverted btn-remove has-tooltip',
+ title: s_("Branches|Delete all branches that are merged into '%{default_branch}'") % { default_branch: @project.repository.root_ref },
+ method: :delete,
+ data: { confirm: s_('Branches|Deleting the merged branches cannot be undone. Are you sure?'),
+ container: 'body' } do
+ = s_('Branches|Delete merged branches')
= link_to new_project_branch_path(@project), class: 'btn btn-create' do
- New branch
+ = s_('Branches|New branch')
- if @branches.any?
%ul.content-list.all-branches
@@ -37,6 +42,7 @@
= render "projects/branches/branch", branch: branch
= paginate @branches, theme: 'gitlab'
- else
- .nothing-here-block No branches to show
+ .nothing-here-block
+ = s_('Branches|No branches to show')
= render 'projects/branches/delete_protected_modal'
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 9d85e027ac9..fa6f6d0b588 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -11,19 +11,15 @@
#{ _('Source code') }
%li
= link_to archive_project_repository_path(project, ref: ref, format: 'zip'), rel: 'nofollow', download: '' do
- %i.fa.fa-download
%span= _('Download zip')
%li
= link_to archive_project_repository_path(project, ref: ref, format: 'tar.gz'), rel: 'nofollow', download: '' do
- %i.fa.fa-download
%span= _('Download tar.gz')
%li
= link_to archive_project_repository_path(project, ref: ref, format: 'tar.bz2'), rel: 'nofollow', download: '' do
- %i.fa.fa-download
%span= _('Download tar.bz2')
%li
= link_to archive_project_repository_path(project, ref: ref, format: 'tar'), rel: 'nofollow', download: '' do
- %i.fa.fa-download
%span= _('Download tar')
- if pipeline && pipeline.latest_builds_with_artifacts.any?
@@ -36,6 +32,5 @@
- pipeline.latest_builds_with_artifacts.each do |job|
%li
= link_to latest_succeeded_project_artifacts_path(project, "#{ref}/download", job: job.name), rel: 'nofollow', download: '' do
- %i.fa.fa-download
%span
#{s_('DownloadArtifacts|Download')} '#{job.name}'
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index b04d6a1fa5e..2589c53beae 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -11,19 +11,16 @@
- if can_create_issue
%li
= link_to new_project_issue_path(@project) do
- = icon('exclamation-circle fw')
#{ _('New issue') }
- if merge_project
%li
= link_to project_new_merge_request_path(merge_project) do
- = icon('tasks fw')
#{ _('New merge request') }
- if can_create_snippet
%li
= link_to new_project_snippet_path(@project) do
- = icon('file-text-o fw')
#{ _('New snippet') }
- if can_create_issue || merge_project || can_create_snippet
@@ -32,20 +29,16 @@
- if can?(current_user, :push_code, @project)
%li
= link_to project_new_blob_path(@project, @project.default_branch || 'master') do
- = icon('file fw')
#{ _('New file') }
%li
= link_to new_project_branch_path(@project) do
- = icon('code-fork fw')
#{ _('New branch') }
%li
= link_to new_project_tag_path(@project) do
- = icon('tags fw')
#{ _('New tag') }
- elsif current_user && current_user.already_forked?(@project)
%li
= link_to project_new_blob_path(@project, @project.default_branch || 'master') do
- = icon('file fw')
#{ _('New file') }
- elsif can?(current_user, :fork_project, @project)
%li
@@ -55,5 +48,4 @@
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
continue: continue_params)
= link_to fork_path, method: :post do
- = icon('file fw')
#{ _('New file') }
diff --git a/app/views/projects/tree/_old_tree_header.html.haml b/app/views/projects/tree/_old_tree_header.html.haml
index 13705ca303b..3a43dde8052 100644
--- a/app/views/projects/tree/_old_tree_header.html.haml
+++ b/app/views/projects/tree/_old_tree_header.html.haml
@@ -20,15 +20,12 @@
- if can_edit_tree?
%li
= link_to project_new_blob_path(@project, @id) do
- = icon('pencil fw')
#{ _('New file') }
%li
= link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do
- = icon('file fw')
#{ _('Upload file') }
%li
= link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
- = icon('folder fw')
#{ _('New directory') }
- elsif can?(current_user, :fork_project, @project)
%li
@@ -38,7 +35,6 @@
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
continue: continue_params)
= link_to fork_path, method: :post do
- = icon('pencil fw')
#{ _('New file') }
%li
- continue_params = { to: request.fullpath,
@@ -47,7 +43,6 @@
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
continue: continue_params)
= link_to fork_path, method: :post do
- = icon('file fw')
#{ _('Upload file') }
%li
- continue_params = { to: request.fullpath,
@@ -56,15 +51,12 @@
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
continue: continue_params)
= link_to fork_path, method: :post do
- = icon('folder fw')
#{ _('New directory') }
%li.divider
%li
= link_to new_project_branch_path(@project) do
- = icon('code-fork fw')
#{ _('New branch') }
%li
= link_to new_project_tag_path(@project) do
- = icon('tags fw')
#{ _('New tag') }
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index 2e5e7911981..722890a01e5 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -31,10 +31,11 @@
":board-id" => "boardId",
":key" => "_uid" }
= render "shared/boards/components/sidebar"
- %board-add-issues-modal{ "new-issue-path" => new_project_issue_path(@project),
- "milestone-path" => milestones_filter_dropdown_path,
- "label-path" => labels_filter_path,
- "empty-state-svg" => image_path('illustrations/issues.svg'),
- ":issue-link-base" => "issueLinkBase",
- ":root-path" => "rootPath",
- ":project-id" => @project.try(:id) }
+ - if @project
+ %board-add-issues-modal{ "new-issue-path" => new_project_issue_path(@project),
+ "milestone-path" => milestones_filter_dropdown_path,
+ "label-path" => labels_filter_path,
+ "empty-state-svg" => image_path('illustrations/issues.svg'),
+ ":issue-link-base" => "issueLinkBase",
+ ":root-path" => "rootPath",
+ ":project-id" => @project.id }
diff --git a/changelogs/unreleased/36549-circuit-breaker-handles-missing-storages.yml b/changelogs/unreleased/36549-circuit-breaker-handles-missing-storages.yml
new file mode 100644
index 00000000000..f5ccb163d98
--- /dev/null
+++ b/changelogs/unreleased/36549-circuit-breaker-handles-missing-storages.yml
@@ -0,0 +1,5 @@
+---
+title: Allow the git circuit breaker to correctly handle missing repository storages
+merge_request: 14417
+author:
+type: fixed
diff --git a/doc/development/ux_guide/basics.md b/doc/development/ux_guide/basics.md
index dcd5f677f25..e215026bcca 100644
--- a/doc/development/ux_guide/basics.md
+++ b/doc/development/ux_guide/basics.md
@@ -33,7 +33,7 @@ This is the typeface used for code blocks and references to commits, branches, a
## Icons
-GitLab has a strong, unique personality. When you look at any screen, you should know immediately know that it is GitLab.
+GitLab has a strong, unique personality. When you look at any screen, you should know immediately that it is GitLab.
Iconography is a powerful visual cue to the user and is a great way for us to reflect our particular sense of style.
- **Standard size:** 16px * 16px
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index 4ab5b3455a5..31a46a738c3 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -64,8 +64,11 @@ module Gitlab
# For performance purposes maximum 20 latest commits
# will be passed as post receive hook data.
- commit_attrs = commits_limited.map do |commit|
- commit.hook_attrs(with_changed_files: true)
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38259
+ commit_attrs = Gitlab::GitalyClient.allow_n_plus_1_calls do
+ commits_limited.map do |commit|
+ commit.hook_attrs(with_changed_files: true)
+ end
end
type = Gitlab::Git.tag_ref?(ref) ? 'tag_push' : 'push'
diff --git a/lib/gitlab/git/storage.rb b/lib/gitlab/git/storage.rb
index e28be4b8a38..08e6c29abad 100644
--- a/lib/gitlab/git/storage.rb
+++ b/lib/gitlab/git/storage.rb
@@ -11,6 +11,7 @@ module Gitlab
end
CircuitOpen = Class.new(Inaccessible)
+ Misconfiguration = Class.new(Inaccessible)
REDIS_KEY_PREFIX = 'storage_accessible:'.freeze
diff --git a/lib/gitlab/git/storage/circuit_breaker.rb b/lib/gitlab/git/storage/circuit_breaker.rb
index 9ea9367d4b7..1eaa2d83fb6 100644
--- a/lib/gitlab/git/storage/circuit_breaker.rb
+++ b/lib/gitlab/git/storage/circuit_breaker.rb
@@ -28,14 +28,26 @@ module Gitlab
def self.for_storage(storage)
cached_circuitbreakers = RequestStore.fetch(:circuitbreaker_cache) do
Hash.new do |hash, storage_name|
- hash[storage_name] = new(storage_name)
+ hash[storage_name] = build(storage_name)
end
end
cached_circuitbreakers[storage]
end
- def initialize(storage, hostname = Gitlab::Environment.hostname)
+ def self.build(storage, hostname = Gitlab::Environment.hostname)
+ config = Gitlab.config.repositories.storages[storage]
+
+ if !config.present?
+ NullCircuitBreaker.new(storage, hostname, error: Misconfiguration.new("Storage '#{storage}' is not configured"))
+ elsif !config['path'].present?
+ NullCircuitBreaker.new(storage, hostname, error: Misconfiguration.new("Path for storage '#{storage}' is not configured"))
+ else
+ new(storage, hostname)
+ end
+ end
+
+ def initialize(storage, hostname)
@storage = storage
@hostname = hostname
@@ -64,6 +76,10 @@ module Gitlab
recent_failure || too_many_failures
end
+ def failure_info
+ @failure_info ||= get_failure_info
+ end
+
# Memoizing the `storage_available` call means we only do it once per
# request when the storage is available.
#
@@ -121,10 +137,12 @@ module Gitlab
end
end
- def failure_info
- @failure_info ||= get_failure_info
+ def cache_key
+ @cache_key ||= "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage}:#{hostname}"
end
+ private
+
def get_failure_info
last_failure, failure_count = Gitlab::Git::Storage.redis.with do |redis|
redis.hmget(cache_key, :last_failure, :failure_count)
@@ -134,10 +152,6 @@ module Gitlab
FailureInfo.new(last_failure, failure_count.to_i)
end
-
- def cache_key
- @cache_key ||= "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage}:#{hostname}"
- end
end
end
end
diff --git a/lib/gitlab/git/storage/health.rb b/lib/gitlab/git/storage/health.rb
index 2d723147f4f..1564e94b7f7 100644
--- a/lib/gitlab/git/storage/health.rb
+++ b/lib/gitlab/git/storage/health.rb
@@ -78,7 +78,7 @@ module Gitlab
def failing_circuit_breakers
@failing_circuit_breakers ||= failing_on_hosts.map do |hostname|
- CircuitBreaker.new(storage_name, hostname)
+ CircuitBreaker.build(storage_name, hostname)
end
end
diff --git a/lib/gitlab/git/storage/null_circuit_breaker.rb b/lib/gitlab/git/storage/null_circuit_breaker.rb
new file mode 100644
index 00000000000..297c043d054
--- /dev/null
+++ b/lib/gitlab/git/storage/null_circuit_breaker.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ module Git
+ module Storage
+ class NullCircuitBreaker
+ # These will have actual values
+ attr_reader :storage,
+ :hostname
+
+ # These will always have nil values
+ attr_reader :storage_path,
+ :failure_wait_time,
+ :failure_reset_time,
+ :storage_timeout
+
+ def initialize(storage, hostname, error: nil)
+ @storage = storage
+ @hostname = hostname
+ @error = error
+ end
+
+ def perform
+ @error ? raise(@error) : yield
+ end
+
+ def circuit_broken?
+ !!@error
+ end
+
+ def failure_count_threshold
+ 1
+ end
+
+ def last_failure
+ circuit_broken? ? Time.now : nil
+ end
+
+ def failure_count
+ circuit_broken? ? 1 : 0
+ end
+
+ def failure_info
+ Gitlab::Git::Storage::CircuitBreaker::FailureInfo.new(last_failure, failure_count)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/health_checks/fs_shards_check.rb b/lib/gitlab/health_checks/fs_shards_check.rb
index a1905b091b4..afaa59b1018 100644
--- a/lib/gitlab/health_checks/fs_shards_check.rb
+++ b/lib/gitlab/health_checks/fs_shards_check.rb
@@ -125,7 +125,7 @@ module Gitlab
end
def storage_circuitbreaker_test(storage_name)
- Gitlab::Git::Storage::CircuitBreaker.new(storage_name).perform { "OK" }
+ Gitlab::Git::Storage::CircuitBreaker.build(storage_name).perform { "OK" }
rescue Gitlab::Git::Storage::Inaccessible
nil
end
diff --git a/spec/javascripts/pipelines/pipelines_artifacts_spec.js b/spec/javascripts/pipelines/pipelines_artifacts_spec.js
index acb67d0ec21..a8a8e3e2cff 100644
--- a/spec/javascripts/pipelines/pipelines_artifacts_spec.js
+++ b/spec/javascripts/pipelines/pipelines_artifacts_spec.js
@@ -34,7 +34,7 @@ describe('Pipelines Artifacts dropdown', () => {
).toEqual(artifacts[0].path);
expect(
- component.$el.querySelector('.dropdown-menu li a span').textContent,
+ component.$el.querySelector('.dropdown-menu li a').textContent,
).toContain(artifacts[0].name);
});
});
diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb
index c86353abb7c..98cf7966dad 100644
--- a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb
+++ b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: true, broken_storage: true do
let(:storage_name) { 'default' }
- let(:circuit_breaker) { described_class.new(storage_name) }
+ let(:circuit_breaker) { described_class.new(storage_name, hostname) }
let(:hostname) { Gitlab::Environment.hostname }
let(:cache_key) { "storage_accessible:#{storage_name}:#{hostname}" }
@@ -22,7 +22,8 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
'failure_wait_time' => 30,
'failure_reset_time' => 1800,
'storage_timeout' => 5
- }
+ },
+ 'nopath' => { 'path' => nil }
)
end
@@ -59,6 +60,14 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
expect(breaker).to be_a(described_class)
expect(described_class.for_storage('default')).to eq(breaker)
end
+
+ it 'returns a broken circuit breaker for an unknown storage' do
+ expect(described_class.for_storage('unknown').circuit_broken?).to be_truthy
+ end
+
+ it 'returns a broken circuit breaker when the path is not set' do
+ expect(described_class.for_storage('nopath').circuit_broken?).to be_truthy
+ end
end
describe '#initialize' do
diff --git a/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb
new file mode 100644
index 00000000000..0e645008c88
--- /dev/null
+++ b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Storage::NullCircuitBreaker do
+ let(:storage) { 'default' }
+ let(:hostname) { 'localhost' }
+ let(:error) { nil }
+
+ subject(:breaker) { described_class.new(storage, hostname, error: error) }
+
+ context 'with an error' do
+ let(:error) { Gitlab::Git::Storage::Misconfiguration.new('error') }
+
+ describe '#perform' do
+ it { expect { breaker.perform { 'ok' } }.to raise_error(error) }
+ end
+
+ describe '#circuit_broken?' do
+ it { expect(breaker.circuit_broken?).to be_truthy }
+ end
+
+ describe '#last_failure' do
+ it { Timecop.freeze { expect(breaker.last_failure).to eq(Time.now) } }
+ end
+
+ describe '#failure_count' do
+ it { expect(breaker.failure_count).to eq(breaker.failure_count_threshold) }
+ end
+
+ describe '#failure_info' do
+ it { Timecop.freeze { expect(breaker.failure_info).to eq(Gitlab::Git::Storage::CircuitBreaker::FailureInfo.new(Time.now, breaker.failure_count_threshold)) } }
+ end
+ end
+
+ context 'not broken' do
+ describe '#perform' do
+ it { expect(breaker.perform { 'ok' }).to eq('ok') }
+ end
+
+ describe '#circuit_broken?' do
+ it { expect(breaker.circuit_broken?).to be_falsy }
+ end
+
+ describe '#last_failure' do
+ it { expect(breaker.last_failure).to be_nil }
+ end
+
+ describe '#failure_count' do
+ it { expect(breaker.failure_count).to eq(0) }
+ end
+
+ describe '#failure_info' do
+ it { expect(breaker.failure_info).to eq(Gitlab::Git::Storage::CircuitBreaker::FailureInfo.new(nil, 0)) }
+ end
+ end
+
+ describe '#failure_count_threshold' do
+ it { expect(breaker.failure_count_threshold).to eq(1) }
+ end
+
+ it 'implements the CircuitBreaker interface' do
+ ours = described_class.public_instance_methods
+ theirs = Gitlab::Git::Storage::CircuitBreaker.public_instance_methods
+
+ # These methods are not part of the public API, but are public to allow the
+ # CircuitBreaker specs to operate. They should be made private over time.
+ exceptions = %i[
+ cache_key
+ check_storage_accessible!
+ no_failures?
+ storage_available?
+ track_storage_accessible
+ track_storage_inaccessible
+ ]
+
+ expect(theirs - ours).to contain_exactly(*exceptions)
+ end
+end
diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
index f5c9680bf59..73dd236a5c6 100644
--- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
@@ -21,7 +21,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
let(:metric_class) { Gitlab::HealthChecks::Metric }
let(:result_class) { Gitlab::HealthChecks::Result }
- let(:repository_storages) { [:default] }
+ let(:repository_storages) { ['default'] }
let(:tmp_dir) { Dir.mktmpdir }
let(:storages_paths) do
@@ -64,7 +64,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
allow(described_class).to receive(:storage_circuitbreaker_test) { true }
end
- it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: :default)) }
+ it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: 'default')) }
end
context 'storage points to directory that has both read and write rights' do
@@ -72,7 +72,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
FileUtils.chmod_R(0755, tmp_dir)
end
- it { is_expected.to include(result_class.new(true, nil, shard: :default)) }
+ it { is_expected.to include(result_class.new(true, nil, shard: 'default')) }
it 'cleans up files used for testing' do
expect(described_class).to receive(:storage_write_test).with(any_args).and_call_original
@@ -85,7 +85,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
allow(described_class).to receive(:storage_read_test).with(any_args).and_return(false)
end
- it { is_expected.to include(result_class.new(false, 'cannot read from storage', shard: :default)) }
+ it { is_expected.to include(result_class.new(false, 'cannot read from storage', shard: 'default')) }
end
context 'write test fails' do
@@ -93,7 +93,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
allow(described_class).to receive(:storage_write_test).with(any_args).and_return(false)
end
- it { is_expected.to include(result_class.new(false, 'cannot write to storage', shard: :default)) }
+ it { is_expected.to include(result_class.new(false, 'cannot write to storage', shard: 'default')) }
end
end
end
@@ -109,7 +109,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
it 'provides metrics' do
metrics = described_class.metrics
- expect(metrics).to all(have_attributes(labels: { shard: :default }))
+ expect(metrics).to all(have_attributes(labels: { shard: 'default' }))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 0))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 0))
@@ -128,7 +128,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
it 'provides metrics' do
metrics = described_class.metrics
- expect(metrics).to all(have_attributes(labels: { shard: :default }))
+ expect(metrics).to all(have_attributes(labels: { shard: 'default' }))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 1))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 1))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 1))
@@ -156,14 +156,14 @@ describe Gitlab::HealthChecks::FsShardsCheck do
describe '#readiness' do
subject { described_class.readiness }
- it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: :default)) }
+ it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: 'default')) }
end
describe '#metrics' do
it 'provides metrics' do
metrics = described_class.metrics
- expect(metrics).to all(have_attributes(labels: { shard: :default }))
+ expect(metrics).to all(have_attributes(labels: { shard: 'default' }))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 0))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 0))
diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb
index 45c10e78789..2dfb4d4a07f 100644
--- a/spec/support/stub_configuration.rb
+++ b/spec/support/stub_configuration.rb
@@ -42,7 +42,7 @@ module StubConfiguration
# Default storage is always required
messages['default'] ||= Gitlab.config.repositories.storages.default
messages.each do |storage_name, storage_settings|
- storage_settings['path'] ||= TestEnv.repos_path
+ storage_settings['path'] = TestEnv.repos_path unless storage_settings.key?('path')
storage_settings['failure_count_threshold'] ||= 10
storage_settings['failure_wait_time'] ||= 30
storage_settings['failure_reset_time'] ||= 1800