From fcca3e6edd4eb83875d77564e98d1fa6ef504332 Mon Sep 17 00:00:00 2001 From: jean Date: Mon, 1 Aug 2016 03:38:16 +0000 Subject: add star for action scope, in order to delete image from registry --- app/services/auth/container_registry_authentication_service.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 5e151b0f044..c066fab04ab 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -103,6 +103,8 @@ module Auth build_can_pull?(requested_project) || user_can_pull?(requested_project) when 'push' build_can_push?(requested_project) || user_can_push?(requested_project) + when '*' + requested_project == project || can?(current_user, :create_container_image, requested_project) else false end -- cgit v1.2.1 From 8ef46b9f46711145496da4b3e64b0b7ace80c2ad Mon Sep 17 00:00:00 2001 From: Jean Praloran Date: Fri, 9 Sep 2016 11:16:42 +1200 Subject: change ability requirement --- app/services/auth/container_registry_authentication_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index c066fab04ab..99a3d9c2cf9 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -104,7 +104,7 @@ module Auth when 'push' build_can_push?(requested_project) || user_can_push?(requested_project) when '*' - requested_project == project || can?(current_user, :create_container_image, requested_project) + requested_project == project || can?(current_user, :admin_container_image, requested_project) else false end -- cgit v1.2.1 From 6c9da292418c606aefe34aab40923d5730ae9aa5 Mon Sep 17 00:00:00 2001 From: Jean Praloran Date: Fri, 17 Mar 2017 11:19:12 +1300 Subject: add test and rebase --- .../container_registry_authentication_service.rb | 7 +- ...ntainer_registry_authentication_service_spec.rb | 92 +++++++++++++++++++++- 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 99a3d9c2cf9..335cfdbc231 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -104,7 +104,7 @@ module Auth when 'push' build_can_push?(requested_project) || user_can_push?(requested_project) when '*' - requested_project == project || can?(current_user, :admin_container_image, requested_project) + user_can_delete?(requested_project) else false end @@ -122,6 +122,11 @@ module Auth (requested_project == project || can?(current_user, :build_read_container_image, requested_project)) end + def user_can_delete(requested_project) + has_authentication_ability?(:admin_container_image) && + can?(current_user, :admin_container_image, requested_project) + end + def user_can_pull?(requested_project) has_authentication_ability?(:read_container_image) && can?(current_user, :read_container_image, requested_project) diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index e273dfe1552..e8d222a2a7f 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -57,6 +57,12 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do it { expect(payload).to include('access' => []) } end + shared_examples 'a deletable' do + it_behaves_like 'a accessible' do + let(:actions) { ['*'] } + end + end + shared_examples 'a pullable' do it_behaves_like 'an accessible' do let(:actions) { ['pull'] } @@ -127,6 +133,16 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do it_behaves_like 'container repository factory' end + context 'disallow developer to delete images' do + before { project.team << [current_user, :developer] } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:*" } + end + + it_behaves_like 'an inaccessible' + end + context 'allow reporter to pull images' do before { project.team << [current_user, :reporter] } @@ -140,6 +156,16 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end end + context 'disallow reporter to delete images' do + before { project.team << [current_user, :reporter] } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:*" } + end + + it_behaves_like 'an inaccessible' + end + context 'return a least of privileges' do before { project.team << [current_user, :reporter] } @@ -161,6 +187,16 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do it_behaves_like 'an inaccessible' it_behaves_like 'not a container repository factory' end + + context 'disallow guest to delete images' do + before { project.team << [current_user, :guest] } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:*" } + end + + it_behaves_like 'an inaccessible' + end end context 'for public project' do @@ -192,6 +228,14 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do it_behaves_like 'an inaccessible' it_behaves_like 'not a container repository factory' end + + context 'disallow anyone to delete images' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:*" } + end + + it_behaves_like 'an inaccessible' + end end context 'for internal project' do @@ -215,17 +259,53 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do it_behaves_like 'an inaccessible' it_behaves_like 'not a container repository factory' end + + context 'disallow anyone to delete images' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:*" } + end + + it_behaves_like 'an inaccessible' + end end context 'for external user' do let(:current_user) { create(:user, external: true) } let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:pull,push" } + { scope: "repository:#{project.path_with_namespace}:pull,push,*" } end it_behaves_like 'an inaccessible' it_behaves_like 'not a container repository factory' end + + end + end + + context 'delete authorized as admin' do + let(:current_project) { create(:empty_project) } + let(:current_user) { create(:user) } + let(:authentication_abilities) do + [ + :build_read_container_image, + :build_create_container_image + ] + end + + before do + current_project.team << [current_project, :admin] + end + + it_behaves_like 'a valid token' + + context 'allow to delete images' do + let(:current_params) do + { scope: "repository:#{current_project.path_with_namespace}:*" } + end + + it_behaves_like 'a deletable' do + let(:project) { current_project } + end end end @@ -257,6 +337,16 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end end + context 'disallow to delete images' do + let(:current_params) do + { scope: "repository:#{current_project.path_with_namespace}:*" } + end + + it_behaves_like 'an inaccessible' do + let(:project) { current_project } + end + end + context 'for other projects' do context 'when pulling' do let(:current_params) do -- cgit v1.2.1 From d890c6e81b696a5c02fe8138a76a4d113ee8ba93 Mon Sep 17 00:00:00 2001 From: Jean Praloran Date: Fri, 17 Mar 2017 16:40:57 +1300 Subject: fix typo and check --- app/services/auth/container_registry_authentication_service.rb | 2 +- .../auth/container_registry_authentication_service_spec.rb | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 335cfdbc231..9622ce64d9f 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -122,7 +122,7 @@ module Auth (requested_project == project || can?(current_user, :build_read_container_image, requested_project)) end - def user_can_delete(requested_project) + def user_can_delete?(requested_project) has_authentication_ability?(:admin_container_image) && can?(current_user, :admin_container_image, requested_project) end diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index e8d222a2a7f..e617f1b0c04 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -278,7 +278,6 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do it_behaves_like 'an inaccessible' it_behaves_like 'not a container repository factory' end - end end @@ -287,15 +286,10 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do let(:current_user) { create(:user) } let(:authentication_abilities) do [ - :build_read_container_image, - :build_create_container_image + :admin_container_image ] end - before do - current_project.team << [current_project, :admin] - end - it_behaves_like 'a valid token' context 'allow to delete images' do -- cgit v1.2.1 From 2822081821f3f1e1e8cf8cfd8228ddb282b2d198 Mon Sep 17 00:00:00 2001 From: Jean Praloran Date: Fri, 17 Mar 2017 18:31:05 +1300 Subject: fix unit testing for admin --- spec/services/auth/container_registry_authentication_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index e617f1b0c04..0609790893d 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -283,7 +283,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do context 'delete authorized as admin' do let(:current_project) { create(:empty_project) } - let(:current_user) { create(:user) } + let(:current_user) { create(:admin) } let(:authentication_abilities) do [ :admin_container_image -- cgit v1.2.1 From 9b9c86787a009c420536c65aecfa5823af88f665 Mon Sep 17 00:00:00 2001 From: Simon Knox Date: Tue, 25 Jul 2017 21:02:17 +1000 Subject: fix Jump to next discussion Notes tab was renamed to show, was previously overridden in merge_request_tabs.js --- .../javascripts/diff_notes/components/jump_to_discussion.js | 10 +++++----- changelogs/unreleased/35232-next-unresolved.yml | 4 ++++ 2 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/35232-next-unresolved.yml diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js index 37ddca29e71..298f737a2bc 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js @@ -94,7 +94,7 @@ const JumpToDiscussion = Vue.extend({ hasDiscussionsToJumpTo = false; } } - } else if (activeTab !== 'notes') { + } else if (activeTab !== 'show') { // If we are on the commits or builds tabs, // there are no discussions to jump to. hasDiscussionsToJumpTo = false; @@ -103,12 +103,12 @@ const JumpToDiscussion = Vue.extend({ if (!hasDiscussionsToJumpTo) { // If there are no discussions to jump to on the current page, // switch to the notes tab and jump to the first disucssion there. - window.mrTabs.activateTab('notes'); - activeTab = 'notes'; + window.mrTabs.activateTab('show'); + activeTab = 'show'; jumpToFirstDiscussion = true; } - if (activeTab === 'notes') { + if (activeTab === 'show') { discussionsSelector = '.discussion[data-discussion-id]'; discussionIdsInScope = discussionIdsForElements($(discussionsSelector)); } @@ -156,7 +156,7 @@ const JumpToDiscussion = Vue.extend({ let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`); - if (activeTab === 'notes') { + if (activeTab === 'show') { $target = $target.closest('.note-discussion'); // If the next discussion is closed, toggle it open. diff --git a/changelogs/unreleased/35232-next-unresolved.yml b/changelogs/unreleased/35232-next-unresolved.yml new file mode 100644 index 00000000000..45f3fb429a8 --- /dev/null +++ b/changelogs/unreleased/35232-next-unresolved.yml @@ -0,0 +1,4 @@ +--- +title: fix jump to next discussion button +merge_request: +author: -- cgit v1.2.1 From aaaa252e30e25afe58a39f97489a9a6eef92093e Mon Sep 17 00:00:00 2001 From: tauriedavis Date: Mon, 3 Jul 2017 10:51:32 -0700 Subject: 34060 Simply general project settings to use expanded panels --- app/assets/javascripts/dispatcher.js | 2 + app/assets/stylesheets/pages/projects.scss | 1 - app/assets/stylesheets/pages/settings.scss | 15 +- .../projects/_merge_request_settings.html.haml | 7 +- app/views/projects/edit.html.haml | 367 +++++++++++---------- 5 files changed, 202 insertions(+), 190 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 9d706b5ba59..8bd10b4954b 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -329,6 +329,8 @@ import GpgBadges from './gpg_badges'; break; case 'projects:edit': setupProjectEdit(); + // Initialize expandable settings panels + initSettingsPanels(); break; case 'projects:pipelines:builds': case 'projects:pipelines:failures': diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index b3a90dff89a..ab8b7defc72 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -36,7 +36,6 @@ } select { - background: transparent; transition: background 2s ease-out; &.highlight-changes { diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index d69a8e0995c..1b53e78c954 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -54,8 +54,7 @@ .settings-content { max-height: 1px; overflow-y: scroll; - margin-right: -20px; - padding-right: 130px; + padding-right: 110px; animation: collapseMaxHeight 300ms ease-out; &.expanded { @@ -87,6 +86,18 @@ overflow: hidden; margin-top: 20px; } + + .sub-section { + margin-bottom: 32px; + padding: 16px; + border: 1px solid $border-color; + background-color: $gray-light; + } + + .bs-callout, + .checkbox:first-child { + margin-top: 0; + } } .settings-list-icon { diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml index 818010bc7d3..2054a1ac787 100644 --- a/app/views/projects/_merge_request_settings.html.haml +++ b/app/views/projects/_merge_request_settings.html.haml @@ -1,8 +1,3 @@ - form = local_assigns.fetch(:form) -%fieldset.features.merge-requests-feature.append-bottom-default - %hr - %h5.prepend-top-0 - Merge Requests - - = render 'projects/merge_request_merge_settings', form: form += render 'projects/merge_request_merge_settings', form: form \ No newline at end of file diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 087cb804449..ef8b58f041c 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -1,13 +1,19 @@ +- page_title "General" - @content_class = "limit-container-width" unless fluid_layout +- expanded = Rails.env.test? = render "projects/settings/head" -.project-edit-container - .row.prepend-top-default - .col-lg-4.profile-settings-sidebar - %h4.prepend-top-0 - Project settings - .col-lg-8 - .project-edit-errors + +%section.settings.general-settings + .settings-header + %h4 + General project settings + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p + Update your project name, description, avatar, and other general settings. + .settings-content.no-animate{ class: ('expanded' if expanded) } + .project-edit-errors = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f| %fieldset .row @@ -35,89 +41,7 @@ = f.label :tag_list, "Tags", class: 'label-light' = f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control" %p.help-block Separate tags with commas. - %hr - %fieldset - %h5.prepend-top-0 - Sharing & Permissions - .form_group.prepend-top-20.sharing-and-permissions - .row.js-visibility-select - .col-md-8 - .label-light - = label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level - = link_to icon('question-circle'), help_page_path("public_access/public_access") - %span.help-block - .col-md-4.visibility-select-container - = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level) - = f.fields_for :project_feature do |feature_fields| - %fieldset.features - .row - .col-md-8.project-feature - = feature_fields.label :repository_access_level, "Repository", class: 'label-light' - %span.help-block View and edit files in this project - .col-md-4.js-repo-access-level - = project_feature_access_select(:repository_access_level) - - .row - .col-md-8.project-feature.nested - = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' - %span.help-block Submit changes to be merged upstream - .col-md-4 - = project_feature_access_select(:merge_requests_access_level) - - .row - .col-md-8.project-feature.nested - = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light' - %span.help-block Build, test, and deploy your changes - .col-md-4 - = project_feature_access_select(:builds_access_level) - - .row - .col-md-8.project-feature - = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' - %span.help-block Share code pastes with others out of Git repository - .col-md-4 - = project_feature_access_select(:snippets_access_level) - - .row - .col-md-8.project-feature - = feature_fields.label :issues_access_level, "Issues", class: 'label-light' - %span.help-block Lightweight issue tracking system for this project - .col-md-4 - = project_feature_access_select(:issues_access_level) - - .row - .col-md-8.project-feature - = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light' - %span.help-block Pages for project documentation - .col-md-4 - = project_feature_access_select(:wiki_access_level) - .form-group - = render 'shared/allow_request_access', form: f - - if Gitlab.config.lfs.enabled && current_user.admin? - .row.js-lfs-enabled - .col-md-8 - = f.label :lfs_enabled, 'LFS', class: 'label-light' - %span.help-block - Git Large File Storage - = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') - .col-md-4 - .select-wrapper - = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select select-control', data: { field: 'lfs_enabled' } - = icon('chevron-down') - - if Gitlab.config.registry.enabled - .form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) } - .checkbox - = f.label :container_registry_enabled do - = f.check_box :container_registry_enabled - %strong Container Registry - %br - %span.descr Enable Container Registry for this project - = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank' - - = render 'merge_request_settings', form: f - - %hr - %fieldset.features.append-bottom-default + %fieldset.features %h5.prepend-top-0 Project avatar .form-group @@ -137,91 +61,183 @@ = link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" = f.submit 'Save changes', class: "btn btn-save" - .row.prepend-top-default - %hr - .row.prepend-top-default - .col-lg-4 - %h4.prepend-top-0 - Housekeeping - %p.append-bottom-0 - %p - Runs a number of housekeeping tasks within the current repository, - such as compressing file revisions and removing unreachable objects. - .col-lg-8 - = link_to 'Housekeeping', housekeeping_project_path(@project), - method: :post, class: "btn btn-default" - %hr - .row.prepend-top-default - .col-lg-4 - %h4.prepend-top-0 - Export project +%section.settings.sharing-permissions + .settings-header + %h4 + Sharing and permissions + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p + Enable or disable certain project features and choose access levels. + .settings-content.no-animate{ class: ('expaneded' if expanded) } + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| + .form_group.sharing-and-permissions + .row.js-visibility-select + .col-md-8 + .label-light + = label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level + = link_to icon('question-circle'), help_page_path("public_access/public_access") + %span.help-block + .col-md-4.visibility-select-container + = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level) + = f.fields_for :project_feature do |feature_fields| + %fieldset.features + .row + .col-md-8.project-feature + = feature_fields.label :repository_access_level, "Repository", class: 'label-light' + %span.help-block View and edit files in this project + .col-md-4.js-repo-access-level + = project_feature_access_select(:repository_access_level) + + .row + .col-md-8.project-feature.nested + = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' + %span.help-block Submit changes to be merged upstream + .col-md-4 + = project_feature_access_select(:merge_requests_access_level) + + .row + .col-md-8.project-feature.nested + = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light' + %span.help-block Build, test, and deploy your changes + .col-md-4 + = project_feature_access_select(:builds_access_level) + + .row + .col-md-8.project-feature + = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' + %span.help-block Share code pastes with others out of Git repository + .col-md-4 + = project_feature_access_select(:snippets_access_level) + + .row + .col-md-8.project-feature + = feature_fields.label :issues_access_level, "Issues", class: 'label-light' + %span.help-block Lightweight issue tracking system for this project + .col-md-4 + = project_feature_access_select(:issues_access_level) + + .row + .col-md-8.project-feature + = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light' + %span.help-block Pages for project documentation + .col-md-4 + = project_feature_access_select(:wiki_access_level) + .form-group + = render 'shared/allow_request_access', form: f + - if Gitlab.config.lfs.enabled && current_user.admin? + .row.js-lfs-enabled.form-group.sharing-and-permissions + .col-md-8 + = f.label :lfs_enabled, 'Git Large File Storage', class: 'label-light' + = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + %span.help-block Manages large files such as audio, video and graphics files. + .col-md-4 + .select-wrapper + = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select select-control', data: { field: 'lfs_enabled' } + = icon('chevron-down') + - if Gitlab.config.registry.enabled + .form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) } + .checkbox + = f.label :container_registry_enabled do + = f.check_box :container_registry_enabled + %strong Container Registry + %br + %span.descr Enable Container Registry for this project + = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank' + = f.submit 'Save changes', class: "btn btn-save" + + +%section.settings.merge-request-settings{ style: ("display: none;" if @project.project_feature.send(:merge_requests_access_level) == 0) } + .settings-header + %h4 + Merge request settings + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p + Customize your merge request restrictions. + .settings-content.no-animate{ class: ('expanded' if expanded) } + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings" }, authenticity_token: true do |f| + = render 'merge_request_settings', form: f + = f.submit 'Save changes', class: "btn btn-save" + +%section.settings + .settings-header + %h4 + Export project + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p + Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page. + .settings-content.no-animate{ class: ('expanded' if expanded) } + .bs-callout.bs-callout-info %p.append-bottom-0 %p - Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page. + The following items will be exported: + %ul + %li Project and wiki repositories + %li Project uploads + %li Project configuration including web hooks and services + %li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities %p - Once the exported file is ready, you will receive a notification email with a download link. - - .col-lg-8 + The following items will NOT be exported: + %ul + %li Job traces and artifacts + %li LFS objects + %li Container registry images + %li CI variables + %li Any encrypted tokens + %p + Once the exported file is ready, you will receive a notification email with a download link. + - if @project.export_project_path + = link_to 'Download export', download_export_project_path(@project), + rel: 'nofollow', download: '', method: :get, class: "btn btn-default" + = link_to 'Generate new export', generate_new_export_project_path(@project), + method: :post, class: "btn btn-default" + - else + = link_to 'Export project', export_project_path(@project), + method: :post, class: "btn btn-default" - - if @project.export_project_path - = link_to 'Download export', download_export_project_path(@project), - rel: 'nofollow', download: '', method: :get, class: "btn btn-default" - = link_to 'Generate new export', generate_new_export_project_path(@project), - method: :post, class: "btn btn-default" - - else - = link_to 'Export project', export_project_path(@project), - method: :post, class: "btn btn-default" - - .bs-callout.bs-callout-info - %p.append-bottom-0 - %p - The following items will be exported: - %ul - %li Project and wiki repositories - %li Project uploads - %li Project configuration including web hooks and services - %li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities - %p - The following items will NOT be exported: - %ul - %li Job traces and artifacts - %li LFS objects - %li Container registry images - %li CI variables - %li Any encrypted tokens - - if can? current_user, :archive_project, @project - %hr - .row.prepend-top-default - .col-lg-4 - %h4.warning-title.prepend-top-0 +%section.settings.advanced-settings + .settings-header + %h4 + Advanced settings + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p + Perform advanced options such as housekeeping, exporting, archiveing, renameing, transfering, or removeing your project. + .settings-content.no-animate{ class: ('expanded' if expanded) } + .sub-section + %h4 Housekeeping + %p + Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects. + = link_to 'Housekeeping', housekeeping_project_path(@project), + method: :post, class: "btn btn-default" + - if can? current_user, :archive_project, @project + .sub-section + %h4.warning-title - if @project.archived? Unarchive project - else Archive project - %p.append-bottom-0 - - if @project.archived? - Unarchiving the project will mark its repository as active. The project can be committed to. - - else - Archiving the project will mark its repository as read-only. It is hidden from the dashboard and doesn't show up in searches. - .col-lg-8 - if @project.archived? %p + Unarchiving the project will mark its repository as active. The project can be committed to. %strong Once active this project shows up in the search and on the dashboard. = link_to 'Unarchive project', unarchive_project_path(@project), data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." }, method: :post, class: "btn btn-success" - else %p + Archiving the project will mark its repository as read-only. It is hidden from the dashboard and doesn't show up in searches. %strong Archived projects cannot be committed to! = link_to 'Archive project', archive_project_path(@project), data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." }, method: :post, class: "btn btn-warning" - %hr - .row.prepend-top-default - .col-lg-4 - %h4.prepend-top-0.warning-title + .sub-section.rename-respository + %h4.warning-title Rename repository - .col-lg-8 + %p + Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page. = render 'projects/errors' = form_for([@project.namespace.becomes(Namespace), @project]) do |f| .form-group.project_name_holder @@ -243,15 +259,11 @@ - if @project.deployment_services.any? %li Your deployment services will be broken, you will need to manually fix the services after renaming. = f.submit 'Rename project', class: "btn btn-warning" - - if can?(current_user, :change_namespace, @project) - %hr - .row.prepend-top-default - .col-lg-4 - %h4.prepend-top-0.danger-title - Transfer project to new group - %p.append-bottom-0 - Please select the group you want to transfer this project to in the dropdown to the right. - .col-lg-8 + + - if can?(current_user, :change_namespace, @project) + .sub-section + %h4.danger-title + Transfer project = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_project_path(@project), method: :put, remote: true, html: { class: 'js-project-transfer-form' } ) do |f| .form-group = label_tag :new_namespace_id, nil, class: 'label-light' do @@ -264,31 +276,24 @@ %li You will need to update your local repositories to point to the new location. %li Project visibility level will be changed to match namespace rules when transfering to a group. = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } - - if @project.forked? && can?(current_user, :remove_fork_project, @project) - %hr - .row.prepend-top-default.append-bottom-default - .col-lg-4 - %h4.prepend-top-0.danger-title + - if @project.forked? && can?(current_user, :remove_fork_project, @project) + .sub-section + %h4.danger-title Remove fork relationship - %p.append-bottom-0 - %p - This will remove the fork relationship to source project - = succeed "." do - = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project) - .col-lg-8 + %p + This will remove the fork relationship to source project + = succeed "." do + = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project) = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f| %p %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source. = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } - - if can?(current_user, :remove_project, @project) - %hr - .row.prepend-top-default.append-bottom-default - .col-lg-4 - %h4.prepend-top-0.danger-title + - if can?(current_user, :remove_project, @project) + .sub-section + %h4.danger-title Remove project - %p.append-bottom-0 + %p Removing the project will delete its repository and all related resources including issues, merge requests etc. - .col-lg-8 = form_tag(project_path(@project), method: :delete) do %p %strong Removed projects cannot be restored! -- cgit v1.2.1 From d7b8b38db7e22e247b0cac30645b1bc7647eeb3a Mon Sep 17 00:00:00 2001 From: tauriedavis Date: Wed, 12 Jul 2017 14:32:04 -0700 Subject: try and fix some tests --- app/views/projects/_merge_request_settings.html.haml | 2 +- spec/features/projects/features_visibility_spec.rb | 12 +++++++++--- spec/features/projects/project_settings_spec.rb | 8 ++++++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml index 2054a1ac787..cc5afa943cf 100644 --- a/app/views/projects/_merge_request_settings.html.haml +++ b/app/views/projects/_merge_request_settings.html.haml @@ -1,3 +1,3 @@ - form = local_assigns.fetch(:form) -= render 'projects/merge_request_merge_settings', form: form \ No newline at end of file += render 'projects/merge_request_merge_settings', form: form diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb index 37fa61d038e..bc58a8695d8 100644 --- a/spec/features/projects/features_visibility_spec.rb +++ b/spec/features/projects/features_visibility_spec.rb @@ -20,17 +20,23 @@ describe 'Edit Project Settings' do visit edit_project_path(project) select 'Disabled', from: "project_project_feature_attributes_#{tool_name}_access_level" - click_button 'Save changes' + page.within('.sharing-permissions') do + click_button 'Save changes' + end wait_for_requests expect(page).not_to have_selector(".shortcuts-#{shortcut_name}") select 'Everyone with access', from: "project_project_feature_attributes_#{tool_name}_access_level" - click_button 'Save changes' + page.within('.sharing-permissions') do + click_button 'Save changes' + end wait_for_requests expect(page).to have_selector(".shortcuts-#{shortcut_name}") select 'Only team members', from: "project_project_feature_attributes_#{tool_name}_access_level" - click_button 'Save changes' + page.within('.sharing-permissions') do + click_button 'Save changes' + end wait_for_requests expect(page).to have_selector(".shortcuts-#{shortcut_name}") diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb index 6001bcfff0a..f9a24db648a 100644 --- a/spec/features/projects/project_settings_spec.rb +++ b/spec/features/projects/project_settings_spec.rb @@ -14,7 +14,9 @@ describe 'Edit Project Settings' do it 'shows errors for invalid project name' do visit edit_project_path(project) fill_in 'project_name_edit', with: 'foo&bar' - click_button 'Save changes' + page.within('.general-settings') do + click_button 'Save changes' + end expect(page).to have_field 'project_name_edit', with: 'foo&bar' expect(page).to have_content "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'." expect(page).to have_button 'Save changes' @@ -23,7 +25,9 @@ describe 'Edit Project Settings' do it 'shows a successful notice when the project is updated' do visit edit_project_path(project) fill_in 'project_name_edit', with: 'hello world' - click_button 'Save changes' + page.within('.general-settings') do + click_button 'Save changes' + end expect(page).to have_content "Project 'hello world' was successfully updated." end end -- cgit v1.2.1 From c8fbba43a8a92f3b96a0940d3439534c6b340f10 Mon Sep 17 00:00:00 2001 From: tauriedavis Date: Fri, 14 Jul 2017 08:40:15 -0700 Subject: css fixes --- app/assets/stylesheets/pages/settings.scss | 7 +- app/views/projects/edit.html.haml | 114 ++++++++++++++--------------- 2 files changed, 63 insertions(+), 58 deletions(-) diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 1b53e78c954..15df51e9c69 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -95,9 +95,14 @@ } .bs-callout, - .checkbox:first-child { + .checkbox:first-child, + .help-block { margin-top: 0; } + + .label-light { + margin-bottom: 0; + } } .settings-list-icon { diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index ef8b58f041c..7af21df2834 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -61,68 +61,68 @@ = link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" = f.submit 'Save changes', class: "btn btn-save" -%section.settings.sharing-permissions - .settings-header - %h4 - Sharing and permissions - %button.btn.js-settings-toggle - = expanded ? 'Collapse' : 'Expand' - %p - Enable or disable certain project features and choose access levels. - .settings-content.no-animate{ class: ('expaneded' if expanded) } - = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| - .form_group.sharing-and-permissions - .row.js-visibility-select - .col-md-8 - .label-light - = label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level - = link_to icon('question-circle'), help_page_path("public_access/public_access") - %span.help-block - .col-md-4.visibility-select-container - = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level) - = f.fields_for :project_feature do |feature_fields| - %fieldset.features - .row - .col-md-8.project-feature - = feature_fields.label :repository_access_level, "Repository", class: 'label-light' - %span.help-block View and edit files in this project - .col-md-4.js-repo-access-level - = project_feature_access_select(:repository_access_level) + %section.settings.sharing-permissions + .settings-header + %h4 + Sharing and permissions + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p + Enable or disable certain project features and choose access levels. + .settings-content.no-animate{ class: ('expaneded' if expanded) } + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| + .form-group.sharing-and-permissions + .row.js-visibility-select + .col-md-8 + .label-light + = label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level + = link_to icon('question-circle'), help_page_path("public_access/public_access") + %span.help-block + .col-md-4.visibility-select-container + = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level) + = f.fields_for :project_feature do |feature_fields| + %fieldset.features + .row + .col-md-8.project-feature + = feature_fields.label :repository_access_level, "Repository", class: 'label-light' + %span.help-block View and edit files in this project + .col-md-4.js-repo-access-level + = project_feature_access_select(:repository_access_level) - .row - .col-md-8.project-feature.nested - = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' - %span.help-block Submit changes to be merged upstream - .col-md-4 - = project_feature_access_select(:merge_requests_access_level) + .row + .col-md-8.project-feature.nested + = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' + %span.help-block Submit changes to be merged upstream + .col-md-4 + = project_feature_access_select(:merge_requests_access_level) + .row + .col-md-8.project-feature.nested + = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light' + %span.help-block Build, test, and deploy your changes + .col-md-4 + = project_feature_access_select(:builds_access_level) - .row - .col-md-8.project-feature.nested - = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light' - %span.help-block Build, test, and deploy your changes - .col-md-4 - = project_feature_access_select(:builds_access_level) + .row + .col-md-8.project-feature + = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' + %span.help-block Share code pastes with others out of Git repository + .col-md-4 + = project_feature_access_select(:snippets_access_level) - .row - .col-md-8.project-feature - = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' - %span.help-block Share code pastes with others out of Git repository - .col-md-4 - = project_feature_access_select(:snippets_access_level) + .row + .col-md-8.project-feature + = feature_fields.label :issues_access_level, "Issues", class: 'label-light' + %span.help-block Lightweight issue tracking system for this project + .col-md-4 + = project_feature_access_select(:issues_access_level) - .row - .col-md-8.project-feature - = feature_fields.label :issues_access_level, "Issues", class: 'label-light' - %span.help-block Lightweight issue tracking system for this project - .col-md-4 - = project_feature_access_select(:issues_access_level) + .row + .col-md-8.project-feature + = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light' + %span.help-block Pages for project documentation + .col-md-4 + = project_feature_access_select(:wiki_access_level) - .row - .col-md-8.project-feature - = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light' - %span.help-block Pages for project documentation - .col-md-4 - = project_feature_access_select(:wiki_access_level) .form-group = render 'shared/allow_request_access', form: f - if Gitlab.config.lfs.enabled && current_user.admin? -- cgit v1.2.1 From 7ea89083060d5f44f9b3fd4e3258798269ac883a Mon Sep 17 00:00:00 2001 From: tauriedavis Date: Tue, 25 Jul 2017 10:34:44 -0700 Subject: fix merge conflicts --- app/views/projects/edit.html.haml | 430 +++++++++++++++++++------------------- 1 file changed, 215 insertions(+), 215 deletions(-) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 7af21df2834..8b1ea8fb6cb 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -4,62 +4,63 @@ = render "projects/settings/head" -%section.settings.general-settings - .settings-header - %h4 - General project settings - %button.btn.js-settings-toggle - = expanded ? 'Collapse' : 'Expand' - %p - Update your project name, description, avatar, and other general settings. - .settings-content.no-animate{ class: ('expanded' if expanded) } - .project-edit-errors - = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f| - %fieldset - .row - .form-group.col-md-9 - = f.label :name, class: 'label-light', for: 'project_name_edit' do - Project name - = f.text_field :name, class: "form-control", id: "project_name_edit" - - .form-group.col-md-3 - = f.label :id, class: 'label-light' do - Project ID - = f.text_field :id, class: 'form-control', readonly: true +.project-edit-container + %section.settings.general-settings + .settings-header + %h4 + General project settings + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p + Update your project name, description, avatar, and other general settings. + .settings-content.no-animate{ class: ('expanded' if expanded) } + .project-edit-errors + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f| + %fieldset + .row + .form-group.col-md-9 + = f.label :name, class: 'label-light', for: 'project_name_edit' do + Project name + = f.text_field :name, class: "form-control", id: "project_name_edit" - .form-group - = f.label :description, class: 'label-light' do - Project description - %span.light (optional) - = f.text_area :description, class: "form-control", rows: 3, maxlength: 250 + .form-group.col-md-3 + = f.label :id, class: 'label-light' do + Project ID + = f.text_field :id, class: 'form-control', readonly: true - - unless @project.empty_repo? .form-group - = f.label :default_branch, "Default Branch", class: 'label-light' - = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) - .form-group - = f.label :tag_list, "Tags", class: 'label-light' - = f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control" - %p.help-block Separate tags with commas. - %fieldset.features - %h5.prepend-top-0 - Project avatar + = f.label :description, class: 'label-light' do + Project description + %span.light (optional) + = f.text_area :description, class: "form-control", rows: 3, maxlength: 250 + + - unless @project.empty_repo? + .form-group + = f.label :default_branch, "Default Branch", class: 'label-light' + = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) .form-group - - if @project.avatar? - .avatar-container.s160 - = project_icon(@project.full_path, alt: '', class: 'avatar project-avatar s160') - %p.light - - if @project.avatar_in_git - Project avatar in repository: #{ @project.avatar_in_git } - %a.choose-btn.btn.js-choose-project-avatar-button - Browse file... - %span.file_name.prepend-left-default.js-avatar-filename No file chosen - = f.file_field :avatar, class: "js-project-avatar-input hidden" - .help-block The maximum file size allowed is 200KB. - - if @project.avatar? - %hr - = link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" - = f.submit 'Save changes', class: "btn btn-save" + = f.label :tag_list, "Tags", class: 'label-light' + = f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control" + %p.help-block Separate tags with commas. + %fieldset.features + %h5.prepend-top-0 + Project avatar + .form-group + - if @project.avatar? + .avatar-container.s160 + = project_icon(@project.full_path, alt: '', class: 'avatar project-avatar s160') + %p.light + - if @project.avatar_in_git + Project avatar in repository: #{ @project.avatar_in_git } + %a.choose-btn.btn.js-choose-project-avatar-button + Browse file... + %span.file_name.prepend-left-default.js-avatar-filename No file chosen + = f.file_field :avatar, class: "js-project-avatar-input hidden" + .help-block The maximum file size allowed is 200KB. + - if @project.avatar? + %hr + = link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" + = f.submit 'Save changes', class: "btn btn-save" %section.settings.sharing-permissions .settings-header @@ -71,7 +72,7 @@ Enable or disable certain project features and choose access levels. .settings-content.no-animate{ class: ('expaneded' if expanded) } = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| - .form-group.sharing-and-permissions + .form_group.sharing-and-permissions .row.js-visibility-select .col-md-8 .label-light @@ -95,6 +96,7 @@ %span.help-block Submit changes to be merged upstream .col-md-4 = project_feature_access_select(:merge_requests_access_level) + .row .col-md-8.project-feature.nested = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light' @@ -122,182 +124,180 @@ %span.help-block Pages for project documentation .col-md-4 = project_feature_access_select(:wiki_access_level) + .form-group + = render 'shared/allow_request_access', form: f + - if Gitlab.config.lfs.enabled && current_user.admin? + .row.js-lfs-enabled.form-group.sharing-and-permissions + .col-md-8 + = f.label :lfs_enabled, 'Git Large File Storage', class: 'label-light' + = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + %span.help-block Manages large files such as audio, video and graphics files. + .col-md-4 + .select-wrapper + = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select select-control', data: { field: 'lfs_enabled' } + = icon('chevron-down') + - if Gitlab.config.registry.enabled + .form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) } + .checkbox + = f.label :container_registry_enabled do + = f.check_box :container_registry_enabled + %strong Container Registry + %br + %span.descr Enable Container Registry for this project + = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank' + = f.submit 'Save changes', class: "btn btn-save" - .form-group - = render 'shared/allow_request_access', form: f - - if Gitlab.config.lfs.enabled && current_user.admin? - .row.js-lfs-enabled.form-group.sharing-and-permissions - .col-md-8 - = f.label :lfs_enabled, 'Git Large File Storage', class: 'label-light' - = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') - %span.help-block Manages large files such as audio, video and graphics files. - .col-md-4 - .select-wrapper - = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select select-control', data: { field: 'lfs_enabled' } - = icon('chevron-down') - - if Gitlab.config.registry.enabled - .form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) } - .checkbox - = f.label :container_registry_enabled do - = f.check_box :container_registry_enabled - %strong Container Registry - %br - %span.descr Enable Container Registry for this project - = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank' - = f.submit 'Save changes', class: "btn btn-save" - - -%section.settings.merge-request-settings{ style: ("display: none;" if @project.project_feature.send(:merge_requests_access_level) == 0) } - .settings-header - %h4 - Merge request settings - %button.btn.js-settings-toggle - = expanded ? 'Collapse' : 'Expand' - %p - Customize your merge request restrictions. - .settings-content.no-animate{ class: ('expanded' if expanded) } - = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings" }, authenticity_token: true do |f| - = render 'merge_request_settings', form: f - = f.submit 'Save changes', class: "btn btn-save" -%section.settings - .settings-header - %h4 - Export project - %button.btn.js-settings-toggle - = expanded ? 'Collapse' : 'Expand' - %p - Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page. - .settings-content.no-animate{ class: ('expanded' if expanded) } - .bs-callout.bs-callout-info - %p.append-bottom-0 - %p - The following items will be exported: - %ul - %li Project and wiki repositories - %li Project uploads - %li Project configuration including web hooks and services - %li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities - %p - The following items will NOT be exported: - %ul - %li Job traces and artifacts - %li LFS objects - %li Container registry images - %li CI variables - %li Any encrypted tokens - %p - Once the exported file is ready, you will receive a notification email with a download link. - - if @project.export_project_path - = link_to 'Download export', download_export_project_path(@project), - rel: 'nofollow', download: '', method: :get, class: "btn btn-default" - = link_to 'Generate new export', generate_new_export_project_path(@project), - method: :post, class: "btn btn-default" - - else - = link_to 'Export project', export_project_path(@project), - method: :post, class: "btn btn-default" + %section.settings.merge-request-settings{ style: ("display: none;" if @project.project_feature.send(:merge_requests_access_level) == 0) } + .settings-header + %h4 + Merge request settings + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p + Customize your merge request restrictions. + .settings-content.no-animate{ class: ('expanded' if expanded) } + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings" }, authenticity_token: true do |f| + = render 'merge_request_settings', form: f + = f.submit 'Save changes', class: "btn btn-save" -%section.settings.advanced-settings - .settings-header - %h4 - Advanced settings - %button.btn.js-settings-toggle - = expanded ? 'Collapse' : 'Expand' - %p - Perform advanced options such as housekeeping, exporting, archiveing, renameing, transfering, or removeing your project. - .settings-content.no-animate{ class: ('expanded' if expanded) } - .sub-section - %h4 Housekeeping + %section.settings + .settings-header + %h4 + Export project + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' %p - Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects. - = link_to 'Housekeeping', housekeeping_project_path(@project), - method: :post, class: "btn btn-default" - - if can? current_user, :archive_project, @project - .sub-section - %h4.warning-title - - if @project.archived? - Unarchive project - - else - Archive project - - if @project.archived? + Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page. + .settings-content.no-animate{ class: ('expanded' if expanded) } + .bs-callout.bs-callout-info + %p.append-bottom-0 %p - Unarchiving the project will mark its repository as active. The project can be committed to. - %strong Once active this project shows up in the search and on the dashboard. - = link_to 'Unarchive project', unarchive_project_path(@project), - data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." }, - method: :post, class: "btn btn-success" - - else + The following items will be exported: + %ul + %li Project and wiki repositories + %li Project uploads + %li Project configuration including web hooks and services + %li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities %p - Archiving the project will mark its repository as read-only. It is hidden from the dashboard and doesn't show up in searches. - %strong Archived projects cannot be committed to! - = link_to 'Archive project', archive_project_path(@project), - data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." }, - method: :post, class: "btn btn-warning" - .sub-section.rename-respository - %h4.warning-title - Rename repository - %p - Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page. - = render 'projects/errors' - = form_for([@project.namespace.becomes(Namespace), @project]) do |f| - .form-group.project_name_holder - = f.label :name, class: 'label-light' do - Project name - .form-group - = f.text_field :name, class: "form-control" - .form-group - = f.label :path, class: 'label-light' do - %span Path - .form-group - .input-group - .input-group-addon - #{URI.join(root_url, @project.namespace.full_path)}/ - = f.text_field :path, class: 'form-control' + The following items will NOT be exported: %ul - %li Be careful. Renaming a project's repository can have unintended side effects. - %li You will need to update your local repositories to point to the new location. - - if @project.deployment_services.any? - %li Your deployment services will be broken, you will need to manually fix the services after renaming. - = f.submit 'Rename project', class: "btn btn-warning" + %li Job traces and artifacts + %li LFS objects + %li Container registry images + %li CI variables + %li Any encrypted tokens + %p + Once the exported file is ready, you will receive a notification email with a download link. + - if @project.export_project_path + = link_to 'Download export', download_export_project_path(@project), + rel: 'nofollow', download: '', method: :get, class: "btn btn-default" + = link_to 'Generate new export', generate_new_export_project_path(@project), + method: :post, class: "btn btn-default" + - else + = link_to 'Export project', export_project_path(@project), + method: :post, class: "btn btn-default" - - if can?(current_user, :change_namespace, @project) + %section.settings.advanced-settings + .settings-header + %h4 + Advanced settings + %button.btn.js-settings-toggle + = expanded ? 'Collapse' : 'Expand' + %p + Perform advanced options such as housekeeping, exporting, archiveing, renameing, transfering, or removeing your project. + .settings-content.no-animate{ class: ('expanded' if expanded) } .sub-section - %h4.danger-title - Transfer project - = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_project_path(@project), method: :put, remote: true, html: { class: 'js-project-transfer-form' } ) do |f| + %h4 Housekeeping + %p + Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects. + = link_to 'Run housekeeping', housekeeping_project_path(@project), + method: :post, class: "btn btn-default" + - if can? current_user, :archive_project, @project + .sub-section + %h4.warning-title + - if @project.archived? + Unarchive project + - else + Archive project + - if @project.archived? + %p + Unarchiving the project will mark its repository as active. The project can be committed to. + %strong Once active this project shows up in the search and on the dashboard. + = link_to 'Unarchive project', unarchive_project_path(@project), + data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." }, + method: :post, class: "btn btn-success" + - else + %p + Archiving the project will mark its repository as read-only. It is hidden from the dashboard and doesn't show up in searches. + %strong Archived projects cannot be committed to! + = link_to 'Archive project', archive_project_path(@project), + data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." }, + method: :post, class: "btn btn-warning" + .sub-section.rename-respository + %h4.warning-title + Rename repository + %p + Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page. + = render 'projects/errors' + = form_for([@project.namespace.becomes(Namespace), @project]) do |f| + .form-group.project_name_holder + = f.label :name, class: 'label-light' do + Project name + .form-group + = f.text_field :name, class: "form-control" .form-group - = label_tag :new_namespace_id, nil, class: 'label-light' do - %span Select a new namespace + = f.label :path, class: 'label-light' do + %span Path .form-group - = select_tag :new_namespace_id, namespaces_options(nil), include_blank: true, class: 'select2' + .input-group + .input-group-addon + #{URI.join(root_url, @project.namespace.full_path)}/ + = f.text_field :path, class: 'form-control' %ul - %li Be careful. Changing the project's namespace can have unintended side effects. - %li You can only transfer the project to namespaces you manage. + %li Be careful. Renaming a project's repository can have unintended side effects. %li You will need to update your local repositories to point to the new location. - %li Project visibility level will be changed to match namespace rules when transfering to a group. - = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } - - if @project.forked? && can?(current_user, :remove_fork_project, @project) - .sub-section - %h4.danger-title - Remove fork relationship - %p - This will remove the fork relationship to source project - = succeed "." do - = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project) - = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f| + - if @project.deployment_services.any? + %li Your deployment services will be broken, you will need to manually fix the services after renaming. + = f.submit 'Rename project', class: "btn btn-warning" + - if can?(current_user, :change_namespace, @project) + .sub-section + %h4.danger-title + Transfer project + = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_project_path(@project), method: :put, remote: true, html: { class: 'js-project-transfer-form' } ) do |f| + .form-group + = label_tag :new_namespace_id, nil, class: 'label-light' do + %span Select a new namespace + .form-group + = select_tag :new_namespace_id, namespaces_options(nil), include_blank: true, class: 'select2' + %ul + %li Be careful. Changing the project's namespace can have unintended side effects. + %li You can only transfer the project to namespaces you manage. + %li You will need to update your local repositories to point to the new location. + %li Project visibility level will be changed to match namespace rules when transfering to a group. + = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } + - if @project.forked? && can?(current_user, :remove_fork_project, @project) + .sub-section + %h4.danger-title + Remove fork relationship %p - %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source. - = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } - - if can?(current_user, :remove_project, @project) - .sub-section - %h4.danger-title - Remove project - %p - Removing the project will delete its repository and all related resources including issues, merge requests etc. - = form_tag(project_path(@project), method: :delete) do + This will remove the fork relationship to source project + = succeed "." do + = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project) + = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f| + %p + %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source. + = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } + - if can?(current_user, :remove_project, @project) + .sub-section + %h4.danger-title + Remove project %p - %strong Removed projects cannot be restored! - = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) } + Removing the project will delete its repository and all related resources including issues, merge requests etc. + = form_tag(project_path(@project), method: :delete) do + %p + %strong Removed projects cannot be restored! + = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) } .save-project-loader.hide .center @@ -306,4 +306,4 @@ Saving project. %p Please wait a moment, this page will automatically refresh when ready. -= render 'shared/confirm_modal', phrase: @project.path += render 'shared/confirm_modal', phrase: @project.path \ No newline at end of file -- cgit v1.2.1 From f8e5941d372b9b41dc76668932920b2af3fd3c28 Mon Sep 17 00:00:00 2001 From: tauriedavis Date: Tue, 25 Jul 2017 12:38:25 -0700 Subject: trying to fix tests --- app/views/projects/edit.html.haml | 4 ++-- .../projects/settings/merge_requests_settings_spec.rb | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 8b1ea8fb6cb..3a2a544b205 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -157,7 +157,7 @@ %p Customize your merge request restrictions. .settings-content.no-animate{ class: ('expanded' if expanded) } - = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings" }, authenticity_token: true do |f| + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f| = render 'merge_request_settings', form: f = f.submit 'Save changes', class: "btn btn-save" @@ -306,4 +306,4 @@ Saving project. %p Please wait a moment, this page will automatically refresh when ready. -= render 'shared/confirm_modal', phrase: @project.path \ No newline at end of file += render 'shared/confirm_modal', phrase: @project.path diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb index 796e2026905..620f769995f 100644 --- a/spec/features/projects/settings/merge_requests_settings_spec.rb +++ b/spec/features/projects/settings/merge_requests_settings_spec.rb @@ -20,6 +20,9 @@ feature 'Project settings > Merge Requests', :js do expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') select 'Disabled', from: "project_project_feature_attributes_merge_requests_access_level" + within('.sharing-permissions-form') do + click_on('Save changes') + end expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved') @@ -37,6 +40,9 @@ feature 'Project settings > Merge Requests', :js do expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') select 'Everyone with access', from: "project_project_feature_attributes_builds_access_level" + within('.sharing-permissions-form') do + click_on('Save changes') + end expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') @@ -55,6 +61,9 @@ feature 'Project settings > Merge Requests', :js do expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved') select 'Everyone with access', from: "project_project_feature_attributes_merge_requests_access_level" + within('.sharing-permissions-form') do + click_on('Save changes') + end expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') @@ -73,7 +82,9 @@ feature 'Project settings > Merge Requests', :js do scenario 'when unchecked sets :printing_merge_request_link_enabled to false' do uncheck('project_printing_merge_request_link_enabled') - click_on('Save') + within('.merge-request-settings-form') do + click_on('Save changes') + end # Wait for save to complete and page to reload checkbox = find_field('project_printing_merge_request_link_enabled') -- cgit v1.2.1 From 6ec561a2f7384e6de59821fa4bcd5b63f605cb51 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 28 Jul 2017 15:26:25 +0100 Subject: fixed up specs --- app/views/projects/edit.html.haml | 88 +++++++++++----------- features/steps/project/project.rb | 16 +++- features/steps/shared/project.rb | 2 +- spec/features/projects/features_visibility_spec.rb | 20 +++-- 4 files changed, 71 insertions(+), 55 deletions(-) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 3a2a544b205..20fceda26dc 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -15,52 +15,52 @@ Update your project name, description, avatar, and other general settings. .settings-content.no-animate{ class: ('expanded' if expanded) } .project-edit-errors - = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f| - %fieldset - .row - .form-group.col-md-9 - = f.label :name, class: 'label-light', for: 'project_name_edit' do - Project name - = f.text_field :name, class: "form-control", id: "project_name_edit" + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f| + %fieldset + .row + .form-group.col-md-9 + = f.label :name, class: 'label-light', for: 'project_name_edit' do + Project name + = f.text_field :name, class: "form-control", id: "project_name_edit" - .form-group.col-md-3 - = f.label :id, class: 'label-light' do - Project ID - = f.text_field :id, class: 'form-control', readonly: true + .form-group.col-md-3 + = f.label :id, class: 'label-light' do + Project ID + = f.text_field :id, class: 'form-control', readonly: true - .form-group - = f.label :description, class: 'label-light' do - Project description - %span.light (optional) - = f.text_area :description, class: "form-control", rows: 3, maxlength: 250 - - - unless @project.empty_repo? - .form-group - = f.label :default_branch, "Default Branch", class: 'label-light' - = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) .form-group - = f.label :tag_list, "Tags", class: 'label-light' - = f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control" - %p.help-block Separate tags with commas. - %fieldset.features - %h5.prepend-top-0 - Project avatar + = f.label :description, class: 'label-light' do + Project description + %span.light (optional) + = f.text_area :description, class: "form-control", rows: 3, maxlength: 250 + + - unless @project.empty_repo? .form-group - - if @project.avatar? - .avatar-container.s160 - = project_icon(@project.full_path, alt: '', class: 'avatar project-avatar s160') - %p.light - - if @project.avatar_in_git - Project avatar in repository: #{ @project.avatar_in_git } - %a.choose-btn.btn.js-choose-project-avatar-button - Browse file... - %span.file_name.prepend-left-default.js-avatar-filename No file chosen - = f.file_field :avatar, class: "js-project-avatar-input hidden" - .help-block The maximum file size allowed is 200KB. - - if @project.avatar? - %hr - = link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" - = f.submit 'Save changes', class: "btn btn-save" + = f.label :default_branch, "Default Branch", class: 'label-light' + = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) + .form-group + = f.label :tag_list, "Tags", class: 'label-light' + = f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control" + %p.help-block Separate tags with commas. + %fieldset.features + %h5.prepend-top-0 + Project avatar + .form-group + - if @project.avatar? + .avatar-container.s160 + = project_icon(@project.full_path, alt: '', class: 'avatar project-avatar s160') + %p.light + - if @project.avatar_in_git + Project avatar in repository: #{ @project.avatar_in_git } + %a.choose-btn.btn.js-choose-project-avatar-button + Browse file... + %span.file_name.prepend-left-default.js-avatar-filename No file chosen + = f.file_field :avatar, class: "js-project-avatar-input hidden" + .help-block The maximum file size allowed is 200KB. + - if @project.avatar? + %hr + = link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" + = f.submit 'Save changes', class: "btn btn-save" %section.settings.sharing-permissions .settings-header @@ -70,7 +70,7 @@ = expanded ? 'Collapse' : 'Expand' %p Enable or disable certain project features and choose access levels. - .settings-content.no-animate{ class: ('expaneded' if expanded) } + .settings-content.no-animate{ class: ('expanded' if expanded) } = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| .form_group.sharing-and-permissions .row.js-visibility-select @@ -148,7 +148,7 @@ = f.submit 'Save changes', class: "btn btn-save" - %section.settings.merge-request-settings{ style: ("display: none;" if @project.project_feature.send(:merge_requests_access_level) == 0) } + %section.settings.merge-requests-feature{ style: ("display: none;" if @project.project_feature.send(:merge_requests_access_level) == 0) } .settings-header %h4 Merge request settings diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index 170e2f16c80..0a89c1baf20 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -10,7 +10,9 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I save project' do - click_button 'Save changes' + page.within '.general-settings' do + click_button 'Save changes' + end end step 'I should see project with new settings' do @@ -31,7 +33,9 @@ class Spinach::Features::Project < Spinach::FeatureSteps :project_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') ) - click_button 'Save changes' + page.within '.general-settings' do + click_button 'Save changes' + end @project.reload end @@ -50,7 +54,9 @@ class Spinach::Features::Project < Spinach::FeatureSteps :project_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') ) - click_button 'Save changes' + page.within '.general-settings' do + click_button 'Save changes' + end @project.reload end @@ -69,7 +75,9 @@ class Spinach::Features::Project < Spinach::FeatureSteps step 'change project default branch' do select 'fix', from: 'project_default_branch' - click_button 'Save changes' + page.within '.general-settings' do + click_button 'Save changes' + end end step 'I should see project default branch changed' do diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index da1cdd9f897..c9a63723212 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -103,7 +103,7 @@ module SharedProject step 'I should see project settings' do expect(current_path).to eq edit_project_path(@project) expect(page).to have_content("Project name") - expect(page).to have_content("Sharing & Permissions") + expect(page).to have_content("Sharing and permissions") end def current_project diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb index bc58a8695d8..4f2dd410b82 100644 --- a/spec/features/projects/features_visibility_spec.rb +++ b/spec/features/projects/features_visibility_spec.rb @@ -39,8 +39,6 @@ describe 'Edit Project Settings' do end wait_for_requests expect(page).to have_selector(".shortcuts-#{shortcut_name}") - - sleep 0.1 end end end @@ -180,7 +178,11 @@ describe 'Edit Project Settings' do it "disables repository related features" do select "Disabled", from: "project_project_feature_attributes_repository_access_level" - expect(find(".edit-project")).to have_selector("select.disabled", count: 2) + page.within('.sharing-permissions') do + click_button "Save changes" + end + + expect(find(".sharing-permissions")).to have_selector("select.disabled", count: 2) end it "shows empty features project homepage" do @@ -188,7 +190,9 @@ describe 'Edit Project Settings' do select "Disabled", from: "project_project_feature_attributes_issues_access_level" select "Disabled", from: "project_project_feature_attributes_wiki_access_level" - click_button "Save changes" + page.within('.sharing-permissions') do + click_button "Save changes" + end wait_for_requests visit project_path(project) @@ -201,7 +205,9 @@ describe 'Edit Project Settings' do select "Disabled", from: "project_project_feature_attributes_issues_access_level" select "Disabled", from: "project_project_feature_attributes_wiki_access_level" - click_button "Save changes" + page.within('.sharing-permissions') do + click_button "Save changes" + end wait_for_requests visit activity_project_path(project) @@ -242,7 +248,9 @@ describe 'Edit Project Settings' do end def save_changes_and_check_activity_tab - click_button "Save changes" + page.within('.sharing-permissions') do + click_button "Save changes" + end wait_for_requests visit activity_project_path(project) -- cgit v1.2.1 From a2ac5720ef982b9ed1012e9b64ab36679a303c52 Mon Sep 17 00:00:00 2001 From: AJ Jordan Date: Fri, 28 Jul 2017 21:19:05 +0000 Subject: Move API "basic usage" to be more visible --- doc/api/README.md | 62 +++++++++++++++++++++++++++---------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/doc/api/README.md b/doc/api/README.md index fe29563eaca..b16f31306a8 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -75,6 +75,37 @@ controller-specific endpoints. GraphQL has a number of benefits: It will co-exist with the current v4 REST API. If we have a v5 API, this should be a compatibility layer on top of GraphQL. +## Basic usage + +API requests should be prefixed with `api` and the API version. The API version +is defined in [`lib/api.rb`][lib-api-url]. + +For endpoints that require [authentication](#authentication), you need to pass +a `private_token` parameter via query string or header. If passed as a header, +the header name must be `PRIVATE-TOKEN` (uppercase and with a dash instead of +an underscore). + +Example of a valid API request: + +``` +GET /projects?private_token=9koXpg98eAheJpvBs5tK +``` + +Example of a valid API request using cURL and authentication via header: + +```shell +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects" +``` + +Example of a valid API request using cURL and authentication via a query string: + +```shell +curl "https://gitlab.example.com/api/v4/projects?private_token=9koXpg98eAheJpvBs5tK" +``` + +The API uses JSON to serialize data. You don't need to specify `.json` at the +end of an API URL. + ## Authentication Most API requests require authentication via a session cookie or token. For @@ -205,37 +236,6 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23 curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https://gitlab.example.com/api/v4/projects" ``` -## Basic usage - -API requests should be prefixed with `api` and the API version. The API version -is defined in [`lib/api.rb`][lib-api-url]. - -For endpoints that require [authentication](#authentication), you need to pass -a `private_token` parameter via query string or header. If passed as a header, -the header name must be `PRIVATE-TOKEN` (uppercase and with a dash instead of -an underscore). - -Example of a valid API request: - -``` -GET /projects?private_token=9koXpg98eAheJpvBs5tK -``` - -Example of a valid API request using cURL and authentication via header: - -```shell -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects" -``` - -Example of a valid API request using cURL and authentication via a query string: - -```shell -curl "https://gitlab.example.com/api/v4/projects?private_token=9koXpg98eAheJpvBs5tK" -``` - -The API uses JSON to serialize data. You don't need to specify `.json` at the -end of an API URL. - ## Status codes The API is designed to return different status codes according to context and -- cgit v1.2.1 From 97f58c78311c453a40498e74114817ffc6f79476 Mon Sep 17 00:00:00 2001 From: AJ Jordan Date: Fri, 28 Jul 2017 14:30:42 -0700 Subject: Explicitly spell out where the API root is I found this basic information extrememly hard to find when I looked at this page. --- doc/api/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/api/README.md b/doc/api/README.md index b16f31306a8..cb4ec88e868 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -78,7 +78,8 @@ be a compatibility layer on top of GraphQL. ## Basic usage API requests should be prefixed with `api` and the API version. The API version -is defined in [`lib/api.rb`][lib-api-url]. +is defined in [`lib/api.rb`][lib-api-url]. For example, the root of the v4 API +is at `/api/v4`. For endpoints that require [authentication](#authentication), you need to pass a `private_token` parameter via query string or header. If passed as a header, -- cgit v1.2.1 From 037b37297572eadab8b50917892eb285efc4383b Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 27 Jul 2017 17:49:49 -0500 Subject: ensure user profile calendar is generated based on server timezone, not local timezone --- app/assets/javascripts/users/activity_calendar.js | 12 ++++++++++-- app/assets/javascripts/users/user_tabs.js | 3 ++- app/views/users/show.html.haml | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/users/activity_calendar.js b/app/assets/javascripts/users/activity_calendar.js index f091e319f44..97b05fbd39e 100644 --- a/app/assets/javascripts/users/activity_calendar.js +++ b/app/assets/javascripts/users/activity_calendar.js @@ -6,6 +6,14 @@ const LOADING_HTML = ` `; +function getSystemDate(systemUtcOffsetSeconds) { + const date = new Date(); + const localUtcOffsetMinutes = 0 - date.getTimezoneOffset(); + const systemUtcOffsetMinutes = systemUtcOffsetSeconds / 60; + date.setMinutes((date.getMinutes() - localUtcOffsetMinutes) + systemUtcOffsetMinutes); + return date; +} + function formatTooltipText({ date, count }) { const dateObject = new Date(date); const dateDayName = gl.utils.getDayName(dateObject); @@ -21,7 +29,7 @@ function formatTooltipText({ date, count }) { const initColorKey = () => d3.scale.linear().range(['#acd5f2', '#254e77']).domain([0, 3]); export default class ActivityCalendar { - constructor(container, timestamps, calendarActivitiesPath) { + constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0) { this.calendarActivitiesPath = calendarActivitiesPath; this.clickDay = this.clickDay.bind(this); this.currentSelectedDate = ''; @@ -36,7 +44,7 @@ export default class ActivityCalendar { this.timestampsTmp = []; let group = 0; - const today = new Date(); + const today = getSystemDate(utcOffset); today.setHours(0, 0, 0, 0, 0); const oneYearAgo = new Date(today); diff --git a/app/assets/javascripts/users/user_tabs.js b/app/assets/javascripts/users/user_tabs.js index 5fe6603ce7b..1f717f08c88 100644 --- a/app/assets/javascripts/users/user_tabs.js +++ b/app/assets/javascripts/users/user_tabs.js @@ -150,6 +150,7 @@ export default class UserTabs { const $calendarWrap = this.$parentEl.find('.user-calendar'); const calendarPath = $calendarWrap.data('calendarPath'); const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath'); + const utcOffset = $calendarWrap.data('utcOffset'); $.ajax({ dataType: 'json', @@ -158,7 +159,7 @@ export default class UserTabs { $calendarWrap.html(CALENDAR_TEMPLATE); // eslint-disable-next-line no-new - new ActivityCalendar('.js-contrib-calendar', activityData, calendarActivitiesPath); + new ActivityCalendar('.js-contrib-calendar', activityData, calendarActivitiesPath, utcOffset); }, }); diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index a449706c567..879e0f99b14 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -104,7 +104,7 @@ .tab-content #activity.tab-pane .row-content-block.calender-block.white.second-block.hidden-xs - .user-calendar{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path } } + .user-calendar{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } } %h4.center.light %i.fa.fa-spinner.fa-spin .user-calendar-activities -- cgit v1.2.1 From 68de3ac129c59dc168bdef84b47954d684d5f074 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 27 Jul 2017 18:21:56 -0500 Subject: display system timezone underneath activity calendar --- app/assets/javascripts/users/user_tabs.js | 5 +++++ app/assets/stylesheets/framework/calendar.scss | 1 + 2 files changed, 6 insertions(+) diff --git a/app/assets/javascripts/users/user_tabs.js b/app/assets/javascripts/users/user_tabs.js index 1f717f08c88..1215b265e28 100644 --- a/app/assets/javascripts/users/user_tabs.js +++ b/app/assets/javascripts/users/user_tabs.js @@ -151,12 +151,17 @@ export default class UserTabs { const calendarPath = $calendarWrap.data('calendarPath'); const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath'); const utcOffset = $calendarWrap.data('utcOffset'); + let utcFormatted = 'UTC'; + if (utcOffset !== 0) { + utcFormatted = `UTC${utcOffset > 0 ? '+' : ''}${(utcOffset / 3600)}`; + } $.ajax({ dataType: 'json', url: calendarPath, success: (activityData) => { $calendarWrap.html(CALENDAR_TEMPLATE); + $calendarWrap.find('.calendar-hint').append(`(Timezone: ${utcFormatted})`); // eslint-disable-next-line no-new new ActivityCalendar('.js-contrib-calendar', activityData, calendarActivitiesPath, utcOffset); diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index 0ac095f7d8f..0ded4a3b423 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -45,6 +45,7 @@ margin-top: -23px; float: right; font-size: 12px; + direction: ltr; } .pika-single.gitlab-theme { -- cgit v1.2.1 From be77d76e7338ee40ecdad54cc40b3b5afa55f2b1 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Sat, 29 Jul 2017 00:36:54 -0500 Subject: use timezone-aware Date.current instead of Date.today in ContributionsCalendar class --- lib/gitlab/contributions_calendar.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index bf557103cfd..b3d84e4b6d5 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -48,7 +48,7 @@ module Gitlab end def starting_month - Date.today.month + Date.current.month end private @@ -66,7 +66,7 @@ module Gitlab .select(:id) conditions = t[:created_at].gteq(date_from.beginning_of_day) - .and(t[:created_at].lteq(Date.today.end_of_day)) + .and(t[:created_at].lteq(Date.current.end_of_day)) .and(t[:author_id].eq(contributor.id)) Event.reorder(nil) -- cgit v1.2.1 From d1f5e81e8b80d8c322bb47c70728a2536d4708cc Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Sat, 29 Jul 2017 01:50:17 -0500 Subject: adjust timezone for date grouping in contributions calendar --- lib/gitlab/contributions_calendar.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index b3d84e4b6d5..c1f84d07477 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -69,9 +69,11 @@ module Gitlab .and(t[:created_at].lteq(Date.current.end_of_day)) .and(t[:author_id].eq(contributor.id)) + timezone_adjust = "INTERVAL '#{Time.zone.now.utc_offset} SECONDS'" + Event.reorder(nil) - .select(t[:project_id], t[:target_type], t[:action], 'date(created_at) AS date', 'count(id) as total_amount') - .group(t[:project_id], t[:target_type], t[:action], 'date(created_at)') + .select(t[:project_id], t[:target_type], t[:action], "date(created_at + #{timezone_adjust}) AS date", 'count(id) as total_amount') + .group(t[:project_id], t[:target_type], t[:action], "date(created_at + #{timezone_adjust})") .where(conditions) .having(t[:project_id].in(Arel::Nodes::SqlLiteral.new(authed_projects.to_sql))) end -- cgit v1.2.1 From cf2d2cd323ad07783a4953144a7abedf37927633 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 31 Jul 2017 16:45:40 -0500 Subject: add tests for proper timezone date grouping within ContributionsCalendar --- spec/lib/gitlab/contributions_calendar_spec.rb | 38 +++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb index 79632e2b6a3..5f2fef42910 100644 --- a/spec/lib/gitlab/contributions_calendar_spec.rb +++ b/spec/lib/gitlab/contributions_calendar_spec.rb @@ -22,12 +22,14 @@ describe Gitlab::ContributionsCalendar do end end - let(:today) { Time.now.to_date } + let(:today) { Time.now.utc.to_date } + let(:yesterday) { today - 1.day } + let(:tomorrow) { today + 1.day } let(:last_week) { today - 7.days } let(:last_year) { today - 1.year } before do - travel_to today + travel_to Time.now.utc.end_of_day end after do @@ -38,7 +40,7 @@ describe Gitlab::ContributionsCalendar do described_class.new(contributor, current_user) end - def create_event(project, day) + def create_event(project, day, hour = 0) @targets ||= {} @targets[project] ||= create(:issue, project: project, author: contributor) @@ -47,7 +49,7 @@ describe Gitlab::ContributionsCalendar do action: Event::CREATED, target: @targets[project], author: contributor, - created_at: day + created_at: DateTime.new(day.year, day.month, day.day, hour) ) end @@ -68,6 +70,34 @@ describe Gitlab::ContributionsCalendar do expect(calendar(user).activity_dates[today]).to eq(0) expect(calendar(contributor).activity_dates[today]).to eq(2) end + + context "when events fall under different dates depending on the time zone" do + before do + create_event(public_project, today, 1) + create_event(public_project, today, 4) + create_event(public_project, today, 10) + create_event(public_project, today, 16) + create_event(public_project, today, 23) + end + + it "renders correct event counts within the UTC timezone" do + Time.use_zone('UTC') do + expect(calendar.activity_dates).to eq(today => 5) + end + end + + it "renders correct event counts within the Sydney timezone" do + Time.use_zone('Sydney') do + expect(calendar.activity_dates).to eq(today => 3, tomorrow => 2) + end + end + + it "renders correct event counts within the US Central timezone" do + Time.use_zone('Central Time (US & Canada)') do + expect(calendar.activity_dates).to eq(yesterday => 2, today => 3) + end + end + end end describe '#events_by_date' do -- cgit v1.2.1 From 1b9b6974fdfb49f10dfdd4bddde05aa8399ff9b1 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 31 Jul 2017 17:09:41 -0500 Subject: fix mysql syntax for date INTERVAL arithmatic --- lib/gitlab/contributions_calendar.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index c1f84d07477..aa261929fa9 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -69,11 +69,15 @@ module Gitlab .and(t[:created_at].lteq(Date.current.end_of_day)) .and(t[:author_id].eq(contributor.id)) - timezone_adjust = "INTERVAL '#{Time.zone.now.utc_offset} SECONDS'" + date_interval = if Gitlab::Database.postgresql? + "INTERVAL '#{Time.zone.now.utc_offset} seconds'" + else + "INTERVAL #{Time.zone.now.utc_offset} SECOND" + end Event.reorder(nil) - .select(t[:project_id], t[:target_type], t[:action], "date(created_at + #{timezone_adjust}) AS date", 'count(id) as total_amount') - .group(t[:project_id], t[:target_type], t[:action], "date(created_at + #{timezone_adjust})") + .select(t[:project_id], t[:target_type], t[:action], "date(created_at + #{date_interval}) AS date", 'count(id) as total_amount') + .group(t[:project_id], t[:target_type], t[:action], "date(created_at + #{date_interval})") .where(conditions) .having(t[:project_id].in(Arel::Nodes::SqlLiteral.new(authed_projects.to_sql))) end -- cgit v1.2.1 From 08c513b6d95c34d96bcc779f4c10318fafda2cb7 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 31 Jul 2017 18:19:18 -0500 Subject: =?UTF-8?q?fix=20rubocop=20violations=20=F0=9F=91=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/gitlab/contributions_calendar.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index aa261929fa9..0735243e021 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -70,10 +70,10 @@ module Gitlab .and(t[:author_id].eq(contributor.id)) date_interval = if Gitlab::Database.postgresql? - "INTERVAL '#{Time.zone.now.utc_offset} seconds'" - else - "INTERVAL #{Time.zone.now.utc_offset} SECOND" - end + "INTERVAL '#{Time.zone.now.utc_offset} seconds'" + else + "INTERVAL #{Time.zone.now.utc_offset} SECOND" + end Event.reorder(nil) .select(t[:project_id], t[:target_type], t[:action], "date(created_at + #{date_interval}) AS date", 'count(id) as total_amount') -- cgit v1.2.1 From 00e38789fef816eb787bf68a3db427f0f325c0e0 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 31 Jul 2017 23:14:32 -0500 Subject: adjust user contribution calendar time formatting --- app/views/users/calendar_activities.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml index 805a346a85e..6b1d75c6e72 100644 --- a/app/views/users/calendar_activities.html.haml +++ b/app/views/users/calendar_activities.html.haml @@ -1,6 +1,6 @@ %h4.prepend-top-20 Contributions for - %strong= @calendar_date.to_s(:short) + %strong= @calendar_date.to_s(:medium) - if @events.any? %ul.bordered-list @@ -8,7 +8,7 @@ %li %span.light %i.fa.fa-clock-o - = event.created_at.to_s(:time) + = event.created_at.strftime('%-I:%M%P') - if event.push? #{event.action_name} #{event.ref_type} %strong @@ -30,4 +30,4 @@ = event.project_name - else %p - No contributions found for #{@calendar_date.to_s(:short)} + No contributions found for #{@calendar_date.to_s(:medium)} -- cgit v1.2.1 From 013fe7643a531d2d705c364940155983a17d95c8 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 31 Jul 2017 23:24:05 -0500 Subject: add CHANGELOG.md for !13208 --- .../unreleased/27616-fix-contributions-graph-utc-offset-mysql.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/27616-fix-contributions-graph-utc-offset-mysql.yml diff --git a/changelogs/unreleased/27616-fix-contributions-graph-utc-offset-mysql.yml b/changelogs/unreleased/27616-fix-contributions-graph-utc-offset-mysql.yml new file mode 100644 index 00000000000..1b3c3b8538d --- /dev/null +++ b/changelogs/unreleased/27616-fix-contributions-graph-utc-offset-mysql.yml @@ -0,0 +1,4 @@ +--- +title: Fix timezone inconsistencies in user contribution graph +merge_request: 13208 +author: -- cgit v1.2.1 From 15643ff91e3a01d6f090c0566abe4ded20ad1eb4 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Tue, 1 Aug 2017 13:20:22 -0700 Subject: implement #inspect for all Referables --- app/models/concerns/referable.rb | 12 ++++++++++++ app/models/user.rb | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb index da803c7f481..10f4be72016 100644 --- a/app/models/concerns/referable.rb +++ b/app/models/concerns/referable.rb @@ -25,6 +25,18 @@ module Referable to_reference(from_project) end + def referable_inspect + if respond_to?(:id) + "#<#{self.class.name} id:#{id} #{to_reference(full: true)}>" + else + "#<#{self.class.name} #{to_reference(full: true)}>" + end + end + + def inspect + referable_inspect + end + module ClassMethods # The character that prefixes the actual reference identifier # diff --git a/app/models/user.rb b/app/models/user.rb index 6e66c587a1f..267eebb42ff 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -47,6 +47,11 @@ class User < ActiveRecord::Base devise :lockable, :recoverable, :rememberable, :trackable, :validatable, :omniauthable, :confirmable, :registerable + # devise overrides #inspect, so we manually use the Referable one + def inspect + referable_inspect + end + # Override Devise::Models::Trackable#update_tracked_fields! # to limit database writes to at most once every hour def update_tracked_fields!(request) -- cgit v1.2.1 From 215e0911e553eb4aa3fe81734006c9b3ba8eb6ba Mon Sep 17 00:00:00 2001 From: Ahmad Sherif Date: Thu, 27 Jul 2017 08:45:04 +0200 Subject: Migrate Gitlab::Git::Repository#count_commits to Gitaly Closes gitaly#415 --- Gemfile | 2 +- Gemfile.lock | 4 +-- lib/gitlab/git/repository.rb | 33 +++++++++++++++------- lib/gitlab/gitaly_client/commit_service.rb | 5 +++- spec/lib/gitlab/git/repository_spec.rb | 44 ++++++++++++++++++------------ 5 files changed, 57 insertions(+), 31 deletions(-) diff --git a/Gemfile b/Gemfile index a9a1cbc144d..403b104a9d6 100644 --- a/Gemfile +++ b/Gemfile @@ -391,7 +391,7 @@ gem 'vmstat', '~> 2.3.0' gem 'sys-filesystem', '~> 1.1.6' # Gitaly GRPC client -gem 'gitaly', '~> 0.21.0' +gem 'gitaly', '~> 0.23.0' gem 'toml-rb', '~> 0.3.15', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 5a327a14c4a..9f90965a567 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -269,7 +269,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly (0.21.0) + gitaly (0.23.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -978,7 +978,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly (~> 0.21.0) + gitaly (~> 0.23.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.5.1) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 88529ba2c47..d79589dfca5 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -300,17 +300,14 @@ module Gitlab raw_log(options).map { |c| Commit.decorate(c) } end - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/382 def count_commits(options) - cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} rev-list] - cmd << "--after=#{options[:after].iso8601}" if options[:after] - cmd << "--before=#{options[:before].iso8601}" if options[:before] - cmd += %W[--count #{options[:ref]}] - cmd += %W[-- #{options[:path]}] if options[:path].present? - - raw_output = IO.popen(cmd) { |io| io.read } - - raw_output.to_i + gitaly_migrate(:count_commits) do |is_enabled| + if is_enabled + count_commits_by_gitaly(options) + else + count_commits_by_shelling_out(options) + end + end end def sha_from_ref(ref) @@ -998,6 +995,22 @@ module Gitlab gitaly_ref_client.tags end + def count_commits_by_gitaly(options) + gitaly_commit_client.commit_count(options[:ref], options) + end + + def count_commits_by_shelling_out(options) + cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} rev-list] + cmd << "--after=#{options[:after].iso8601}" if options[:after] + cmd << "--before=#{options[:before].iso8601}" if options[:before] + cmd += %W[--count #{options[:ref]}] + cmd += %W[-- #{options[:path]}] if options[:path].present? + + raw_output = IO.popen(cmd) { |io| io.read } + + raw_output.to_i + end + def gitaly_migrate(method, &block) Gitlab::GitalyClient.migrate(method, &block) rescue GRPC::NotFound => e diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index a834781b1f1..b1424a458e9 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -85,11 +85,14 @@ module Gitlab end end - def commit_count(ref) + def commit_count(ref, options = {}) request = Gitaly::CountCommitsRequest.new( repository: @gitaly_repo, revision: ref ) + request.after = Google::Protobuf::Timestamp.new(seconds: options[:after].to_i) if options[:after].present? + request.before = Google::Protobuf::Timestamp.new(seconds: options[:before].to_i) if options[:before].present? + request.path = options[:path] if options[:path].present? GitalyClient.call(@repository.storage, :commit_service, :count_commits, request).count end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 8e4a1f31ced..9bfad0c9bdf 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -361,20 +361,20 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#commit_count' do - shared_examples 'counting commits' do + shared_examples 'simple commit counting' do it { expect(repository.commit_count("master")).to eq(25) } it { expect(repository.commit_count("feature")).to eq(9) } end context 'when Gitaly commit_count feature is enabled' do - it_behaves_like 'counting commits' + it_behaves_like 'simple commit counting' it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::CommitService, :commit_count do subject { repository.commit_count('master') } end end context 'when Gitaly commit_count feature is disabled', skip_gitaly_mock: true do - it_behaves_like 'counting commits' + it_behaves_like 'simple commit counting' end end @@ -797,29 +797,39 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#count_commits' do - context 'with after timestamp' do - it 'returns the number of commits after timestamp' do - options = { ref: 'master', limit: nil, after: Time.iso8601('2013-03-03T20:15:01+00:00') } + shared_examples 'extended commit counting' do + context 'with after timestamp' do + it 'returns the number of commits after timestamp' do + options = { ref: 'master', limit: nil, after: Time.iso8601('2013-03-03T20:15:01+00:00') } - expect(repository.count_commits(options)).to eq(25) + expect(repository.count_commits(options)).to eq(25) + end end - end - context 'with before timestamp' do - it 'returns the number of commits after timestamp' do - options = { ref: 'feature', limit: nil, before: Time.iso8601('2015-03-03T20:15:01+00:00') } + context 'with before timestamp' do + it 'returns the number of commits before timestamp' do + options = { ref: 'feature', limit: nil, before: Time.iso8601('2015-03-03T20:15:01+00:00') } - expect(repository.count_commits(options)).to eq(9) + expect(repository.count_commits(options)).to eq(9) + end end - end - context 'with path' do - it 'returns the number of commits with path ' do - options = { ref: 'master', limit: nil, path: "encoding" } + context 'with path' do + it 'returns the number of commits with path ' do + options = { ref: 'master', limit: nil, path: "encoding" } - expect(repository.count_commits(options)).to eq(2) + expect(repository.count_commits(options)).to eq(2) + end end end + + context 'when Gitaly count_commits feature is enabled' do + it_behaves_like 'extended commit counting' + end + + context 'when Gitaly count_commits feature is disabled', skip_gitaly_mock: true do + it_behaves_like 'extended commit counting' + end end describe "branch_names_contains" do -- cgit v1.2.1 From 1cdc76f4559ff6d1ee0a1e6f277923094bff6f6c Mon Sep 17 00:00:00 2001 From: Maxime Visonneau Date: Tue, 23 May 2017 23:45:01 +0200 Subject: Implemented star auth capabilities on docker registry to enable deletion of images --- ...ntainer_registry_authentication_service_spec.rb | 34 ++++++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 7f704629bfa..a19ac911315 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -163,7 +163,9 @@ describe Auth::ContainerRegistryAuthenticationService do end context 'disallow reporter to delete images' do - before { project.team << [current_user, :reporter] } + before do + project.add_reporter(current_user) + end let(:current_params) do { scope: "repository:#{project.path_with_namespace}:*" } @@ -230,6 +232,14 @@ describe Auth::ContainerRegistryAuthenticationService do it_behaves_like 'not a container repository factory' end + context 'disallow anyone to delete images' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:*" } + end + + it_behaves_like 'an inaccessible' + end + context 'when repository name is invalid' do let(:current_params) do { scope: 'repository:invalid:push' } @@ -280,13 +290,25 @@ describe Auth::ContainerRegistryAuthenticationService do end context 'for external user' do - let(:current_user) { create(:user, external: true) } - let(:current_params) do - { scope: "repository:#{project.full_path}:pull,push,*" } + context 'disallow anyone to pull or push images' do + let(:current_user) { create(:user, external: true) } + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull,push" } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' end - it_behaves_like 'an inaccessible' - it_behaves_like 'not a container repository factory' + context 'disallow anyone to delete images' do + let(:current_user) { create(:user, external: true) } + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:*" } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end end end end -- cgit v1.2.1 From b3e058996c70aeae6f00cad7195bce421e02b39b Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 2 Aug 2017 17:27:21 +0800 Subject: Cleanup tests and add admin_container_image to full_authentication_abilities. This is fine because we're going to check with can?(..) anyway --- lib/gitlab/auth.rb | 3 +- spec/lib/gitlab/auth_spec.rb | 3 +- ...ntainer_registry_authentication_service_spec.rb | 54 ++++++++++++---------- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 9bed81e7327..7d3aa532750 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -218,7 +218,8 @@ module Gitlab def full_authentication_abilities read_authentication_abilities + [ :push_code, - :create_container_image + :create_container_image, + :admin_container_image ] end alias_method :api_scope_authentication_abilities, :full_authentication_abilities diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index a9db0d5164d..20bca4306fb 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -313,7 +313,8 @@ describe Gitlab::Auth do def full_authentication_abilities read_authentication_abilities + [ :push_code, - :create_container_image + :create_container_image, + :admin_container_image ] end end diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index a19ac911315..7ae7f4688a2 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -8,7 +8,7 @@ describe Auth::ContainerRegistryAuthenticationService do let(:payload) { JWT.decode(subject[:token], rsa_key).first } let(:authentication_abilities) do - [:read_container_image, :create_container_image] + [:read_container_image, :create_container_image, :admin_container_image] end subject do @@ -60,7 +60,7 @@ describe Auth::ContainerRegistryAuthenticationService do end shared_examples 'a deletable' do - it_behaves_like 'a accessible' do + it_behaves_like 'an accessible' do let(:actions) { ['*'] } end end @@ -126,7 +126,7 @@ describe Auth::ContainerRegistryAuthenticationService do context 'allow developer to push images' do before do - project.team << [current_user, :developer] + project.add_developer(current_user) end let(:current_params) do @@ -138,18 +138,21 @@ describe Auth::ContainerRegistryAuthenticationService do end context 'disallow developer to delete images' do - before { project.team << [current_user, :developer] } + before do + project.add_developer(current_user) + end let(:current_params) do { scope: "repository:#{project.path_with_namespace}:*" } end it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' end context 'allow reporter to pull images' do before do - project.team << [current_user, :reporter] + project.add_reporter(current_user) end context 'when pulling from root level repository' do @@ -172,11 +175,12 @@ describe Auth::ContainerRegistryAuthenticationService do end it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' end context 'return a least of privileges' do before do - project.team << [current_user, :reporter] + project.add_reporter(current_user) end let(:current_params) do @@ -189,7 +193,7 @@ describe Auth::ContainerRegistryAuthenticationService do context 'disallow guest to pull or push images' do before do - project.team << [current_user, :guest] + project.add_guest(current_user) end let(:current_params) do @@ -201,13 +205,16 @@ describe Auth::ContainerRegistryAuthenticationService do end context 'disallow guest to delete images' do - before { project.team << [current_user, :guest] } + before do + project.add_guest(current_user) + end let(:current_params) do { scope: "repository:#{project.path_with_namespace}:*" } end it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' end end @@ -238,6 +245,7 @@ describe Auth::ContainerRegistryAuthenticationService do end it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' end context 'when repository name is invalid' do @@ -248,14 +256,6 @@ describe Auth::ContainerRegistryAuthenticationService do it_behaves_like 'an inaccessible' it_behaves_like 'not a container repository factory' end - - context 'disallow anyone to delete images' do - let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:*" } - end - - it_behaves_like 'an inaccessible' - end end context 'for internal project' do @@ -286,6 +286,7 @@ describe Auth::ContainerRegistryAuthenticationService do end it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' end end @@ -313,13 +314,16 @@ describe Auth::ContainerRegistryAuthenticationService do end end - context 'delete authorized as admin' do + context 'delete authorized as master' do let(:current_project) { create(:empty_project) } - let(:current_user) { create(:admin) } + let(:current_user) { create(:user) } + let(:authentication_abilities) do - [ - :admin_container_image - ] + [:admin_container_image] + end + + before do + current_project.add_master(current_user) end it_behaves_like 'a valid token' @@ -344,7 +348,7 @@ describe Auth::ContainerRegistryAuthenticationService do end before do - current_project.team << [current_user, :developer] + current_project.add_developer(current_user) end it_behaves_like 'a valid token' @@ -394,7 +398,7 @@ describe Auth::ContainerRegistryAuthenticationService do context 'when you are member' do before do - project.team << [current_user, :developer] + project.add_developer(current_user) end it_behaves_like 'a pullable' @@ -424,7 +428,7 @@ describe Auth::ContainerRegistryAuthenticationService do context 'when you are member' do before do - project.team << [current_user, :developer] + project.add_developer(current_user) end it_behaves_like 'a pullable' @@ -451,7 +455,7 @@ describe Auth::ContainerRegistryAuthenticationService do let(:project) { create(:empty_project, :public) } before do - project.team << [current_user, :developer] + project.add_developer(current_user) end it_behaves_like 'an inaccessible' -- cgit v1.2.1 From f097e4dbcd26a0d122776b72ca4370890a0a3f76 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 2 Aug 2017 23:13:27 +0800 Subject: Don't send rejection mails for all auto-generated mails Also make it easier to have mailer helper --- app/workers/email_receiver_worker.rb | 2 -- .../28472-ignore-auto-generated-mails.yml | 4 ++++ lib/gitlab/email/handler/create_note_handler.rb | 1 - lib/gitlab/email/receiver.rb | 4 ++++ .../email/handler/create_note_handler_spec.rb | 9 -------- spec/lib/gitlab/email/receiver_spec.rb | 16 ++++++------- spec/spec_helper.rb | 6 ++++- spec/workers/email_receiver_worker_spec.rb | 26 +++++++++++++++++----- 8 files changed, 42 insertions(+), 26 deletions(-) create mode 100644 changelogs/unreleased/28472-ignore-auto-generated-mails.yml diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb index d3f7e479a8d..1afa24c8e2a 100644 --- a/app/workers/email_receiver_worker.rb +++ b/app/workers/email_receiver_worker.rb @@ -31,8 +31,6 @@ class EmailReceiverWorker when Gitlab::Email::EmptyEmailError can_retry = true "It appears that the email is blank. Make sure your reply is at the top of the email, we can't process inline replies." - when Gitlab::Email::AutoGeneratedEmailError - "The email was marked as 'auto generated', which we can't accept. Please create your comment through the web interface." when Gitlab::Email::UserNotFoundError "We couldn't figure out what user corresponds to the email. Please create your comment through the web interface." when Gitlab::Email::UserBlockedError diff --git a/changelogs/unreleased/28472-ignore-auto-generated-mails.yml b/changelogs/unreleased/28472-ignore-auto-generated-mails.yml new file mode 100644 index 00000000000..af63b43e62e --- /dev/null +++ b/changelogs/unreleased/28472-ignore-auto-generated-mails.yml @@ -0,0 +1,4 @@ +--- +title: Don't send rejection mails for all auto-generated mails +merge_request: 13254 +author: diff --git a/lib/gitlab/email/handler/create_note_handler.rb b/lib/gitlab/email/handler/create_note_handler.rb index 31579e94a87..8eea33b9ab5 100644 --- a/lib/gitlab/email/handler/create_note_handler.rb +++ b/lib/gitlab/email/handler/create_note_handler.rb @@ -15,7 +15,6 @@ module Gitlab def execute raise SentNotificationNotFoundError unless sent_notification - raise AutoGeneratedEmailError if mail.header.to_s =~ /auto-(generated|replied)/ validate_permission!(:create_note) diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index 0d6b08b5d29..b3c2a8fad3e 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -26,6 +26,10 @@ module Gitlab raise EmptyEmailError if @raw.blank? mail = build_mail + + raise AutoGeneratedEmailError if + mail.header.to_s =~ /auto-(generated|replied)/ + mail_key = extract_mail_key(mail) handler = Handler.for(mail, mail_key) diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb index 0127b012c91..d0fa16ce4d1 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -36,15 +36,6 @@ describe Gitlab::Email::Handler::CreateNoteHandler do end end - context "when the email was auto generated" do - let!(:mail_key) { '636ca428858779856c226bb145ef4fad' } - let!(:email_raw) { fixture_file("emails/auto_reply.eml") } - - it "raises an AutoGeneratedEmailError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::AutoGeneratedEmailError) - end - end - context "when the noteable could not be found" do before do noteable.destroy diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb index 88565ea5311..59f43abf26d 100644 --- a/spec/lib/gitlab/email/receiver_spec.rb +++ b/spec/lib/gitlab/email/receiver_spec.rb @@ -28,14 +28,6 @@ describe Gitlab::Email::Receiver do it "raises an UnknownIncomingEmail error" do expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail) end - - context "and the email contains no references header" do - let(:email_raw) { fixture_file("emails/auto_reply.eml").gsub(mail_key, "!!!") } - - it "raises an UnknownIncomingEmail error" do - expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail) - end - end end context "when the email is blank" do @@ -45,4 +37,12 @@ describe Gitlab::Email::Receiver do expect { receiver.execute }.to raise_error(Gitlab::Email::EmptyEmailError) end end + + context "when the email was auto generated" do + let(:email_raw) { fixture_file("emails/auto_reply.eml") } + + it "raises an AutoGeneratedEmailError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::AutoGeneratedEmailError) + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 609998d6e9c..06769b241ad 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -49,7 +49,7 @@ RSpec.configure do |config| config.include SearchHelpers, type: :feature config.include WaitForRequests, :js config.include StubConfiguration - config.include EmailHelpers, type: :mailer + config.include EmailHelpers, :mailer, type: :mailer config.include TestEnv config.include ActiveJob::TestHelper config.include ActiveSupport::Testing::TimeHelpers @@ -93,6 +93,10 @@ RSpec.configure do |config| RequestStore.clear! end + config.before(:example, :mailer) do + reset_delivered_emails! + end + if ENV['CI'] config.around(:each) do |ex| ex.run_with_retry retry: 2 diff --git a/spec/workers/email_receiver_worker_spec.rb b/spec/workers/email_receiver_worker_spec.rb index fe70501eeac..e4e77c667b3 100644 --- a/spec/workers/email_receiver_worker_spec.rb +++ b/spec/workers/email_receiver_worker_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe EmailReceiverWorker do +describe EmailReceiverWorker, :mailer do let(:raw_message) { fixture_file('emails/valid_reply.eml') } context "when reply by email is enabled" do @@ -17,12 +17,16 @@ describe EmailReceiverWorker do context "when an error occurs" do before do - allow_any_instance_of(Gitlab::Email::Receiver).to receive(:execute).and_raise(Gitlab::Email::EmptyEmailError) + allow_any_instance_of(Gitlab::Email::Receiver).to receive(:execute).and_raise(error) end - it "sends out a rejection email" do - perform_enqueued_jobs do - described_class.new.perform(raw_message) + context 'when the error is Gitlab::Email::EmptyEmailError' do + let(:error) { Gitlab::Email::EmptyEmailError } + + it 'sends out a rejection email' do + perform_enqueued_jobs do + described_class.new.perform(raw_message) + end email = ActionMailer::Base.deliveries.last expect(email).not_to be_nil @@ -30,6 +34,18 @@ describe EmailReceiverWorker do expect(email.subject).to include("Rejected") end end + + context 'when the error is Gitlab::Email::AutoGeneratedEmailError' do + let(:error) { Gitlab::Email::AutoGeneratedEmailError } + + it 'does not send out any rejection email' do + perform_enqueued_jobs do + described_class.new.perform(raw_message) + end + + should_not_email_anyone + end + end end end -- cgit v1.2.1 From 6a29d3a4875fa25bfe3ea0a8afb747cb7708e6a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 2 Aug 2017 11:39:16 +0200 Subject: Ensure we run installation Rake tasks in a clean env in TestEnv MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If we call `system('rake', 'taks_name')`, `ENV['RUBYOPT']` is set to `'-rbundler/setup'` but some tasks (e.g. `gitlab:gitaly:install` need a clean env since they install their own Gem bundle. Signed-off-by: Rémy Coutable --- lib/tasks/gitlab/gitaly.rake | 2 +- spec/support/test_env.rb | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index 680e76af471..3703f9cfb5c 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -21,7 +21,7 @@ namespace :gitlab do create_gitaly_configuration # In CI we run scripts/gitaly-test-build instead of this command unless ENV['CI'].present? - Bundler.with_original_env { run_command!(%w[/usr/bin/env -u BUNDLE_GEMFILE] + [command]) } + Bundler.with_original_env { run_command!([command]) } end end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index f0603dfadde..8a509cf8a9c 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -63,6 +63,8 @@ module TestEnv # See gitlab.yml.example test section for paths # def init(opts = {}) + Gitlab::Application.load_tasks + # Disable mailer for spinach tests disable_mailer if opts[:mailer] == false @@ -122,11 +124,13 @@ module TestEnv end def setup_gitlab_shell - shell_needs_update = component_needs_update?(Gitlab.config.gitlab_shell.path, + gitlab_shell_dir = File.dirname(Gitlab.config.gitlab_shell.path) + gitlab_shell_needs_update = component_needs_update?(gitlab_shell_dir, Gitlab::Shell.version_required) - unless !shell_needs_update || system('rake', 'gitlab:shell:install') - raise 'Can`t clone gitlab-shell' + unless !gitlab_shell_needs_update || Rake.application.invoke_task('gitlab:shell:install') + FileUtils.rm_rf(gitlab_shell_dir) + raise "Can't install gitlab-shell" end end @@ -136,8 +140,9 @@ module TestEnv gitaly_needs_update = component_needs_update?(gitaly_dir, Gitlab::GitalyClient.expected_server_version) - unless !gitaly_needs_update || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]") - raise "Can't clone gitaly" + unless !gitaly_needs_update || Rake.application.invoke_task("gitlab:gitaly:install[#{gitaly_dir}]") + FileUtils.rm_rf(gitaly_dir) + raise "Can't install gitaly" end start_gitaly(gitaly_dir) -- cgit v1.2.1 From 532ad2e56e10ff0e7922980d48a8c639efac8809 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 2 Aug 2017 19:01:14 +0200 Subject: Don't call load_tasks as this would load the tasks twice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- spec/support/test_env.rb | 6 ++++-- spec/tasks/gitlab/gitaly_rake_spec.rb | 10 ++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 8a509cf8a9c..f88924e84fd 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -63,8 +63,8 @@ module TestEnv # See gitlab.yml.example test section for paths # def init(opts = {}) - Gitlab::Application.load_tasks - + Rake.application.rake_require 'tasks/gitlab/helpers' + Rake::Task.define_task :environment # Disable mailer for spinach tests disable_mailer if opts[:mailer] == false @@ -128,6 +128,7 @@ module TestEnv gitlab_shell_needs_update = component_needs_update?(gitlab_shell_dir, Gitlab::Shell.version_required) + Rake.application.rake_require 'tasks/gitlab/shell' unless !gitlab_shell_needs_update || Rake.application.invoke_task('gitlab:shell:install') FileUtils.rm_rf(gitlab_shell_dir) raise "Can't install gitlab-shell" @@ -140,6 +141,7 @@ module TestEnv gitaly_needs_update = component_needs_update?(gitaly_dir, Gitlab::GitalyClient.expected_server_version) + Rake.application.rake_require 'tasks/gitlab/gitaly' unless !gitaly_needs_update || Rake.application.invoke_task("gitlab:gitaly:install[#{gitaly_dir}]") FileUtils.rm_rf(gitaly_dir) raise "Can't install gitaly" diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index 695231c7d15..a2f4ec39d89 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -41,8 +41,6 @@ describe 'gitlab:gitaly namespace rake task' do end describe 'gmake/make' do - let(:command_preamble) { %w[/usr/bin/env -u BUNDLE_GEMFILE] } - before(:all) do @old_env_ci = ENV.delete('CI') end @@ -59,12 +57,12 @@ describe 'gitlab:gitaly namespace rake task' do context 'gmake is available' do before do expect_any_instance_of(Object).to receive(:checkout_or_clone_version) - allow_any_instance_of(Object).to receive(:run_command!).with(command_preamble + ['gmake']).and_return(true) + allow_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true) end it 'calls gmake in the gitaly directory' do expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['/usr/bin/gmake', 0]) - expect_any_instance_of(Object).to receive(:run_command!).with(command_preamble + ['gmake']).and_return(true) + expect_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true) run_rake_task('gitlab:gitaly:install', clone_path) end @@ -73,12 +71,12 @@ describe 'gitlab:gitaly namespace rake task' do context 'gmake is not available' do before do expect_any_instance_of(Object).to receive(:checkout_or_clone_version) - allow_any_instance_of(Object).to receive(:run_command!).with(command_preamble + ['make']).and_return(true) + allow_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true) end it 'calls make in the gitaly directory' do expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['', 42]) - expect_any_instance_of(Object).to receive(:run_command!).with(command_preamble + ['make']).and_return(true) + expect_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true) run_rake_task('gitlab:gitaly:install', clone_path) end -- cgit v1.2.1 From 29be4e0f58622aea146aa8c362eb30c08b082839 Mon Sep 17 00:00:00 2001 From: wendy0402 Date: Sun, 23 Jul 2017 15:19:10 +0700 Subject: Allow wiki pages to be renamed in the UI --- app/models/project_wiki.rb | 4 +- app/models/wiki_page.rb | 78 ++++++++++++---------- app/services/wiki_pages/update_service.rb | 2 +- app/views/projects/wikis/_form.html.haml | 5 +- changelogs/unreleased/wiki_title.yml | 4 ++ features/steps/project/wiki.rb | 2 +- .../projects/wiki/user_updates_wiki_page_spec.rb | 2 +- spec/models/project_wiki_spec.rb | 14 +++- spec/models/wiki_page_spec.rb | 35 +++++++--- spec/services/wiki_pages/update_service_spec.rb | 4 +- 10 files changed, 95 insertions(+), 55 deletions(-) create mode 100644 changelogs/unreleased/wiki_title.yml diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index e8929a35836..698fdf7a20c 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -113,10 +113,10 @@ class ProjectWiki return false end - def update_page(page, content, format = :markdown, message = nil) + def update_page(page, content:, title: nil, format: :markdown, message: nil) commit = commit_details(:updated, message, page.title) - wiki.update_page(page, page.name, format.to_sym, content, commit) + wiki.update_page(page, title || page.name, format.to_sym, content, commit) update_project_activity end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 148998bc9be..5c7c2204374 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -180,31 +180,50 @@ class WikiPage # # Returns the String SHA1 of the newly created page # or False if the save was unsuccessful. - def create(attr = {}) - @attributes.merge!(attr) + def create(attrs = {}) + @attributes.merge!(attrs) - save :create_page, title, content, format, message + save(page_details: title) do + wiki.create_page(title, content, format, message) + end end # Updates an existing Wiki Page, creating a new version. # - # new_content - The raw markup content to replace the existing. - # format - Optional symbol representing the content format. - # See ProjectWiki::MARKUPS Hash for available formats. - # message - Optional commit message to set on the new version. - # last_commit_sha - Optional last commit sha to validate the page unchanged. + # attrs - Hash of attributes to be updated on the page. + # :content - The raw markup content to replace the existing. + # :format - Optional symbol representing the content format. + # See ProjectWiki::MARKUPS Hash for available formats. + # :message - Optional commit message to set on the new version. + # :last_commit_sha - Optional last commit sha to validate the page unchanged. + # :title - The Title to replace existing title # # Returns the String SHA1 of the newly created page # or False if the save was unsuccessful. - def update(new_content, format: :markdown, message: nil, last_commit_sha: nil) - @attributes[:content] = new_content - @attributes[:format] = format - + def update(attrs = {}) + last_commit_sha = attrs.delete(:last_commit_sha) if last_commit_sha && last_commit_sha != self.last_commit_sha raise PageChangedError.new("You are attempting to update a page that has changed since you started editing it.") end - save :update_page, @page, content, format, message + attrs.slice!(:content, :format, :message, :title) + @attributes.merge!(attrs) + page_details = + if title.present? && @page.title != title + title + else + @page.url_path + end + + save(page_details: page_details) do + wiki.update_page( + @page, + content: content, + format: format, + message: attrs[:message], + title: title + ) + end end # Destroys the Wiki Page. @@ -236,30 +255,19 @@ class WikiPage attributes[:format] = @page.format end - def save(method, *args) - saved = false + def save(page_details:) + return unless valid? - project_wiki = wiki - if valid? && project_wiki.send(method, *args) - - page_details = if method == :update_page - # Use url_path instead of path to omit format extension - @page.url_path - else - title - end - - page_title, page_dir = project_wiki.page_title_and_dir(page_details) - gollum_wiki = project_wiki.wiki - @page = gollum_wiki.paged(page_title, page_dir) + unless yield + errors.add(:base, wiki.error_message) + return false + end - set_attributes + page_title, page_dir = wiki.page_title_and_dir(page_details) + gollum_wiki = wiki.wiki + @page = gollum_wiki.paged(page_title, page_dir) - @persisted = true - saved = true - else - errors.add(:base, project_wiki.error_message) if project_wiki.error_message - end - saved + set_attributes + @persisted = errors.blank? end end diff --git a/app/services/wiki_pages/update_service.rb b/app/services/wiki_pages/update_service.rb index c628e6781af..93cbd9a509f 100644 --- a/app/services/wiki_pages/update_service.rb +++ b/app/services/wiki_pages/update_service.rb @@ -1,7 +1,7 @@ module WikiPages class UpdateService < WikiPages::BaseService def execute(page) - if page.update(@params[:content], format: @params[:format], message: @params[:message], last_commit_sha: @params[:last_commit_sha]) + if page.update(@params) execute_hooks(page, 'update') end diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index adb8d5aaecb..e5a1fccf9ba 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -3,9 +3,12 @@ = form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f| = form_errors(@page) - = f.hidden_field :title, value: @page.title - if @page.persisted? = f.hidden_field :last_commit_sha, value: @page.last_commit_sha + + .form-group + .col-sm-12= f.label :title, class: 'control-label-full-width' + .col-sm-12= f.text_field :title, class: 'form-control', value: @page.title .form-group .col-sm-12= f.label :format, class: 'control-label-full-width' .col-sm-12 diff --git a/changelogs/unreleased/wiki_title.yml b/changelogs/unreleased/wiki_title.yml new file mode 100644 index 00000000000..3ef5fa2969b --- /dev/null +++ b/changelogs/unreleased/wiki_title.yml @@ -0,0 +1,4 @@ +--- +title: Allow wiki pages to be renamed in the UI +merge_request: 10069 +author: wendy0402 diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb index 2b8da2a6f19..855757e34b3 100644 --- a/features/steps/project/wiki.rb +++ b/features/steps/project/wiki.rb @@ -63,7 +63,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'That page has two revisions' do - @page.update("new content", message: "second commit") + @page.update(content: "new content", message: "second commit") end step 'I click the History button' do diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb index 8271428582d..90983ad60f4 100644 --- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -55,7 +55,7 @@ feature 'Projects > Wiki > User updates wiki page' do scenario 'page has been updated since the user opened the edit page' do click_link 'Edit' - wiki_page.update('Update') + wiki_page.update(content: 'Update') click_button 'Save changes' diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 484fcfc88a3..a6f210f4bd5 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -223,7 +223,12 @@ describe ProjectWiki do before do create_page("update-page", "some content") @gollum_page = subject.wiki.paged("update-page") - subject.update_page(@gollum_page, "some other content", :markdown, "updated page") + subject.update_page( + @gollum_page, + content: "some other content", + format: :markdown, + message: "updated page" + ) @page = subject.pages.first.page end @@ -240,7 +245,12 @@ describe ProjectWiki do end it 'updates project activity' do - subject.update_page(@gollum_page, 'Yet more content', :markdown, 'Updated page again') + subject.update_page( + @gollum_page, + content: 'Yet more content', + format: :markdown, + message: 'Updated page again' + ) project.reload diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 55d9c4ddd7b..f3ef11aa3a8 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -178,12 +178,12 @@ describe WikiPage do end it "updates the content of the page" do - @page.update("new content") + @page.update(content: "new content") @page = wiki.find_page(title) end it "returns true" do - expect(@page.update("more content")).to be_truthy + expect(@page.update(content: "more content")).to be_truthy end end end @@ -195,29 +195,42 @@ describe WikiPage do end after do - destroy_page("Update") + destroy_page(@page.title) end context "with valid attributes" do it "updates the content of the page" do - @page.update("new content") + new_content = "new content" + + @page.update(content: new_content) @page = wiki.find_page("Update") + + expect(@page.content).to eq("new content") + end + + it "updates the title of the page" do + new_title = "Index v.1.2.4" + + @page.update(title: new_title) + @page = wiki.find_page(new_title) + + expect(@page.title).to eq(new_title) end it "returns true" do - expect(@page.update("more content")).to be_truthy + expect(@page.update(content: "more content")).to be_truthy end end context 'with same last commit sha' do it 'returns true' do - expect(@page.update('more content', last_commit_sha: @page.last_commit_sha)).to be_truthy + expect(@page.update(content: 'more content', last_commit_sha: @page.last_commit_sha)).to be_truthy end end context 'with different last commit sha' do it 'raises exception' do - expect { @page.update('more content', last_commit_sha: 'xxx') }.to raise_error(WikiPage::PageChangedError) + expect { @page.update(content: 'more content', last_commit_sha: 'xxx') }.to raise_error(WikiPage::PageChangedError) end end end @@ -249,7 +262,7 @@ describe WikiPage do end it "returns an array of all commits for the page" do - 3.times { |i| @page.update("content #{i}") } + 3.times { |i| @page.update(content: "content #{i}") } expect(@page.versions.count).to eq(4) end end @@ -294,7 +307,7 @@ describe WikiPage do before do create_page('Update', 'content') @page = wiki.find_page('Update') - 3.times { |i| @page.update("content #{i}") } + 3.times { |i| @page.update(content: "content #{i}") } end after do @@ -338,7 +351,7 @@ describe WikiPage do end it 'returns false for updated wiki page' do - updated_wiki_page = original_wiki_page.update("Updated content") + updated_wiki_page = original_wiki_page.update(content: "Updated content") expect(original_wiki_page).not_to eq(updated_wiki_page) end end @@ -360,7 +373,7 @@ describe WikiPage do it 'is changed after page updated' do last_commit_sha_before_update = @page.last_commit_sha - @page.update("new content") + @page.update(content: "new content") @page = wiki.find_page("Update") expect(@page.last_commit_sha).not_to eq last_commit_sha_before_update diff --git a/spec/services/wiki_pages/update_service_spec.rb b/spec/services/wiki_pages/update_service_spec.rb index a672c84034b..ec3613f74f7 100644 --- a/spec/services/wiki_pages/update_service_spec.rb +++ b/spec/services/wiki_pages/update_service_spec.rb @@ -9,7 +9,8 @@ describe WikiPages::UpdateService do { content: 'New content for wiki page', format: 'markdown', - message: 'New wiki message' + message: 'New wiki message', + title: 'New Title' } end @@ -27,6 +28,7 @@ describe WikiPages::UpdateService do expect(updated_page.message).to eq(opts[:message]) expect(updated_page.content).to eq(opts[:content]) expect(updated_page.format).to eq(opts[:format].to_sym) + expect(updated_page.title).to eq(opts[:title]) end it 'executes webhooks' do -- cgit v1.2.1 From 42feb55f20a6d2bab2d07f2f32bccb1b4ae28ba8 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 3 Aug 2017 10:20:02 +0100 Subject: Fixed sign-in restrictions buttons not toggling active state Closes #35882 --- app/assets/javascripts/commons/bootstrap.js | 1 + changelogs/unreleased/fix-oauth-checkboxes.yml | 4 ++++ spec/features/admin/admin_settings_spec.rb | 8 ++++++++ 3 files changed, 13 insertions(+) create mode 100644 changelogs/unreleased/fix-oauth-checkboxes.yml diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js index 510bedbf641..389587a2596 100644 --- a/app/assets/javascripts/commons/bootstrap.js +++ b/app/assets/javascripts/commons/bootstrap.js @@ -9,6 +9,7 @@ import 'bootstrap-sass/assets/javascripts/bootstrap/tab'; import 'bootstrap-sass/assets/javascripts/bootstrap/transition'; import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip'; import 'bootstrap-sass/assets/javascripts/bootstrap/popover'; +import 'bootstrap-sass/assets/javascripts/bootstrap/button'; // custom jQuery functions $.fn.extend({ diff --git a/changelogs/unreleased/fix-oauth-checkboxes.yml b/changelogs/unreleased/fix-oauth-checkboxes.yml new file mode 100644 index 00000000000..2839ccc42cb --- /dev/null +++ b/changelogs/unreleased/fix-oauth-checkboxes.yml @@ -0,0 +1,4 @@ +--- +title: Fixed sign-in restrictions buttons not toggling active state +merge_request: +author: diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index c1eced417cf..c9591a7d854 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -69,6 +69,14 @@ feature 'Admin updates settings' do expect(find('#service_push_channel').value).to eq '#test_channel' end + context 'sign-in restrictions', :js do + it 'de-activates oauth sign-in source' do + find('.btn', text: 'GitLab.com').click + + expect(find('.btn', text: 'GitLab.com')).not_to have_css('.active') + end + end + def check_all_events page.check('Active') page.check('Push') -- cgit v1.2.1 From 86e1f41b83448423a035707a971acc7ee76a0a8d Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 3 Aug 2017 19:29:18 +0800 Subject: Check against "Auto-Submitted: no" instead This would be much more accurate. We assume this is an auto-generated email if such header is provided, and the value is not "no". It could also be: "auto-generated", "auto-replied", or other values from extension. It seems that only "no" could mean that this is sent by a human. See: https://tools.ietf.org/html/rfc3834 --- lib/gitlab/email/receiver.rb | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index b3c2a8fad3e..c8f4591d060 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -27,8 +27,7 @@ module Gitlab mail = build_mail - raise AutoGeneratedEmailError if - mail.header.to_s =~ /auto-(generated|replied)/ + ignore_auto_submitted!(mail) mail_key = extract_mail_key(mail) handler = Handler.for(mail, mail_key) @@ -91,6 +90,16 @@ module Gitlab break key if key end end + + def ignore_auto_submitted!(mail) + # Mail::Header#[] is case-insensitive + auto_submitted = mail.header['Auto-Submitted']&.value + + # Mail::Field#value would strip leading and trailing whitespace + raise AutoGeneratedEmailError if + # See also https://tools.ietf.org/html/rfc3834 + auto_submitted && auto_submitted != 'no' + end end end end -- cgit v1.2.1 From 6c95abf1734bf7bcfb52a9be28837b99a6a7cf8c Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 3 Aug 2017 13:44:09 +0200 Subject: Install 'yarn' globally to avoid PATH issues --- doc/install/installation.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 8ded607bcab..769c1fd3664 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -168,8 +168,10 @@ are out of date, so we'll need to install through the following commands: curl --location https://deb.nodesource.com/setup_7.x | sudo bash - sudo apt-get install -y nodejs - # install yarn - curl --location https://yarnpkg.com/install.sh | bash - + curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - + echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list + sudo apt-get update + sudo apt-get install yarn Visit the official websites for [node](https://nodejs.org/en/download/package-manager/) and [yarn](https://yarnpkg.com/en/docs/install/) if you have any trouble with these steps. -- cgit v1.2.1 From f2d50af917b878a98e06b994ac32c0718f3d0b78 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 3 Jul 2017 15:48:59 +0100 Subject: Migrate MR commits and diffs to new tables Previously, we stored these as serialised fields - `st_{commits,diffs}` - on the `merge_request_diffs` table. These now have their own tables - `merge_request_diff_{commits,diffs}` - with a column for each attribute of the serialised data. Add a background migration to go through the existing MR diffs and migrate them to the new format. Ignore any contents that cannot be displayed. Assuming that we have 5 million rows to migrate, and each batch of 2,500 rows can be completed in 5 minutes, this will take about 7 days to migrate everything. --- app/models/merge_request_diff.rb | 6 +- ...30158_schedule_merge_request_diff_migrations.rb | 33 ++++ .../deserialize_merge_request_diffs_and_commits.rb | 107 ++++++++++++ ...rialize_merge_request_diffs_and_commits_spec.rb | 188 +++++++++++++++++++++ .../schedule_merge_request_diff_migrations_spec.rb | 59 +++++++ 5 files changed, 388 insertions(+), 5 deletions(-) create mode 100644 db/post_migrate/20170703130158_schedule_merge_request_diff_migrations.rb create mode 100644 lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb create mode 100644 spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb create mode 100644 spec/migrations/schedule_merge_request_diff_migrations_spec.rb diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index ec87aee9310..d9d746ccf41 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -85,11 +85,7 @@ class MergeRequestDiff < ActiveRecord::Base def raw_diffs(options = {}) if options[:ignore_whitespace_change] - @diffs_no_whitespace ||= - Gitlab::Git::Compare.new( - repository.raw_repository, - safe_start_commit_sha, - head_commit_sha).diffs(options) + @diffs_no_whitespace ||= compare.diffs(options) else @raw_diffs ||= {} @raw_diffs[options] ||= load_diffs(options) diff --git a/db/post_migrate/20170703130158_schedule_merge_request_diff_migrations.rb b/db/post_migrate/20170703130158_schedule_merge_request_diff_migrations.rb new file mode 100644 index 00000000000..17a9dc293f1 --- /dev/null +++ b/db/post_migrate/20170703130158_schedule_merge_request_diff_migrations.rb @@ -0,0 +1,33 @@ +class ScheduleMergeRequestDiffMigrations < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + BATCH_SIZE = 2500 + MIGRATION = 'DeserializeMergeRequestDiffsAndCommits' + + disable_ddl_transaction! + + class MergeRequestDiff < ActiveRecord::Base + self.table_name = 'merge_request_diffs' + + include ::EachBatch + end + + # Assuming that there are 5 million rows affected (which is more than on + # GitLab.com), and that each batch of 2,500 rows takes up to 5 minutes, then + # we can migrate all the rows in 7 days. + # + # On staging, plucking the IDs themselves takes 5 seconds. + def up + non_empty = 'st_commits IS NOT NULL OR st_diffs IS NOT NULL' + + MergeRequestDiff.where(non_empty).each_batch(of: BATCH_SIZE) do |relation, index| + range = relation.pluck('MIN(id)', 'MAX(id)').first + + BackgroundMigrationWorker.perform_in(index * 5.minutes, MIGRATION, range) + end + end + + def down + end +end diff --git a/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb b/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb new file mode 100644 index 00000000000..0fbc6b70989 --- /dev/null +++ b/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb @@ -0,0 +1,107 @@ +module Gitlab + module BackgroundMigration + class DeserializeMergeRequestDiffsAndCommits + attr_reader :diff_ids, :commit_rows, :file_rows + + class MergeRequestDiff < ActiveRecord::Base + self.table_name = 'merge_request_diffs' + end + + BUFFER_ROWS = 1000 + + def perform(start_id, stop_id) + merge_request_diffs = MergeRequestDiff + .select(:id, :st_commits, :st_diffs) + .where('st_commits IS NOT NULL OR st_diffs IS NOT NULL') + .where(id: start_id..stop_id) + + reset_buffers! + + merge_request_diffs.each do |merge_request_diff| + commits, files = single_diff_rows(merge_request_diff) + + diff_ids << merge_request_diff.id + commit_rows.concat(commits) + file_rows.concat(files) + + if diff_ids.length > BUFFER_ROWS || + commit_rows.length > BUFFER_ROWS || + file_rows.length > BUFFER_ROWS + + flush_buffers! + end + end + + flush_buffers! + end + + private + + def reset_buffers! + @diff_ids = [] + @commit_rows = [] + @file_rows = [] + end + + def flush_buffers! + if diff_ids.any? + MergeRequestDiff.transaction do + Gitlab::Database.bulk_insert('merge_request_diff_commits', commit_rows) + Gitlab::Database.bulk_insert('merge_request_diff_files', file_rows) + + MergeRequestDiff.where(id: diff_ids).update_all(st_commits: nil, st_diffs: nil) + end + end + + reset_buffers! + end + + def single_diff_rows(merge_request_diff) + sha_attribute = Gitlab::Database::ShaAttribute.new + commits = YAML.load(merge_request_diff.st_commits) rescue [] + + commit_rows = commits.map.with_index do |commit, index| + commit_hash = commit.to_hash.with_indifferent_access.except(:parent_ids) + sha = commit_hash.delete(:id) + + commit_hash.merge( + merge_request_diff_id: merge_request_diff.id, + relative_order: index, + sha: sha_attribute.type_cast_for_database(sha) + ) + end + + diffs = YAML.load(merge_request_diff.st_diffs) rescue [] + diffs = [] unless valid_raw_diffs?(diffs) + + file_rows = diffs.map.with_index do |diff, index| + diff_hash = diff.to_hash.with_indifferent_access.merge( + binary: false, + merge_request_diff_id: merge_request_diff.id, + relative_order: index + ) + + # Compatibility with old diffs created with Psych. + diff_hash.tap do |hash| + diff_text = hash[:diff] + + if diff_text.encoding == Encoding::BINARY && !diff_text.ascii_only? + hash[:binary] = true + hash[:diff] = [diff_text].pack('m0') + end + end + end + + [commit_rows, file_rows] + end + + # Unlike MergeRequestDiff#valid_raw_diff?, don't count Rugged objects as + # valid, because we don't render them usefully anyway. + def valid_raw_diffs?(diffs) + return false unless diffs.respond_to?(:each) + + diffs.all? { |diff| diff.is_a?(Hash) } + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb new file mode 100644 index 00000000000..18843cbe992 --- /dev/null +++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb @@ -0,0 +1,188 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do + describe '#perform' do + set(:merge_request) { create(:merge_request) } + set(:merge_request_diff) { merge_request.merge_request_diff } + let(:updated_merge_request_diff) { MergeRequestDiff.find(merge_request_diff.id) } + + def diffs_to_hashes(diffs) + diffs.as_json(only: Gitlab::Git::Diff::SERIALIZE_KEYS).map(&:with_indifferent_access) + end + + def quote_yaml(value) + MergeRequestDiff.connection.quote(YAML.dump(value)) + end + + def convert_to_yaml(merge_request_diff_id, commits, diffs) + MergeRequestDiff.where(id: merge_request_diff_id).update_all( + "st_commits = #{quote_yaml(commits)}, st_diffs = #{quote_yaml(diffs)}" + ) + end + + shared_examples 'updated MR diff' do + before do + convert_to_yaml(merge_request_diff.id, commits, diffs) + + MergeRequestDiffCommit.delete_all + MergeRequestDiffFile.delete_all + + subject.perform(merge_request_diff.id, merge_request_diff.id) + end + + it 'creates correct entries in the merge_request_diff_commits table' do + expect(updated_merge_request_diff.merge_request_diff_commits.count).to eq(commits.count) + expect(updated_merge_request_diff.commits.map(&:to_hash)).to eq(commits) + end + + it 'creates correct entries in the merge_request_diff_files table' do + expect(updated_merge_request_diff.merge_request_diff_files.count).to eq(expected_diffs.count) + expect(diffs_to_hashes(updated_merge_request_diff.raw_diffs)).to eq(expected_diffs) + end + + it 'sets the st_commits and st_diffs columns to nil' do + expect(updated_merge_request_diff.st_commits_before_type_cast).to be_nil + expect(updated_merge_request_diff.st_diffs_before_type_cast).to be_nil + end + end + + context 'when the diff IDs passed do not exist' do + it 'does not raise' do + expect { subject.perform(0, 0) }.not_to raise_exception + end + end + + context 'when the merge request diff has no serialised commits or diffs' do + before do + merge_request_diff.update(st_commits: nil, st_diffs: nil) + end + + it 'does not raise' do + expect { subject.perform(merge_request_diff.id, merge_request_diff.id) } + .not_to raise_exception + end + end + + context 'processing multiple merge request diffs' do + let(:start_id) { described_class::MergeRequestDiff.minimum(:id) } + let(:stop_id) { described_class::MergeRequestDiff.maximum(:id) } + + before do + merge_request.reload_diff(true) + + convert_to_yaml(start_id, merge_request_diff.commits, merge_request_diff.diffs) + convert_to_yaml(stop_id, updated_merge_request_diff.commits, updated_merge_request_diff.diffs) + + MergeRequestDiffCommit.delete_all + MergeRequestDiffFile.delete_all + end + + context 'when BUFFER_ROWS is exceeded' do + before do + stub_const("#{described_class}::BUFFER_ROWS", 1) + end + + it 'updates and continues' do + expect(described_class::MergeRequestDiff).to receive(:transaction).twice + + subject.perform(start_id, stop_id) + end + end + + context 'when BUFFER_ROWS is not exceeded' do + it 'only updates once' do + expect(described_class::MergeRequestDiff).to receive(:transaction).once + + subject.perform(start_id, stop_id) + end + end + end + + context 'when the merge request diff update fails' do + before do + allow(described_class::MergeRequestDiff) + .to receive(:update_all).and_raise(ActiveRecord::Rollback) + end + + it 'does not add any diff commits' do + expect { subject.perform(merge_request_diff.id, merge_request_diff.id) } + .not_to change { MergeRequestDiffCommit.count } + end + + it 'does not add any diff files' do + expect { subject.perform(merge_request_diff.id, merge_request_diff.id) } + .not_to change { MergeRequestDiffFile.count } + end + end + + context 'when the merge request diff has valid commits and diffs' do + let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:diffs) { diffs_to_hashes(merge_request_diff.merge_request_diff_files) } + let(:expected_diffs) { diffs } + + include_examples 'updated MR diff' + end + + context 'when the merge request diffs have binary content' do + let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:expected_diffs) { diffs } + + # The start of a PDF created by Illustrator + let(:binary_string) do + "\x25\x50\x44\x46\x2d\x31\x2e\x35\x0d\x25\xe2\xe3\xcf\xd3\x0d\x0a".force_encoding(Encoding::BINARY) + end + + let(:diffs) do + [ + { + 'diff' => binary_string, + 'new_path' => 'path', + 'old_path' => 'path', + 'a_mode' => '100644', + 'b_mode' => '100644', + 'new_file' => false, + 'renamed_file' => false, + 'deleted_file' => false, + 'too_large' => false + } + ] + end + + include_examples 'updated MR diff' + end + + context 'when the merge request diff has commits, but no diffs' do + let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:diffs) { [] } + let(:expected_diffs) { diffs } + + include_examples 'updated MR diff' + end + + context 'when the merge request diffs have invalid content' do + let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:diffs) { ['--broken-diff'] } + let(:expected_diffs) { [] } + + include_examples 'updated MR diff' + end + + context 'when the merge request diffs are Rugged::Patch instances' do + let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:first_commit) { merge_request.project.repository.commit(merge_request_diff.head_commit_sha) } + let(:diffs) { first_commit.diff_from_parent.patches } + let(:expected_diffs) { [] } + + include_examples 'updated MR diff' + end + + context 'when the merge request diffs are Rugged::Diff::Delta instances' do + let(:commits) { merge_request_diff.commits.map(&:to_hash) } + let(:first_commit) { merge_request.project.repository.commit(merge_request_diff.head_commit_sha) } + let(:diffs) { first_commit.diff_from_parent.deltas } + let(:expected_diffs) { [] } + + include_examples 'updated MR diff' + end + end +end diff --git a/spec/migrations/schedule_merge_request_diff_migrations_spec.rb b/spec/migrations/schedule_merge_request_diff_migrations_spec.rb new file mode 100644 index 00000000000..f95bd6e3511 --- /dev/null +++ b/spec/migrations/schedule_merge_request_diff_migrations_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170703130158_schedule_merge_request_diff_migrations') + +describe ScheduleMergeRequestDiffMigrations, :migration, :sidekiq do + matcher :be_scheduled_migration do |time, *expected| + match do |migration| + BackgroundMigrationWorker.jobs.any? do |job| + job['args'] == [migration, expected] && + job['at'].to_i == time.to_i + end + end + + failure_message do |migration| + "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!" + end + end + + let(:merge_request_diffs) { table(:merge_request_diffs) } + let(:merge_requests) { table(:merge_requests) } + let(:projects) { table(:projects) } + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 1) + + projects.create!(id: 1, name: 'gitlab', path: 'gitlab') + + merge_requests.create!(id: 1, target_project_id: 1, source_project_id: 1, target_branch: 'feature', source_branch: 'master') + + merge_request_diffs.create!(id: 1, merge_request_id: 1, st_commits: YAML.dump([]), st_diffs: nil) + merge_request_diffs.create!(id: 2, merge_request_id: 1, st_commits: nil, st_diffs: YAML.dump([])) + merge_request_diffs.create!(id: 3, merge_request_id: 1, st_commits: nil, st_diffs: nil) + merge_request_diffs.create!(id: 4, merge_request_id: 1, st_commits: YAML.dump([]), st_diffs: YAML.dump([])) + end + + it 'correctly schedules background migrations' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes.from_now, 1, 1) + expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes.from_now, 2, 2) + expect(described_class::MIGRATION).to be_scheduled_migration(15.minutes.from_now, 4, 4) + expect(BackgroundMigrationWorker.jobs.size).to eq 3 + end + end + end + + it 'schedules background migrations' do + Sidekiq::Testing.inline! do + non_empty = 'st_commits IS NOT NULL OR st_diffs IS NOT NULL' + + expect(merge_request_diffs.where(non_empty).count).to eq 3 + + migrate! + + expect(merge_request_diffs.where(non_empty).count).to eq 0 + end + end +end -- cgit v1.2.1 From a7102fb7908afe893dc2e8622a286a3a3edfba74 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Thu, 3 Aug 2017 13:11:17 +0000 Subject: Make dropdown style on project page consistent --- app/assets/javascripts/project.js | 9 +++-- app/assets/stylesheets/framework/dropdowns.scss | 45 +++++++++++++++++++++++++ app/assets/stylesheets/pages/projects.scss | 2 ++ app/views/shared/_clone_panel.html.haml | 2 +- 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index a3f7d69b98d..6e1744e8e72 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -10,14 +10,19 @@ import Cookies from 'js-cookie'; const $projectCloneField = $('#project_clone'); const $cloneBtnText = $('a.clone-dropdown-btn span'); + const selectedCloneOption = $cloneBtnText.text().trim(); + if (selectedCloneOption.length > 0) { + $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active'); + } + $('a', $cloneOptions).on('click', (e) => { const $this = $(e.currentTarget); const url = $this.attr('href'); e.preventDefault(); - $('.active', $cloneOptions).not($this).removeClass('active'); - $this.toggleClass('active'); + $('.is-active', $cloneOptions).not($this).removeClass('is-active'); + $this.toggleClass('is-active'); $projectCloneField.val(url); $cloneBtnText.text($this.text()); diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 3f934403147..572203bce34 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -722,3 +722,48 @@ @include set-invisible; overflow: hidden; } + +// TODO: change global style and remove mixin +@mixin new-style-dropdown { + .dropdown-menu { + li { + padding: 0 1px; + + &.dropdown-header { + padding: 8px 16px; + } + + a { + border-radius: 0; + padding: 8px 16px; + + &.is-focused, + &:hover, + &:active, + &:focus { + background-color: $gray-darker; + } + + &.is-active { + font-weight: inherit; + + &::before { + top: 16px; + } + } + } + } + + &.dropdown-menu-selectable { + li { + a { + padding: 8px 40px; + + &.is-active::before { + left: 16px; + } + } + } + } + } +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index b3a90dff89a..d29421aa1b3 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -282,6 +282,8 @@ } .project-repo-buttons { + @include new-style-dropdown; + .project-action-button .dropdown-menu { max-height: 250px; overflow-y: auto; diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index b4843eafdb7..3d9c90c38fe 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -11,7 +11,7 @@ %span = default_clone_protocol.upcase = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-right.clone-options-dropdown + %ul.dropdown-menu.dropdown-menu-selectable.dropdown-menu-right.clone-options-dropdown %li = ssh_clone_button(project) %li -- cgit v1.2.1 From 9b3f0569fa298f192735af10d685ffc68b86afe3 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 3 Aug 2017 15:18:57 +0200 Subject: Re-organise "issues" indexes for faster ordering By adding various composite indexes we can reduce the time spent retrieving issue lists. Because of the way these indexes are built column wise we can also remove some standalone indexes, keeping the total number of indexes in check. --- .../reorganise-issues-indexes-for-sorting.yml | 4 ++ ...reorganise_issues_indexes_for_faster_sorting.rb | 43 ++++++++++++++++++++++ db/schema.rb | 7 ++-- 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/reorganise-issues-indexes-for-sorting.yml create mode 100644 db/migrate/20170803130232_reorganise_issues_indexes_for_faster_sorting.rb diff --git a/changelogs/unreleased/reorganise-issues-indexes-for-sorting.yml b/changelogs/unreleased/reorganise-issues-indexes-for-sorting.yml new file mode 100644 index 00000000000..5bfe55e562f --- /dev/null +++ b/changelogs/unreleased/reorganise-issues-indexes-for-sorting.yml @@ -0,0 +1,4 @@ +--- +title: Re-organise "issues" indexes for faster ordering +merge_request: +author: diff --git a/db/migrate/20170803130232_reorganise_issues_indexes_for_faster_sorting.rb b/db/migrate/20170803130232_reorganise_issues_indexes_for_faster_sorting.rb new file mode 100644 index 00000000000..eb7d1be1732 --- /dev/null +++ b/db/migrate/20170803130232_reorganise_issues_indexes_for_faster_sorting.rb @@ -0,0 +1,43 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class ReorganiseIssuesIndexesForFasterSorting < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + REMOVE_INDEX_COLUMNS = %i[project_id created_at due_date updated_at].freeze + + ADD_INDEX_COLUMNS = [ + %i[project_id created_at id state], + %i[project_id due_date id state], + %i[project_id updated_at id state] + ].freeze + + TABLE = :issues + + def up + add_indexes(ADD_INDEX_COLUMNS) + remove_indexes(REMOVE_INDEX_COLUMNS) + end + + def down + add_indexes(REMOVE_INDEX_COLUMNS) + remove_indexes(ADD_INDEX_COLUMNS) + end + + def add_indexes(columns) + columns.each do |column| + add_concurrent_index(TABLE, column) unless index_exists?(TABLE, column) + end + end + + def remove_indexes(columns) + columns.each do |column| + remove_concurrent_index(TABLE, column) if index_exists?(TABLE, column) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 4ba218e1e0d..f2f35acef95 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170728101014) do +ActiveRecord::Schema.define(version: 20170803130232) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -641,12 +641,13 @@ ActiveRecord::Schema.define(version: 20170728101014) do add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree add_index "issues", ["confidential"], name: "index_issues_on_confidential", using: :btree - add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} - add_index "issues", ["due_date"], name: "index_issues_on_due_date", using: :btree add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree + add_index "issues", ["project_id", "created_at", "id", "state"], name: "index_issues_on_project_id_and_created_at_and_id_and_state", using: :btree + add_index "issues", ["project_id", "due_date", "id", "state"], name: "index_issues_on_project_id_and_due_date_and_id_and_state", using: :btree add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree + add_index "issues", ["project_id", "updated_at", "id", "state"], name: "index_issues_on_project_id_and_updated_at_and_id_and_state", using: :btree add_index "issues", ["relative_position"], name: "index_issues_on_relative_position", using: :btree add_index "issues", ["state"], name: "index_issues_on_state", using: :btree add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} -- cgit v1.2.1 From 03d199fb10bd03c6d6602b23e5f5454dd956e945 Mon Sep 17 00:00:00 2001 From: Mehdi Lahmam Date: Tue, 1 Aug 2017 21:19:54 +0200 Subject: Ensure `JIRA::Resource::Issue` responds to `resolution` before calling it --- app/models/project_services/jira_service.rb | 8 ++++++-- spec/models/project_services/jira_service_spec.rb | 9 +++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index c2414885368..9ee3a533c1e 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -104,7 +104,7 @@ class JiraService < IssueTrackerService def close_issue(entity, external_issue) issue = jira_request { client.Issue.find(external_issue.iid) } - return if issue.nil? || issue.resolution.present? || !jira_issue_transition_id.present? + return if issue.nil? || has_resolution?(issue) || !jira_issue_transition_id.present? commit_id = if entity.is_a?(Commit) entity.id @@ -118,7 +118,7 @@ class JiraService < IssueTrackerService # may or may not be allowed. Refresh the issue after transition and check # if it is closed, so we don't have one comment for every commit. issue = jira_request { client.Issue.find(issue.key) } if transition_issue(issue) - add_issue_solved_comment(issue, commit_id, commit_url) if issue.resolution + add_issue_solved_comment(issue, commit_id, commit_url) if has_resolution?(issue) end def create_cross_reference_note(mentioned, noteable, author) @@ -216,6 +216,10 @@ class JiraService < IssueTrackerService end end + def has_resolution?(issue) + issue.respond_to?(:resolution) && issue.resolution.present? + end + def comment_exists?(issue, message) comments = jira_request { issue.comments } diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index bc9374d6dbb..fcc0f00d01d 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -153,6 +153,15 @@ describe JiraService do expect(WebMock).not_to have_requested(:post, @remote_link_url) end + it "does not send comment or remote links to issues with unknown resolution" do + allow_any_instance_of(JIRA::Resource::Issue).to receive(:respond_to?).with(:resolution).and_return(false) + + @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) + + expect(WebMock).not_to have_requested(:post, @comment_url) + expect(WebMock).not_to have_requested(:post, @remote_link_url) + end + it "references the GitLab commit/merge request" do stub_config_setting(base_url: custom_base_url) -- cgit v1.2.1 From 3c8b2ae168edafbd6158d392f14a757873f817fd Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 3 Aug 2017 10:43:24 +0300 Subject: Expose more attributes to unauthenticated GET /projects/:id --- lib/api/entities.rb | 25 +++++++++++-------------- spec/requests/api/environments_spec.rb | 9 ++++++++- spec/requests/api/projects_spec.rb | 10 +++++++++- spec/requests/api/v3/projects_spec.rb | 10 +++++++++- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 298831a8fdb..3afa44a7428 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -66,13 +66,6 @@ module API expose :job_events end - class BasicProjectDetails < Grape::Entity - expose :id - expose :http_url_to_repo, :web_url - expose :name, :name_with_namespace - expose :path, :path_with_namespace - end - class SharedGroup < Grape::Entity expose :group_id expose :group_name do |group_link, options| @@ -81,7 +74,16 @@ module API expose :group_access, as: :group_access_level end - class Project < Grape::Entity + class BasicProjectDetails < Grape::Entity + expose :id, :description, :default_branch, :tag_list + expose :ssh_url_to_repo, :http_url_to_repo, :web_url + expose :name, :name_with_namespace + expose :path, :path_with_namespace + expose :star_count, :forks_count + expose :created_at, :last_activity_at + end + + class Project < BasicProjectDetails include ::API::Helpers::RelatedResourcesHelpers expose :_links do @@ -114,12 +116,9 @@ module API end end - expose :id, :description, :default_branch, :tag_list expose :archived?, as: :archived - expose :visibility, :ssh_url_to_repo, :http_url_to_repo, :web_url + expose :visibility expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } - expose :name, :name_with_namespace - expose :path, :path_with_namespace expose :container_registry_enabled # Expose old field names with the new permissions methods to keep API compatible @@ -129,7 +128,6 @@ module API expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) } expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) } - expose :created_at, :last_activity_at expose :shared_runners_enabled expose :lfs_enabled?, as: :lfs_enabled expose :creator_id @@ -140,7 +138,6 @@ module API expose :avatar_url do |user, options| user.avatar_url(only_path: false) end - expose :star_count, :forks_count expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :public_builds, as: :public_jobs diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 4c5ded7a492..87716c6fe3a 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -13,7 +13,14 @@ describe API::Environments do describe 'GET /projects/:id/environments' do context 'as member of the project' do it 'returns project environments' do - project_data_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace) + project_data_keys = %w( + id description default_branch tag_list + ssh_url_to_repo http_url_to_repo web_url + name name_with_namespace + path path_with_namespace + star_count forks_count + created_at last_activity_at + ) get api("/projects/#{project.id}/environments", user) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index b9ebf6c4c16..9baac12821f 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -186,7 +186,14 @@ describe API::Projects do context 'and with simple=true' do it 'returns a simplified version of all the projects' do - expected_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace) + expected_keys = %w( + id description default_branch tag_list + ssh_url_to_repo http_url_to_repo web_url + name name_with_namespace + path path_with_namespace + star_count forks_count + created_at last_activity_at + ) get api('/projects?simple=true', user) @@ -689,6 +696,7 @@ describe API::Projects do expect(response).to have_http_status(200) expect(json_response['id']).to eq(public_project.id) expect(json_response['description']).to eq(public_project.description) + expect(json_response['default_branch']).to eq(public_project.default_branch) expect(json_response.keys).not_to include('permissions') end end diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index c211cc20e53..fca5b5b5d82 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -82,7 +82,14 @@ describe API::V3::Projects do context 'GET /projects?simple=true' do it 'returns a simplified version of all the projects' do - expected_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace) + expected_keys = %w( + id description default_branch tag_list + ssh_url_to_repo http_url_to_repo web_url + name name_with_namespace + path path_with_namespace + star_count forks_count + created_at last_activity_at + ) get v3_api('/projects?simple=true', user) @@ -644,6 +651,7 @@ describe API::V3::Projects do expect(response).to have_http_status(200) expect(json_response['id']).to eq(public_project.id) expect(json_response['description']).to eq(public_project.description) + expect(json_response['default_branch']).to eq(public_project.default_branch) expect(json_response.keys).not_to include('permissions') end end -- cgit v1.2.1 From 29a07fd3a12115377f726248a342052a5ae07ed2 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 3 Aug 2017 15:47:43 +0200 Subject: Rename 'commits_between' to 'rugged_commits_between' --- lib/gitlab/git/commit.rb | 2 +- lib/gitlab/git/repository.rb | 4 +++- spec/lib/gitlab/git/repository_spec.rb | 10 +++++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index ca7e3a7c4be..600d886e818 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -102,7 +102,7 @@ module Gitlab if is_enabled repo.gitaly_commit_client.between(base, head) else - repo.commits_between(base, head).map { |c| decorate(c) } + repo.rugged_commits_between(base, head).map { |c| decorate(c) } end end rescue Rugged::ReferenceError diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 70d793f8e4a..26ad40c3eed 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -327,7 +327,9 @@ module Gitlab # Return a collection of Rugged::Commits between the two revspec arguments. # See http://git-scm.com/docs/git-rev-parse.html#_specifying_revisions for # a detailed list of valid arguments. - def commits_between(from, to) + # + # Gitaly note: JV: to be deprecated in favor of Commit.between + def rugged_commits_between(from, to) walker = Rugged::Walker.new(rugged) walker.sorting(Rugged::SORT_NONE | Rugged::SORT_REVERSE) diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 8e4a1f31ced..e375dbfc078 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -757,13 +757,13 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe "#commits_between" do + describe "#rugged_commits_between" do context 'two SHAs' do let(:first_sha) { 'b0e52af38d7ea43cf41d8a6f2471351ac036d6c9' } let(:second_sha) { '0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326' } it 'returns the number of commits between' do - expect(repository.commits_between(first_sha, second_sha).count).to eq(3) + expect(repository.rugged_commits_between(first_sha, second_sha).count).to eq(3) end end @@ -772,11 +772,11 @@ describe Gitlab::Git::Repository, seed_helper: true do let(:branch) { 'master' } it 'returns the number of commits between a sha and a branch' do - expect(repository.commits_between(sha, branch).count).to eq(5) + expect(repository.rugged_commits_between(sha, branch).count).to eq(5) end it 'returns the number of commits between a branch and a sha' do - expect(repository.commits_between(branch, sha).count).to eq(0) # sha is before branch + expect(repository.rugged_commits_between(branch, sha).count).to eq(0) # sha is before branch end end @@ -785,7 +785,7 @@ describe Gitlab::Git::Repository, seed_helper: true do let(:second_branch) { 'master' } it 'returns the number of commits between' do - expect(repository.commits_between(first_branch, second_branch).count).to eq(17) + expect(repository.rugged_commits_between(first_branch, second_branch).count).to eq(17) end end end -- cgit v1.2.1 From 3a8f4a34604d8b15d3e602269934ea5c073ff0d9 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 3 Aug 2017 15:52:14 +0200 Subject: Remove unnecessary 'raw' alias --- lib/gitlab/git/repository.rb | 5 ----- spec/lib/gitlab/git/commit_spec.rb | 2 +- spec/lib/gitlab/git/repository_spec.rb | 1 - 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 26ad40c3eed..3a9740092cd 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -58,11 +58,6 @@ module Gitlab end end - # Alias to old method for compatibility - def raw - rugged - end - def rugged @rugged ||= Rugged::Repository.new(path, alternates: alternate_object_directories) rescue Rugged::RepositoryError, Rugged::OSError diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 730fdb112d9..26d7a364f5b 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -114,7 +114,7 @@ describe Gitlab::Git::Commit, seed_helper: true do describe '.find' do it "should return first head commit if without params" do expect(Gitlab::Git::Commit.last(repository).id).to eq( - repository.raw.head.target.oid + repository.rugged.head.target.oid ) end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index e375dbfc078..cadca6cee23 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -22,7 +22,6 @@ describe Gitlab::Git::Repository, seed_helper: true do describe "Respond to" do subject { repository } - it { is_expected.to respond_to(:raw) } it { is_expected.to respond_to(:rugged) } it { is_expected.to respond_to(:root_ref) } it { is_expected.to respond_to(:tags) } -- cgit v1.2.1 From bb5f79d43e0bb20bacd5ecd8b66832d9857bd079 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 3 Aug 2017 00:43:51 +0800 Subject: Don't include EmailHelpers manually, pick with rspec `:mailer` is needed to pick it easily, while `type: :mailer` is needed for picking it automatically for tests located in spec/mailers/*_spec.rb It's a bit complicated in spec/services/notification_service_spec.rb but we'll leave it alone for now. --- spec/models/ci/pipeline_spec.rb | 6 +----- spec/models/deploy_key_spec.rb | 4 +--- spec/models/gpg_key_spec.rb | 4 +--- spec/models/key_spec.rb | 4 +--- spec/models/project_services/pipelines_email_service_spec.rb | 8 +------- spec/requests/api/issues_spec.rb | 4 +--- spec/requests/api/v3/issues_spec.rb | 4 +--- spec/services/issues/update_service_spec.rb | 4 +--- spec/services/merge_requests/update_service_spec.rb | 4 +--- spec/services/notification_service_spec.rb | 11 ++++------- spec/support/notify_shared_examples.rb | 1 - spec/support/updating_mentions_shared_examples.rb | 2 -- spec/workers/emails_on_push_worker_spec.rb | 6 +----- spec/workers/pipeline_notification_worker_spec.rb | 4 +--- 14 files changed, 15 insertions(+), 51 deletions(-) diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index f63ff19c2fc..ac75c6501ee 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' -describe Ci::Pipeline do - include EmailHelpers - +describe Ci::Pipeline, :mailer do let(:user) { create(:user) } let(:project) { create(:project) } @@ -1248,8 +1246,6 @@ describe Ci::Pipeline do pipeline.user.global_notification_setting .update(level: 'custom', failed_pipeline: true, success_pipeline: true) - reset_delivered_emails! - perform_enqueued_jobs do pipeline.enqueue pipeline.run diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb index 2aece75b817..3d7283e2164 100644 --- a/spec/models/deploy_key_spec.rb +++ b/spec/models/deploy_key_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' -describe DeployKey do - include EmailHelpers - +describe DeployKey, :mailer do describe "Associations" do it { is_expected.to have_many(:deploy_keys_projects) } it { is_expected.to have_many(:projects) } diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index 59c074199db..e48f20bf53b 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -114,9 +114,7 @@ describe GpgKey do end end - describe 'notification' do - include EmailHelpers - + describe 'notification', :mailer do let(:user) { create(:user) } it 'sends a notification' do diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index d41717d0223..882522c0a97 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' -describe Key do - include EmailHelpers - +describe Key, :mailer do describe "Associations" do it { is_expected.to belong_to(:user) } end diff --git a/spec/models/project_services/pipelines_email_service_spec.rb b/spec/models/project_services/pipelines_email_service_spec.rb index 03932895b0e..5faab9ba38b 100644 --- a/spec/models/project_services/pipelines_email_service_spec.rb +++ b/spec/models/project_services/pipelines_email_service_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' -describe PipelinesEmailService do - include EmailHelpers - +describe PipelinesEmailService, :mailer do let(:pipeline) do create(:ci_pipeline, project: project, sha: project.commit('master').sha) end @@ -14,10 +12,6 @@ describe PipelinesEmailService do Gitlab::DataBuilder::Pipeline.build(pipeline) end - before do - reset_delivered_emails! - end - describe 'Validations' do context 'when service is active' do before do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 60687db9316..7d120e4a234 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' -describe API::Issues do - include EmailHelpers - +describe API::Issues, :mailer do set(:user) { create(:user) } set(:project) do create(:project, :public, creator_id: user.id, namespace: user.namespace) diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb index b092c863c8a..9eb538c4b09 100644 --- a/spec/requests/api/v3/issues_spec.rb +++ b/spec/requests/api/v3/issues_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' -describe API::V3::Issues do - include EmailHelpers - +describe API::V3::Issues, :mailer do let(:user) { create(:user) } let(:user2) { create(:user) } let(:non_member) { create(:user) } diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index ff0d876e6da..814e2cfbed0 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -1,9 +1,7 @@ # coding: utf-8 require 'spec_helper' -describe Issues::UpdateService do - include EmailHelpers - +describe Issues::UpdateService, :mailer do let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index dd3ac9c4ac6..9368594bc86 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' -describe MergeRequests::UpdateService do - include EmailHelpers - +describe MergeRequests::UpdateService, :mailer do let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:user2) { create(:user) } diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 882ee7751b5..5354591642b 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' -describe NotificationService do - include EmailHelpers - +describe NotificationService, :mailer do let(:notification) { described_class.new } let(:assignee) { create(:user) } @@ -14,7 +12,6 @@ describe NotificationService do shared_examples 'notifications for new mentions' do def send_notifications(*new_mentions) - reset_delivered_emails! notification.send(notification_method, mentionable, new_mentions, @u_disabled) end @@ -137,12 +134,11 @@ describe NotificationService do describe '#new_note' do it do add_users_with_subscription(note.project, issue) + reset_delivered_emails! # Ensure create SentNotification by noteable = issue 6 times, not noteable = note expect(SentNotification).to receive(:record).with(issue, any_args).exactly(8).times - reset_delivered_emails! - notification.new_note(note) should_email(@u_watcher) @@ -165,9 +161,10 @@ describe NotificationService do it "emails the note author if they've opted into notifications about their activity" do add_users_with_subscription(note.project, issue) - note.author.notified_of_own_activity = true reset_delivered_emails! + note.author.notified_of_own_activity = true + notification.new_note(note) should_email(note.author) diff --git a/spec/support/notify_shared_examples.rb b/spec/support/notify_shared_examples.rb index d6117d604f2..136f92c6419 100644 --- a/spec/support/notify_shared_examples.rb +++ b/spec/support/notify_shared_examples.rb @@ -7,7 +7,6 @@ shared_context 'gitlab email notification' do let(:new_user_address) { 'newguy@example.com' } before do - reset_delivered_emails! email = recipient.emails.create(email: "notifications@example.com") recipient.update_attribute(:notification_email, email.email) stub_incoming_email_setting(enabled: true, address: "reply+%{key}@#{Gitlab.config.gitlab.host}") diff --git a/spec/support/updating_mentions_shared_examples.rb b/spec/support/updating_mentions_shared_examples.rb index eeec3e1d79b..565d3203e4f 100644 --- a/spec/support/updating_mentions_shared_examples.rb +++ b/spec/support/updating_mentions_shared_examples.rb @@ -7,8 +7,6 @@ RSpec.shared_examples 'updating mentions' do |service_class| end def update_mentionable(opts) - reset_delivered_emails! - perform_enqueued_jobs do service_class.new(project, user, opts).execute(mentionable) end diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb index 5b6b38e0f76..318aad4bc1e 100644 --- a/spec/workers/emails_on_push_worker_spec.rb +++ b/spec/workers/emails_on_push_worker_spec.rb @@ -1,8 +1,7 @@ require 'spec_helper' -describe EmailsOnPushWorker do +describe EmailsOnPushWorker, :mailer do include RepoHelpers - include EmailHelpers include EmailSpec::Matchers let(:project) { create(:project, :repository) } @@ -90,7 +89,6 @@ describe EmailsOnPushWorker do context "when there is an SMTP error" do before do - reset_delivered_emails! allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError) allow(subject).to receive_message_chain(:logger, :info) perform @@ -114,8 +112,6 @@ describe EmailsOnPushWorker do allow_any_instance_of(Mail::TestMailer).to receive(:deliver!).and_wrap_original do |original, mail| original.call(Mail.new(mail.encoded)) end - - reset_delivered_emails! end it "sends the mail to each of the recipients" do diff --git a/spec/workers/pipeline_notification_worker_spec.rb b/spec/workers/pipeline_notification_worker_spec.rb index 139032d77bd..eb539ffd893 100644 --- a/spec/workers/pipeline_notification_worker_spec.rb +++ b/spec/workers/pipeline_notification_worker_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' -describe PipelineNotificationWorker do - include EmailHelpers - +describe PipelineNotificationWorker, :mailer do let(:pipeline) { create(:ci_pipeline) } describe '#execute' do -- cgit v1.2.1 From 00c9c6b458f3aff3d378aa53a3c3652f04368fc9 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 3 Aug 2017 15:58:36 +0200 Subject: Remove unused methods --- lib/gitlab/git/repository.rb | 77 -------------------------------------------- 1 file changed, 77 deletions(-) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 3a9740092cd..779c6117bb2 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -849,46 +849,6 @@ module Gitlab submodule_data.select { |path, data| data['id'] } end - # Returns true if +commit+ introduced changes to +path+, using commit - # trees to make that determination. Uses the history simplification - # rules that `git log` uses by default, where a commit is omitted if it - # is TREESAME to any parent. - # - # If the +follow+ option is true and the file specified by +path+ was - # renamed, then the path value is set to the old path. - def commit_touches_path?(commit, path, follow, walker) - entry = tree_entry(commit, path) - - if commit.parents.empty? - # This is the root commit, return true if it has +path+ in its tree - return !entry.nil? - end - - num_treesame = 0 - commit.parents.each do |parent| - parent_entry = tree_entry(parent, path) - - # Only follow the first TREESAME parent for merge commits - if num_treesame > 0 - walker.hide(parent) - next - end - - if entry.nil? && parent_entry.nil? - num_treesame += 1 - elsif entry && parent_entry && entry[:oid] == parent_entry[:oid] - num_treesame += 1 - end - end - - case num_treesame - when 0 - detect_rename(commit, commit.parents.first, path) if follow - true - else false - end - end - # Find the entry for +path+ in the tree for +commit+ def tree_entry(commit, path) pathname = Pathname.new(path) @@ -916,43 +876,6 @@ module Gitlab tmp_entry end - # Compare +commit+ and +parent+ for +path+. If +path+ is a file and was - # renamed in +commit+, then set +path+ to the old filename. - def detect_rename(commit, parent, path) - diff = parent.diff(commit, paths: [path], disable_pathspec_match: true) - - # If +path+ is a filename, not a directory, then we should only have - # one delta. We don't need to follow renames for directories. - return nil if diff.each_delta.count > 1 - - delta = diff.each_delta.first - if delta.added? - full_diff = parent.diff(commit) - full_diff.find_similar! - - full_diff.each_delta do |full_delta| - if full_delta.renamed? && path == full_delta.new_file[:path] - # Look for the old path in ancestors - path.replace(full_delta.old_file[:path]) - end - end - end - end - - # Returns true if the index entry has the special file mode that denotes - # a submodule. - def submodule?(index_entry) - index_entry[:mode] == 57344 - end - - # Return a Rugged::Index that has read from the tree at +ref_name+ - def populated_index(ref_name) - commit = rev_parse_target(ref_name) - index = rugged.index - index.read_tree(commit.tree) - index - end - # Return the Rugged patches for the diff between +from+ and +to+. def diff_patches(from, to, options = {}, *paths) options ||= {} -- cgit v1.2.1 From fbdfafe6ed469fdca9a735428b4636e74b1dd255 Mon Sep 17 00:00:00 2001 From: Abubakar Ango Date: Thu, 3 Aug 2017 14:07:16 +0000 Subject: Update reply_by_email_postfix_setup.md, included starting courier-authdaemon after installation. --- doc/administration/reply_by_email_postfix_setup.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/administration/reply_by_email_postfix_setup.md b/doc/administration/reply_by_email_postfix_setup.md index 3b8c716eff5..a1bb3851951 100644 --- a/doc/administration/reply_by_email_postfix_setup.md +++ b/doc/administration/reply_by_email_postfix_setup.md @@ -177,6 +177,20 @@ Courier, which we will install later to add IMAP authentication, requires mailbo ```sh sudo apt-get install courier-imap ``` + + And start `imapd`: + ```sh + imapd start + ``` + +1. The courier-authdaemon isn't started after installation. Without it, imap authentication will fail: + ```sh + sudo service courier-authdaemon start + ``` + You can also configure courier-authdaemon to start on boot: + ```sh + sudo systemctl enable courier-authdaemon + ``` ## Configure Postfix to receive email from the internet -- cgit v1.2.1 From a723cba57493ec3220596ca8543a8b1b1ec118fe Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Fri, 21 Apr 2017 11:36:34 +0200 Subject: Avoid plucking Todo ids and use sub-queries instead TodoService should not call `.select(&:id)` on todos, because this is bad performance. So instead use sub-queries, which will result in a single SQL query to the database. https://docs.gitlab.com/ee/development/sql.html#plucking-ids --- app/controllers/dashboard/todos_controller.rb | 4 +-- app/services/issuable_base_service.rb | 2 +- app/services/todo_service.rb | 16 +++++++---- .../unreleased/tc-no-todo-service-select.yml | 4 +++ lib/api/todos.rb | 6 ++-- lib/api/v3/todos.rb | 6 ++-- spec/services/todo_service_spec.rb | 33 ++++++++++++++++++---- 7 files changed, 51 insertions(+), 20 deletions(-) create mode 100644 changelogs/unreleased/tc-no-todo-service-select.yml diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index 59e5b5e4775..a8b2b93b458 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -13,7 +13,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController end def destroy - TodoService.new.mark_todos_as_done_by_ids([params[:id]], current_user) + TodoService.new.mark_todos_as_done_by_ids(params[:id], current_user) respond_to do |format| format.html do @@ -37,7 +37,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController end def restore - TodoService.new.mark_todos_as_pending_by_ids([params[:id]], current_user) + TodoService.new.mark_todos_as_pending_by_ids(params[:id], current_user) render json: todos_counts end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index ea497729115..760a15e3ed0 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -288,7 +288,7 @@ class IssuableBaseService < BaseService todo_service.mark_todo(issuable, current_user) when 'done' todo = TodosFinder.new(current_user).execute.find_by(target: issuable) - todo_service.mark_todos_as_done([todo], current_user) if todo + todo_service.mark_todos_as_done_by_ids(todo, current_user) if todo end end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 322c6286365..6ee96d6a0f8 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -170,20 +170,22 @@ class TodoService # When user marks some todos as done def mark_todos_as_done(todos, current_user) - update_todos_state_by_ids(todos.select(&:id), current_user, :done) + update_todos_state(todos, current_user, :done) end def mark_todos_as_done_by_ids(ids, current_user) - update_todos_state_by_ids(ids, current_user, :done) + todos = todos_by_ids(ids, current_user) + mark_todos_as_done(todos, current_user) end # When user marks some todos as pending def mark_todos_as_pending(todos, current_user) - update_todos_state_by_ids(todos.select(&:id), current_user, :pending) + update_todos_state(todos, current_user, :pending) end def mark_todos_as_pending_by_ids(ids, current_user) - update_todos_state_by_ids(ids, current_user, :pending) + todos = todos_by_ids(ids, current_user) + mark_todos_as_pending(todos, current_user) end # When user marks an issue as todo @@ -198,9 +200,11 @@ class TodoService private - def update_todos_state_by_ids(ids, current_user, state) - todos = current_user.todos.where(id: ids) + def todos_by_ids(ids, current_user) + current_user.todos.where(id: Array(ids)) + end + def update_todos_state(todos, current_user, state) # Only update those that are not really on that state todos = todos.where.not(state: state) todos_ids = todos.pluck(:id) diff --git a/changelogs/unreleased/tc-no-todo-service-select.yml b/changelogs/unreleased/tc-no-todo-service-select.yml new file mode 100644 index 00000000000..ddcae334aa7 --- /dev/null +++ b/changelogs/unreleased/tc-no-todo-service-select.yml @@ -0,0 +1,4 @@ +--- +title: Avoid plucking Todo ids in TodoService +merge_request: 10845 +author: diff --git a/lib/api/todos.rb b/lib/api/todos.rb index d1f7e364029..55191169dd4 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -59,10 +59,10 @@ module API requires :id, type: Integer, desc: 'The ID of the todo being marked as done' end post ':id/mark_as_done' do - todo = current_user.todos.find(params[:id]) - TodoService.new.mark_todos_as_done([todo], current_user) + TodoService.new.mark_todos_as_done_by_ids(params[:id], current_user) + todo = Todo.find(params[:id]) - present todo.reload, with: Entities::Todo, current_user: current_user + present todo, with: Entities::Todo, current_user: current_user end desc 'Mark all todos as done' diff --git a/lib/api/v3/todos.rb b/lib/api/v3/todos.rb index e3b311d61cd..2f2cf259987 100644 --- a/lib/api/v3/todos.rb +++ b/lib/api/v3/todos.rb @@ -11,10 +11,10 @@ module API requires :id, type: Integer, desc: 'The ID of the todo being marked as done' end delete ':id' do - todo = current_user.todos.find(params[:id]) - TodoService.new.mark_todos_as_done([todo], current_user) + TodoService.new.mark_todos_as_done_by_ids(params[:id], current_user) + todo = Todo.find(params[:id]) - present todo.reload, with: ::API::Entities::Todo, current_user: current_user + present todo, with: ::API::Entities::Todo, current_user: current_user end desc 'Mark all todos as done' diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 534d3e65be2..80d05451e09 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -336,7 +336,7 @@ describe TodoService do describe '#mark_todos_as_done' do it_behaves_like 'updating todos state', :mark_todos_as_done, :pending, :done do - let(:collection) { [first_todo, second_todo] } + let(:collection) { Todo.all } end end @@ -348,7 +348,7 @@ describe TodoService do describe '#mark_todos_as_pending' do it_behaves_like 'updating todos state', :mark_todos_as_pending, :done, :pending do - let(:collection) { [first_todo, second_todo] } + let(:collection) { Todo.all } end end @@ -880,14 +880,16 @@ describe TodoService do it 'marks an array of todos as done' do todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) - expect { described_class.new.mark_todos_as_done([todo], john_doe) } + todos = TodosFinder.new(john_doe, {}).execute + expect { described_class.new.mark_todos_as_done(todos, john_doe) } .to change { todo.reload.state }.from('pending').to('done') end it 'returns the ids of updated todos' do # Needed on API todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) - expect(described_class.new.mark_todos_as_done([todo], john_doe)).to eq([todo.id]) + todos = TodosFinder.new(john_doe, {}).execute + expect(described_class.new.mark_todos_as_done(todos, john_doe)).to eq([todo.id]) end context 'when some of the todos are done already' do @@ -907,11 +909,32 @@ describe TodoService do expect(described_class.new.mark_todos_as_done(Todo.all, john_doe)).to eq([]) end end + end + + describe '#mark_todos_as_done_by_ids' do + let(:issue) { create(:issue, project: project, author: author, assignees: [john_doe]) } + let(:another_issue) { create(:issue, project: project, author: author, assignees: [john_doe]) } + + it 'marks an array of todo ids as done' do + todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) + another_todo = create(:todo, :mentioned, user: john_doe, target: another_issue, project: project) + + expect { described_class.new.mark_todos_as_done_by_ids([todo.id, another_todo.id], john_doe) } + .to change { john_doe.todos.done.count }.from(0).to(2) + end + + it 'marks a single todo id as done' do + todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) + + expect { described_class.new.mark_todos_as_done_by_ids(todo.id, john_doe) } + .to change { todo.reload.state }.from('pending').to('done') + end it 'caches the number of todos of a user', :use_clean_rails_memory_store_caching do create(:todo, :mentioned, user: john_doe, target: issue, project: project) todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) - described_class.new.mark_todos_as_done([todo], john_doe) + + described_class.new.mark_todos_as_done_by_ids(todo, john_doe) expect_any_instance_of(TodosFinder).not_to receive(:execute) -- cgit v1.2.1 From a488fc0add73963c4839f49a7fe0f7b5b014d15a Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Fri, 14 Jul 2017 16:56:36 +0200 Subject: Add workaround for UPDATE with subquery when using MySQL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When trying to run an UPDATE, this query is ran: ```sql UPDATE `todos` INNER JOIN `projects` ON `projects`.`id` = `todos`.`project_id` SET `todos`.`state` = 'done' WHERE `todos`.`user_id` = 4 AND (`todos`.`state` IN ('pending')) AND (EXISTS (SELECT 1 FROM `project_authorizations` WHERE `project_authorizations`.`user_id` = 4 AND (project_authorizations.project_id = projects.id)) OR projects.visibility_level IN (10, 20)) AND `projects`.`id` IN (SELECT `todos`.`project_id` FROM `todos` WHERE `todos`.`user_id` = 4 AND (`todos`.`state` IN ('pending'))) AND (`todos`.`state` != 'done') ``` But MySQL does not like the subquery used to filter on `projects.id IN (SELECT ...` Because the subquery queries from the same table: > Error: You can’t specify target table ‘todos’ for update in FROM clause So as workaround, wrap it in another subquery, where the original subquery is aliased using the `AS` statement. Mostly inspired by https://stackoverflow.com/a/43610081/89376 --- app/finders/todos_finder.rb | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 3fe37c75381..b276116f0c6 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -95,9 +95,18 @@ class TodosFinder @project end + def project_ids(items) + ids = items.except(:order).select(:project_id) + if Gitlab::Database.mysql? + # To make UPDATE work on MySQL, wrap it in a SELECT with an alias + ids = Todo.except(:order).select('*').from("(#{ids.to_sql}) AS t") + end + + ids + end + def projects(items) - item_project_ids = items.reorder(nil).select(:project_id) - ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids).execute + ProjectsFinder.new(current_user: current_user, project_ids_relation: project_ids(items)).execute end def type? -- cgit v1.2.1 From e15d339a5cca13bc476b87cbac89975e7a76e37b Mon Sep 17 00:00:00 2001 From: Toon Claes Date: Mon, 8 May 2017 14:28:15 +0200 Subject: Add spec to test combining API merge_requests query parameters To investigate gitlab-org/gitlab-ce#31599 --- spec/requests/api/merge_requests_spec.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index d8dfe71342d..9eda6836ded 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -293,6 +293,26 @@ describe API::MergeRequests do expect(json_response.length).to eq(0) end + it 'returns an array of labeled merge requests that are merged for a milestone' do + bug_label = create(:label, title: 'bug', color: '#FFAABB', project: project) + + mr1 = create(:merge_request, state: "merged", source_project: project, target_project: project, milestone: milestone) + mr2 = create(:merge_request, state: "merged", source_project: project, target_project: project, milestone: milestone1) + mr3 = create(:merge_request, state: "closed", source_project: project, target_project: project, milestone: milestone1) + _mr = create(:merge_request, state: "merged", source_project: project, target_project: project, milestone: milestone1) + + create(:label_link, label: bug_label, target: mr1) + create(:label_link, label: bug_label, target: mr2) + create(:label_link, label: bug_label, target: mr3) + + get api("/projects/#{project.id}/merge_requests?labels=#{bug_label.title}&milestone=#{milestone1.title}&state=merged", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(mr2.id) + end + context "with ordering" do before do @mr_later = mr_with_later_created_and_updated_at_time -- cgit v1.2.1 From f7a61c4a444cafd52fa8b90cc3c0a700dcefc26b Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 3 Aug 2017 10:47:11 -0400 Subject: Ensure DeviseHelpers is loaded before inclusion in LoginHelpers --- spec/support/login_helpers.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index c714d1b08a6..3e117530151 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -1,3 +1,5 @@ +require_relative 'devise_helpers' + module LoginHelpers include DeviseHelpers -- cgit v1.2.1 From 25a6a0725c5388872cba4df880be6bfe9f6c1340 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 3 Aug 2017 16:48:11 +0200 Subject: Migration link for Repository#log --- lib/gitlab/git/repository.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 779c6117bb2..b0f51a96718 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -291,6 +291,7 @@ module Gitlab # after: Time.new(2016, 4, 21, 14, 32, 10) # ) # + # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/446 def log(options) raw_log(options).map { |c| Commit.decorate(c) } end -- cgit v1.2.1 From 86a66647a7a3ee926772bdc055b089e4e4c8447f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 3 Aug 2017 16:23:04 +0100 Subject: Increase the z-index of the navbar Closes #35935 --- app/assets/stylesheets/framework/header.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 1c4238bc564..15de46dc5a9 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -24,7 +24,7 @@ header { &.navbar-gitlab { padding: 0 16px; - z-index: 400; + z-index: 2000; margin-bottom: 0; min-height: $header-height; background-color: $gray-light; -- cgit v1.2.1 From 85582b0537c50304f30b71a0de61954b1f6c508a Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 27 Jul 2017 11:01:09 +0100 Subject: Moved changed files into a dropdown This makes navigating through diff files quickly. Currently we just toggle a list, which could be pretty big. This moves it into a dropdown to make it much easier. Also includes a filter bar to quickly search for certain files/extensions. Closes #29778 --- app/assets/javascripts/merge_request_tabs.js | 9 ++++ app/assets/stylesheets/pages/diff.scss | 17 +++++- app/helpers/diff_helper.rb | 18 +++++++ app/views/projects/diffs/_stats.html.haml | 60 +++++++++------------- .../unreleased/diff-changed-files-dropdown.yml | 4 ++ 5 files changed, 70 insertions(+), 38 deletions(-) create mode 100644 changelogs/unreleased/diff-changed-files-dropdown.yml diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 7840f05a8ae..d3faa2d8e51 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -266,6 +266,8 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; const $container = $('#diffs'); $container.html(data.html); + this.initChangesDropdown(); + if (typeof gl.diffNotesCompileComponents !== 'undefined') { gl.diffNotesCompileComponents(); } @@ -314,6 +316,13 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; }); } + initChangesDropdown() { + $('.js-diff-stats-dropdown').glDropdown({ + filterable: true, + remoteFilter: false, + }); + } + // Show or hide the loading spinner // // status - Boolean, true to show, false to hide diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 398fd4444ea..972e6c7425f 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -395,12 +395,11 @@ background-color: transparent; border: 0; color: $gl-link-color; - transition: color 0.1s linear; + font-weight: 600; &:hover, &:focus { outline: none; - text-decoration: underline; color: $gl-link-hover-color; } } @@ -559,3 +558,17 @@ outline: 0; } } + +.diff-file-changes { + width: 450px; + z-index: 150; + + a { + padding-top: 8px; + padding-bottom: 8px; + } +} + +.diff-file-changes-path { + @include str-truncated(78%); +} diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 91ddd73fac1..087f7f88fb5 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -148,6 +148,24 @@ module DiffHelper options end + def diff_file_changed_icon(diff_file) + if diff_file.deleted_file? || diff_file.renamed_file? + "minus" + elsif diff_file.new_file? + "plus" + else + "adjust" + end + end + + def diff_file_changed_icon_color(diff_file) + if diff_file.deleted_file? + "cred" + elsif diff_file.new_file? + "cgreen" + end + end + private def diff_btn(title, name, selected) diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index e69c7f20d49..4c7af37e305 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -1,36 +1,24 @@ -.js-toggle-container - .commit-stat-summary - Showing - %button.diff-stats-summary-toggler.js-toggle-button{ type: "button" } - %strong= pluralize(diff_files.size, "changed file") - with - %strong.cgreen #{diff_files.sum(&:added_lines)} additions - and - %strong.cred #{diff_files.sum(&:removed_lines)} deletions - .file-stats.js-toggle-content.hide - %ul - - diff_files.each do |diff_file| - - file_hash = hexdigest(diff_file.file_path) - %li - - if diff_file.deleted_file? - %span.deleted-file - %a{ href: "##{file_hash}" } - %i.fa.fa-minus - = diff_file.old_path - - elsif diff_file.renamed_file? - %span.renamed-file - %a{ href: "##{file_hash}" } - %i.fa.fa-minus - = diff_file.old_path - → - = diff_file.new_path - - elsif diff_file.new_file? - %span.new-file - %a{ href: "##{file_hash}" } - %i.fa.fa-plus - = diff_file.new_path - - else - %span.edit-file - %a{ href: "##{file_hash}" } - %i.fa.fa-adjust - = diff_file.new_path +.commit-stat-summary.dropdown + Showing + %button.diff-stats-summary-toggler.js-diff-stats-dropdown{ type: "button", data: { toggle: "dropdown" } }< + = pluralize(diff_files.size, "changed file") + = icon("caret-down fw") + with + %strong.cgreen #{diff_files.sum(&:added_lines)} additions + and + %strong.cred #{diff_files.sum(&:removed_lines)} deletions + .dropdown-menu.diff-file-changes + = dropdown_filter("Search file") + .dropdown-content + %ul + - diff_files.each do |diff_file| + - file_hash = hexdigest(diff_file.file_path) + - added_lines = diff_file.added_lines + - removed_lines = diff_file.removed_lines + %li + %a{ href: "##{file_hash}", title: diff_file.new_path } + = icon("#{diff_file_changed_icon(diff_file)} fw", class: "#{diff_file_changed_icon_color(diff_file)} append-right-5") + %span.diff-file-changes-path= diff_file.new_path + .pull-right + %span.cgreen= "+#{added_lines}" + %span.cred= "-#{removed_lines}" diff --git a/changelogs/unreleased/diff-changed-files-dropdown.yml b/changelogs/unreleased/diff-changed-files-dropdown.yml new file mode 100644 index 00000000000..2d2a26ffea2 --- /dev/null +++ b/changelogs/unreleased/diff-changed-files-dropdown.yml @@ -0,0 +1,4 @@ +--- +title: Moved diff changed files into a dropdown +merge_request: +author: -- cgit v1.2.1 From 655510ec9a658c28f50ccb0caea394f5db7cae59 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 27 Jul 2017 15:30:57 +0100 Subject: fixed haml lint --- app/assets/javascripts/gl_dropdown.js | 6 ++++++ app/assets/stylesheets/framework/dropdowns.scss | 1 + app/views/projects/diffs/_diffs.html.haml | 2 +- app/views/projects/diffs/_stats.html.haml | 6 ++++-- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 9475498e176..85d720c3a69 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -731,9 +731,15 @@ GitLabDropdown = (function() { GitLabDropdown.prototype.focusTextInput = function(triggerFocus = false) { if (this.options.filterable) { this.dropdown.one('transitionend', () => { + const initialScrollTop = $(window).scrollTop(); + if (this.dropdown.is('.open')) { this.filterInput.focus(); } + + if ($(window).scrollTop() < initialScrollTop) { + $(window).scrollTop(initialScrollTop); + } }); if (triggerFocus) { diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 572203bce34..9b8176a4a90 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -574,6 +574,7 @@ .dropdown-input-field, .default-dropdown-input { + display: block; width: 100%; min-height: 30px; padding: 0 7px; diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index f9385459a66..8c8aa4c78f5 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -3,7 +3,7 @@ - can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project) - diff_files = diffs.diff_files -.content-block.oneline-block.files-changed +.content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed .inline-parallel-buttons - if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? } = link_to 'Expand all', url_for(params.merge(expanded: 1, format: nil)), class: 'btn btn-default' diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 4c7af37e305..307311a090f 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -20,5 +20,7 @@ = icon("#{diff_file_changed_icon(diff_file)} fw", class: "#{diff_file_changed_icon_color(diff_file)} append-right-5") %span.diff-file-changes-path= diff_file.new_path .pull-right - %span.cgreen= "+#{added_lines}" - %span.cred= "-#{removed_lines}" + %span.cgreen< + +#{added_lines} + %span.cred< + \-#{removed_lines} -- cgit v1.2.1 From b507682d6e799f737c05f27201dc4a0dbe3aba1d Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 27 Jul 2017 15:53:04 +0100 Subject: made the changed files holder sticky --- app/assets/javascripts/lib/utils/sticky.js | 21 ++++++++++++++ app/assets/javascripts/merge_request_tabs.js | 3 ++ app/assets/stylesheets/pages/diff.scss | 41 ++++++++++++++++++++++++++++ app/views/projects/diffs/_stats.html.haml | 16 ++++++++--- 4 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 app/assets/javascripts/lib/utils/sticky.js diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js new file mode 100644 index 00000000000..f53acaa17b1 --- /dev/null +++ b/app/assets/javascripts/lib/utils/sticky.js @@ -0,0 +1,21 @@ +export const isSticky = (el, stickyTop) => { + const top = el.getBoundingClientRect().top; + + if (top === stickyTop) { + el.classList.add('is-stuck'); + } else { + el.classList.remove('is-stuck'); + } +}; + +export default (el) => { + const computedStyle = window.getComputedStyle(el); + + if (!/sticky/.test(computedStyle.position)) return; + + const stickyTop = parseInt(computedStyle.top, 10); + + document.addEventListener('scroll', () => isSticky(el, stickyTop), { + passive: true, + }); +}; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index d3faa2d8e51..4ffd71d9de5 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -7,6 +7,7 @@ import Cookies from 'js-cookie'; import './breakpoints'; import './flash'; import BlobForkSuggestion from './blob/blob_fork_suggestion'; +import stickyMonitor from './lib/utils/sticky'; /* eslint-disable max-len */ // MergeRequestTabs @@ -268,6 +269,8 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; this.initChangesDropdown(); + stickyMonitor(document.querySelector('.js-diff-files-changed')); + if (typeof gl.diffNotesCompileComponents !== 'undefined') { gl.diffNotesCompileComponents(); } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 972e6c7425f..7d765f45812 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -559,6 +559,47 @@ } } +.diff-files-changed { + .commit-stat-summary { + z-index: -1; + } + + @media (min-width: $screen-sm-min) { + position: -webkit-sticky; + position: sticky; + top: 100px; + background-color: $white-light; + z-index: 190; + + + .files { + margin-top: 1px; + } + + .diff-stats-additions-deletions-collapsed { + display: none; + } + + &.is-stuck { + padding-top: 0; + padding-bottom: 0; + border-bottom: 1px solid $white-dark; + + .diff-stats-additions-deletions-expanded, + .inline-parallel-buttons { + display: none; + } + + .diff-stats-additions-deletions-collapsed { + display: block; + } + + + .files { + margin-top: 16px; + } + } + } +} + .diff-file-changes { width: 450px; z-index: 150; diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 307311a090f..4f784f87f2b 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -1,12 +1,20 @@ +- sum_added_lines = diff_files.sum(&:added_lines) +- sum_removed_lines = diff_files.sum(&:removed_lines) .commit-stat-summary.dropdown Showing %button.diff-stats-summary-toggler.js-diff-stats-dropdown{ type: "button", data: { toggle: "dropdown" } }< = pluralize(diff_files.size, "changed file") = icon("caret-down fw") - with - %strong.cgreen #{diff_files.sum(&:added_lines)} additions - and - %strong.cred #{diff_files.sum(&:removed_lines)} deletions + %span.diff-stats-additions-deletions-expanded + with + %strong.cgreen #{sum_added_lines} additions + and + %strong.cred #{sum_removed_lines} deletions + .diff-stats-additions-deletions-collapsed.pull-right{ "aria-hidden": "true" } + %strong.cgreen< + +#{sum_added_lines} + %strong.cred< + \-#{sum_removed_lines} .dropdown-menu.diff-file-changes = dropdown_filter("Search file") .dropdown-content -- cgit v1.2.1 From 30777178e3311c8aa7f4ecae82bd970fda63d85c Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 28 Jul 2017 11:54:51 +0100 Subject: fixed jumping when bar gets stuck added specs to sticky util file added `No files found.` text when results are empty --- app/assets/javascripts/gl_dropdown.js | 6 ++-- app/assets/javascripts/lib/utils/sticky.js | 6 ++-- app/assets/stylesheets/pages/diff.scss | 21 ++++++++---- app/views/projects/diffs/_stats.html.haml | 7 ++-- spec/javascripts/lib/utils/sticky_spec.js | 52 ++++++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 14 deletions(-) create mode 100644 spec/javascripts/lib/utils/sticky_spec.js diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 85d720c3a69..73874363c1f 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -114,7 +114,7 @@ GitLabDropdownFilter = (function() { } else { elements = this.options.elements(); if (search_text) { - return elements.each(function() { + elements.each(function() { var $el, matches; $el = $(this); matches = fuzzaldrinPlus.match($el.text().trim(), search_text); @@ -127,8 +127,10 @@ GitLabDropdownFilter = (function() { } }); } else { - return elements.show().removeClass('option-hidden'); + elements.show().removeClass('option-hidden'); } + + elements.parent().find('.dropdown-menu-empty-link').toggleClass('hidden', elements.is(':visible')); } }; diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js index f53acaa17b1..b5fde040a17 100644 --- a/app/assets/javascripts/lib/utils/sticky.js +++ b/app/assets/javascripts/lib/utils/sticky.js @@ -1,5 +1,5 @@ -export const isSticky = (el, stickyTop) => { - const top = el.getBoundingClientRect().top; +export const isSticky = (el, scrollY, stickyTop) => { + const top = el.offsetTop - scrollY; if (top === stickyTop) { el.classList.add('is-stuck'); @@ -15,7 +15,7 @@ export default (el) => { const stickyTop = parseInt(computedStyle.top, 10); - document.addEventListener('scroll', () => isSticky(el, stickyTop), { + document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop), { passive: true, }); }; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 7d765f45812..f98129bc09d 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -562,12 +562,18 @@ .diff-files-changed { .commit-stat-summary { z-index: -1; + + @media (min-width: $screen-sm-min) { + margin-left: -$gl-padding; + padding-left: $gl-padding; + background-color: $white-light; + } } @media (min-width: $screen-sm-min) { position: -webkit-sticky; position: sticky; - top: 100px; + top: 84px; background-color: $white-light; z-index: 190; @@ -575,7 +581,7 @@ margin-top: 1px; } - .diff-stats-additions-deletions-collapsed { + &:not(.is-stuck) .diff-stats-additions-deletions-collapsed { display: none; } @@ -583,18 +589,15 @@ padding-top: 0; padding-bottom: 0; border-bottom: 1px solid $white-dark; + transform: translateY(16px); .diff-stats-additions-deletions-expanded, .inline-parallel-buttons { display: none; } - .diff-stats-additions-deletions-collapsed { - display: block; - } - + .files { - margin-top: 16px; + margin-top: 30px; } } } @@ -604,6 +607,10 @@ width: 450px; z-index: 150; + @media (min-width: $screen-sm-min) { + left: $gl-padding; + } + a { padding-top: 8px; padding-bottom: 8px; diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 4f784f87f2b..7c2941e6be3 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -5,12 +5,12 @@ %button.diff-stats-summary-toggler.js-diff-stats-dropdown{ type: "button", data: { toggle: "dropdown" } }< = pluralize(diff_files.size, "changed file") = icon("caret-down fw") - %span.diff-stats-additions-deletions-expanded + %span.diff-stats-additions-deletions-expanded#diff-stats with %strong.cgreen #{sum_added_lines} additions and %strong.cred #{sum_removed_lines} deletions - .diff-stats-additions-deletions-collapsed.pull-right{ "aria-hidden": "true" } + .diff-stats-additions-deletions-collapsed.pull-right{ "aria-hidden": "true", "aria-describedby": "diff-stats" } %strong.cgreen< +#{sum_added_lines} %strong.cred< @@ -32,3 +32,6 @@ +#{added_lines} %span.cred< \-#{removed_lines} + %li.dropdown-menu-empty-link.hidden + %a{ href: "#" } + No files found. diff --git a/spec/javascripts/lib/utils/sticky_spec.js b/spec/javascripts/lib/utils/sticky_spec.js new file mode 100644 index 00000000000..c3ee3ef9825 --- /dev/null +++ b/spec/javascripts/lib/utils/sticky_spec.js @@ -0,0 +1,52 @@ +import { isSticky } from '~/lib/utils/sticky'; + +describe('sticky', () => { + const el = { + offsetTop: 0, + classList: {}, + }; + + beforeEach(() => { + el.offsetTop = 0; + el.classList.add = jasmine.createSpy('spy'); + el.classList.remove = jasmine.createSpy('spy'); + }); + + describe('classList.remove', () => { + it('does not call classList.remove when stuck', () => { + isSticky(el, 0, 0); + + expect( + el.classList.remove, + ).not.toHaveBeenCalled(); + }); + + it('calls classList.remove when not stuck', () => { + el.offsetTop = 10; + isSticky(el, 0, 0); + + expect( + el.classList.remove, + ).toHaveBeenCalledWith('is-stuck'); + }); + }); + + describe('classList.add', () => { + it('calls classList.add when stuck', () => { + isSticky(el, 0, 0); + + expect( + el.classList.add, + ).toHaveBeenCalledWith('is-stuck'); + }); + + it('does not call classList.add when not stuck', () => { + el.offsetTop = 10; + isSticky(el, 0, 0); + + expect( + el.classList.add, + ).not.toHaveBeenCalled(); + }); + }); +}); -- cgit v1.2.1 From d86cbe69410a9a20bdeb996435a134c71ee87863 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 28 Jul 2017 12:11:05 +0100 Subject: fixed alignment in button correctly scroll to element with sticky height offset --- app/assets/javascripts/lib/utils/common_utils.js | 10 ++++++++-- app/views/projects/diffs/_stats.html.haml | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 122ec138c59..e916724b666 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -86,8 +86,9 @@ // This is required to handle non-unicode characters in hash hash = decodeURIComponent(hash); - var fixedTabs = document.querySelector('.js-tabs-affix'); - var fixedNav = document.querySelector('.navbar-gitlab'); + const fixedTabs = document.querySelector('.js-tabs-affix'); + const fixedDiffStats = document.querySelector('.js-diff-files-changed.is-stuck'); + const fixedNav = document.querySelector('.navbar-gitlab'); var adjustment = 0; if (fixedNav) adjustment -= fixedNav.offsetHeight; @@ -104,6 +105,11 @@ if (fixedTabs) { adjustment -= fixedTabs.offsetHeight; } + + if (fixedDiffStats) { + adjustment -= fixedDiffStats.offsetHeight; + } + window.scrollBy(0, adjustment); } }; diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 7c2941e6be3..38fccb3d44f 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -4,7 +4,7 @@ Showing %button.diff-stats-summary-toggler.js-diff-stats-dropdown{ type: "button", data: { toggle: "dropdown" } }< = pluralize(diff_files.size, "changed file") - = icon("caret-down fw") + = icon("caret-down", class: "prepend-left-5") %span.diff-stats-additions-deletions-expanded#diff-stats with %strong.cgreen #{sum_added_lines} additions @@ -16,7 +16,7 @@ %strong.cred< \-#{sum_removed_lines} .dropdown-menu.diff-file-changes - = dropdown_filter("Search file") + = dropdown_filter("Search files") .dropdown-content %ul - diff_files.each do |diff_file| -- cgit v1.2.1 From b8770ba98bf5e077a81b8f543a045580088a548e Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 31 Jul 2017 11:23:19 +0100 Subject: fixed karma failure --- app/assets/javascripts/lib/utils/sticky.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js index b5fde040a17..43a808b6ab3 100644 --- a/app/assets/javascripts/lib/utils/sticky.js +++ b/app/assets/javascripts/lib/utils/sticky.js @@ -9,6 +9,8 @@ export const isSticky = (el, scrollY, stickyTop) => { }; export default (el) => { + if (!el) return; + const computedStyle = window.getComputedStyle(el); if (!/sticky/.test(computedStyle.position)) return; -- cgit v1.2.1 From 0c5e41a20fcdafb351eb2e62279b89eed81de426 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 31 Jul 2017 13:50:41 +0100 Subject: fixed z-index issue with merge request versions widget --- app/assets/stylesheets/pages/merge_requests.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 4693b2434c7..a4e19094508 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -691,8 +691,10 @@ } .mr-version-controls { + position: relative; background: $gray-light; color: $gl-text-color; + z-index: 199; .mr-version-menus-container { display: -webkit-flex; -- cgit v1.2.1 From 86d34921fee9ec324fdce3d36a8557604851a619 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 1 Aug 2017 17:56:09 +0100 Subject: dont assign to variable, just output directly --- app/views/projects/diffs/_stats.html.haml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 38fccb3d44f..efc0ea31917 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -20,18 +20,15 @@ .dropdown-content %ul - diff_files.each do |diff_file| - - file_hash = hexdigest(diff_file.file_path) - - added_lines = diff_file.added_lines - - removed_lines = diff_file.removed_lines %li - %a{ href: "##{file_hash}", title: diff_file.new_path } + %a{ href: "##{hexdigest(diff_file.file_path)}", title: diff_file.new_path } = icon("#{diff_file_changed_icon(diff_file)} fw", class: "#{diff_file_changed_icon_color(diff_file)} append-right-5") %span.diff-file-changes-path= diff_file.new_path .pull-right %span.cgreen< - +#{added_lines} + +#{diff_file.added_lines} %span.cred< - \-#{removed_lines} + \-#{diff_file.removed_lines} %li.dropdown-menu-empty-link.hidden %a{ href: "#" } No files found. -- cgit v1.2.1 From 370839cf10828e791893adaf81ab2e39c2b419cb Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 1 Aug 2017 18:04:03 +0100 Subject: fixed alert box moving when is-stuck gets added --- app/assets/stylesheets/pages/diff.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index f98129bc09d..cfc801859a7 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -577,7 +577,8 @@ background-color: $white-light; z-index: 190; - + .files { + + .files, + + .alert { margin-top: 1px; } @@ -596,7 +597,8 @@ display: none; } - + .files { + + .files, + + .alert { margin-top: 30px; } } -- cgit v1.2.1 From 80647364ffaad3d100c64dd8a1ab32e8a9b4bcf9 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Wed, 19 Jul 2017 11:17:29 -0700 Subject: move the ability check to reject_users_without_access --- app/services/notification_recipient_service.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 9ac561e4bd2..1abf0335dc4 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -253,7 +253,6 @@ class NotificationRecipientService end users = users.to_a.compact.uniq - users = users.select { |u| u.can?(:receive_notifications) } users.reject do |user| global_notification_setting = user.global_notification_setting @@ -287,6 +286,8 @@ class NotificationRecipientService end def reject_users_without_access(recipients, target) + recipients = recipients.select { |u| u.can?(:receive_notifications) } + ability = case target when Issuable :"read_#{target.to_ability_name}" -- cgit v1.2.1 From b188e1b9e66eb02ac9b51364dfa33e206ea636c3 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Thu, 20 Jul 2017 11:57:42 -0700 Subject: move notification_setting_for_user_project to a public class method --- app/services/notification_recipient_service.rb | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 1abf0335dc4..07f5655b826 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -4,6 +4,18 @@ class NotificationRecipientService attr_reader :project + def self.notification_setting_for_user_project(user, project) + project_setting = user.notification_settings_for(project) + + return project_setting unless project_setting.global? + + group_setting = user.notification_settings_for(project.group) + + return group_setting unless group_setting.global? + + user.global_notification_setting + end + def initialize(project) @project = project end @@ -55,7 +67,7 @@ class NotificationRecipientService :success_pipeline end - notification_setting = notification_setting_for_user_project(current_user, target.project) + notification_setting = NotificationRecipientService.notification_setting_for_user_project(current_user, target.project) return [] if notification_setting.mention? || notification_setting.disabled? @@ -317,16 +329,4 @@ class NotificationRecipientService def build_custom_key(action, object) "#{action}_#{object.class.model_name.name.underscore}".to_sym end - - def notification_setting_for_user_project(user, project) - project_setting = user.notification_settings_for(project) - - return project_setting unless project_setting.global? - - group_setting = user.notification_settings_for(project.group) - - return group_setting unless group_setting.global? - - user.global_notification_setting - end end -- cgit v1.2.1 From 9cd46811d3dd2e6a0cb1b68ba390f9ab3fc587b7 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Thu, 20 Jul 2017 11:58:30 -0700 Subject: protect against nil project/group/setting --- app/services/notification_recipient_service.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 07f5655b826..e1456158ff7 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -5,13 +5,13 @@ class NotificationRecipientService attr_reader :project def self.notification_setting_for_user_project(user, project) - project_setting = user.notification_settings_for(project) + project_setting = project && user.notification_settings_for(project) - return project_setting unless project_setting.global? + return project_setting unless project_setting.nil? || project_setting.global? - group_setting = user.notification_settings_for(project.group) + group_setting = project&.group && user.notification_settings_for(project.group) - return group_setting unless group_setting.global? + return group_setting unless group_setting.nil? || group_setting.global? user.global_notification_setting end -- cgit v1.2.1 From cce1bc9a2f3bbba5ad2a256c96d85eb2276e6ac6 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Thu, 20 Jul 2017 12:02:12 -0700 Subject: use notification_setting_for_user_project in reject_users --- app/services/notification_recipient_service.rb | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index e1456158ff7..92049add32d 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -267,24 +267,8 @@ class NotificationRecipientService users = users.to_a.compact.uniq users.reject do |user| - global_notification_setting = user.global_notification_setting - - next global_notification_setting.level == level unless project - - setting = user.notification_settings_for(project) - - if project.group && (setting.nil? || setting.global?) - setting = user.notification_settings_for(project.group) - end - - # reject users who globally set mention notification and has no setting per project/group - next global_notification_setting.level == level unless setting - - # reject users who set mention notification in project - next true if setting.level == level - - # reject users who have mention level in project and disabled in global settings - setting.global? && global_notification_setting.level == level + setting = NotificationRecipientService.notification_setting_for_user_project(user, project) + setting.present? && setting.level == level end end -- cgit v1.2.1 From b1334ed94e09f584505f9804566e51a34817c8da Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Thu, 20 Jul 2017 12:21:47 -0700 Subject: move the builders to classes with a #build --- app/services/notification_recipient_service.rb | 476 +++++++++++++------------ 1 file changed, 257 insertions(+), 219 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 92049add32d..97bbc47efb3 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -20,297 +20,335 @@ class NotificationRecipientService @project = project end - def build_recipients(target, current_user, action:, previous_assignee: nil, skip_current_user: true) - custom_action = build_custom_key(action, target) - - recipients = participants(target, current_user) - recipients = add_project_watchers(recipients) - recipients = add_custom_notifications(recipients, custom_action) - recipients = reject_mention_users(recipients) - - # Re-assign is considered as a mention of the new assignee so we add the - # new assignee to the list of recipients after we rejected users with - # the "on mention" notification level - case custom_action - when :reassign_merge_request - recipients << previous_assignee if previous_assignee - recipients << target.assignee - when :reassign_issue - previous_assignees = Array(previous_assignee) - recipients.concat(previous_assignees) - recipients.concat(target.assignees) - end - - recipients = reject_muted_users(recipients) - recipients = add_subscribed_users(recipients, target) + module Builder - if [:new_issue, :new_merge_request].include?(custom_action) - recipients = add_labels_subscribers(recipients, target) - end + class Base + attr_reader :project + def initialize(project) + @project = project + end - recipients = reject_unsubscribed_users(recipients, target) - recipients = reject_users_without_access(recipients, target) + def build(*) + raise 'abstract' + end - recipients.delete(current_user) if skip_current_user && !current_user.notified_of_own_activity? + # Remove users with disabled notifications from array + # Also remove duplications and nil recipients + def reject_muted_users(users) + reject_users(users, :disabled) + end - recipients.uniq - end + protected - def build_pipeline_recipients(target, current_user, action:) - return [] unless current_user + # Ensure that if we modify this array, we aren't modifying the memoised + # participants on the target. + def participants(target, user) + return unless target.respond_to?(:participants) - custom_action = - case action.to_s - when 'failed' - :failed_pipeline - when 'success' - :success_pipeline + target.participants(user).dup end - notification_setting = NotificationRecipientService.notification_setting_for_user_project(current_user, target.project) + # Get project/group users with CUSTOM notification level + def add_custom_notifications(recipients, action) + user_ids = [] - return [] if notification_setting.mention? || notification_setting.disabled? + # Users with a notification setting on group or project + user_ids += user_ids_notifiable_on(project, :custom, action) + user_ids += user_ids_notifiable_on(project.group, :custom, action) - return [] if notification_setting.custom? && !notification_setting.event_enabled?(custom_action) + # Users with global level custom + user_ids_with_project_level_global = user_ids_notifiable_on(project, :global) + user_ids_with_group_level_global = user_ids_notifiable_on(project.group, :global) - return [] if (notification_setting.watch? || notification_setting.participating?) && NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action) + global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global) + user_ids += user_ids_with_global_level_custom(global_users_ids, action) - reject_users_without_access([current_user], target) - end + recipients.concat(User.find(user_ids)) + end - def build_relabeled_recipients(target, current_user, labels:) - recipients = add_labels_subscribers([], target, labels: labels) - recipients = reject_unsubscribed_users(recipients, target) - recipients = reject_users_without_access(recipients, target) - recipients.delete(current_user) unless current_user.notified_of_own_activity? - recipients.uniq - end + def add_project_watchers(recipients) + recipients.concat(project_watchers).compact + end - def build_new_note_recipients(note) - target = note.noteable + # Get project users with WATCH notification level + def project_watchers + project_members_ids = user_ids_notifiable_on(project) - ability, subject = if note.for_personal_snippet? - [:read_personal_snippet, note.noteable] - else - [:read_project, note.project] - end + user_ids_with_project_global = user_ids_notifiable_on(project, :global) + user_ids_with_group_global = user_ids_notifiable_on(project.group, :global) - mentioned_users = note.mentioned_users.select { |user| user.can?(ability, subject) } + user_ids = user_ids_with_global_level_watch((user_ids_with_project_global + user_ids_with_group_global).uniq) - # Add all users participating in the thread (author, assignee, comment authors) - recipients = participants(target, note.author) || mentioned_users + user_ids_with_project_setting = select_project_members_ids(project, user_ids_with_project_global, user_ids) + user_ids_with_group_setting = select_group_members_ids(project.group, project_members_ids, user_ids_with_group_global, user_ids) - unless note.for_personal_snippet? - # Merge project watchers - recipients = add_project_watchers(recipients) + User.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq).to_a + end - # Merge project with custom notification - recipients = add_custom_notifications(recipients, :new_note) - end + # Remove users with notification level 'Mentioned' + def reject_mention_users(users) + reject_users(users, :mention) + end - # Reject users with Mention notification level, except those mentioned in _this_ note. - recipients = reject_mention_users(recipients - mentioned_users) - recipients = recipients + mentioned_users + def add_subscribed_users(recipients, target) + return recipients unless target.respond_to? :subscribers - recipients = reject_muted_users(recipients) + recipients + target.subscribers(project) + end - recipients = add_subscribed_users(recipients, note.noteable) - recipients = reject_unsubscribed_users(recipients, note.noteable) - recipients = reject_users_without_access(recipients, note.noteable) + def user_ids_notifiable_on(resource, notification_level = nil, action = nil) + return [] unless resource - recipients.delete(note.author) unless note.author.notified_of_own_activity? - recipients.uniq - end + if notification_level + settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level]) + settings = settings.select { |setting| setting.event_enabled?(action) } if action.present? + settings.map(&:user_id) + else + resource.notification_settings.pluck(:user_id) + end + end - # Remove users with disabled notifications from array - # Also remove duplications and nil recipients - def reject_muted_users(users) - reject_users(users, :disabled) - end + # Build a list of user_ids based on project notification settings + def select_project_members_ids(project, global_setting, user_ids_global_level_watch) + user_ids = user_ids_notifiable_on(project, :watch) - protected + # If project setting is global, add to watch list if global setting is watch + global_setting.each do |user_id| + if user_ids_global_level_watch.include?(user_id) + user_ids << user_id + end + end - # Ensure that if we modify this array, we aren't modifying the memoised - # participants on the target. - def participants(target, user) - return unless target.respond_to?(:participants) + user_ids + end - target.participants(user).dup - end + # Build a list of user_ids based on group notification settings + def select_group_members_ids(group, project_members, global_setting, user_ids_global_level_watch) + uids = user_ids_notifiable_on(group, :watch) + + # Group setting is watch, add to user_ids list if user is not project member + user_ids = [] + uids.each do |user_id| + if project_members.exclude?(user_id) + user_ids << user_id + end + end + + # Group setting is global, add to user_ids list if global setting is watch + global_setting.each do |user_id| + if project_members.exclude?(user_id) && user_ids_global_level_watch.include?(user_id) + user_ids << user_id + end + end + + user_ids + end - # Get project/group users with CUSTOM notification level - def add_custom_notifications(recipients, action) - user_ids = [] + def user_ids_with_global_level_watch(ids) + settings_with_global_level_of(:watch, ids).pluck(:user_id) + end - # Users with a notification setting on group or project - user_ids += user_ids_notifiable_on(project, :custom, action) - user_ids += user_ids_notifiable_on(project.group, :custom, action) + def user_ids_with_global_level_custom(ids, action) + settings = settings_with_global_level_of(:custom, ids) + settings = settings.select { |setting| setting.event_enabled?(action) } + settings.map(&:user_id) + end - # Users with global level custom - user_ids_with_project_level_global = user_ids_notifiable_on(project, :global) - user_ids_with_group_level_global = user_ids_notifiable_on(project.group, :global) + def settings_with_global_level_of(level, ids) + NotificationSetting.where( + user_id: ids, + source_type: nil, + level: NotificationSetting.levels[level] + ) + end - global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global) - user_ids += user_ids_with_global_level_custom(global_users_ids, action) + # Reject users which has certain notification level + # + # Example: + # reject_users(users, :watch, project) + # + def reject_users(users, level) + level = level.to_s - recipients.concat(User.find(user_ids)) - end + unless NotificationSetting.levels.keys.include?(level) + raise 'Invalid notification level' + end - def add_project_watchers(recipients) - recipients.concat(project_watchers).compact - end + users = users.to_a.compact.uniq - # Get project users with WATCH notification level - def project_watchers - project_members_ids = user_ids_notifiable_on(project) + users.reject do |user| + setting = NotificationRecipientService.notification_setting_for_user_project(user, project) + setting.present? && setting.level == level + end + end - user_ids_with_project_global = user_ids_notifiable_on(project, :global) - user_ids_with_group_global = user_ids_notifiable_on(project.group, :global) + def reject_unsubscribed_users(recipients, target) + return recipients unless target.respond_to? :subscriptions - user_ids = user_ids_with_global_level_watch((user_ids_with_project_global + user_ids_with_group_global).uniq) + recipients.reject do |user| + subscription = target.subscriptions.find_by_user_id(user.id) + subscription && !subscription.subscribed + end + end - user_ids_with_project_setting = select_project_members_ids(project, user_ids_with_project_global, user_ids) - user_ids_with_group_setting = select_group_members_ids(project.group, project_members_ids, user_ids_with_group_global, user_ids) + def reject_users_without_access(recipients, target) + recipients = recipients.select { |u| u.can?(:receive_notifications) } - User.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq).to_a - end + ability = case target + when Issuable + :"read_#{target.to_ability_name}" + when Ci::Pipeline + :read_build # We have build trace in pipeline emails + end - # Remove users with notification level 'Mentioned' - def reject_mention_users(users) - reject_users(users, :mention) - end + return recipients unless ability - def add_subscribed_users(recipients, target) - return recipients unless target.respond_to? :subscribers + recipients.select do |user| + user.can?(ability, target) + end + end - recipients + target.subscribers(project) - end + def add_labels_subscribers(recipients, target, labels: nil) + return recipients unless target.respond_to? :labels - def user_ids_notifiable_on(resource, notification_level = nil, action = nil) - return [] unless resource + (labels || target.labels).each do |label| + recipients += label.subscribers(project) + end - if notification_level - settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level]) - settings = settings.select { |setting| setting.event_enabled?(action) } if action.present? - settings.map(&:user_id) - else - resource.notification_settings.pluck(:user_id) - end - end + recipients + end - # Build a list of user_ids based on project notification settings - def select_project_members_ids(project, global_setting, user_ids_global_level_watch) - user_ids = user_ids_notifiable_on(project, :watch) + # Build event key to search on custom notification level + # Check NotificationSetting::EMAIL_EVENTS + def build_custom_key(action, object) + "#{action}_#{object.class.model_name.name.underscore}".to_sym + end + end - # If project setting is global, add to watch list if global setting is watch - global_setting.each do |user_id| - if user_ids_global_level_watch.include?(user_id) - user_ids << user_id + class Default < Base + def build(target, current_user, action:, previous_assignee: nil, skip_current_user: true) + custom_action = build_custom_key(action, target) + + recipients = participants(target, current_user) + recipients = add_project_watchers(recipients) + recipients = add_custom_notifications(recipients, custom_action) + recipients = reject_mention_users(recipients) + + # Re-assign is considered as a mention of the new assignee so we add the + # new assignee to the list of recipients after we rejected users with + # the "on mention" notification level + case custom_action + when :reassign_merge_request + recipients << previous_assignee if previous_assignee + recipients << target.assignee + when :reassign_issue + previous_assignees = Array(previous_assignee) + recipients.concat(previous_assignees) + recipients.concat(target.assignees) + end + + recipients = reject_muted_users(recipients) + recipients = add_subscribed_users(recipients, target) + + if [:new_issue, :new_merge_request].include?(custom_action) + recipients = add_labels_subscribers(recipients, target) + end + + recipients = reject_unsubscribed_users(recipients, target) + recipients = reject_users_without_access(recipients, target) + + recipients.delete(current_user) if skip_current_user && !current_user.notified_of_own_activity? + + recipients.uniq end end - user_ids - end + class Pipeline < Base + def build(target, current_user, action:) + return [] unless current_user + + custom_action = + case action.to_s + when 'failed' + :failed_pipeline + when 'success' + :success_pipeline + end + + notification_setting = NotificationRecipientService.notification_setting_for_user_project(current_user, target.project) + + return [] if notification_setting.mention? || notification_setting.disabled? - # Build a list of user_ids based on group notification settings - def select_group_members_ids(group, project_members, global_setting, user_ids_global_level_watch) - uids = user_ids_notifiable_on(group, :watch) + return [] if notification_setting.custom? && !notification_setting.event_enabled?(custom_action) - # Group setting is watch, add to user_ids list if user is not project member - user_ids = [] - uids.each do |user_id| - if project_members.exclude?(user_id) - user_ids << user_id + return [] if (notification_setting.watch? || notification_setting.participating?) && NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action) + + reject_users_without_access([current_user], target) end end - # Group setting is global, add to user_ids list if global setting is watch - global_setting.each do |user_id| - if project_members.exclude?(user_id) && user_ids_global_level_watch.include?(user_id) - user_ids << user_id + class Relabeled < Base + def build(target, current_user, labels:) + recipients = add_labels_subscribers([], target, labels: labels) + recipients = reject_unsubscribed_users(recipients, target) + recipients = reject_users_without_access(recipients, target) + recipients.delete(current_user) unless current_user.notified_of_own_activity? + recipients.uniq end end - user_ids - end + class NewNote < Base + def build(note) + target = note.noteable - def user_ids_with_global_level_watch(ids) - settings_with_global_level_of(:watch, ids).pluck(:user_id) - end + ability, subject = if note.for_personal_snippet? + [:read_personal_snippet, note.noteable] + else + [:read_project, note.project] + end - def user_ids_with_global_level_custom(ids, action) - settings = settings_with_global_level_of(:custom, ids) - settings = settings.select { |setting| setting.event_enabled?(action) } - settings.map(&:user_id) - end + mentioned_users = note.mentioned_users.select { |user| user.can?(ability, subject) } - def settings_with_global_level_of(level, ids) - NotificationSetting.where( - user_id: ids, - source_type: nil, - level: NotificationSetting.levels[level] - ) - end + # Add all users participating in the thread (author, assignee, comment authors) + recipients = participants(target, note.author) || mentioned_users - # Reject users which has certain notification level - # - # Example: - # reject_users(users, :watch, project) - # - def reject_users(users, level) - level = level.to_s + unless note.for_personal_snippet? + # Merge project watchers + recipients = add_project_watchers(recipients) - unless NotificationSetting.levels.keys.include?(level) - raise 'Invalid notification level' - end + # Merge project with custom notification + recipients = add_custom_notifications(recipients, :new_note) + end - users = users.to_a.compact.uniq + # Reject users with Mention notification level, except those mentioned in _this_ note. + recipients = reject_mention_users(recipients - mentioned_users) + recipients = recipients + mentioned_users - users.reject do |user| - setting = NotificationRecipientService.notification_setting_for_user_project(user, project) - setting.present? && setting.level == level - end - end + recipients = reject_muted_users(recipients) - def reject_unsubscribed_users(recipients, target) - return recipients unless target.respond_to? :subscriptions + recipients = add_subscribed_users(recipients, note.noteable) + recipients = reject_unsubscribed_users(recipients, note.noteable) + recipients = reject_users_without_access(recipients, note.noteable) - recipients.reject do |user| - subscription = target.subscriptions.find_by_user_id(user.id) - subscription && !subscription.subscribed + recipients.delete(note.author) unless note.author.notified_of_own_activity? + recipients.uniq + end end end - def reject_users_without_access(recipients, target) - recipients = recipients.select { |u| u.can?(:receive_notifications) } - - ability = case target - when Issuable - :"read_#{target.to_ability_name}" - when Ci::Pipeline - :read_build # We have build trace in pipeline emails - end - - return recipients unless ability - - recipients.select do |user| - user.can?(ability, target) - end + def build_recipients(*a) + Builder::Default.new(@project).build(*a) end - def add_labels_subscribers(recipients, target, labels: nil) - return recipients unless target.respond_to? :labels - - (labels || target.labels).each do |label| - recipients += label.subscribers(project) - end + def build_pipeline_recipients(*a) + Builder::Pipeline.new(@project).build(*a) + end - recipients + def build_relabeled_recipients(*a) + Builder::Relabeled.new(@project).build(*a) end - # Build event key to search on custom notification level - # Check NotificationSetting::EMAIL_EVENTS - def build_custom_key(action, object) - "#{action}_#{object.class.model_name.name.underscore}".to_sym + def build_new_note_recipients(note) + Builder::NewNote.new(@project).build(note) end end -- cgit v1.2.1 From 947bff886ba62087715dbc8b4d48e85588bd5322 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Thu, 20 Jul 2017 12:25:32 -0700 Subject: move the build arguments to the initializers --- app/services/notification_recipient_service.rb | 74 +++++++++++++++++++++----- 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 97bbc47efb3..a68967d38b9 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -21,11 +21,13 @@ class NotificationRecipientService end module Builder - class Base - attr_reader :project - def initialize(project) - @project = project + def initialize(*) + raise 'abstract' + end + + def build + raise 'abstract' end def build(*) @@ -226,7 +228,22 @@ class NotificationRecipientService end class Default < Base - def build(target, current_user, action:, previous_assignee: nil, skip_current_user: true) + attr_reader :project + attr_reader :target + attr_reader :current_user + attr_reader :action + attr_reader :previous_assignee + attr_reader :skip_current_user + def initialize(project, target, current_user, action:, previous_assignee: nil, skip_current_user: true) + @project = project + @target = target + @current_user = current_user + @action = action + @previous_assignee = previous_assignee + @skip_current_user = skip_current_user + end + + def build custom_action = build_custom_key(action, target) recipients = participants(target, current_user) @@ -264,7 +281,18 @@ class NotificationRecipientService end class Pipeline < Base - def build(target, current_user, action:) + attr_reader :project + attr_reader :target + attr_reader :current_user + attr_reader :action + def initialize(project, target, current_user, action:) + @project = project + @target = target + @current_user = current_user + @action = action + end + + def build return [] unless current_user custom_action = @@ -288,7 +316,18 @@ class NotificationRecipientService end class Relabeled < Base - def build(target, current_user, labels:) + attr_reader :project + attr_reader :target + attr_reader :current_user + attr_reader :labels + def initialize(project, target, current_user, labels:) + @project = project + @target = target + @current_user = current_user + @labels = labels + end + + def build recipients = add_labels_subscribers([], target, labels: labels) recipients = reject_unsubscribed_users(recipients, target) recipients = reject_users_without_access(recipients, target) @@ -298,9 +337,16 @@ class NotificationRecipientService end class NewNote < Base - def build(note) - target = note.noteable + attr_reader :project + attr_reader :note + attr_reader :target + def initialize(project, note) + @project = project + @note = note + @target = note.noteable + end + def build(note) ability, subject = if note.for_personal_snippet? [:read_personal_snippet, note.noteable] else @@ -337,18 +383,18 @@ class NotificationRecipientService end def build_recipients(*a) - Builder::Default.new(@project).build(*a) + Builder::Default.new(@project, *a).build end def build_pipeline_recipients(*a) - Builder::Pipeline.new(@project).build(*a) + Builder::Pipeline.new(@project, *a).build end def build_relabeled_recipients(*a) - Builder::Relabeled.new(@project).build(*a) + Builder::Relabeled.new(@project, *a).build end - def build_new_note_recipients(note) - Builder::NewNote.new(@project).build(note) + def build_new_note_recipients(*a) + Builder::NewNote.new(@project, *a).build end end -- cgit v1.2.1 From b010a556b6b0314b333eb13ecc7095dc361f67a2 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Thu, 20 Jul 2017 12:36:37 -0700 Subject: factor out the `target` argument to helpers --- app/services/notification_recipient_service.rb | 38 +++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index a68967d38b9..edad4ff021a 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -30,7 +30,7 @@ class NotificationRecipientService raise 'abstract' end - def build(*) + def target raise 'abstract' end @@ -44,7 +44,7 @@ class NotificationRecipientService # Ensure that if we modify this array, we aren't modifying the memoised # participants on the target. - def participants(target, user) + def participants(user) return unless target.respond_to?(:participants) target.participants(user).dup @@ -92,7 +92,7 @@ class NotificationRecipientService reject_users(users, :mention) end - def add_subscribed_users(recipients, target) + def add_subscribed_users(recipients) return recipients unless target.respond_to? :subscribers recipients + target.subscribers(project) @@ -184,7 +184,7 @@ class NotificationRecipientService end end - def reject_unsubscribed_users(recipients, target) + def reject_unsubscribed_users(recipients) return recipients unless target.respond_to? :subscriptions recipients.reject do |user| @@ -193,7 +193,7 @@ class NotificationRecipientService end end - def reject_users_without_access(recipients, target) + def reject_users_without_access(recipients) recipients = recipients.select { |u| u.can?(:receive_notifications) } ability = case target @@ -210,7 +210,7 @@ class NotificationRecipientService end end - def add_labels_subscribers(recipients, target, labels: nil) + def add_labels_subscribers(recipients, labels: nil) return recipients unless target.respond_to? :labels (labels || target.labels).each do |label| @@ -246,7 +246,7 @@ class NotificationRecipientService def build custom_action = build_custom_key(action, target) - recipients = participants(target, current_user) + recipients = participants(current_user) recipients = add_project_watchers(recipients) recipients = add_custom_notifications(recipients, custom_action) recipients = reject_mention_users(recipients) @@ -265,14 +265,14 @@ class NotificationRecipientService end recipients = reject_muted_users(recipients) - recipients = add_subscribed_users(recipients, target) + recipients = add_subscribed_users(recipients) if [:new_issue, :new_merge_request].include?(custom_action) - recipients = add_labels_subscribers(recipients, target) + recipients = add_labels_subscribers(recipients) end - recipients = reject_unsubscribed_users(recipients, target) - recipients = reject_users_without_access(recipients, target) + recipients = reject_unsubscribed_users(recipients) + recipients = reject_users_without_access(recipients) recipients.delete(current_user) if skip_current_user && !current_user.notified_of_own_activity? @@ -311,7 +311,7 @@ class NotificationRecipientService return [] if (notification_setting.watch? || notification_setting.participating?) && NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action) - reject_users_without_access([current_user], target) + reject_users_without_access([current_user]) end end @@ -328,9 +328,9 @@ class NotificationRecipientService end def build - recipients = add_labels_subscribers([], target, labels: labels) - recipients = reject_unsubscribed_users(recipients, target) - recipients = reject_users_without_access(recipients, target) + recipients = add_labels_subscribers([], labels: labels) + recipients = reject_unsubscribed_users(recipients) + recipients = reject_users_without_access(recipients) recipients.delete(current_user) unless current_user.notified_of_own_activity? recipients.uniq end @@ -356,7 +356,7 @@ class NotificationRecipientService mentioned_users = note.mentioned_users.select { |user| user.can?(ability, subject) } # Add all users participating in the thread (author, assignee, comment authors) - recipients = participants(target, note.author) || mentioned_users + recipients = participants(note.author) || mentioned_users unless note.for_personal_snippet? # Merge project watchers @@ -372,9 +372,9 @@ class NotificationRecipientService recipients = reject_muted_users(recipients) - recipients = add_subscribed_users(recipients, note.noteable) - recipients = reject_unsubscribed_users(recipients, note.noteable) - recipients = reject_users_without_access(recipients, note.noteable) + recipients = add_subscribed_users(recipients) + recipients = reject_unsubscribed_users(recipients) + recipients = reject_users_without_access(recipients) recipients.delete(note.author) unless note.author.notified_of_own_activity? recipients.uniq -- cgit v1.2.1 From 734e21054a71be06aec897404d95b0905e6ed6c7 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Thu, 20 Jul 2017 12:37:36 -0700 Subject: move build_custom_key to Default --- app/services/notification_recipient_service.rb | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index edad4ff021a..3b7a13ffed6 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -219,12 +219,6 @@ class NotificationRecipientService recipients end - - # Build event key to search on custom notification level - # Check NotificationSetting::EMAIL_EVENTS - def build_custom_key(action, object) - "#{action}_#{object.class.model_name.name.underscore}".to_sym - end end class Default < Base @@ -244,8 +238,6 @@ class NotificationRecipientService end def build - custom_action = build_custom_key(action, target) - recipients = participants(current_user) recipients = add_project_watchers(recipients) recipients = add_custom_notifications(recipients, custom_action) @@ -278,6 +270,12 @@ class NotificationRecipientService recipients.uniq end + + # Build event key to search on custom notification level + # Check NotificationSetting::EMAIL_EVENTS + def custom_action + @custom_action ||= "#{action}_#{target.class.model_name.name.underscore}".to_sym + end end class Pipeline < Base -- cgit v1.2.1 From cdc4b4d7a0ef75d5ecb5e7e8e69d35cf2a3bcf1c Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Thu, 20 Jul 2017 16:10:01 -0700 Subject: make recipients mutative during the build --- app/services/notification_recipient_service.rb | 137 +++++++++++++------------ 1 file changed, 73 insertions(+), 64 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 3b7a13ffed6..0bada20444d 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -34,24 +34,35 @@ class NotificationRecipientService raise 'abstract' end + def recipients + @recipients ||= [] + end + + def to_a + return recipients if @already_built + @already_built = true + build + recipients.uniq! + recipients.freeze + recipients + end + # Remove users with disabled notifications from array # Also remove duplications and nil recipients - def reject_muted_users(users) - reject_users(users, :disabled) + def reject_muted_users + reject_users(:disabled) end protected - # Ensure that if we modify this array, we aren't modifying the memoised - # participants on the target. - def participants(user) + def add_participants(user) return unless target.respond_to?(:participants) - target.participants(user).dup + recipients.concat(target.participants(user)) end # Get project/group users with CUSTOM notification level - def add_custom_notifications(recipients, action) + def add_custom_notifications(action) user_ids = [] # Users with a notification setting on group or project @@ -68,8 +79,9 @@ class NotificationRecipientService recipients.concat(User.find(user_ids)) end - def add_project_watchers(recipients) - recipients.concat(project_watchers).compact + def add_project_watchers + recipients.concat(project_watchers) + recipients.compact! end # Get project users with WATCH notification level @@ -88,14 +100,14 @@ class NotificationRecipientService end # Remove users with notification level 'Mentioned' - def reject_mention_users(users) - reject_users(users, :mention) + def reject_mention_users + reject_users(:mention) end - def add_subscribed_users(recipients) - return recipients unless target.respond_to? :subscribers + def add_subscribed_users + return unless target.respond_to? :subscribers - recipients + target.subscribers(project) + recipients.concat(target.subscribers(project)) end def user_ids_notifiable_on(resource, notification_level = nil, action = nil) @@ -167,34 +179,35 @@ class NotificationRecipientService # Reject users which has certain notification level # # Example: - # reject_users(users, :watch, project) + # reject_users(:watch, project) # - def reject_users(users, level) + def reject_users(level) level = level.to_s unless NotificationSetting.levels.keys.include?(level) raise 'Invalid notification level' end - users = users.to_a.compact.uniq + recipients.compact! + recipients.uniq! - users.reject do |user| + recipients.reject! do |user| setting = NotificationRecipientService.notification_setting_for_user_project(user, project) setting.present? && setting.level == level end end - def reject_unsubscribed_users(recipients) - return recipients unless target.respond_to? :subscriptions + def reject_unsubscribed_users + return unless target.respond_to? :subscriptions - recipients.reject do |user| + recipients.reject! do |user| subscription = target.subscriptions.find_by_user_id(user.id) subscription && !subscription.subscribed end end - def reject_users_without_access(recipients) - recipients = recipients.select { |u| u.can?(:receive_notifications) } + def reject_users_without_access + recipients.select! { |u| u.can?(:receive_notifications) } ability = case target when Issuable @@ -203,21 +216,19 @@ class NotificationRecipientService :read_build # We have build trace in pipeline emails end - return recipients unless ability + return unless ability - recipients.select do |user| + recipients.select! do |user| user.can?(ability, target) end end - def add_labels_subscribers(recipients, labels: nil) - return recipients unless target.respond_to? :labels + def add_labels_subscribers(labels: nil) + return unless target.respond_to? :labels (labels || target.labels).each do |label| - recipients += label.subscribers(project) + recipients.concat(label.subscribers(project)) end - - recipients end end @@ -238,10 +249,10 @@ class NotificationRecipientService end def build - recipients = participants(current_user) - recipients = add_project_watchers(recipients) - recipients = add_custom_notifications(recipients, custom_action) - recipients = reject_mention_users(recipients) + add_participants(current_user) + add_project_watchers + add_custom_notifications(custom_action) + reject_mention_users # Re-assign is considered as a mention of the new assignee so we add the # new assignee to the list of recipients after we rejected users with @@ -256,19 +267,17 @@ class NotificationRecipientService recipients.concat(target.assignees) end - recipients = reject_muted_users(recipients) - recipients = add_subscribed_users(recipients) + reject_muted_users + add_subscribed_users if [:new_issue, :new_merge_request].include?(custom_action) - recipients = add_labels_subscribers(recipients) + add_labels_subscribers end - recipients = reject_unsubscribed_users(recipients) - recipients = reject_users_without_access(recipients) + reject_unsubscribed_users + reject_users_without_access recipients.delete(current_user) if skip_current_user && !current_user.notified_of_own_activity? - - recipients.uniq end # Build event key to search on custom notification level @@ -303,13 +312,14 @@ class NotificationRecipientService notification_setting = NotificationRecipientService.notification_setting_for_user_project(current_user, target.project) - return [] if notification_setting.mention? || notification_setting.disabled? + return if notification_setting.mention? || notification_setting.disabled? - return [] if notification_setting.custom? && !notification_setting.event_enabled?(custom_action) + return if notification_setting.custom? && !notification_setting.event_enabled?(custom_action) - return [] if (notification_setting.watch? || notification_setting.participating?) && NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action) + return if (notification_setting.watch? || notification_setting.participating?) && NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action) - reject_users_without_access([current_user]) + recipients << current_user + reject_users_without_access end end @@ -326,11 +336,10 @@ class NotificationRecipientService end def build - recipients = add_labels_subscribers([], labels: labels) - recipients = reject_unsubscribed_users(recipients) - recipients = reject_users_without_access(recipients) + add_labels_subscribers(labels: labels) + reject_unsubscribed_users + reject_users_without_access recipients.delete(current_user) unless current_user.notified_of_own_activity? - recipients.uniq end end @@ -344,7 +353,7 @@ class NotificationRecipientService @target = note.noteable end - def build(note) + def build ability, subject = if note.for_personal_snippet? [:read_personal_snippet, note.noteable] else @@ -354,45 +363,45 @@ class NotificationRecipientService mentioned_users = note.mentioned_users.select { |user| user.can?(ability, subject) } # Add all users participating in the thread (author, assignee, comment authors) - recipients = participants(note.author) || mentioned_users + add_participants(note.author) + recipients.concat(mentioned_users) if recipients.empty? unless note.for_personal_snippet? # Merge project watchers - recipients = add_project_watchers(recipients) + add_project_watchers # Merge project with custom notification - recipients = add_custom_notifications(recipients, :new_note) + add_custom_notifications(:new_note) end # Reject users with Mention notification level, except those mentioned in _this_ note. - recipients = reject_mention_users(recipients - mentioned_users) - recipients = recipients + mentioned_users + reject_mention_users + recipients.concat(mentioned_users) - recipients = reject_muted_users(recipients) + reject_muted_users - recipients = add_subscribed_users(recipients) - recipients = reject_unsubscribed_users(recipients) - recipients = reject_users_without_access(recipients) + add_subscribed_users + reject_unsubscribed_users + reject_users_without_access recipients.delete(note.author) unless note.author.notified_of_own_activity? - recipients.uniq end end end def build_recipients(*a) - Builder::Default.new(@project, *a).build + Builder::Default.new(@project, *a).to_a end def build_pipeline_recipients(*a) - Builder::Pipeline.new(@project, *a).build + Builder::Pipeline.new(@project, *a).to_a end def build_relabeled_recipients(*a) - Builder::Relabeled.new(@project, *a).build + Builder::Relabeled.new(@project, *a).to_a end def build_new_note_recipients(*a) - Builder::NewNote.new(@project, *a).build + Builder::NewNote.new(@project, *a).to_a end end -- cgit v1.2.1 From b05e6efd535990c8a54f470faa319e4b59ec12a3 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Tue, 1 Aug 2017 12:55:04 -0700 Subject: add builder helpers with levels and start to separate out #filter! --- app/services/notification_recipient_service.rb | 167 +++++++++++++++++-------- spec/services/notification_service_spec.rb | 5 + 2 files changed, 118 insertions(+), 54 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 0bada20444d..8d15202ef1c 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -20,13 +20,35 @@ class NotificationRecipientService @project = project end + class Recipient + attr_reader :user, :type + def initialize(builder, user, type) + @builder = builder + @user = user + @type = type + end + + def notification_setting + @notification_setting ||= + NotificationRecipientService.notification_setting_for_user_project(user, @builder.project) + end + + def notification_level + notification_setting&.level&.to_sym + end + end + module Builder class Base def initialize(*) raise 'abstract' end - def build + def build! + raise 'abstract' + end + + def filter! raise 'abstract' end @@ -38,13 +60,22 @@ class NotificationRecipientService @recipients ||= [] end - def to_a - return recipients if @already_built - @already_built = true - build - recipients.uniq! - recipients.freeze - recipients + def <<(arg) + users, type = arg + users = Array(users) + users.compact! + recipients.concat(users.map { |u| Recipient.new(self, u, type) }) + end + + def recipient_users + @recipient_users ||= + begin + build! + filter! + users = recipients.map(&:user) + users.uniq! + users.freeze + end end # Remove users with disabled notifications from array @@ -53,12 +84,22 @@ class NotificationRecipientService reject_users(:disabled) end + def read_ability + @read_ability ||= + case target + when Issuable + :"read_#{target.to_ability_name}" + when Ci::Pipeline + :read_build # We have build trace in pipeline emails + end + end + protected def add_participants(user) return unless target.respond_to?(:participants) - recipients.concat(target.participants(user)) + self << [target.participants(user), :participating] end # Get project/group users with CUSTOM notification level @@ -76,12 +117,11 @@ class NotificationRecipientService global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global) user_ids += user_ids_with_global_level_custom(global_users_ids, action) - recipients.concat(User.find(user_ids)) + self << [User.find(user_ids), :watch] end def add_project_watchers - recipients.concat(project_watchers) - recipients.compact! + self << [project_watchers, :watch] end # Get project users with WATCH notification level @@ -101,13 +141,17 @@ class NotificationRecipientService # Remove users with notification level 'Mentioned' def reject_mention_users - reject_users(:mention) + recipients.select! do |r| + next true if r.type == :mention + next true if r.type == :subscription + r.notification_level != :mention + end end def add_subscribed_users return unless target.respond_to? :subscribers - recipients.concat(target.subscribers(project)) + self << [target.subscribers(project), :subscription] end def user_ids_notifiable_on(resource, notification_level = nil, action = nil) @@ -188,10 +232,9 @@ class NotificationRecipientService raise 'Invalid notification level' end - recipients.compact! - recipients.uniq! + recipients.reject! do |recipient| + user = recipient.user - recipients.reject! do |user| setting = NotificationRecipientService.notification_setting_for_user_project(user, project) setting.present? && setting.level == level end @@ -200,34 +243,34 @@ class NotificationRecipientService def reject_unsubscribed_users return unless target.respond_to? :subscriptions - recipients.reject! do |user| + recipients.reject! do |recipient| + user = recipient.user subscription = target.subscriptions.find_by_user_id(user.id) subscription && !subscription.subscribed end end def reject_users_without_access - recipients.select! { |u| u.can?(:receive_notifications) } - - ability = case target - when Issuable - :"read_#{target.to_ability_name}" - when Ci::Pipeline - :read_build # We have build trace in pipeline emails - end + recipients.select! { |r| r.user.can?(:receive_notifications) } - return unless ability + return unless read_ability - recipients.select! do |user| - user.can?(ability, target) + DeclarativePolicy.subject_scope do + recipients.select! do |recipient| + recipient.user.can?(read_ability, target) + end end end + def reject_user(user) + recipients.reject! { |r| r.user == user } + end + def add_labels_subscribers(labels: nil) return unless target.respond_to? :labels (labels || target.labels).each do |label| - recipients.concat(label.subscribers(project)) + self << [label.subscribers(project), :subscription] end end end @@ -248,7 +291,7 @@ class NotificationRecipientService @skip_current_user = skip_current_user end - def build + def build! add_participants(current_user) add_project_watchers add_custom_notifications(custom_action) @@ -259,12 +302,12 @@ class NotificationRecipientService # the "on mention" notification level case custom_action when :reassign_merge_request - recipients << previous_assignee if previous_assignee - recipients << target.assignee + self << [previous_assignee, :mention] + self << [target.assignee, :mention] when :reassign_issue previous_assignees = Array(previous_assignee) - recipients.concat(previous_assignees) - recipients.concat(target.assignees) + self << [previous_assignees, :mention] + self << [target.assignees, :mention] end reject_muted_users @@ -273,13 +316,16 @@ class NotificationRecipientService if [:new_issue, :new_merge_request].include?(custom_action) add_labels_subscribers end + end + def filter! reject_unsubscribed_users reject_users_without_access - recipients.delete(current_user) if skip_current_user && !current_user.notified_of_own_activity? + reject_user(current_user) if skip_current_user && !current_user.notified_of_own_activity? end + private # Build event key to search on custom notification level # Check NotificationSetting::EMAIL_EVENTS def custom_action @@ -299,7 +345,7 @@ class NotificationRecipientService @action = action end - def build + def build! return [] unless current_user custom_action = @@ -318,7 +364,10 @@ class NotificationRecipientService return if (notification_setting.watch? || notification_setting.participating?) && NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action) - recipients << current_user + self << [current_user, :subscriber] + end + + def filter! reject_users_without_access end end @@ -335,11 +384,14 @@ class NotificationRecipientService @labels = labels end - def build + def build! add_labels_subscribers(labels: labels) + end + + def filter! reject_unsubscribed_users reject_users_without_access - recipients.delete(current_user) unless current_user.notified_of_own_activity? + reject_user(current_user) unless current_user.notified_of_own_activity? end end @@ -353,18 +405,22 @@ class NotificationRecipientService @target = note.noteable end - def build - ability, subject = if note.for_personal_snippet? - [:read_personal_snippet, note.noteable] - else - [:read_project, note.project] - end + def read_ability + @read_ability ||= + case target + when Commit then nil + else :"read_#{target.class.model_name.name.underscore}" + end + end - mentioned_users = note.mentioned_users.select { |user| user.can?(ability, subject) } + def subject + note.for_personal_snippet? ? note.noteable : note.project + end + def build! # Add all users participating in the thread (author, assignee, comment authors) add_participants(note.author) - recipients.concat(mentioned_users) if recipients.empty? + self << [note.mentioned_users, :mention] if recipients.empty? unless note.for_personal_snippet? # Merge project watchers @@ -376,32 +432,35 @@ class NotificationRecipientService # Reject users with Mention notification level, except those mentioned in _this_ note. reject_mention_users - recipients.concat(mentioned_users) + self << [note.mentioned_users, :mention] reject_muted_users add_subscribed_users + end + + def filter! reject_unsubscribed_users reject_users_without_access - recipients.delete(note.author) unless note.author.notified_of_own_activity? + reject_user(note.author) unless note.author.notified_of_own_activity? end end end def build_recipients(*a) - Builder::Default.new(@project, *a).to_a + Builder::Default.new(@project, *a).recipient_users end def build_pipeline_recipients(*a) - Builder::Pipeline.new(@project, *a).to_a + Builder::Pipeline.new(@project, *a).recipient_users end def build_relabeled_recipients(*a) - Builder::Relabeled.new(@project, *a).to_a + Builder::Relabeled.new(@project, *a).recipient_users end def build_new_note_recipients(*a) - Builder::NewNote.new(@project, *a).to_a + Builder::NewNote.new(@project, *a).recipient_users end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 882ee7751b5..45d5d0ac9e0 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -309,6 +309,11 @@ describe NotificationService do before do build_team(note.project) + + # make sure these users can read the project snippet! + project.add_guest(@u_guest_watcher) + project.add_guest(@u_guest_custom) + note.project.add_master(note.author) reset_delivered_emails! end -- cgit v1.2.1 From 943baa13922a5a2b7aab5a46765846996f23486b Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Tue, 1 Aug 2017 12:55:13 -0700 Subject: move filtering logic into Recipient class --- app/services/notification_recipient_service.rb | 201 +++++++++++++------------ 1 file changed, 102 insertions(+), 99 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 8d15202ef1c..3d3ace29c16 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -33,9 +33,69 @@ class NotificationRecipientService NotificationRecipientService.notification_setting_for_user_project(user, @builder.project) end - def notification_level + def raw_notification_level notification_setting&.level&.to_sym end + + def notification_level + # custom is treated the same as watch if it's enabled - otherwise it's + # as :disabled. + @notification_level ||= + case raw_notification_level + when :custom + notification_setting.event_enabled?(@builder.custom_action) ? :watch : :custom + else + raw_notification_level + end + end + + def notifiable? + return false unless has_access? + return false if own_activity? + + return true if @type == :subscription + + return false if notification_level.nil? || notification_level == :disabled + + return %i[participating mention].include?(@type) if notification_level == :custom + + return false if %i[watch participating].include?(notification_level) && excluded_watcher_action? + + return false unless NotificationSetting.levels[notification_level] <= NotificationSetting.levels[type] + + return false if unsubscribed? + + true + end + + def unsubscribed? + return false unless @builder.target.respond_to?(:subscriptions) + + subscription = @builder.target.subscriptions.find_by_user_id(@user.id) + subscription && !subscription.subscribed + end + + def own_activity? + return false unless @builder.acting_user + return false if @builder.acting_user.notified_of_own_activity? + + user == @builder.acting_user + end + + def has_access? + return false unless user.can?(:receive_notifications) + return true unless @builder.read_ability + + DeclarativePolicy.subject_scope do + user.can?(@builder.read_ability, @builder.target) + end + end + + def excluded_watcher_action? + return false if raw_notification_level == :custom + + NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(@builder.custom_action) + end end module Builder @@ -49,7 +109,11 @@ class NotificationRecipientService end def filter! - raise 'abstract' + recipients.select!(&:notifiable?) + end + + def acting_user + current_user end def target @@ -78,12 +142,6 @@ class NotificationRecipientService end end - # Remove users with disabled notifications from array - # Also remove duplications and nil recipients - def reject_muted_users - reject_users(:disabled) - end - def read_ability @read_ability ||= case target @@ -99,23 +157,23 @@ class NotificationRecipientService def add_participants(user) return unless target.respond_to?(:participants) - self << [target.participants(user), :participating] + self << [target.participants(user), :watch] end # Get project/group users with CUSTOM notification level - def add_custom_notifications(action) + def add_custom_notifications user_ids = [] # Users with a notification setting on group or project - user_ids += user_ids_notifiable_on(project, :custom, action) - user_ids += user_ids_notifiable_on(project.group, :custom, action) + user_ids += user_ids_notifiable_on(project, :custom) + user_ids += user_ids_notifiable_on(project.group, :custom) # Users with global level custom user_ids_with_project_level_global = user_ids_notifiable_on(project, :global) user_ids_with_group_level_global = user_ids_notifiable_on(project.group, :global) global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global) - user_ids += user_ids_with_global_level_custom(global_users_ids, action) + user_ids += user_ids_with_global_level_custom(global_users_ids, custom_action) self << [User.find(user_ids), :watch] end @@ -139,31 +197,22 @@ class NotificationRecipientService User.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq).to_a end - # Remove users with notification level 'Mentioned' - def reject_mention_users - recipients.select! do |r| - next true if r.type == :mention - next true if r.type == :subscription - r.notification_level != :mention - end - end - def add_subscribed_users return unless target.respond_to? :subscribers self << [target.subscribers(project), :subscription] end - def user_ids_notifiable_on(resource, notification_level = nil, action = nil) + def user_ids_notifiable_on(resource, notification_level = nil) return [] unless resource + scope = resource.notification_settings + if notification_level - settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level]) - settings = settings.select { |setting| setting.event_enabled?(action) } if action.present? - settings.map(&:user_id) - else - resource.notification_settings.pluck(:user_id) + scope = scope.where(level: NotificationSetting.levels[notification_level]) end + + scope.pluck(:user_id) end # Build a list of user_ids based on project notification settings @@ -220,26 +269,6 @@ class NotificationRecipientService ) end - # Reject users which has certain notification level - # - # Example: - # reject_users(:watch, project) - # - def reject_users(level) - level = level.to_s - - unless NotificationSetting.levels.keys.include?(level) - raise 'Invalid notification level' - end - - recipients.reject! do |recipient| - user = recipient.user - - setting = NotificationRecipientService.notification_setting_for_user_project(user, project) - setting.present? && setting.level == level - end - end - def reject_unsubscribed_users return unless target.respond_to? :subscriptions @@ -294,12 +323,9 @@ class NotificationRecipientService def build! add_participants(current_user) add_project_watchers - add_custom_notifications(custom_action) - reject_mention_users + add_custom_notifications - # Re-assign is considered as a mention of the new assignee so we add the - # new assignee to the list of recipients after we rejected users with - # the "on mention" notification level + # Re-assign is considered as a mention of the new assignee case custom_action when :reassign_merge_request self << [previous_assignee, :mention] @@ -310,7 +336,6 @@ class NotificationRecipientService self << [target.assignees, :mention] end - reject_muted_users add_subscribed_users if [:new_issue, :new_merge_request].include?(custom_action) @@ -318,14 +343,10 @@ class NotificationRecipientService end end - def filter! - reject_unsubscribed_users - reject_users_without_access - - reject_user(current_user) if skip_current_user && !current_user.notified_of_own_activity? + def acting_user + current_user if skip_current_user end - private # Build event key to search on custom notification level # Check NotificationSetting::EMAIL_EVENTS def custom_action @@ -345,30 +366,23 @@ class NotificationRecipientService @action = action end - def build! - return [] unless current_user - - custom_action = - case action.to_s - when 'failed' - :failed_pipeline - when 'success' - :success_pipeline - end - - notification_setting = NotificationRecipientService.notification_setting_for_user_project(current_user, target.project) - - return if notification_setting.mention? || notification_setting.disabled? - - return if notification_setting.custom? && !notification_setting.event_enabled?(custom_action) - - return if (notification_setting.watch? || notification_setting.participating?) && NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action) + def acting_user + nil + end - self << [current_user, :subscriber] + def custom_action + case action.to_s + when 'failed' + :failed_pipeline + when 'success' + :success_pipeline + end end - def filter! - reject_users_without_access + def build! + return [] unless current_user + + self << [current_user, :watch] end end @@ -387,12 +401,6 @@ class NotificationRecipientService def build! add_labels_subscribers(labels: labels) end - - def filter! - reject_unsubscribed_users - reject_users_without_access - reject_user(current_user) unless current_user.notified_of_own_activity? - end end class NewNote < Base @@ -420,30 +428,25 @@ class NotificationRecipientService def build! # Add all users participating in the thread (author, assignee, comment authors) add_participants(note.author) - self << [note.mentioned_users, :mention] if recipients.empty? + self << [note.mentioned_users, :mention] unless note.for_personal_snippet? # Merge project watchers add_project_watchers # Merge project with custom notification - add_custom_notifications(:new_note) + add_custom_notifications end - # Reject users with Mention notification level, except those mentioned in _this_ note. - reject_mention_users - self << [note.mentioned_users, :mention] - - reject_muted_users - add_subscribed_users end - def filter! - reject_unsubscribed_users - reject_users_without_access + def custom_action + :new_note + end - reject_user(note.author) unless note.author.notified_of_own_activity? + def acting_user + note.author end end end -- cgit v1.2.1 From b69e81c3e993939228046516be897233dcaf4442 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Tue, 25 Jul 2017 22:22:31 -0700 Subject: rm the @builder argument and factor out .notifiable_users --- app/services/notification_recipient_service.rb | 50 +++++++++++++++++++------- app/services/notification_service.rb | 3 +- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 3d3ace29c16..f944c1ae7f4 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -21,16 +21,25 @@ class NotificationRecipientService end class Recipient + def self.notifiable_users(users, *args) + users.map { |u| new(u, *args) }.select(&:notifiable?).map(&:user) + end + attr_reader :user, :type - def initialize(builder, user, type) - @builder = builder + def initialize(user, project, type, + custom_action: nil, target: nil, acting_user: nil, read_ability: nil) + @project = project + @custom_action = custom_action + @acting_user = acting_user + @read_ability = read_ability + @target = target @user = user @type = type end def notification_setting @notification_setting ||= - NotificationRecipientService.notification_setting_for_user_project(user, @builder.project) + NotificationRecipientService.notification_setting_for_user_project(user, @project) end def raw_notification_level @@ -43,7 +52,11 @@ class NotificationRecipientService @notification_level ||= case raw_notification_level when :custom - notification_setting.event_enabled?(@builder.custom_action) ? :watch : :custom + if !@custom_action || notification_setting.event_enabled?(@custom_action) + :watch + else + :custom + end else raw_notification_level end @@ -69,32 +82,34 @@ class NotificationRecipientService end def unsubscribed? - return false unless @builder.target.respond_to?(:subscriptions) + return false unless @target + return false unless @target.respond_to?(:subscriptions) - subscription = @builder.target.subscriptions.find_by_user_id(@user.id) + subscription = @target.subscriptions.find_by_user_id(@user.id) subscription && !subscription.subscribed end def own_activity? - return false unless @builder.acting_user - return false if @builder.acting_user.notified_of_own_activity? + return false unless @acting_user + return false if @acting_user.notified_of_own_activity? - user == @builder.acting_user + user == @acting_user end def has_access? return false unless user.can?(:receive_notifications) - return true unless @builder.read_ability + return true unless @read_ability DeclarativePolicy.subject_scope do - user.can?(@builder.read_ability, @builder.target) + user.can?(@read_ability, @target) end end def excluded_watcher_action? + return false unless @custom_action return false if raw_notification_level == :custom - NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(@builder.custom_action) + NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(@custom_action) end end @@ -128,7 +143,16 @@ class NotificationRecipientService users, type = arg users = Array(users) users.compact! - recipients.concat(users.map { |u| Recipient.new(self, u, type) }) + recipients.concat(users.map { |u| make_recipient(u, type) }) + end + + def make_recipient(user, type) + Recipient.new(user, project, type, + custom_action: custom_action, + target: target, + acting_user: acting_user, + read_ability: read_ability + ) end def recipient_users diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index b94921d2a08..a094d97a295 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -270,8 +270,7 @@ class NotificationService end def project_was_moved(project, old_path_with_namespace) - recipients = project.team.members - recipients = NotificationRecipientService.new(project).reject_muted_users(recipients) + recipients = NotificationRecipientService::Recipient.notifiable_users(project.team.members, project, :watch) recipients.each do |recipient| mailer.project_was_moved_email( -- cgit v1.2.1 From a4932d2da72af14eeea99e466117bdb767d111c7 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Tue, 25 Jul 2017 22:24:22 -0700 Subject: don't elevate to :watch if no @custom_action is provided --- app/services/notification_recipient_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index f944c1ae7f4..20d8a06903c 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -52,7 +52,7 @@ class NotificationRecipientService @notification_level ||= case raw_notification_level when :custom - if !@custom_action || notification_setting.event_enabled?(@custom_action) + if @custom_action && notification_setting.event_enabled?(@custom_action) :watch else :custom -- cgit v1.2.1 From 4caa5a7e14a8aa1e973662bd1394468eac74f3ad Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Wed, 26 Jul 2017 08:25:29 -0700 Subject: factor out .notifiable_users --- app/services/notification_recipient_service.rb | 40 +++----------------------- app/services/notification_service.rb | 9 +++--- 2 files changed, 8 insertions(+), 41 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 20d8a06903c..3d36660495f 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -378,38 +378,6 @@ class NotificationRecipientService end end - class Pipeline < Base - attr_reader :project - attr_reader :target - attr_reader :current_user - attr_reader :action - def initialize(project, target, current_user, action:) - @project = project - @target = target - @current_user = current_user - @action = action - end - - def acting_user - nil - end - - def custom_action - case action.to_s - when 'failed' - :failed_pipeline - when 'success' - :success_pipeline - end - end - - def build! - return [] unless current_user - - self << [current_user, :watch] - end - end - class Relabeled < Base attr_reader :project attr_reader :target @@ -475,12 +443,12 @@ class NotificationRecipientService end end - def build_recipients(*a) - Builder::Default.new(@project, *a).recipient_users + def self.notifiable_users(*a) + Recipient.notifiable_users(*a) end - def build_pipeline_recipients(*a) - Builder::Pipeline.new(@project, *a).recipient_users + def build_recipients(*a) + Builder::Default.new(@project, *a).recipient_users end def build_relabeled_recipients(*a) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index a094d97a295..8a6ed88923d 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -270,7 +270,7 @@ class NotificationService end def project_was_moved(project, old_path_with_namespace) - recipients = NotificationRecipientService::Recipient.notifiable_users(project.team.members, project, :watch) + recipients = NotificationRecipientService.notifiable_users(project.team.members, project, :mention) recipients.each do |recipient| mailer.project_was_moved_email( @@ -304,10 +304,9 @@ class NotificationService return unless mailer.respond_to?(email_template) - recipients ||= NotificationRecipientService.new(pipeline.project).build_pipeline_recipients( - pipeline, - pipeline.user, - action: pipeline.status + recipients ||= NotificationRecipientService.notifiable_users( + [pipeline.user], pipeline.project, :watch, + custom_action: :"#{pipeline.status}_pipeline" ).map(&:notification_email) if recipients.any? -- cgit v1.2.1 From 6aa44382acbe1439c48da056ab9089e36124878f Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Wed, 26 Jul 2017 08:36:14 -0700 Subject: rm a now-useless spec --- .../notification_recipient_service_spec.rb | 34 ---------------------- 1 file changed, 34 deletions(-) delete mode 100644 spec/services/notification_recipient_service_spec.rb diff --git a/spec/services/notification_recipient_service_spec.rb b/spec/services/notification_recipient_service_spec.rb deleted file mode 100644 index 0eb0771fd29..00000000000 --- a/spec/services/notification_recipient_service_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'spec_helper' - -describe NotificationRecipientService do - set(:user) { create(:user) } - set(:project) { create(:project, :public) } - set(:issue) { create(:issue, project: project) } - - set(:watcher) do - watcher = create(:user) - setting = watcher.notification_settings_for(project) - setting.level = :watch - setting.save - - watcher - end - - subject { described_class.new(project) } - - describe '#build_recipients' do - it 'does not modify the participants of the target' do - expect { subject.build_recipients(issue, user, action: :new_issue) } - .not_to change { issue.participants(user) } - end - end - - describe '#build_new_note_recipients' do - set(:note) { create(:note_on_issue, noteable: issue, project: project) } - - it 'does not modify the participants of the target' do - expect { subject.build_new_note_recipients(note) } - .not_to change { note.noteable.participants(note.author) } - end - end -end -- cgit v1.2.1 From 618b5d34463b1a54cca0d47d3d815b7e388a1db2 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Wed, 26 Jul 2017 08:36:49 -0700 Subject: move the #build_* methods to static, parameterize the project --- app/services/notification_recipient_service.rb | 57 ++++++++++++-------------- app/services/notification_service.rb | 24 ++++++----- 2 files changed, 40 insertions(+), 41 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 3d36660495f..788e06502ee 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -1,23 +1,21 @@ # # Used by NotificationService to determine who should receive notification # -class NotificationRecipientService - attr_reader :project - - def self.notification_setting_for_user_project(user, project) - project_setting = project && user.notification_settings_for(project) - - return project_setting unless project_setting.nil? || project_setting.global? - - group_setting = project&.group && user.notification_settings_for(project.group) +module NotificationRecipientService + def self.notifiable_users(*a) + Recipient.notifiable_users(*a) + end - return group_setting unless group_setting.nil? || group_setting.global? + def self.build_recipients(*a) + Builder::Default.new(*a).recipient_users + end - user.global_notification_setting + def self.build_relabeled_recipients(*a) + Builder::Relabeled.new(*a).recipient_users end - def initialize(project) - @project = project + def self.build_new_note_recipients(*a) + Builder::NewNote.new(*a).recipient_users end class Recipient @@ -38,8 +36,7 @@ class NotificationRecipientService end def notification_setting - @notification_setting ||= - NotificationRecipientService.notification_setting_for_user_project(user, @project) + @notification_setting ||= find_notification_setting end def raw_notification_level @@ -111,6 +108,20 @@ class NotificationRecipientService NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(@custom_action) end + + private + + def find_notification_setting + project_setting = project && user.notification_settings_for(project) + + return project_setting unless project_setting.nil? || project_setting.global? + + group_setting = project&.group && user.notification_settings_for(project.group) + + return group_setting unless group_setting.nil? || group_setting.global? + + user.global_notification_setting + end end module Builder @@ -442,20 +453,4 @@ class NotificationRecipientService end end end - - def self.notifiable_users(*a) - Recipient.notifiable_users(*a) - end - - def build_recipients(*a) - Builder::Default.new(@project, *a).recipient_users - end - - def build_relabeled_recipients(*a) - Builder::Relabeled.new(@project, *a).recipient_users - end - - def build_new_note_recipients(*a) - Builder::NewNote.new(@project, *a).recipient_users - end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 8a6ed88923d..675b4536a26 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -77,7 +77,8 @@ class NotificationService # * users with custom level checked with "reassign issue" # def reassigned_issue(issue, current_user, previous_assignees = []) - recipients = NotificationRecipientService.new(issue.project).build_recipients( + recipients = NotificationRecipientService.build_recipients( + issue.project, issue, current_user, action: "reassign", @@ -177,7 +178,8 @@ class NotificationService end def resolve_all_discussions(merge_request, current_user) - recipients = NotificationRecipientService.new(merge_request.target_project).build_recipients( + recipients = NotificationRecipientService.build_recipients( + merge_request.target_project, merge_request, current_user, action: "resolve_all_discussions") @@ -202,7 +204,7 @@ class NotificationService notify_method = "note_#{note.to_ability_name}_email".to_sym - recipients = NotificationRecipientService.new(note.project).build_new_note_recipients(note) + recipients = NotificationRecipientService.build_new_note_recipients(note.project, note) recipients.each do |recipient| mailer.send(notify_method, recipient.id, note.id).deliver_later end @@ -282,7 +284,7 @@ class NotificationService end def issue_moved(issue, new_issue, current_user) - recipients = NotificationRecipientService.new(issue.project).build_recipients(issue, current_user, action: 'moved') + recipients = NotificationRecipientService.build_recipients(issue.project, issue, current_user, action: 'moved') recipients.map do |recipient| email = mailer.issue_moved_email(recipient, issue, new_issue, current_user) @@ -317,7 +319,7 @@ class NotificationService protected def new_resource_email(target, project, method) - recipients = NotificationRecipientService.new(project).build_recipients(target, target.author, action: "new") + recipients = NotificationRecipientService.build_recipients(project, target, target.author, action: "new") recipients.each do |recipient| mailer.send(method, recipient.id, target.id).deliver_later @@ -325,7 +327,7 @@ class NotificationService end def new_mentions_in_resource_email(target, project, new_mentioned_users, current_user, method) - recipients = NotificationRecipientService.new(project).build_recipients(target, current_user, action: "new") + recipients = NotificationRecipientService.build_recipients(project, target, current_user, action: "new") recipients = recipients & new_mentioned_users recipients.each do |recipient| @@ -336,7 +338,8 @@ class NotificationService def close_resource_email(target, project, current_user, method, skip_current_user: true) action = method == :merged_merge_request_email ? "merge" : "close" - recipients = NotificationRecipientService.new(project).build_recipients( + recipients = NotificationRecipientService.build_recipients( + project, target, current_user, action: action, @@ -352,7 +355,8 @@ class NotificationService previous_assignee_id = previous_record(target, 'assignee_id') previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id - recipients = NotificationRecipientService.new(project).build_recipients( + recipients = NotificationRecipientService.build_recipients( + project, target, current_user, action: "reassign", @@ -371,7 +375,7 @@ class NotificationService end def relabeled_resource_email(target, project, labels, current_user, method) - recipients = NotificationRecipientService.new(project).build_relabeled_recipients(target, current_user, labels: labels) + recipients = NotificationRecipientService.build_relabeled_recipients(project, target, current_user, labels: labels) label_names = labels.map(&:name) recipients.each do |recipient| @@ -380,7 +384,7 @@ class NotificationService end def reopen_resource_email(target, project, current_user, method, status) - recipients = NotificationRecipientService.new(project).build_recipients(target, current_user, action: "reopen") + recipients = NotificationRecipientService.build_recipients(project, target, current_user, action: "reopen") recipients.each do |recipient| mailer.send(method, recipient.id, target.id, status, current_user.id).deliver_later -- cgit v1.2.1 From 618a3d125c0faf3b6e0484bc9955ffd0a597ad33 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Wed, 26 Jul 2017 08:43:27 -0700 Subject: move Recipient to its own NotificationRecipient file --- app/models/notification_recipient.rb | 102 ++++++++++++++++++++++ app/services/notification_recipient_service.rb | 116 ++----------------------- 2 files changed, 109 insertions(+), 109 deletions(-) create mode 100644 app/models/notification_recipient.rb diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb new file mode 100644 index 00000000000..0b2eee56ff8 --- /dev/null +++ b/app/models/notification_recipient.rb @@ -0,0 +1,102 @@ +class NotificationRecipient + attr_reader :user, :project, :type + def initialize(user, project, type, + custom_action: nil, target: nil, acting_user: nil, read_ability: nil) + @project = project + @custom_action = custom_action + @acting_user = acting_user + @read_ability = read_ability + @target = target + @user = user + @type = type + end + + def notification_setting + @notification_setting ||= find_notification_setting + end + + def raw_notification_level + notification_setting&.level&.to_sym + end + + def notification_level + # custom is treated the same as watch if it's enabled - otherwise it's + # as :disabled. + @notification_level ||= + case raw_notification_level + when :custom + if @custom_action && notification_setting.event_enabled?(@custom_action) + :watch + else + :custom + end + else + raw_notification_level + end + end + + def notifiable? + return false unless has_access? + return false if own_activity? + + return true if @type == :subscription + + return false if notification_level.nil? || notification_level == :disabled + + return %i[participating mention].include?(@type) if notification_level == :custom + + return false if %i[watch participating].include?(notification_level) && excluded_watcher_action? + + return false unless NotificationSetting.levels[notification_level] <= NotificationSetting.levels[type] + + return false if unsubscribed? + + true + end + + def unsubscribed? + return false unless @target + return false unless @target.respond_to?(:subscriptions) + + subscription = @target.subscriptions.find_by_user_id(@user.id) + subscription && !subscription.subscribed + end + + def own_activity? + return false unless @acting_user + return false if @acting_user.notified_of_own_activity? + + user == @acting_user + end + + def has_access? + return false unless user.can?(:receive_notifications) + return true unless @read_ability + + DeclarativePolicy.subject_scope do + user.can?(@read_ability, @target) + end + end + + def excluded_watcher_action? + return false unless @custom_action + return false if raw_notification_level == :custom + + NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(@custom_action) + end + + private + + def find_notification_setting + project_setting = @project && user.notification_settings_for(@project) + + return project_setting unless project_setting.nil? || project_setting.global? + + group_setting = @project&.group && user.notification_settings_for(@project.group) + + return group_setting unless group_setting.nil? || group_setting.global? + + user.global_notification_setting + end +end + diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 788e06502ee..5d8c3a39af1 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -2,8 +2,12 @@ # Used by NotificationService to determine who should receive notification # module NotificationRecipientService - def self.notifiable_users(*a) - Recipient.notifiable_users(*a) + def self.notifiable_users(users, *args) + users.map { |u| NotificationRecipient.new(u, *args) }.select(&:notifiable?).map(&:user) + end + + def self.notifiable?(user, *args) + NotificationRecipient.new(user, *args).notifiable? end def self.build_recipients(*a) @@ -18,112 +22,6 @@ module NotificationRecipientService Builder::NewNote.new(*a).recipient_users end - class Recipient - def self.notifiable_users(users, *args) - users.map { |u| new(u, *args) }.select(&:notifiable?).map(&:user) - end - - attr_reader :user, :type - def initialize(user, project, type, - custom_action: nil, target: nil, acting_user: nil, read_ability: nil) - @project = project - @custom_action = custom_action - @acting_user = acting_user - @read_ability = read_ability - @target = target - @user = user - @type = type - end - - def notification_setting - @notification_setting ||= find_notification_setting - end - - def raw_notification_level - notification_setting&.level&.to_sym - end - - def notification_level - # custom is treated the same as watch if it's enabled - otherwise it's - # as :disabled. - @notification_level ||= - case raw_notification_level - when :custom - if @custom_action && notification_setting.event_enabled?(@custom_action) - :watch - else - :custom - end - else - raw_notification_level - end - end - - def notifiable? - return false unless has_access? - return false if own_activity? - - return true if @type == :subscription - - return false if notification_level.nil? || notification_level == :disabled - - return %i[participating mention].include?(@type) if notification_level == :custom - - return false if %i[watch participating].include?(notification_level) && excluded_watcher_action? - - return false unless NotificationSetting.levels[notification_level] <= NotificationSetting.levels[type] - - return false if unsubscribed? - - true - end - - def unsubscribed? - return false unless @target - return false unless @target.respond_to?(:subscriptions) - - subscription = @target.subscriptions.find_by_user_id(@user.id) - subscription && !subscription.subscribed - end - - def own_activity? - return false unless @acting_user - return false if @acting_user.notified_of_own_activity? - - user == @acting_user - end - - def has_access? - return false unless user.can?(:receive_notifications) - return true unless @read_ability - - DeclarativePolicy.subject_scope do - user.can?(@read_ability, @target) - end - end - - def excluded_watcher_action? - return false unless @custom_action - return false if raw_notification_level == :custom - - NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(@custom_action) - end - - private - - def find_notification_setting - project_setting = project && user.notification_settings_for(project) - - return project_setting unless project_setting.nil? || project_setting.global? - - group_setting = project&.group && user.notification_settings_for(project.group) - - return group_setting unless group_setting.nil? || group_setting.global? - - user.global_notification_setting - end - end - module Builder class Base def initialize(*) @@ -158,7 +56,7 @@ module NotificationRecipientService end def make_recipient(user, type) - Recipient.new(user, project, type, + NotificationRecipient.new(user, project, type, custom_action: custom_action, target: target, acting_user: acting_user, -- cgit v1.2.1 From 651f50ac00bdf14ae660af0fea3d463d07d8d25f Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Wed, 26 Jul 2017 09:32:37 -0700 Subject: make sure #custom_action is always defined --- app/services/notification_recipient_service.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 5d8c3a39af1..b286cc373bd 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -85,6 +85,10 @@ module NotificationRecipientService end end + def custom_action + nil + end + protected def add_participants(user) -- cgit v1.2.1 From 46d0bfdcb982d76b8595921a956b088df1573905 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Wed, 26 Jul 2017 09:33:01 -0700 Subject: use intersection and diff operators instead of each --- app/services/notification_recipient_service.rb | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index b286cc373bd..6b4e97aaab4 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -157,35 +157,15 @@ module NotificationRecipientService user_ids = user_ids_notifiable_on(project, :watch) # If project setting is global, add to watch list if global setting is watch - global_setting.each do |user_id| - if user_ids_global_level_watch.include?(user_id) - user_ids << user_id - end - end - - user_ids + user_ids + (global_setting & user_ids_global_level_watch) end # Build a list of user_ids based on group notification settings def select_group_members_ids(group, project_members, global_setting, user_ids_global_level_watch) uids = user_ids_notifiable_on(group, :watch) - # Group setting is watch, add to user_ids list if user is not project member - user_ids = [] - uids.each do |user_id| - if project_members.exclude?(user_id) - user_ids << user_id - end - end - # Group setting is global, add to user_ids list if global setting is watch - global_setting.each do |user_id| - if project_members.exclude?(user_id) && user_ids_global_level_watch.include?(user_id) - user_ids << user_id - end - end - - user_ids + uids + (global_setting & user_ids_global_level_watch) - project_members end def user_ids_with_global_level_watch(ids) -- cgit v1.2.1 From d45b4065d8e0b6cc893ccde66e0c37f459ca901b Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Wed, 26 Jul 2017 09:33:23 -0700 Subject: make sure users have to be able to read_pipeline to get pipeline failed notifications --- app/services/notification_service.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 675b4536a26..c330d62a2ce 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -308,7 +308,8 @@ class NotificationService recipients ||= NotificationRecipientService.notifiable_users( [pipeline.user], pipeline.project, :watch, - custom_action: :"#{pipeline.status}_pipeline" + custom_action: :"#{pipeline.status}_pipeline", + read_ability: :read_pipeline, ).map(&:notification_email) if recipients.any? -- cgit v1.2.1 From 3829d7245a446fe80745f5e7c082e005fc013365 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Fri, 28 Jul 2017 11:38:35 -0700 Subject: require that the user be able to :read_build to get a pipeline_failed email --- app/services/notification_service.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index c330d62a2ce..e9a67cac4d6 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -309,7 +309,8 @@ class NotificationService recipients ||= NotificationRecipientService.notifiable_users( [pipeline.user], pipeline.project, :watch, custom_action: :"#{pipeline.status}_pipeline", - read_ability: :read_pipeline, + read_ability: :read_build, + target: pipeline ).map(&:notification_email) if recipients.any? -- cgit v1.2.1 From 19309b970556797027e57c212d9b9aa053c36892 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Mon, 31 Jul 2017 09:30:35 -0700 Subject: default the project to target.project --- app/models/notification_recipient.rb | 12 +++++++++--- app/services/notification_recipient_service.rb | 3 ++- app/services/notification_service.rb | 4 ++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index 0b2eee56ff8..96e8e32c644 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -1,14 +1,20 @@ class NotificationRecipient attr_reader :user, :project, :type - def initialize(user, project, type, - custom_action: nil, target: nil, acting_user: nil, read_ability: nil) - @project = project + def initialize(user, type, + custom_action: nil, + target: nil, + acting_user: nil, + read_ability: nil, + project: nil) @custom_action = custom_action @acting_user = acting_user @read_ability = read_ability @target = target + @project = project || @target&.project @user = user @type = type + + raise ArgumentError, "Project is missing" if @project.nil? end def notification_setting diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 6b4e97aaab4..0627cca9214 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -56,7 +56,8 @@ module NotificationRecipientService end def make_recipient(user, type) - NotificationRecipient.new(user, project, type, + NotificationRecipient.new(user, type, + project: project, custom_action: custom_action, target: target, acting_user: acting_user, diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index e9a67cac4d6..f5366b9ceab 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -272,7 +272,7 @@ class NotificationService end def project_was_moved(project, old_path_with_namespace) - recipients = NotificationRecipientService.notifiable_users(project.team.members, project, :mention) + recipients = NotificationRecipientService.notifiable_users(project.team.members, :mention, project: project) recipients.each do |recipient| mailer.project_was_moved_email( @@ -307,7 +307,7 @@ class NotificationService return unless mailer.respond_to?(email_template) recipients ||= NotificationRecipientService.notifiable_users( - [pipeline.user], pipeline.project, :watch, + [pipeline.user], :watch, custom_action: :"#{pipeline.status}_pipeline", read_ability: :read_build, target: pipeline -- cgit v1.2.1 From 8386d69f8f4a75a45e0226f4ad89b7d200c6d84d Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Tue, 1 Aug 2017 12:51:14 -0700 Subject: .notifiable_users: compact the passed-in users --- app/services/notification_recipient_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 0627cca9214..d412e5414b3 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -3,7 +3,7 @@ # module NotificationRecipientService def self.notifiable_users(users, *args) - users.map { |u| NotificationRecipient.new(u, *args) }.select(&:notifiable?).map(&:user) + users.compact.map { |u| NotificationRecipient.new(u, *args) }.select(&:notifiable?).map(&:user) end def self.notifiable?(user, *args) -- cgit v1.2.1 From e7d136ebdabf3f784412b859bf3ad7427c88e7f6 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Mon, 31 Jul 2017 11:32:17 -0700 Subject: don't require project it's not there in the case of personal snippets, f. ex., and we've already guarded against its being missing in #find_notification_setting --- app/models/notification_recipient.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index 96e8e32c644..1856dfe5734 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -1,5 +1,5 @@ class NotificationRecipient - attr_reader :user, :project, :type + attr_reader :user, :type def initialize(user, type, custom_action: nil, target: nil, @@ -13,8 +13,6 @@ class NotificationRecipient @project = project || @target&.project @user = user @type = type - - raise ArgumentError, "Project is missing" if @project.nil? end def notification_setting -- cgit v1.2.1 From 18288fe21e12bb524757a97472008eabfca5a352 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Mon, 31 Jul 2017 15:04:42 -0700 Subject: style fixes --- app/models/notification_recipient.rb | 15 ++++++++------- app/services/notification_recipient_service.rb | 3 ++- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index 1856dfe5734..28ac22da6e2 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -1,11 +1,13 @@ class NotificationRecipient attr_reader :user, :type - def initialize(user, type, - custom_action: nil, - target: nil, - acting_user: nil, - read_ability: nil, - project: nil) + def initialize( + user, type, + custom_action: nil, + target: nil, + acting_user: nil, + read_ability: nil, + project: nil + ) @custom_action = custom_action @acting_user = acting_user @read_ability = read_ability @@ -103,4 +105,3 @@ class NotificationRecipient user.global_notification_setting end end - diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index d412e5414b3..083b5f2d229 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -56,7 +56,8 @@ module NotificationRecipientService end def make_recipient(user, type) - NotificationRecipient.new(user, type, + NotificationRecipient.new( + user, type, project: project, custom_action: custom_action, target: target, -- cgit v1.2.1 From 3c56e41402301f26bf70afe13d2565e74e4427eb Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Mon, 31 Jul 2017 17:09:01 -0700 Subject: use a simple pluck, since equivalent filtering happens later --- app/services/notification_recipient_service.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 083b5f2d229..7c9bb54f020 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -175,9 +175,7 @@ module NotificationRecipientService end def user_ids_with_global_level_custom(ids, action) - settings = settings_with_global_level_of(:custom, ids) - settings = settings.select { |setting| setting.event_enabled?(action) } - settings.map(&:user_id) + settings_with_global_level_of(:custom, ids).pluck(:user_id) end def settings_with_global_level_of(level, ids) -- cgit v1.2.1 From 488e8e79dd85e973e1b562fe0320f69b2bedec06 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Mon, 31 Jul 2017 18:56:56 -0700 Subject: force queries to include notification settings --- app/services/notification_recipient_service.rb | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 7c9bb54f020..1ce92c8cbdc 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -50,11 +50,20 @@ module NotificationRecipientService def <<(arg) users, type = arg + + if users.is_a?(ActiveRecord::Relation) + users = users.includes(:notification_settings) + end + users = Array(users) users.compact! recipients.concat(users.map { |u| make_recipient(u, type) }) end + def user_scope + User.includes(:notification_settings) + end + def make_recipient(user, type) NotificationRecipient.new( user, type, @@ -114,7 +123,7 @@ module NotificationRecipientService global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global) user_ids += user_ids_with_global_level_custom(global_users_ids, custom_action) - self << [User.find(user_ids), :watch] + self << [user_scope.where(id: user_ids), :watch] end def add_project_watchers @@ -133,7 +142,7 @@ module NotificationRecipientService user_ids_with_project_setting = select_project_members_ids(project, user_ids_with_project_global, user_ids) user_ids_with_group_setting = select_group_members_ids(project.group, project_members_ids, user_ids_with_group_global, user_ids) - User.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq).to_a + user_scope.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq) end def add_subscribed_users -- cgit v1.2.1 From c2dd4239c939e003dfe569196ec2d39e2478606e Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Tue, 1 Aug 2017 10:42:54 -0700 Subject: short-circuit if there is no policy, and add :read_project check --- app/models/notification_recipient.rb | 9 ++++++--- app/services/notification_recipient_service.rb | 6 +----- lib/declarative_policy.rb | 13 +++++++++---- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index 28ac22da6e2..837b62ec0cb 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -76,10 +76,13 @@ class NotificationRecipient end def has_access? - return false unless user.can?(:receive_notifications) - return true unless @read_ability - DeclarativePolicy.subject_scope do + return false unless user.can?(:receive_notifications) + return false if @project && !user.can?(:read_project, @project) + + return true unless @read_ability + return true unless DeclarativePolicy.has_policy?(@target) + user.can?(@read_ability, @target) end end diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 1ce92c8cbdc..540e568fed2 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -308,11 +308,7 @@ module NotificationRecipientService end def read_ability - @read_ability ||= - case target - when Commit then nil - else :"read_#{target.class.model_name.name.underscore}" - end + @read_ability ||= :"read_#{target.class.model_name.name.underscore}" end def subject diff --git a/lib/declarative_policy.rb b/lib/declarative_policy.rb index b1eb1a6cef1..4936669a73a 100644 --- a/lib/declarative_policy.rb +++ b/lib/declarative_policy.rb @@ -28,7 +28,12 @@ module DeclarativePolicy subject = find_delegate(subject) - class_for_class(subject.class) + class_for_class(subject.class) \ + or raise "no policy for #{subject.class.name}" + end + + def has_policy?(subject) + !class_for_class(subject.class).nil? end private @@ -51,9 +56,7 @@ module DeclarativePolicy end end - policy_class = subject_class.instance_variable_get(CLASS_CACHE_IVAR) - raise "no policy for #{subject.class.name}" if policy_class.nil? - policy_class + subject_class.instance_variable_get(CLASS_CACHE_IVAR) end def compute_class_for_class(subject_class) @@ -71,6 +74,8 @@ module DeclarativePolicy nil end end + + nil end def find_delegate(subject) -- cgit v1.2.1 From 444c8584491ce0efe2314747326da6ab7d17490c Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Tue, 1 Aug 2017 11:42:24 -0700 Subject: use safe navigation on notification_setting --- app/models/notification_recipient.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index 837b62ec0cb..a14254178e1 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -31,7 +31,7 @@ class NotificationRecipient @notification_level ||= case raw_notification_level when :custom - if @custom_action && notification_setting.event_enabled?(@custom_action) + if @custom_action && notification_setting&.event_enabled?(@custom_action) :watch else :custom -- cgit v1.2.1 From 4af2c647132bee8b2ef87d0d190936449759071c Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Tue, 1 Aug 2017 11:42:46 -0700 Subject: fix comment --- app/models/notification_recipient.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index a14254178e1..6a8287c88b8 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -27,7 +27,8 @@ class NotificationRecipient def notification_level # custom is treated the same as watch if it's enabled - otherwise it's - # as :disabled. + # set to :custom, meaning to send exactly when our type is :participating + # or :mention. @notification_level ||= case raw_notification_level when :custom -- cgit v1.2.1 From e5496e1e8ef3fa82e6cefcababcb9ad3b55a11c3 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Tue, 1 Aug 2017 11:42:59 -0700 Subject: use the accessor for `type` --- app/models/notification_recipient.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index 6a8287c88b8..c307f0ad5b6 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -54,7 +54,7 @@ class NotificationRecipient return false if %i[watch participating].include?(notification_level) && excluded_watcher_action? - return false unless NotificationSetting.levels[notification_level] <= NotificationSetting.levels[type] + return false unless NotificationSetting.levels[notification_level] <= NotificationSetting.levels[@type] return false if unsubscribed? -- cgit v1.2.1 From 9d53418acb736476e4ede216c1ba9d08a5e3554a Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Tue, 1 Aug 2017 11:43:24 -0700 Subject: clearer argument name --- app/services/notification_recipient_service.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 540e568fed2..cfc1fab210a 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -48,8 +48,8 @@ module NotificationRecipientService @recipients ||= [] end - def <<(arg) - users, type = arg + def <<(pair) + users, type = pair if users.is_a?(ActiveRecord::Relation) users = users.includes(:notification_settings) -- cgit v1.2.1 From 934305ffa3eb1662a367761058ae8ded210d3336 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Tue, 1 Aug 2017 11:51:01 -0700 Subject: rm unused NewNote#subject its functionality is swept into the project permission check in NotificationRecipient#has_access? now --- app/services/notification_recipient_service.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index cfc1fab210a..b2d805e9f27 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -311,10 +311,6 @@ module NotificationRecipientService @read_ability ||= :"read_#{target.class.model_name.name.underscore}" end - def subject - note.for_personal_snippet? ? note.noteable : note.project - end - def build! # Add all users participating in the thread (author, assignee, comment authors) add_participants(note.author) -- cgit v1.2.1 From 246951bba7965f4257aa50377a981f3c85c67f1e Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Tue, 1 Aug 2017 11:52:43 -0700 Subject: unmemoize read_ability since it's only called once now in make_recipient --- app/services/notification_recipient_service.rb | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index b2d805e9f27..97ff1f99f02 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -87,13 +87,12 @@ module NotificationRecipientService end def read_ability - @read_ability ||= - case target - when Issuable - :"read_#{target.to_ability_name}" - when Ci::Pipeline - :read_build # We have build trace in pipeline emails - end + case target + when Issuable + :"read_#{target.to_ability_name}" + when Ci::Pipeline + :read_build # We have build trace in pipeline emails + end end def custom_action @@ -308,7 +307,9 @@ module NotificationRecipientService end def read_ability - @read_ability ||= :"read_#{target.class.model_name.name.underscore}" + return nil if target.nil? + + :"read_#{target.class.model_name.name.underscore}" end def build! -- cgit v1.2.1 From 0487009d3707c8180211bfc4b3c5a8a9daec50b8 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Tue, 1 Aug 2017 11:53:43 -0700 Subject: deparameterize `project` since 99% of the time it's `target.project` anyways. --- app/services/notification_recipient_service.rb | 30 +++++++++------- app/services/notification_service.rb | 48 +++++++++++--------------- 2 files changed, 37 insertions(+), 41 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 97ff1f99f02..b36bc90c884 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -44,6 +44,10 @@ module NotificationRecipientService raise 'abstract' end + def project + target.project + end + def recipients @recipients ||= [] end @@ -138,7 +142,7 @@ module NotificationRecipientService user_ids = user_ids_with_global_level_watch((user_ids_with_project_global + user_ids_with_group_global).uniq) - user_ids_with_project_setting = select_project_members_ids(project, user_ids_with_project_global, user_ids) + user_ids_with_project_setting = select_project_members_ids(user_ids_with_project_global, user_ids) user_ids_with_group_setting = select_group_members_ids(project.group, project_members_ids, user_ids_with_group_global, user_ids) user_scope.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq) @@ -163,7 +167,7 @@ module NotificationRecipientService end # Build a list of user_ids based on project notification settings - def select_project_members_ids(project, global_setting, user_ids_global_level_watch) + def select_project_members_ids(global_setting, user_ids_global_level_watch) user_ids = user_ids_notifiable_on(project, :watch) # If project setting is global, add to watch list if global setting is watch @@ -230,14 +234,12 @@ module NotificationRecipientService end class Default < Base - attr_reader :project attr_reader :target attr_reader :current_user attr_reader :action attr_reader :previous_assignee attr_reader :skip_current_user - def initialize(project, target, current_user, action:, previous_assignee: nil, skip_current_user: true) - @project = project + def initialize(target, current_user, action:, previous_assignee: nil, skip_current_user: true) @target = target @current_user = current_user @action = action @@ -280,12 +282,10 @@ module NotificationRecipientService end class Relabeled < Base - attr_reader :project attr_reader :target attr_reader :current_user attr_reader :labels - def initialize(project, target, current_user, labels:) - @project = project + def initialize(target, current_user, labels:) @target = target @current_user = current_user @labels = labels @@ -297,13 +297,17 @@ module NotificationRecipientService end class NewNote < Base - attr_reader :project attr_reader :note - attr_reader :target - def initialize(project, note) - @project = project + def initialize(note) @note = note - @target = note.noteable + end + + def target + note.noteable + end + + def project + note.project end def read_ability diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index f5366b9ceab..c93f82999dc 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -42,7 +42,7 @@ class NotificationService # * users with custom level checked with "new issue" # def new_issue(issue, current_user) - new_resource_email(issue, issue.project, :new_issue_email) + new_resource_email(issue, :new_issue_email) end # When issue text is updated, we should send an email to: @@ -52,7 +52,6 @@ class NotificationService def new_mentions_in_issue(issue, new_mentioned_users, current_user) new_mentions_in_resource_email( issue, - issue.project, new_mentioned_users, current_user, :new_mention_in_issue_email @@ -67,7 +66,7 @@ class NotificationService # * users with custom level checked with "close issue" # def close_issue(issue, current_user) - close_resource_email(issue, issue.project, current_user, :closed_issue_email) + close_resource_email(issue, current_user, :closed_issue_email) end # When we reassign an issue we should send an email to: @@ -78,7 +77,6 @@ class NotificationService # def reassigned_issue(issue, current_user, previous_assignees = []) recipients = NotificationRecipientService.build_recipients( - issue.project, issue, current_user, action: "reassign", @@ -103,7 +101,7 @@ class NotificationService # * watchers of the issue's labels # def relabeled_issue(issue, added_labels, current_user) - relabeled_resource_email(issue, issue.project, added_labels, current_user, :relabeled_issue_email) + relabeled_resource_email(issue, added_labels, current_user, :relabeled_issue_email) end # When create a merge request we should send an email to: @@ -114,7 +112,7 @@ class NotificationService # * users with custom level checked with "new merge request" # def new_merge_request(merge_request, current_user) - new_resource_email(merge_request, merge_request.target_project, :new_merge_request_email) + new_resource_email(merge_request, :new_merge_request_email) end # When merge request text is updated, we should send an email to: @@ -124,7 +122,6 @@ class NotificationService def new_mentions_in_merge_request(merge_request, new_mentioned_users, current_user) new_mentions_in_resource_email( merge_request, - merge_request.target_project, new_mentioned_users, current_user, :new_mention_in_merge_request_email @@ -138,7 +135,7 @@ class NotificationService # * users with custom level checked with "reassign merge request" # def reassigned_merge_request(merge_request, current_user) - reassign_resource_email(merge_request, merge_request.target_project, current_user, :reassigned_merge_request_email) + reassign_resource_email(merge_request, current_user, :reassigned_merge_request_email) end # When we add labels to a merge request we should send an email to: @@ -146,21 +143,20 @@ class NotificationService # * watchers of the mr's labels # def relabeled_merge_request(merge_request, added_labels, current_user) - relabeled_resource_email(merge_request, merge_request.target_project, added_labels, current_user, :relabeled_merge_request_email) + relabeled_resource_email(merge_request, added_labels, current_user, :relabeled_merge_request_email) end def close_mr(merge_request, current_user) - close_resource_email(merge_request, merge_request.target_project, current_user, :closed_merge_request_email) + close_resource_email(merge_request, current_user, :closed_merge_request_email) end def reopen_issue(issue, current_user) - reopen_resource_email(issue, issue.project, current_user, :issue_status_changed_email, 'reopened') + reopen_resource_email(issue, current_user, :issue_status_changed_email, 'reopened') end def merge_mr(merge_request, current_user) close_resource_email( merge_request, - merge_request.target_project, current_user, :merged_merge_request_email, skip_current_user: !merge_request.merge_when_pipeline_succeeds? @@ -170,7 +166,6 @@ class NotificationService def reopen_mr(merge_request, current_user) reopen_resource_email( merge_request, - merge_request.target_project, current_user, :merge_request_status_email, 'reopened' @@ -179,7 +174,6 @@ class NotificationService def resolve_all_discussions(merge_request, current_user) recipients = NotificationRecipientService.build_recipients( - merge_request.target_project, merge_request, current_user, action: "resolve_all_discussions") @@ -204,7 +198,7 @@ class NotificationService notify_method = "note_#{note.to_ability_name}_email".to_sym - recipients = NotificationRecipientService.build_new_note_recipients(note.project, note) + recipients = NotificationRecipientService.build_new_note_recipients(note) recipients.each do |recipient| mailer.send(notify_method, recipient.id, note.id).deliver_later end @@ -284,7 +278,7 @@ class NotificationService end def issue_moved(issue, new_issue, current_user) - recipients = NotificationRecipientService.build_recipients(issue.project, issue, current_user, action: 'moved') + recipients = NotificationRecipientService.build_recipients(issue, current_user, action: 'moved') recipients.map do |recipient| email = mailer.issue_moved_email(recipient, issue, new_issue, current_user) @@ -320,16 +314,16 @@ class NotificationService protected - def new_resource_email(target, project, method) - recipients = NotificationRecipientService.build_recipients(project, target, target.author, action: "new") + def new_resource_email(target, method) + recipients = NotificationRecipientService.build_recipients(target, target.author, action: "new") recipients.each do |recipient| mailer.send(method, recipient.id, target.id).deliver_later end end - def new_mentions_in_resource_email(target, project, new_mentioned_users, current_user, method) - recipients = NotificationRecipientService.build_recipients(project, target, current_user, action: "new") + def new_mentions_in_resource_email(target, new_mentioned_users, current_user, method) + recipients = NotificationRecipientService.build_recipients(target, current_user, action: "new") recipients = recipients & new_mentioned_users recipients.each do |recipient| @@ -337,11 +331,10 @@ class NotificationService end end - def close_resource_email(target, project, current_user, method, skip_current_user: true) + def close_resource_email(target, current_user, method, skip_current_user: true) action = method == :merged_merge_request_email ? "merge" : "close" recipients = NotificationRecipientService.build_recipients( - project, target, current_user, action: action, @@ -353,12 +346,11 @@ class NotificationService end end - def reassign_resource_email(target, project, current_user, method) + def reassign_resource_email(target, current_user, method) previous_assignee_id = previous_record(target, 'assignee_id') previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id recipients = NotificationRecipientService.build_recipients( - project, target, current_user, action: "reassign", @@ -376,8 +368,8 @@ class NotificationService end end - def relabeled_resource_email(target, project, labels, current_user, method) - recipients = NotificationRecipientService.build_relabeled_recipients(project, target, current_user, labels: labels) + def relabeled_resource_email(target, labels, current_user, method) + recipients = NotificationRecipientService.build_relabeled_recipients(target, current_user, labels: labels) label_names = labels.map(&:name) recipients.each do |recipient| @@ -385,8 +377,8 @@ class NotificationService end end - def reopen_resource_email(target, project, current_user, method, status) - recipients = NotificationRecipientService.build_recipients(project, target, current_user, action: "reopen") + def reopen_resource_email(target, current_user, method, status) + recipients = NotificationRecipientService.build_recipients(target, current_user, action: "reopen") recipients.each do |recipient| mailer.send(method, recipient.id, target.id, status, current_user.id).deliver_later -- cgit v1.2.1 From 4dfda5f2662afe32fcaa95b851fe6bdff9e20c08 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Tue, 1 Aug 2017 11:58:44 -0700 Subject: declarative_policy rubocop fix --- lib/declarative_policy.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/declarative_policy.rb b/lib/declarative_policy.rb index 4936669a73a..ae65653645b 100644 --- a/lib/declarative_policy.rb +++ b/lib/declarative_policy.rb @@ -28,8 +28,9 @@ module DeclarativePolicy subject = find_delegate(subject) - class_for_class(subject.class) \ - or raise "no policy for #{subject.class.name}" + policy_class = class_for_class(subject.class) + raise "no policy for #{subject.class.name}" if policy_class.nil? + policy_class end def has_policy?(subject) -- cgit v1.2.1 From 1ccdd99fbd82c082a992ea344938dca17c337140 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Tue, 1 Aug 2017 13:18:51 -0700 Subject: disable the delegate cop --- app/services/notification_recipient_service.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index b36bc90c884..0e1527c1a7a 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -44,6 +44,7 @@ module NotificationRecipientService raise 'abstract' end + # rubocop:disable Rails/Delegate def project target.project end -- cgit v1.2.1 From 56a40a2b6548d57c2c2a32b34a76c157ae5fdeab Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Tue, 1 Aug 2017 13:26:55 -0700 Subject: rm unused methods --- app/services/notification_recipient_service.rb | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 0e1527c1a7a..5d394f67631 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -199,32 +199,6 @@ module NotificationRecipientService ) end - def reject_unsubscribed_users - return unless target.respond_to? :subscriptions - - recipients.reject! do |recipient| - user = recipient.user - subscription = target.subscriptions.find_by_user_id(user.id) - subscription && !subscription.subscribed - end - end - - def reject_users_without_access - recipients.select! { |r| r.user.can?(:receive_notifications) } - - return unless read_ability - - DeclarativePolicy.subject_scope do - recipients.select! do |recipient| - recipient.user.can?(read_ability, target) - end - end - end - - def reject_user(user) - recipients.reject! { |r| r.user == user } - end - def add_labels_subscribers(labels: nil) return unless target.respond_to? :labels -- cgit v1.2.1 From eaa503d679e3c2a1396091efebda49a91637cb02 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Wed, 2 Aug 2017 14:14:53 -0700 Subject: move the read_ability logic into NotificationRecipient --- app/models/notification_recipient.rb | 22 ++++++++++++++++++---- app/services/notification_recipient_service.rb | 16 ---------------- app/services/notification_service.rb | 1 - 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index c307f0ad5b6..418b42d8f1d 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -5,12 +5,10 @@ class NotificationRecipient custom_action: nil, target: nil, acting_user: nil, - read_ability: nil, project: nil ) @custom_action = custom_action @acting_user = acting_user - @read_ability = read_ability @target = target @project = project || @target&.project @user = user @@ -81,10 +79,10 @@ class NotificationRecipient return false unless user.can?(:receive_notifications) return false if @project && !user.can?(:read_project, @project) - return true unless @read_ability + return true unless read_ability return true unless DeclarativePolicy.has_policy?(@target) - user.can?(@read_ability, @target) + user.can?(read_ability, @target) end end @@ -97,6 +95,22 @@ class NotificationRecipient private + def read_ability + return @read_ability if instance_variable_defined?(:@read_ability) + + @read_ability = + case @target + when Issuable + :"read_#{@target.to_ability_name}" + when Ci::Pipeline + :read_build # We have build trace in pipeline emails + when ActiveRecord::Base + :"read_#{@target.class.model_name.name.underscore}" + else + nil + end + end + def find_notification_setting project_setting = @project && user.notification_settings_for(@project) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 5d394f67631..7d4f8110f84 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -76,7 +76,6 @@ module NotificationRecipientService custom_action: custom_action, target: target, acting_user: acting_user, - read_ability: read_ability ) end @@ -91,15 +90,6 @@ module NotificationRecipientService end end - def read_ability - case target - when Issuable - :"read_#{target.to_ability_name}" - when Ci::Pipeline - :read_build # We have build trace in pipeline emails - end - end - def custom_action nil end @@ -285,12 +275,6 @@ module NotificationRecipientService note.project end - def read_ability - return nil if target.nil? - - :"read_#{target.class.model_name.name.underscore}" - end - def build! # Add all users participating in the thread (author, assignee, comment authors) add_participants(note.author) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index c93f82999dc..2e52296f048 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -303,7 +303,6 @@ class NotificationService recipients ||= NotificationRecipientService.notifiable_users( [pipeline.user], :watch, custom_action: :"#{pipeline.status}_pipeline", - read_ability: :read_build, target: pipeline ).map(&:notification_email) -- cgit v1.2.1 From 273cd9c3c9d94e1d40249073ec23e4eba820f260 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Wed, 2 Aug 2017 14:16:00 -0700 Subject: remove build_relabeled_recipients --- app/services/notification_recipient_service.rb | 19 ------------------- app/services/notification_service.rb | 8 +++++++- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 7d4f8110f84..8c7a36d4cb0 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -14,10 +14,6 @@ module NotificationRecipientService Builder::Default.new(*a).recipient_users end - def self.build_relabeled_recipients(*a) - Builder::Relabeled.new(*a).recipient_users - end - def self.build_new_note_recipients(*a) Builder::NewNote.new(*a).recipient_users end @@ -246,21 +242,6 @@ module NotificationRecipientService end end - class Relabeled < Base - attr_reader :target - attr_reader :current_user - attr_reader :labels - def initialize(target, current_user, labels:) - @target = target - @current_user = current_user - @labels = labels - end - - def build! - add_labels_subscribers(labels: labels) - end - end - class NewNote < Base attr_reader :note def initialize(note) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 2e52296f048..a62085dffd4 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -368,7 +368,13 @@ class NotificationService end def relabeled_resource_email(target, labels, current_user, method) - recipients = NotificationRecipientService.build_relabeled_recipients(target, current_user, labels: labels) + recipients = labels.flat_map { |l| l.subscribers(target.project) } + recipients = NotificationRecipientService.notifiable_users( + recipients, :subscription, + target: target, + acting_user: current_user, + ) + label_names = labels.map(&:name) recipients.each do |recipient| -- cgit v1.2.1 From e4789cdfdcc2cebb080c96b5165733043db0ce5e Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Wed, 2 Aug 2017 14:21:31 -0700 Subject: add a comment warning that note.project may be nil --- app/services/notification_recipient_service.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 8c7a36d4cb0..0382ecd4abb 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -252,6 +252,10 @@ module NotificationRecipientService note.noteable end + # NOTE: may be nil, in the case of a PersonalSnippet + # + # (this is okay because NotificationRecipient is written + # to handle nil projects) def project note.project end -- cgit v1.2.1 From 17147f2027896c557cb52dbb5344b87e7d4db1ea Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Wed, 2 Aug 2017 18:54:18 -0700 Subject: rubocop style fix --- app/services/notification_recipient_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 0382ecd4abb..21c9c314a2a 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -71,7 +71,7 @@ module NotificationRecipientService project: project, custom_action: custom_action, target: target, - acting_user: acting_user, + acting_user: acting_user ) end -- cgit v1.2.1 From ae96e6a3a6944d743eea2eb6334d6f48d6f12320 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 3 Aug 2017 17:17:22 +0100 Subject: use new dropdown styling --- app/assets/stylesheets/pages/diff.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index cfc801859a7..da77346d8b2 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -561,6 +561,7 @@ .diff-files-changed { .commit-stat-summary { + @include new-style-dropdown; z-index: -1; @media (min-width: $screen-sm-min) { -- cgit v1.2.1 From ae99f05b059f613296c39dfa45c37dbcab40f4cd Mon Sep 17 00:00:00 2001 From: haseeb Date: Thu, 3 Aug 2017 16:39:10 +0000 Subject: fix #35133 strip new lines from ssh keys --- app/models/key.rb | 3 +-- spec/models/key_spec.rb | 14 ++++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/models/key.rb b/app/models/key.rb index cb8f10f6d55..49bc26122fa 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -16,8 +16,6 @@ class Key < ActiveRecord::Base presence: true, length: { maximum: 5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ } - validates :key, - format: { without: /\n|\r/, message: 'should be a single line' } validates :fingerprint, uniqueness: true, presence: { message: 'cannot be generated' } @@ -31,6 +29,7 @@ class Key < ActiveRecord::Base after_destroy :post_destroy_hook def key=(value) + value&.delete!("\n\r") value.strip! unless value.blank? write_attribute(:key, value) end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index d41717d0223..251d0cfd08c 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -94,15 +94,17 @@ describe Key do expect(key).not_to be_valid end - it 'rejects the unfingerprintable key (not a key)' do - expect(build(:key, key: 'ssh-rsa an-invalid-key==')).not_to be_valid + it 'accepts a key with newline charecters after stripping them' do + key = build(:key) + key.key = key.key.insert(100, "\n") + key.key = key.key.insert(40, "\r\n") + expect(key).to be_valid end - it 'rejects the multiple line key' do - key = build(:key) - key.key.tr!(' ', "\n") - expect(key).not_to be_valid + it 'rejects the unfingerprintable key (not a key)' do + expect(build(:key, key: 'ssh-rsa an-invalid-key==')).not_to be_valid end + end context 'callbacks' do -- cgit v1.2.1 From 4974f1ef86f697fa410093720f51414791f210c6 Mon Sep 17 00:00:00 2001 From: haseeb Date: Thu, 3 Aug 2017 16:44:48 +0000 Subject: fix #3686 make tarball download url to end with extension --- changelogs/unreleased/3686_make_tarball_download_url.yml | 4 ++++ config/routes/repository.rb | 2 +- spec/controllers/projects/repositories_controller_spec.rb | 2 +- spec/routing/project_routing_spec.rb | 10 +++++++--- 4 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/3686_make_tarball_download_url.yml diff --git a/changelogs/unreleased/3686_make_tarball_download_url.yml b/changelogs/unreleased/3686_make_tarball_download_url.yml new file mode 100644 index 00000000000..4e75e52e3ac --- /dev/null +++ b/changelogs/unreleased/3686_make_tarball_download_url.yml @@ -0,0 +1,4 @@ +--- +title: repository archive download url now ends with selected file extension +merge_request: 13178 +author: haseebeqx diff --git a/config/routes/repository.rb b/config/routes/repository.rb index edcf3ddf57b..2ba16035ece 100644 --- a/config/routes/repository.rb +++ b/config/routes/repository.rb @@ -2,7 +2,7 @@ resource :repository, only: [:create] do member do - get 'archive', constraints: { format: Gitlab::PathRegex.archive_formats_regex } + get ':ref/archive', constraints: { format: Gitlab::PathRegex.archive_formats_regex, ref: /.+/ }, action: 'archive', as: 'archive' end end diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb index 9c55d159fa0..f712d1e0d63 100644 --- a/spec/controllers/projects/repositories_controller_spec.rb +++ b/spec/controllers/projects/repositories_controller_spec.rb @@ -6,7 +6,7 @@ describe Projects::RepositoriesController do describe "GET archive" do context 'as a guest' do it 'responds with redirect in correct format' do - get :archive, namespace_id: project.namespace, project_id: project, format: "zip" + get :archive, namespace_id: project.namespace, project_id: project, format: "zip", ref: 'master' expect(response.header["Content-Type"]).to start_with('text/html') expect(response).to be_redirect diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index c02409b2e0b..39d44245c3f 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -165,15 +165,19 @@ describe 'project routing' do # edit_project_repository GET /:project_id/repository/edit(.:format) projects/repositories#edit describe Projects::RepositoriesController, 'routing' do it 'to #archive' do - expect(get('/gitlab/gitlabhq/repository/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(get('/gitlab/gitlabhq/repository/master/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', ref: 'master') end it 'to #archive format:zip' do - expect(get('/gitlab/gitlabhq/repository/archive.zip')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'zip') + expect(get('/gitlab/gitlabhq/repository/master/archive.zip')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'zip', ref: 'master') end it 'to #archive format:tar.bz2' do - expect(get('/gitlab/gitlabhq/repository/archive.tar.bz2')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.bz2') + expect(get('/gitlab/gitlabhq/repository/master/archive.tar.bz2')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.bz2', ref: 'master') + end + + it 'to #archive with "/" in route' do + expect(get('/gitlab/gitlabhq/repository/improve/awesome/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', ref: 'improve/awesome') end end -- cgit v1.2.1 From 03440eed20cc36a2f9836dc078d2101849e11319 Mon Sep 17 00:00:00 2001 From: Ahmad Sherif Date: Fri, 28 Jul 2017 14:16:26 +0200 Subject: Migrate blame loading to Gitaly Closes gitaly#421 --- GITALY_SERVER_VERSION | 2 +- Gemfile | 2 +- Gemfile.lock | 4 +- lib/gitlab/git/blame.rb | 24 +++++-- lib/gitlab/git/repository.rb | 16 ++--- lib/gitlab/gitaly_client/commit_service.rb | 11 ++++ spec/lib/gitlab/git/blame_spec.rb | 102 ++++++++++++++++------------- 7 files changed, 97 insertions(+), 64 deletions(-) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 4e8f395fa5e..1b58cc10180 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.26.0 +0.27.0 diff --git a/Gemfile b/Gemfile index 403b104a9d6..34f231a04b8 100644 --- a/Gemfile +++ b/Gemfile @@ -391,7 +391,7 @@ gem 'vmstat', '~> 2.3.0' gem 'sys-filesystem', '~> 1.1.6' # Gitaly GRPC client -gem 'gitaly', '~> 0.23.0' +gem 'gitaly', '~> 0.24.0' gem 'toml-rb', '~> 0.3.15', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 9f90965a567..a17aa3ee1ed 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -269,7 +269,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly (0.23.0) + gitaly (0.24.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -978,7 +978,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly (~> 0.23.0) + gitaly (~> 0.24.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.5.1) diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb index 0deaab01b5b..8dbe25e55f6 100644 --- a/lib/gitlab/git/blame.rb +++ b/lib/gitlab/git/blame.rb @@ -1,5 +1,3 @@ -# Gitaly note: JV: needs 1 RPC for #load_blame. - module Gitlab module Git class Blame @@ -26,15 +24,29 @@ module Gitlab private - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/376 def load_blame - cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path}) - # Read in binary mode to ensure ASCII-8BIT - raw_output = IO.popen(cmd, 'rb') {|io| io.read } + raw_output = @repo.gitaly_migrate(:blame) do |is_enabled| + if is_enabled + load_blame_by_gitaly + else + load_blame_by_shelling_out + end + end + output = encode_utf8(raw_output) process_raw_blame output end + def load_blame_by_gitaly + @repo.gitaly_commit_client.raw_blame(@sha, @path) + end + + def load_blame_by_shelling_out + cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path}) + # Read in binary mode to ensure ASCII-8BIT + IO.popen(cmd, 'rb') {|io| io.read } + end + def process_raw_blame(output) lines, final = [], [] info, commits = {}, {} diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index ffe2c8b91bb..1c3beb5e834 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -683,6 +683,14 @@ module Gitlab @gitaly_repository_client ||= Gitlab::GitalyClient::RepositoryService.new(self) end + def gitaly_migrate(method, &block) + Gitlab::GitalyClient.migrate(method, &block) + rescue GRPC::NotFound => e + raise NoRepository.new(e) + rescue GRPC::BadStatus => e + raise CommandError.new(e) + end + private # Gitaly note: JV: Trying to get rid of the 'filter' option so we can implement this with 'git'. @@ -1017,14 +1025,6 @@ module Gitlab raw_output.to_i end - - def gitaly_migrate(method, &block) - Gitlab::GitalyClient.migrate(method, &block) - rescue GRPC::NotFound => e - raise NoRepository.new(e) - rescue GRPC::BadStatus => e - raise CommandError.new(e) - end end end end diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index b1424a458e9..1ae13677b42 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -128,6 +128,17 @@ module Gitlab response.languages.map { |l| { value: l.share.round(2), label: l.name, color: l.color, highlight: l.color } } end + def raw_blame(revision, path) + request = Gitaly::RawBlameRequest.new( + repository: @gitaly_repo, + revision: revision, + path: path + ) + + response = GitalyClient.call(@repository.storage, :commit_service, :raw_blame, request) + response.reduce("") { |memo, msg| memo << msg.data } + end + private def commit_diff_request_params(commit, options = {}) diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb index 66c016d14b3..800c245b130 100644 --- a/spec/lib/gitlab/git/blame_spec.rb +++ b/spec/lib/gitlab/git/blame_spec.rb @@ -7,63 +7,73 @@ describe Gitlab::Git::Blame, seed_helper: true do Gitlab::Git::Blame.new(repository, SeedRepo::Commit::ID, "CONTRIBUTING.md") end - context "each count" do - it do - data = [] - blame.each do |commit, line| - data << { - commit: commit, - line: line - } - end - - expect(data.size).to eq(95) - expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) - expect(data.first[:line]).to eq("# Contribute to GitLab") - expect(data.first[:line]).to be_utf8 - end - end + shared_examples 'blaming a file' do + context "each count" do + it do + data = [] + blame.each do |commit, line| + data << { + commit: commit, + line: line + } + end - context "ISO-8859 encoding" do - let(:blame) do - Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt") + expect(data.size).to eq(95) + expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) + expect(data.first[:line]).to eq("# Contribute to GitLab") + expect(data.first[:line]).to be_utf8 + end end - it 'converts to UTF-8' do - data = [] - blame.each do |commit, line| - data << { - commit: commit, - line: line - } + context "ISO-8859 encoding" do + let(:blame) do + Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt") end - expect(data.size).to eq(1) - expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) - expect(data.first[:line]).to eq("Ä ü") - expect(data.first[:line]).to be_utf8 - end - end + it 'converts to UTF-8' do + data = [] + blame.each do |commit, line| + data << { + commit: commit, + line: line + } + end - context "unknown encoding" do - let(:blame) do - Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt") + expect(data.size).to eq(1) + expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) + expect(data.first[:line]).to eq("Ä ü") + expect(data.first[:line]).to be_utf8 + end end - it 'converts to UTF-8' do - expect(CharlockHolmes::EncodingDetector).to receive(:detect).and_return(nil) - data = [] - blame.each do |commit, line| - data << { + context "unknown encoding" do + let(:blame) do + Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt") + end + + it 'converts to UTF-8' do + expect(CharlockHolmes::EncodingDetector).to receive(:detect).and_return(nil) + data = [] + blame.each do |commit, line| + data << { commit: commit, line: line - } - end + } + end - expect(data.size).to eq(1) - expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) - expect(data.first[:line]).to eq(" ") - expect(data.first[:line]).to be_utf8 + expect(data.size).to eq(1) + expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) + expect(data.first[:line]).to eq(" ") + expect(data.first[:line]).to be_utf8 + end end end + + context 'when Gitaly blame feature is enabled' do + it_behaves_like 'blaming a file' + end + + context 'when Gitaly blame feature is disabled', skip_gitaly_mock: true do + it_behaves_like 'blaming a file' + end end -- cgit v1.2.1 From f81c07eacc6c37092b13559dcaf479805138eb45 Mon Sep 17 00:00:00 2001 From: Ahmad Sherif Date: Mon, 31 Jul 2017 19:38:36 +0200 Subject: Migrate Repository#last_commit_for_path to Gitaly Closes gitaly#433 --- GITALY_SERVER_VERSION | 2 +- Gemfile | 2 +- Gemfile.lock | 4 +-- app/models/repository.rb | 34 +++++++++++++++++++++---- lib/gitlab/git/repository.rb | 21 ++++++++++------ lib/gitlab/gitaly_client/commit_service.rb | 14 +++++++++++ spec/models/repository_spec.rb | 40 ++++++++++++++++++++++-------- 7 files changed, 90 insertions(+), 27 deletions(-) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 4e8f395fa5e..1b58cc10180 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.26.0 +0.27.0 diff --git a/Gemfile b/Gemfile index 403b104a9d6..34f231a04b8 100644 --- a/Gemfile +++ b/Gemfile @@ -391,7 +391,7 @@ gem 'vmstat', '~> 2.3.0' gem 'sys-filesystem', '~> 1.1.6' # Gitaly GRPC client -gem 'gitaly', '~> 0.23.0' +gem 'gitaly', '~> 0.24.0' gem 'toml-rb', '~> 0.3.15', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 9f90965a567..a17aa3ee1ed 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -269,7 +269,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly (0.23.0) + gitaly (0.24.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -978,7 +978,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly (~> 0.23.0) + gitaly (~> 0.24.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.5.1) diff --git a/app/models/repository.rb b/app/models/repository.rb index 4e9fe759fdc..2dd48290e58 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -613,17 +613,26 @@ class Repository end def last_commit_for_path(sha, path) - sha = last_commit_id_for_path(sha, path) - commit(sha) + raw_repository.gitaly_migrate(:last_commit_for_path) do |is_enabled| + if is_enabled + last_commit_for_path_by_gitaly(sha, path) + else + last_commit_for_path_by_rugged(sha, path) + end + end end - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/383 def last_commit_id_for_path(sha, path) key = path.blank? ? "last_commit_id_for_path:#{sha}" : "last_commit_id_for_path:#{sha}:#{Digest::SHA1.hexdigest(path)}" cache.fetch(key) do - args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path}) - Gitlab::Popen.popen(args, path_to_repo).first.strip + raw_repository.gitaly_migrate(:last_commit_for_path) do |is_enabled| + if is_enabled + last_commit_for_path_by_gitaly(sha, path).id + else + last_commit_id_for_path_by_shelling_out(sha, path) + end + end end end @@ -1138,6 +1147,21 @@ class Repository Rugged::Commit.create(rugged, params) end + def last_commit_for_path_by_gitaly(sha, path) + c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path) + commit(c) + end + + def last_commit_for_path_by_rugged(sha, path) + sha = last_commit_id_for_path_by_shelling_out(sha, path) + commit(sha) + end + + def last_commit_id_for_path_by_shelling_out(sha, path) + args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path}) + Gitlab::Popen.popen(args, path_to_repo).first.strip + end + def repository_storage_path @project.repository_storage_path end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index ffe2c8b91bb..734aed8fbc1 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -683,6 +683,14 @@ module Gitlab @gitaly_repository_client ||= Gitlab::GitalyClient::RepositoryService.new(self) end + def gitaly_migrate(method, &block) + Gitlab::GitalyClient.migrate(method, &block) + rescue GRPC::NotFound => e + raise NoRepository.new(e) + rescue GRPC::BadStatus => e + raise CommandError.new(e) + end + private # Gitaly note: JV: Trying to get rid of the 'filter' option so we can implement this with 'git'. @@ -998,6 +1006,11 @@ module Gitlab end.sort_by(&:name) end + def last_commit_for_path_by_rugged(sha, path) + sha = last_commit_id_for_path(sha, path) + commit(sha) + end + def tags_from_gitaly gitaly_ref_client.tags end @@ -1017,14 +1030,6 @@ module Gitlab raw_output.to_i end - - def gitaly_migrate(method, &block) - Gitlab::GitalyClient.migrate(method, &block) - rescue GRPC::NotFound => e - raise NoRepository.new(e) - rescue GRPC::BadStatus => e - raise CommandError.new(e) - end end end end diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index b1424a458e9..3f6c2c09418 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -97,6 +97,20 @@ module Gitlab GitalyClient.call(@repository.storage, :commit_service, :count_commits, request).count end + def last_commit_for_path(revision, path) + request = Gitaly::LastCommitForPathRequest.new( + repository: @gitaly_repo, + revision: revision.force_encoding(Encoding::ASCII_8BIT), + path: path.to_s.force_encoding(Encoding::ASCII_8BIT) + ) + + gitaly_commit = GitalyClient.call(@repository.storage, :commit_service, :last_commit_for_path, request).commit + return unless gitaly_commit + + commit = GitalyClient::Commit.new(@repository, gitaly_commit) + Gitlab::Git::Commit.new(commit) + end + def between(from, to) request = Gitaly::CommitsBetweenRequest.new( repository: @gitaly_repo, diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 764f548be45..f876baaa805 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -139,24 +139,44 @@ describe Repository do end describe '#last_commit_for_path' do - subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id } + shared_examples 'getting last commit for path' do + subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id } - it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } + it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } + end + + context 'when Gitaly feature last_commit_for_path is enabled' do + it_behaves_like 'getting last commit for path' + end + + context 'when Gitaly feature last_commit_for_path is disabled', skip_gitaly_mock: true do + it_behaves_like 'getting last commit for path' + end end describe '#last_commit_id_for_path' do - subject { repository.last_commit_id_for_path(sample_commit.id, '.gitignore') } + shared_examples 'getting last commit ID for path' do + subject { repository.last_commit_id_for_path(sample_commit.id, '.gitignore') } - it "returns last commit id for a given path" do - is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') + it "returns last commit id for a given path" do + is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') + end + + it "caches last commit id for a given path" do + cache = repository.send(:cache) + key = "last_commit_id_for_path:#{sample_commit.id}:#{Digest::SHA1.hexdigest('.gitignore')}" + + expect(cache).to receive(:fetch).with(key).and_return('c1acaa5') + is_expected.to eq('c1acaa5') + end end - it "caches last commit id for a given path" do - cache = repository.send(:cache) - key = "last_commit_id_for_path:#{sample_commit.id}:#{Digest::SHA1.hexdigest('.gitignore')}" + context 'when Gitaly feature last_commit_for_path is enabled' do + it_behaves_like 'getting last commit ID for path' + end - expect(cache).to receive(:fetch).with(key).and_return('c1acaa5') - is_expected.to eq('c1acaa5') + context 'when Gitaly feature last_commit_for_path is disabled', skip_gitaly_mock: true do + it_behaves_like 'getting last commit ID for path' end end -- cgit v1.2.1 From 8b7f4a6a2cc2e4d68b0e55f4110b848b408ccae9 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Thu, 3 Aug 2017 17:54:12 +0000 Subject: Use mixin for new dropdown style --- app/assets/stylesheets/framework/dropdowns.scss | 7 ++++++- app/assets/stylesheets/framework/header.scss | 21 ++------------------ app/assets/stylesheets/pages/cycle_analytics.scss | 24 ++--------------------- app/assets/stylesheets/pages/tree.scss | 23 +--------------------- 4 files changed, 11 insertions(+), 64 deletions(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 572203bce34..89dd99831e4 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -725,7 +725,8 @@ // TODO: change global style and remove mixin @mixin new-style-dropdown { - .dropdown-menu { + .dropdown-menu, + .dropdown-menu-nav { li { padding: 0 1px; @@ -766,4 +767,8 @@ } } } + + .dropdown-menu-align-right { + margin-top: 2px; + } } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 1c4238bc564..555e444a062 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -4,6 +4,8 @@ */ header { + @include new-style-dropdown; + transition: padding $sidebar-transition-duration; &.navbar-empty { @@ -313,25 +315,6 @@ header { .impersonation i { color: $red-500; } - - // TODO: fallback to global style - .dropdown-menu, - .dropdown-menu-nav { - li { - padding: 0 1px; - - a { - border-radius: 0; - padding: 8px 16px; - - &:hover, - &:active, - &:focus { - background-color: $gray-darker; - } - } - } - } } .with-performance-bar header.navbar-gitlab { diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 87b50c7687e..6753eb08285 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -1,4 +1,6 @@ #cycle-analytics { + @include new-style-dropdown; + max-width: 1000px; margin: 24px auto 0; position: relative; @@ -110,10 +112,6 @@ .js-ca-dropdown { top: $gl-padding-top; - - .dropdown-menu-align-right { - margin-top: 2px; - } } .content-list { @@ -446,24 +444,6 @@ margin-bottom: 20px; } } - - // TODO: fallback to global style - .dropdown-menu { - li { - padding: 0 1px; - - a { - border-radius: 0; - padding: 8px 16px; - - &:hover, - &:active, - &:focus { - background-color: $gray-darker; - } - } - } - } } .cycle-analytics-overview { diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index e0f46172769..44ab07a4367 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -1,4 +1,5 @@ .tree-holder { + @include new-style-dropdown; .nav-block { margin: 10px 0; @@ -202,28 +203,6 @@ } } } - - // TODO: fallback to global style - .dropdown-menu:not(.dropdown-menu-selectable) { - li { - padding: 0 1px; - - &.dropdown-header { - padding: 8px 16px; - } - - a { - border-radius: 0; - padding: 8px 16px; - - &:hover, - &:active, - &:focus { - background-color: $gray-darker; - } - } - } - } } .blob-commit-info { -- cgit v1.2.1 From 120fd3b3aea9cda81384c6488a1136c176ab3965 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Thu, 3 Aug 2017 11:52:53 -0700 Subject: another rubocop style fix --- app/services/notification_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index a62085dffd4..df04b1a4fe3 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -372,7 +372,7 @@ class NotificationService recipients = NotificationRecipientService.notifiable_users( recipients, :subscription, target: target, - acting_user: current_user, + acting_user: current_user ) label_names = labels.map(&:name) -- cgit v1.2.1 From b9075d22d7315bd9f408e0ec460b8b3c7273891d Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 3 Aug 2017 19:21:43 +0000 Subject: Revert "Merge branch 'rs-warm-capybara-only-in-ci' into 'master'" This reverts merge request !12761 --- spec/support/test_env.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index bed78928f14..c1298ed9cae 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -229,7 +229,6 @@ module TestEnv # Otherwise they'd be created by the first test, often timing out and # causing a transient test failure def eager_load_driver_server - return unless ENV['CI'] return unless defined?(Capybara) puts "Starting the Capybara driver server..." -- cgit v1.2.1 From dab08be606878139ce3fe3d8e197ef73badf8e88 Mon Sep 17 00:00:00 2001 From: Tim Zallmann Date: Thu, 3 Aug 2017 20:31:53 +0000 Subject: Resolve "Specific Async Script Loading by using a Page Variable" --- app/assets/javascripts/awards_handler.js | 2 +- app/assets/javascripts/behaviors/requires_input.js | 9 ++++++--- app/assets/javascripts/behaviors/toggler_behavior.js | 1 - app/assets/javascripts/boards/boards_bundle.js | 1 + .../javascripts/boards/components/board_blank_state.js | 1 + .../javascripts/boards/components/new_list_dropdown.js | 1 + app/assets/javascripts/boards/stores/boards_store.js | 2 +- app/assets/javascripts/commons/index.js | 1 + app/assets/javascripts/copy_as_gfm.js | 2 +- app/assets/javascripts/dispatcher.js | 8 ++++---- app/assets/javascripts/dropzone_input.js | 3 +-- app/assets/javascripts/emoji/index.js | 1 + app/assets/javascripts/filterable_list.js | 2 ++ app/assets/javascripts/filtered_search/dropdown_utils.js | 1 + app/assets/javascripts/gfm_auto_complete.js | 1 + app/assets/javascripts/gl_dropdown.js | 1 + app/assets/javascripts/graphs/stat_graph_contributors.js | 1 + .../javascripts/graphs/stat_graph_contributors_graph.js | 2 +- .../javascripts/graphs/stat_graph_contributors_util.js | 1 + app/assets/javascripts/issuable_bulk_update_actions.js | 1 + app/assets/javascripts/issuable_index.js | 2 +- app/assets/javascripts/labels_select.js | 2 +- app/assets/javascripts/lib/utils/pretty_time.js | 2 ++ app/assets/javascripts/main.js | 11 +++++++---- app/assets/javascripts/milestone_select.js | 1 + app/assets/javascripts/notes.js | 1 + .../components/interval_pattern_input.vue | 2 ++ app/assets/javascripts/profile/gl_crop.js | 1 + app/assets/javascripts/project_edit.js | 2 +- .../protected_branches/protected_branch_dropdown.js | 2 ++ .../javascripts/protected_tags/protected_tag_dropdown.js | 2 ++ app/assets/javascripts/right_sidebar.js | 1 + app/assets/javascripts/shortcuts_issuable.js | 3 ++- .../components/time_tracking/sidebar_time_tracking.js | 2 ++ app/assets/javascripts/sidebar_height_manager.js | 3 ++- app/assets/javascripts/todos.js | 3 +++ app/assets/javascripts/u2f/authenticate.js | 2 ++ app/assets/javascripts/u2f/register.js | 2 ++ app/assets/javascripts/username_validator.js | 2 ++ app/assets/javascripts/users/activity_calendar.js | 1 + app/assets/javascripts/users_select.js | 1 + app/helpers/defer_script_tag_helper.rb | 6 ++++++ app/views/layouts/_bootlint.haml | 7 ++++--- app/views/layouts/_init_auto_complete.html.haml | 1 + app/views/projects/merge_requests/show.html.haml | 1 + spec/helpers/defer_script_tag_helper_spec.rb | 13 +++++++++++++ 46 files changed, 92 insertions(+), 26 deletions(-) create mode 100644 app/helpers/defer_script_tag_helper.rb create mode 100644 spec/helpers/defer_script_tag_helper_spec.rb diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 18cd04b176a..097f79a250a 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,6 +1,6 @@ /* eslint-disable class-methods-use-this */ /* global Flash */ - +import _ from 'underscore'; import Cookies from 'js-cookie'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js index b20d108aa25..035a7e5c431 100644 --- a/app/assets/javascripts/behaviors/requires_input.js +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import '../commons/bootstrap'; // Requires Input behavior @@ -48,7 +49,9 @@ function hideOrShowHelpBlock(form) { $(() => { const $form = $('form.js-requires-input'); - $form.requiresInput(); - hideOrShowHelpBlock($form); - $('.select2.js-select-namespace').change(() => hideOrShowHelpBlock($form)); + if ($form) { + $form.requiresInput(); + hideOrShowHelpBlock($form); + $('.select2.js-select-namespace').change(() => hideOrShowHelpBlock($form)); + } }); diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 77e92ff8caf..b70b0a9bbf8 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -1,4 +1,3 @@ - // Toggle button. Show/hide content inside parent container. // Button does not change visibility. If button has icon - it changes chevron style. // diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 88b054b76e6..89c14180149 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -2,6 +2,7 @@ /* global BoardService */ /* global Flash */ +import _ from 'underscore'; import Vue from 'vue'; import VueResource from 'vue-resource'; import FilteredSearchBoards from './filtered_search_boards'; diff --git a/app/assets/javascripts/boards/components/board_blank_state.js b/app/assets/javascripts/boards/components/board_blank_state.js index e7f16899362..edfe7c326db 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.js +++ b/app/assets/javascripts/boards/components/board_blank_state.js @@ -1,5 +1,6 @@ /* global ListLabel */ +import _ from 'underscore'; import Cookies from 'js-cookie'; const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index f29b6caa1ac..72bb9e10fbc 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -1,5 +1,6 @@ /* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var, promise/catch-or-return */ +import _ from 'underscore'; window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 1e12d4ca415..43928e602d6 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -1,6 +1,6 @@ /* eslint-disable comma-dangle, space-before-function-paren, one-var, no-shadow, dot-notation, max-len */ /* global List */ - +import _ from 'underscore'; import Cookies from 'js-cookie'; window.gl = window.gl || {}; diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js index 7063f59d446..6db8b3afbef 100644 --- a/app/assets/javascripts/commons/index.js +++ b/app/assets/javascripts/commons/index.js @@ -1,3 +1,4 @@ +import 'underscore'; import './polyfills'; import './jquery'; import './bootstrap'; diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js index 54257531284..13ba4a57293 100644 --- a/app/assets/javascripts/copy_as_gfm.js +++ b/app/assets/javascripts/copy_as_gfm.js @@ -1,5 +1,5 @@ /* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */ - +import _ from 'underscore'; import './lib/utils/common_utils'; import { placeholderImage } from './lazy_loader'; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 178e72a1127..070d0a01c0f 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -79,10 +79,6 @@ import GpgBadges from './gpg_badges'; (function() { var Dispatcher; - $(function() { - return new Dispatcher(); - }); - Dispatcher = (function() { function Dispatcher() { this.initSearch(); @@ -637,4 +633,8 @@ import GpgBadges from './gpg_badges'; return Dispatcher; })(); + + $(function() { + new Dispatcher(); + }); }).call(window); diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 9ebbb22e807..bd1864b8321 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -1,11 +1,10 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, prefer-arrow-callback */ /* global Dropzone */ - +import _ from 'underscore'; import './preview_markdown'; window.DropzoneInput = (function() { function DropzoneInput(form) { - Dropzone.autoDiscover = false; const divHover = '
'; const iconPaperclip = ''; const $attachButton = form.find('.button-attach-file'); diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js index cac35d6eed5..dc7672560ea 100644 --- a/app/assets/javascripts/emoji/index.js +++ b/app/assets/javascripts/emoji/index.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import emojiMap from 'emojis/digests.json'; import emojiAliases from 'emojis/aliases.json'; diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index 139206cc185..6d516a253bb 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -1,3 +1,5 @@ +import _ from 'underscore'; + /** * Makes search request for content when user types a value in the search input. * Updates the html content of the page with the received one. diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index 9c7a4d9f6ad..db5560fa753 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import FilteredSearchContainer from './container'; class DropdownUtils { diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 6cb9cfe1382..5c624b79d45 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import glRegexp from './lib/utils/regexp'; import AjaxCache from './lib/utils/ajax_cache'; diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 9475498e176..a17680794cc 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */ /* global fuzzaldrinPlus */ +import _ from 'underscore'; import { isObject } from './lib/utils/type_utility'; var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote; diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index c6be4c9e8fe..cdc4fcf6573 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, no-shadow */ +import _ from 'underscore'; import d3 from 'd3'; import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph'; import ContributorsStatGraphUtil from './stat_graph_contributors_util'; diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js index 0deb27e522b..f64b4638485 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -1,5 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */ - +import _ from 'underscore'; import d3 from 'd3'; const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js index c583757f3f2..77135ad1f0e 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js @@ -1,4 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */ +import _ from 'underscore'; export default { parse_log: function(log) { diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js index e46c0e90255..c39ffdb2e0f 100644 --- a/app/assets/javascripts/issuable_bulk_update_actions.js +++ b/app/assets/javascripts/issuable_bulk_update_actions.js @@ -1,6 +1,7 @@ /* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */ /* global IssuableIndex */ /* global Flash */ +import _ from 'underscore'; export default { init({ container, form, issues, prefixId } = {}) { diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js index 5c96646def8..ece0220c927 100644 --- a/app/assets/javascripts/issuable_index.js +++ b/app/assets/javascripts/issuable_index.js @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */ /* global IssuableIndex */ - +import _ from 'underscore'; import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index f0e02ca0fb2..7d7f91227f9 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -1,7 +1,7 @@ /* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread */ /* global Issuable */ /* global ListLabel */ - +import _ from 'underscore'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import DropdownUtils from './filtered_search/dropdown_utils'; diff --git a/app/assets/javascripts/lib/utils/pretty_time.js b/app/assets/javascripts/lib/utils/pretty_time.js index ae397212e55..716aefbfcb7 100644 --- a/app/assets/javascripts/lib/utils/pretty_time.js +++ b/app/assets/javascripts/lib/utils/pretty_time.js @@ -1,3 +1,5 @@ +import _ from 'underscore'; + (() => { /* * TODO: Make these methods more configurable (e.g. parseSeconds timePeriodContstraints, diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index cd45091c211..a807ce326a6 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -36,9 +36,6 @@ import './shortcuts_find_file'; import './shortcuts_issuable'; import './shortcuts_network'; -// behaviors -import './behaviors/'; - // templates import './templates/issuable_template_selector'; import './templates/issuable_template_selectors'; @@ -56,6 +53,9 @@ import './lib/utils/pretty_time'; import './lib/utils/text_utility'; import './lib/utils/url_utility'; +// behaviors +import './behaviors/'; + // u2f import './u2f/authenticate'; import './u2f/error'; @@ -86,7 +86,6 @@ import './copy_as_gfm'; import './copy_to_clipboard'; import './create_label'; import './diff'; -import './dispatcher'; import './dropzone_input'; import './due_date_select'; import './files_comment_button'; @@ -150,9 +149,13 @@ import './subscription'; import './subscription_select'; import './syntax_highlight'; +import './dispatcher'; + // eslint-disable-next-line global-require, import/no-commonjs if (process.env.NODE_ENV !== 'production') require('./test_utils/'); +Dropzone.autoDiscover = false; + document.addEventListener('beforeunload', function () { // Unbind scroll events $(document).off('scroll'); diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 6756ab0b3aa..04579058688 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -1,6 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */ /* global Issuable */ /* global ListMilestone */ +import _ from 'underscore'; (function() { this.MilestoneSelect = (function() { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index dfa07a2def4..b38a6abc8d1 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -11,6 +11,7 @@ newline-per-chained-call, no-useless-escape, class-methods-use-this */ /* global mrRefreshWidgetUrl */ import $ from 'jquery'; +import _ from 'underscore'; import Cookies from 'js-cookie'; import autosize from 'vendor/autosize'; import Dropzone from 'dropzone'; diff --git a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue index ce46b3fa3fa..b5d85299cf8 100644 --- a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue +++ b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue @@ -1,4 +1,6 @@ " + end + end +end -- cgit v1.2.1 From e18aef5f11365a3dae7639146a4e2d5412749df4 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 3 Aug 2017 16:38:29 -0400 Subject: Bump rspec to 3.6.0 --- Gemfile | 4 ++-- Gemfile.lock | 48 ++++++++++++++++++++++-------------------------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/Gemfile b/Gemfile index 403b104a9d6..8e4a90094a8 100644 --- a/Gemfile +++ b/Gemfile @@ -314,11 +314,11 @@ group :development, :test do gem 'pry-rails', '~> 0.3.4' gem 'awesome_print', '~> 1.2.0', require: false - gem 'fuubar', '~> 2.0.0' + gem 'fuubar', '~> 2.2.0' gem 'database_cleaner', '~> 1.5.0' gem 'factory_girl_rails', '~> 4.7.0' - gem 'rspec-rails', '~> 3.5.0' + gem 'rspec-rails', '~> 3.6.0' gem 'rspec-retry', '~> 0.4.5' gem 'spinach-rails', '~> 0.2.1' gem 'spinach-rerun-reporter', '~> 0.0.2' diff --git a/Gemfile.lock b/Gemfile.lock index 9f90965a567..73f45f3ccb6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -156,7 +156,7 @@ GEM devise (~> 4.0) railties rotp (~> 2.0) - diff-lcs (1.2.5) + diff-lcs (1.3) diffy (3.1.0) docile (1.1.5) domain_name (0.5.20161021) @@ -250,8 +250,8 @@ GEM foreman (0.78.0) thor (~> 0.19.1) formatador (0.2.5) - fuubar (2.0.0) - rspec (~> 3.0) + fuubar (2.2.0) + rspec-core (~> 3.0) ruby-progressbar (~> 1.4) gemnasium-gitlab-service (0.2.6) rugged (~> 0.21) @@ -393,7 +393,7 @@ GEM json (~> 1.8) multi_xml (>= 0.5.2) httpclient (2.8.2) - i18n (0.8.1) + i18n (0.8.6) ice_nine (0.11.2) influxdb (0.2.3) cause @@ -610,7 +610,7 @@ GEM pry-rails (0.3.5) pry (>= 0.9.10) pyu-ruby-sasl (0.0.3.3) - rack (1.6.5) + rack (1.6.8) rack-accept (0.4.5) rack (>= 0.4) rack-attack (4.4.1) @@ -658,7 +658,7 @@ GEM rainbow (2.2.2) rake raindrops (0.18.0) - rake (10.5.0) + rake (12.0.0) rblineprof (0.3.6) debugger-ruby_core_source (~> 1.3) rdoc (4.2.2) @@ -702,30 +702,26 @@ GEM chunky_png rqrcode-rails3 (0.1.7) rqrcode (>= 0.4.2) - rspec (3.5.0) - rspec-core (~> 3.5.0) - rspec-expectations (~> 3.5.0) - rspec-mocks (~> 3.5.0) - rspec-core (3.5.0) - rspec-support (~> 3.5.0) - rspec-expectations (3.5.0) + rspec-core (3.6.0) + rspec-support (~> 3.6.0) + rspec-expectations (3.6.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-mocks (3.5.0) + rspec-support (~> 3.6.0) + rspec-mocks (3.6.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-rails (3.5.0) + rspec-support (~> 3.6.0) + rspec-rails (3.6.0) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec-core (~> 3.5.0) - rspec-expectations (~> 3.5.0) - rspec-mocks (~> 3.5.0) - rspec-support (~> 3.5.0) + rspec-core (~> 3.6.0) + rspec-expectations (~> 3.6.0) + rspec-mocks (~> 3.6.0) + rspec-support (~> 3.6.0) rspec-retry (0.4.5) rspec-core rspec-set (0.1.3) - rspec-support (3.5.0) + rspec-support (3.6.0) rspec_profiling (0.0.5) activerecord pg @@ -860,7 +856,7 @@ GEM truncato (0.7.8) htmlentities (~> 4.3.1) nokogiri (~> 1.6.1) - tzinfo (1.2.2) + tzinfo (1.2.3) thread_safe (~> 0.1) u2f (0.2.1) uglifier (2.7.2) @@ -972,7 +968,7 @@ DEPENDENCIES fog-rackspace (~> 0.1.1) font-awesome-rails (~> 4.7) foreman (~> 0.78.0) - fuubar (~> 2.0.0) + fuubar (~> 2.2.0) gemnasium-gitlab-service (~> 0.2) gemojione (~> 3.0) gettext (~> 3.2.2) @@ -1076,7 +1072,7 @@ DEPENDENCIES responders (~> 2.0) rouge (~> 2.0) rqrcode-rails3 (~> 0.1.7) - rspec-rails (~> 3.5.0) + rspec-rails (~> 3.6.0) rspec-retry (~> 0.4.5) rspec-set (~> 0.1.3) rspec_profiling (~> 0.0.5) @@ -1130,4 +1126,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.15.1 + 1.15.3 -- cgit v1.2.1 From 9ca7abfbdc3c81470e643c617b30866122ec5e95 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Thu, 3 Aug 2017 17:32:59 -0500 Subject: Add active state for pipelines settings on old nav --- app/views/layouts/nav/_group.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 8605380848d..261445ecd2b 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -25,7 +25,7 @@ %span Members - if current_user && can?(current_user, :admin_group, @group) - = nav_link(path: %w[groups#projects groups#edit]) do + = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do = link_to edit_group_path(@group), title: 'Settings' do %span Settings -- cgit v1.2.1 From 01dc8e6d66598c42edb58e35c7da79c3b4cad425 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 3 Aug 2017 21:00:20 -0400 Subject: Don't bother going through an entire Banzai pipeline for empty text This bails out of `cacheless_render` immediately unless the provided text is present, since there's no point. This is a slight improvement in our test performance. Across the creation of 1,000 `Namespace` records, which caches the `description` field and which is blank by default in its factory, this saves about four seconds, which... sure. Why not. --- lib/banzai/renderer.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb index c7801cb5baf..ad08c0905e2 100644 --- a/lib/banzai/renderer.rb +++ b/lib/banzai/renderer.rb @@ -132,6 +132,8 @@ module Banzai end def self.cacheless_render(text, context = {}) + return text.to_s unless text.present? + Gitlab::Metrics.measure(:banzai_cacheless_render) do result = render_result(text, context) -- cgit v1.2.1 From e9b079d023df7df1b4aca051a1e347b1a8ad810a Mon Sep 17 00:00:00 2001 From: tauriedavis Date: Tue, 1 Aug 2017 15:13:39 -0700 Subject: 35659 Rename Pipelines tab to CI / CD in new navigation --- app/views/layouts/nav/_new_group_sidebar.html.haml | 4 ++-- app/views/layouts/nav/_new_project_sidebar.html.haml | 8 ++++---- changelogs/unreleased/35659-rename-pipeline.yml | 4 ++++ 3 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 changelogs/unreleased/35659-rename-pipeline.yml diff --git a/app/views/layouts/nav/_new_group_sidebar.html.haml b/app/views/layouts/nav/_new_group_sidebar.html.haml index fdfd7e60732..33a83866cbf 100644 --- a/app/views/layouts/nav/_new_group_sidebar.html.haml +++ b/app/views/layouts/nav/_new_group_sidebar.html.haml @@ -85,6 +85,6 @@ Projects = nav_link(controller: :ci_cd) do - = link_to group_settings_ci_cd_path(@group), title: 'Pipelines' do + = link_to group_settings_ci_cd_path(@group), title: 'CI / CD' do %span - Pipelines + CI / CD diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml index 9f1cb248c4e..8e246b6e91f 100644 --- a/app/views/layouts/nav/_new_project_sidebar.html.haml +++ b/app/views/layouts/nav/_new_project_sidebar.html.haml @@ -122,9 +122,9 @@ - if project_nav_tab? :pipelines = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do - = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do + = link_to project_pipelines_path(@project), title: 'CI / CD', class: 'shortcuts-pipelines' do .nav-icon-container - = custom_icon('pipeline') + = custom_icon('CI / CD') %span.nav-item-name Pipelines @@ -205,9 +205,9 @@ Repository - if @project.feature_available?(:builds, current_user) = nav_link(controller: :ci_cd) do - = link_to project_settings_ci_cd_path(@project), title: 'Pipelines' do + = link_to project_settings_ci_cd_path(@project), title: 'CI / CD' do %span - Pipelines + CI / CD - if Gitlab.config.pages.enabled = nav_link(controller: :pages) do = link_to project_pages_path(@project), title: 'Pages' do diff --git a/changelogs/unreleased/35659-rename-pipeline.yml b/changelogs/unreleased/35659-rename-pipeline.yml new file mode 100644 index 00000000000..0fe211868e4 --- /dev/null +++ b/changelogs/unreleased/35659-rename-pipeline.yml @@ -0,0 +1,4 @@ +--- +title: Rename Pipelines tab to CI / CD in new navigation +merge_request: +author: -- cgit v1.2.1 From 3ec93e79de20bdc61594daf63766df7e6cbd20aa Mon Sep 17 00:00:00 2001 From: Joshua Lambert Date: Fri, 4 Aug 2017 00:00:26 -0400 Subject: Openshift Getting Started --- doc/articles/index.md | 2 +- .../img/add-gitlab-to-project.png | Bin 0 -> 37386 bytes .../openshift_and_gitlab/img/add-to-project.png | Bin 0 -> 21672 bytes .../openshift_and_gitlab/img/create-project-ui.png | Bin 0 -> 22290 bytes .../openshift_and_gitlab/img/gitlab-logs.png | Bin 0 -> 70858 bytes .../openshift_and_gitlab/img/gitlab-overview.png | Bin 0 -> 106432 bytes .../openshift_and_gitlab/img/gitlab-running.png | Bin 0 -> 107993 bytes .../openshift_and_gitlab/img/gitlab-scale.png | Bin 0 -> 36628 bytes .../openshift_and_gitlab/img/gitlab-settings.png | Bin 0 -> 111366 bytes .../openshift_and_gitlab/img/no-resources.png | Bin 0 -> 34669 bytes .../img/openshift-infra-project.png | Bin 0 -> 95725 bytes .../openshift_and_gitlab/img/pods-overview.png | Bin 0 -> 106861 bytes doc/articles/openshift_and_gitlab/img/rc-name.png | Bin 0 -> 51390 bytes .../openshift_and_gitlab/img/running-pods.png | Bin 0 -> 29818 bytes .../openshift_and_gitlab/img/storage-volumes.png | Bin 0 -> 49584 bytes .../openshift_and_gitlab/img/web-console.png | Bin 0 -> 34774 bytes doc/articles/openshift_and_gitlab/index.md | 534 +++++++++++++++++++++ 17 files changed, 535 insertions(+), 1 deletion(-) create mode 100644 doc/articles/openshift_and_gitlab/img/add-gitlab-to-project.png create mode 100644 doc/articles/openshift_and_gitlab/img/add-to-project.png create mode 100644 doc/articles/openshift_and_gitlab/img/create-project-ui.png create mode 100644 doc/articles/openshift_and_gitlab/img/gitlab-logs.png create mode 100644 doc/articles/openshift_and_gitlab/img/gitlab-overview.png create mode 100644 doc/articles/openshift_and_gitlab/img/gitlab-running.png create mode 100644 doc/articles/openshift_and_gitlab/img/gitlab-scale.png create mode 100644 doc/articles/openshift_and_gitlab/img/gitlab-settings.png create mode 100644 doc/articles/openshift_and_gitlab/img/no-resources.png create mode 100644 doc/articles/openshift_and_gitlab/img/openshift-infra-project.png create mode 100644 doc/articles/openshift_and_gitlab/img/pods-overview.png create mode 100644 doc/articles/openshift_and_gitlab/img/rc-name.png create mode 100644 doc/articles/openshift_and_gitlab/img/running-pods.png create mode 100644 doc/articles/openshift_and_gitlab/img/storage-volumes.png create mode 100644 doc/articles/openshift_and_gitlab/img/web-console.png create mode 100644 doc/articles/openshift_and_gitlab/index.md diff --git a/doc/articles/index.md b/doc/articles/index.md index 9d2e5956029..335cb4f177b 100644 --- a/doc/articles/index.md +++ b/doc/articles/index.md @@ -80,7 +80,7 @@ Install, upgrade, integrate, migrate to GitLab: | :------------ | :------: | --------------: | | [Video Tutorial: Idea to Production on Google Container Engine (GKE)](https://about.gitlab.com/2017/01/23/video-tutorial-idea-to-production-on-google-container-engine-gke/) | Tutorial | 2017/01/23 | | [How to Setup a GitLab Instance on Microsoft Azure](https://about.gitlab.com/2016/07/13/how-to-setup-a-gitlab-instance-on-microsoft-azure/) | Tutorial | 2016/07/13 | -| [Get started with OpenShift Origin 3 and GitLab](https://about.gitlab.com/2016/06/28/get-started-with-openshift-origin-3-and-gitlab/) | Tutorial | 2016/06/28 | +| [Get started with OpenShift Origin 3 and GitLab](https://docs.gitlab.com/ee/articles/openshift_and_gitlab/) | Tutorial | 2016/06/28 | | [Getting started with GitLab and DigitalOcean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/) | Tutorial | 2016/04/27 | ## Software development diff --git a/doc/articles/openshift_and_gitlab/img/add-gitlab-to-project.png b/doc/articles/openshift_and_gitlab/img/add-gitlab-to-project.png new file mode 100644 index 00000000000..fcad4e59ae3 Binary files /dev/null and b/doc/articles/openshift_and_gitlab/img/add-gitlab-to-project.png differ diff --git a/doc/articles/openshift_and_gitlab/img/add-to-project.png b/doc/articles/openshift_and_gitlab/img/add-to-project.png new file mode 100644 index 00000000000..bd915a229f6 Binary files /dev/null and b/doc/articles/openshift_and_gitlab/img/add-to-project.png differ diff --git a/doc/articles/openshift_and_gitlab/img/create-project-ui.png b/doc/articles/openshift_and_gitlab/img/create-project-ui.png new file mode 100644 index 00000000000..e72866f252a Binary files /dev/null and b/doc/articles/openshift_and_gitlab/img/create-project-ui.png differ diff --git a/doc/articles/openshift_and_gitlab/img/gitlab-logs.png b/doc/articles/openshift_and_gitlab/img/gitlab-logs.png new file mode 100644 index 00000000000..1e24080c7df Binary files /dev/null and b/doc/articles/openshift_and_gitlab/img/gitlab-logs.png differ diff --git a/doc/articles/openshift_and_gitlab/img/gitlab-overview.png b/doc/articles/openshift_and_gitlab/img/gitlab-overview.png new file mode 100644 index 00000000000..3c5df0ea101 Binary files /dev/null and b/doc/articles/openshift_and_gitlab/img/gitlab-overview.png differ diff --git a/doc/articles/openshift_and_gitlab/img/gitlab-running.png b/doc/articles/openshift_and_gitlab/img/gitlab-running.png new file mode 100644 index 00000000000..c7db691cb30 Binary files /dev/null and b/doc/articles/openshift_and_gitlab/img/gitlab-running.png differ diff --git a/doc/articles/openshift_and_gitlab/img/gitlab-scale.png b/doc/articles/openshift_and_gitlab/img/gitlab-scale.png new file mode 100644 index 00000000000..4903c7d7498 Binary files /dev/null and b/doc/articles/openshift_and_gitlab/img/gitlab-scale.png differ diff --git a/doc/articles/openshift_and_gitlab/img/gitlab-settings.png b/doc/articles/openshift_and_gitlab/img/gitlab-settings.png new file mode 100644 index 00000000000..db4360ffef0 Binary files /dev/null and b/doc/articles/openshift_and_gitlab/img/gitlab-settings.png differ diff --git a/doc/articles/openshift_and_gitlab/img/no-resources.png b/doc/articles/openshift_and_gitlab/img/no-resources.png new file mode 100644 index 00000000000..480fb766468 Binary files /dev/null and b/doc/articles/openshift_and_gitlab/img/no-resources.png differ diff --git a/doc/articles/openshift_and_gitlab/img/openshift-infra-project.png b/doc/articles/openshift_and_gitlab/img/openshift-infra-project.png new file mode 100644 index 00000000000..8b9f85aa341 Binary files /dev/null and b/doc/articles/openshift_and_gitlab/img/openshift-infra-project.png differ diff --git a/doc/articles/openshift_and_gitlab/img/pods-overview.png b/doc/articles/openshift_and_gitlab/img/pods-overview.png new file mode 100644 index 00000000000..e1cf08bd217 Binary files /dev/null and b/doc/articles/openshift_and_gitlab/img/pods-overview.png differ diff --git a/doc/articles/openshift_and_gitlab/img/rc-name.png b/doc/articles/openshift_and_gitlab/img/rc-name.png new file mode 100644 index 00000000000..889e34adbec Binary files /dev/null and b/doc/articles/openshift_and_gitlab/img/rc-name.png differ diff --git a/doc/articles/openshift_and_gitlab/img/running-pods.png b/doc/articles/openshift_and_gitlab/img/running-pods.png new file mode 100644 index 00000000000..3fd4e56662f Binary files /dev/null and b/doc/articles/openshift_and_gitlab/img/running-pods.png differ diff --git a/doc/articles/openshift_and_gitlab/img/storage-volumes.png b/doc/articles/openshift_and_gitlab/img/storage-volumes.png new file mode 100644 index 00000000000..ae1e5381faa Binary files /dev/null and b/doc/articles/openshift_and_gitlab/img/storage-volumes.png differ diff --git a/doc/articles/openshift_and_gitlab/img/web-console.png b/doc/articles/openshift_and_gitlab/img/web-console.png new file mode 100644 index 00000000000..aa1425d4f94 Binary files /dev/null and b/doc/articles/openshift_and_gitlab/img/web-console.png differ diff --git a/doc/articles/openshift_and_gitlab/index.md b/doc/articles/openshift_and_gitlab/index.md new file mode 100644 index 00000000000..96a83089670 --- /dev/null +++ b/doc/articles/openshift_and_gitlab/index.md @@ -0,0 +1,534 @@ +# Getting started with OpenShift Origin 3 and GitLab + +> **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** tutorial || +> **Level:** intermediary || +> **Author:** [Achilleas Pipinellis](https://gitlab.com/axil) || +> **Publication date:** 2016/06/28 + + +## Introduction + +[OpenShift Origin][openshift] is an open source container application +platform created by [RedHat], based on [kubernetes] and [Docker]. That means +you can host your own PaaS for free and almost with no hassle. + +In this tutorial, we will see how to deploy GitLab in OpenShift using GitLab's +official Docker image while getting familiar with the web interface and CLI +tools that will help us achieve our goal. + +--- + +### What's on this page? +{: .no_toc} + +- TOC +{:toc} + +--- + +## Prerequisites + +OpenShift 3 is not yet deployed on RedHat's offered Online platform ([openshift.com]), +so in order to test it, we will use an [all-in-one Virtualbox image][vm] that is +offered by the OpenShift developers and managed by Vagrant. If you haven't done +already, go ahead and install the following components as they are essential to +test OpenShift easily: + +- [VirtualBox] +- [Vagrant] +- [OpenShift Client][oc] (`oc` for short) + +It is also important to mention that for the purposes of this tutorial, the +latest Origin release is used: + +- **oc** `v1.3.0` (must be [installed][oc-gh] locally on your computer) +- **openshift** `v1.3.0` (is pre-installed in the [VM image][vm-new]) +- **kubernetes** `v1.3.0` (is pre-installed in the [VM image][vm-new]) + +
+**Note** +{: .panel-heading} +
+If you intend to deploy GitLab on a production OpenShift cluster, there are some +limitations to bare in mind. Read on the [limitations](#current-limitations) +section for more information and follow the linked links for the relevant +discussions. +
+
+ +Now that you have all batteries, let's see how easy it is to test OpenShift +on your computer. + +## Getting familiar with OpenShift Origin + +The environment we are about to use is based on CentOS 7 which comes with all +the tools needed pre-installed: Docker, kubernetes, OpenShift, etcd. + +### Test OpenShift using Vagrant + +As of this writing, the all-in-one VM is at version 1.3, and that's +what we will use in this tutorial. + +In short: + +1. Open a terminal and in a new directory run: + ```sh + vagrant init openshift/origin-all-in-one + ``` +1. This will generate a Vagrantfile based on the all-in-one VM image +1. In the same directory where you generated the Vagrantfile + enter: + + ```sh + vagrant up + ``` + +This will download the VirtualBox image and fire up the VM with some preconfigured +values as you can see in the Vagrantfile. As you may have noticed, you need +plenty of RAM (5GB in our example), so make sure you have enough. + +Now that OpenShift is setup, let's see how the web console looks like. + +### Explore the OpenShift web console + +Once Vagrant finishes its thing with the VM, you will be presented with a +message which has some important information. One of them is the IP address +of the deployed OpenShift platform and in particular . +Open this link with your browser and accept the self-signed certificate in +order to proceed. + +Let's login as admin with username/password `admin/admin`. This is what the +landing page looks like: + +![openshift web console](img/web-console.png) + +You can see that a number of [projects] are already created for testing purposes. + +If you head over the `openshift-infra` project, a number of services with their +respective pods are there to explore. + +![openshift web console](img/openshift-infra-project.png) + +We are not going to explore the whole interface, but if you want to learn about +the key concepts of OpenShift, read the [core concepts reference][core] in the +official documentation. + +### Explore the OpenShift CLI + +OpenShift Client (`oc`), is a powerful CLI tool that talks to the OpenShift API +and performs pretty much everything you can do from the web UI and much more. + +Assuming you have [installed][oc] it, let's explore some of its main +functionalities. + +Let's first see the version of `oc`: + +```sh +$ oc version + +oc v1.3.0 +kubernetes v1.3.0+52492b4 +``` + +With `oc help` you can see the top level arguments you can run with `oc` and +interact with your cluster, kubernetes, run applications, create projects and +much more. + +Let's login to the all-in-one VM and see how to achieve the same results like +when we visited the web console earlier. The username/password for the +administrator user is `admin/admin`. There is also a test user with username/ +password `user/user`, with limited access. Let's login as admin for the moment: + +```sh +$ oc login https://10.2.2.2:8443 + +Authentication required for https://10.2.2.2:8443 (openshift) +Username: admin +Password: +Login successful. + +You have access to the following projects and can switch between them with 'oc project ': + + * cockpit + * default (current) + * delete + * openshift + * openshift-infra + * sample + +Using project "default". +``` + +Switch to the `openshift-infra` project with: + +```sh +oc project openshift-infra +``` + +And finally, see its status: + +```sh +oc status +``` + +The last command should spit a bunch of information about the statuses of the +pods and the services, which if you look closely is what we encountered in the +second image when we explored the web console. + +You can always read more about `oc` in the [OpenShift CLI documentation][oc]. + +### Troubleshooting the all-in-one VM + +Using the all-in-one VM gives you the ability to test OpenShift whenever you +want. That means you get to play with it, shutdown the VM, and pick up where +you left off. + +Sometimes though, you may encounter some issues, like OpenShift not running +when booting up the VM. The web UI may not responding or you may see issues +when trying to login with `oc`, like: + +``` +The connection to the server 10.2.2.2:8443 was refused - did you specify the right host or port? +``` + +In that case, the OpenShift service might not be running, so in order to fix it: + +1. SSH into the VM by going to the directory where the Vagrantfile is and then + run: + + ```sh + vagrant ssh + ``` + +1. Run `systemctl` and verify by the output that the `openshift` service is not + running (it will be in red color). If that's the case start the service with: + + ```sh + sudo systemctl start openshift + ``` + +1. Verify the service is up with: + + ```sh + systemctl status openshift -l + ``` + +Now you will be able to login using `oc` (like we did before) and visit the web +console. + +## Deploy GitLab + +Now that you got a taste of what OpenShift looks like, let's deploy GitLab! + +### Create a new project + +First, we will create a new project to host our application. You can do this +either by running the CLI client: + +```bash +$ oc new-project gitlab +``` + +or by using the web interface: + +![Create a new project from the UI](img/create-project-ui.png) + +If you used the command line, `oc` automatically uses the new project and you +can see its status with: + +```sh +$ oc status + +In project gitlab on server https://10.2.2.2:8443 + +You have no services, deployment configs, or build configs. +Run 'oc new-app' to create an application. +``` + +If you visit the web console, you can now see `gitlab` listed in the projects list. + +The next step is to import the OpenShift template for GitLab. + +### Import the template + +The [template][templates] is basically a JSON file which describes a set of +related object definitions to be created together, as well as a set of +parameters for those objects. + +The template for GitLab resides in the Omnibus GitLab repository under the +docker directory. Let's download it locally with `wget`: + +```bash +wget https://gitlab.com/gitlab-org/omnibus-gitlab/raw/master/docker/openshift-template.json +``` + +And then let's import it in OpenShift: + +```bash +oc create -f openshift-template.json -n openshift +``` + +
+**Note** +{: .panel-heading} +
+The `-n openshift` namespace flag is a trick to make the template available to all +projects. If you recall from when we created the `gitlab` project, `oc` switched +to it automatically, and that can be verified by the `oc status` command. If +you omit the namespace flag, the application will be available only to the +current project, in our case `gitlab`. The `openshift` namespace is a global +one that the administrators should use if they want the application to be +available to all users. +
+
+ +We are now ready to finally deploy GitLab! + +### Create a new application + +The next step is to use the template we previously imported. Head over to the +`gitlab` project and hit the **Add to Project** button. + +![Add to project](img/add-to-project.png) + +This will bring you to the catalog where you can find all the pre-defined +applications ready to deploy with the click of a button. Search for `gitlab` +and you will see the previously imported template: + +![Add GitLab to project](img/add-gitlab-to-project.png) + +Select it, and in the following screen you will be presented with the predefined +values used with the GitLab template: + +![GitLab settings](img/gitlab-settings.png) + +Notice at the top that there are three resources to be created with this +template: + +- `gitlab-ce` +- `gitlab-ce-redis` +- `gitlab-ce-postgresql` + +While PostgreSQL and Redis are bundled in Omnibus GitLab, the template is using +separate images as you can see from [this line][line] in the template. + +The predefined values have been calculated for the purposes of testing out +GitLab in the all-in-one VM. You don't need to change anything here, hit +**Create** to start the deployment. + +If you are deploying to production you will want to change the **GitLab instance +hostname** and use greater values for the volume sizes. If you don't provide a +password for PostgreSQL, it will be created automatically. + +
+**Note** +{: .panel-heading} +
+The `gitlab.apps.10.2.2.2.xip.io` hostname that is used by default will +resolve to the host with IP `10.2.2.2` which is the IP our VM uses. It is a +trick to have distinct FQDNs pointing to services that are on our local network. +Read more on how this works in . +
+
+ +Now that we configured this, let's see how to manage and scale GitLab. + +## Manage and scale GitLab + +Setting up GitLab for the first time might take a while depending on your +internet connection and the resources you have attached to the all-in-one VM. +GitLab's docker image is quite big (~500MB), so you'll have to wait until +it's downloaded and configured before you use it. + +### Watch while GitLab gets deployed + +Navigate to the `gitlab` project at **Overview**. You can notice that the +deployment is in progress by the orange color. The Docker images are being +downloaded and soon they will be up and running. + +![GitLab overview](img/gitlab-overview.png) + +Switch to the **Browse > Pods** and you will eventually see all 3 pods in a +running status. Remember the 3 resources that were to be created when we first +created the GitLab app? This is where you can see them in action. + +![Running pods](img/running-pods.png) + +You can see GitLab being reconfigured by taking look at the logs in realtime. +Click on `gitlab-ce-2-j7ioe` (your ID will be different) and go to the **Logs** +tab. + +![GitLab logs](img/gitlab-logs.png) + +At a point you should see a _**gitlab Reconfigured!**_ message in the logs. +Navigate back to the **Overview** and hopefully all pods will be up and running. + +![GitLab running](img/gitlab-running.png) + +Congratulations! You can now navigate to your new shinny GitLab instance by +visiting where you will be asked to +change the root user password. Login using `root` as username and providing the +password you just set, and start using GitLab! + +### Scale GitLab with the push of a button + +If you reach to a point where your GitLab instance could benefit from a boost +of resources, you'd be happy to know that you can scale up with the push of a +button. + +In the **Overview** page just click the up arrow button in the pod where +GitLab is. The change is instant and you can see the number of [replicas] now +running scaled to 2. + +![GitLab scale](img/gitlab-scale.png) + +Upping the GitLab pods is actually like adding new application servers to your +cluster. You can see how that would work if you didn't use GitLab with +OpenShift by following the [HA documentation][ha] for the application servers. + +Bare in mind that you may need more resources (CPU, RAM, disk space) when you +scale up. If a pod is in pending state for too long, you can navigate to +**Browse > Events** and see the reason and message of the state. + +![No resources](img/no-resources.png) + +### Scale GitLab using the `oc` CLI + +Using `oc` is super easy to scale up the replicas of a pod. You may want to +skim through the [basic CLI operations][basic-cli] to get a taste how the CLI +commands are used. Pay extra attention to the object types as we will use some +of them and their abbreviated versions below. + +In order to scale up, we need to find out the name of the replication controller. +Let's see how to do that using the following steps. + +1. Make sure you are in the `gitlab` project: + + ```sh + oc project gitlab + ``` + +1. See what services are used for this project: + + ```sh + oc get svc + ``` + + The output will be similar to: + + ``` + NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE + gitlab-ce 172.30.243.177 22/TCP,80/TCP 5d + gitlab-ce-postgresql 172.30.116.75 5432/TCP 5d + gitlab-ce-redis 172.30.105.88 6379/TCP 5d + ``` + +1. We need to see the replication controllers of the `gitlab-ce` service. + Get a detailed view of the current ones: + + ```sh + oc describe rc gitlab-ce + ``` + + This will return a large detailed list of the current replication controllers. + Search for the name of the GitLab controller, usually `gitlab-ce-1` or if + that failed at some point and you spawned another one, it will be named + `gitlab-ce-2`. + +1. Scale GitLab using the previous information: + + ```sh + oc scale --replicas=2 replicationcontrollers gitlab-ce-2 + ``` + +1. Get the new replicas number to make sure scaling worked: + + ```sh + oc get rc gitlab-ce-2 + ``` + + which will return something like: + + ``` + NAME DESIRED CURRENT AGE + gitlab-ce-2 2 2 5d + ``` + +And that's it! We successfully scaled the replicas to 2 using the CLI. + +As always, you can find the name of the controller using the web console. Just +click on the service you are interested in and you will see the details in the +right sidebar. + +![Replication controller name](img/rc-name.png) + +### Autoscaling GitLab + +In case you were wondering whether there is an option to autoscale a pod based +on the resources of your server, the answer is yes, of course there is. + +We will not expand on this matter, but feel free to read the documentation on +OpenShift's website about [autoscaling]. + +## Current limitations + +As stated in the [all-in-one VM][vm] page: + +> By default, OpenShift will not allow a container to run as root or even a +non-random container assigned userid. Most Docker images in the Dockerhub do not +follow this best practice and instead run as root. + +The all-in-one VM we are using has this security turned off so it will not +bother us. In any case, it is something to keep in mind when deploying GitLab +on a production cluster. + +In order to deploy GitLab on a production cluster, you will need to assign the +GitLab service account to the `anyuid` Security Context. + +1. Edit the Security Context: + ```sh + oc edit scc anyuid + ``` + +1. Add `system:serviceaccount::gitlab-ce-user` to the `users` section. + If you changed the Application Name from the default the user will + will be `-user` instead of `gitlab-ce-user` + +1. Save and exit the editor + +## Conclusion + +By now, you should have an understanding of the basic OpenShift Origin concepts +and a sense of how things work using the web console or the CLI. + +GitLab was hard to install in previous versions of OpenShift, +but now that belongs to the past. Upload a template, create a project, add an +application and you are done. You are ready to login to your new GitLab instance. + +And remember that in this tutorial we just scratched the surface of what Origin +is capable of. As always, you can refer to the detailed +[documentation][openshift-docs] to learn more about deploying your own OpenShift +PaaS and managing your applications with the ease of containers. + +[RedHat]: https://www.redhat.com/en "RedHat website" +[openshift]: https://www.openshift.org "OpenShift Origin website" +[vm]: https://www.openshift.org/vm/ "OpenShift All-in-one VM" +[vm-new]: https://atlas.hashicorp.com/openshift/boxes/origin-all-in-one "Official OpenShift Vagrant box on Atlas" +[template]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/docker/openshift-template.json "OpenShift template for GitLab" +[openshift.com]: https://openshift.com "OpenShift Online" +[kubernetes]: http://kubernetes.io/ "Kubernetes website" +[Docker]: https://www.docker.com "Docker website" +[oc]: https://docs.openshift.org/latest/cli_reference/get_started_cli.html "Documentation - oc CLI documentation" +[VirtualBox]: https://www.virtualbox.org/wiki/Downloads "VirtualBox downloads" +[Vagrant]: https://www.vagrantup.com/downloads.html "Vagrant downloads" +[projects]: https://docs.openshift.org/latest/dev_guide/projects.html "Documentation - Projects overview" +[core]: https://docs.openshift.org/latest/architecture/core_concepts/index.html "Documentation - Core concepts of OpenShift Origin" +[templates]: https://docs.openshift.org/latest/architecture/core_concepts/templates.html "Documentation - OpenShift templates" +[old-post]: https://blog.openshift.com/deploy-gitlab-openshift/ "Old post - Deploy GitLab on OpenShift" +[line]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/658c065c8d022ce858dd63eaeeadb0b2ddc8deea/docker/openshift-template.json#L239 "GitLab - OpenShift template" +[oc-gh]: https://github.com/openshift/origin/releases/tag/v1.3.0 "Openshift 1.3.0 release on GitHub" +[ha]: http://docs.gitlab.com/ce/administration/high_availability/gitlab.html "Documentation - GitLab High Availability" +[replicas]: https://docs.openshift.org/latest/architecture/core_concepts/deployments.html#replication-controllers "Documentation - Replication controller" +[autoscaling]: https://docs.openshift.org/latest/dev_guide/pod_autoscaling.html "Documentation - Autoscale" +[basic-cli]: https://docs.openshift.org/latest/cli_reference/basic_cli_operations.html "Documentation - Basic CLI operations" +[openshift-docs]: https://docs.openshift.org "OpenShift documentation" -- cgit v1.2.1 From 1fcf3f3d94a157e2b57735ca9d0a306b91e75323 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Wed, 2 Aug 2017 22:11:32 +0900 Subject: essential --- app/services/merge_requests/create_service.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index 19189e64acf..5414fa79def 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -12,7 +12,6 @@ module MergeRequests merge_request.source_project = source_project merge_request.source_branch = params[:source_branch] merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch) - merge_request.head_pipeline = head_pipeline_for(merge_request) create(merge_request) end @@ -22,10 +21,16 @@ module MergeRequests notification_service.new_merge_request(issuable, current_user) todo_service.new_merge_request(issuable, current_user) issuable.cache_merge_request_closes_issues!(current_user) + update_merge_requests_head_pipeline(issuable) end private + def update_merge_requests_head_pipeline(merge_request) + pipeline = head_pipeline_for(merge_request) + merge_request.update(head_pipeline_id: pipeline.id) if pipeline + end + def head_pipeline_for(merge_request) return unless merge_request.source_project -- cgit v1.2.1 From 28b442989b7d03432d300bedac89e5971d9a69b7 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Fri, 4 Aug 2017 14:15:38 +0900 Subject: Add changelog --- .../fix-sm-34547-cannot-connect-to-ci-server-error-messages.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/fix-sm-34547-cannot-connect-to-ci-server-error-messages.yml diff --git a/changelogs/unreleased/fix-sm-34547-cannot-connect-to-ci-server-error-messages.yml b/changelogs/unreleased/fix-sm-34547-cannot-connect-to-ci-server-error-messages.yml new file mode 100644 index 00000000000..ddaec4f19f9 --- /dev/null +++ b/changelogs/unreleased/fix-sm-34547-cannot-connect-to-ci-server-error-messages.yml @@ -0,0 +1,5 @@ +--- +title: Fix an order of operations for CI connection error message in merge request + widget +merge_request: 13252 +author: -- cgit v1.2.1 From 7256626d6023869fac7ffdcfe3d291b76fb19e64 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 4 Aug 2017 08:07:05 +0000 Subject: Remove monkey-patched Array.prototype.first() and last() methods --- app/assets/javascripts/ajax_loading_spinner.js | 2 +- .../cycle_analytics/cycle_analytics_bundle.js | 2 +- app/assets/javascripts/dispatcher.js | 2 +- app/assets/javascripts/dropzone_input.js | 2 +- app/assets/javascripts/extensions/array.js | 11 ----------- .../javascripts/filtered_search/dropdown_utils.js | 4 ++-- .../filtered_search_dropdown_manager.js | 2 +- .../filtered_search/filtered_search_manager.js | 2 +- app/assets/javascripts/main.js | 3 --- spec/javascripts/abuse_reports_spec.js | 6 +++--- spec/javascripts/ajax_loading_spinner_spec.js | 1 - spec/javascripts/extensions/array_spec.js | 22 ---------------------- .../filtered_search/dropdown_utils_spec.js | 1 - .../filtered_search_dropdown_manager_spec.js | 1 - .../filtered_search_token_keys_spec.js | 1 - .../filtered_search_tokenizer_spec.js | 1 - 16 files changed, 11 insertions(+), 52 deletions(-) delete mode 100644 app/assets/javascripts/extensions/array.js delete mode 100644 spec/javascripts/extensions/array_spec.js diff --git a/app/assets/javascripts/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js index 38a8317dbd7..8f5e2e545ec 100644 --- a/app/assets/javascripts/ajax_loading_spinner.js +++ b/app/assets/javascripts/ajax_loading_spinner.js @@ -10,7 +10,7 @@ class AjaxLoadingSpinner { e.target.setAttribute('disabled', ''); const iconElement = e.target.querySelector('i'); // get first fa- icon - const originalIcon = iconElement.className.match(/(fa-)([^\s]+)/g).first(); + const originalIcon = iconElement.className.match(/(fa-)([^\s]+)/g)[0]; iconElement.dataset.icon = originalIcon; AjaxLoadingSpinner.toggleLoadingIcon(iconElement); $(e.target).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 44791a93936..6583e471a48 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -92,7 +92,7 @@ $(() => { }); }, selectDefaultStage() { - const stage = this.state.stages.first(); + const stage = this.state.stages[0]; this.selectStage(stage); }, selectStage(stage) { diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 070d0a01c0f..ad5ff19ec58 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -504,7 +504,7 @@ import GpgBadges from './gpg_badges'; new gl.DueDateSelectors(); break; } - switch (path.first()) { + switch (path[0]) { case 'sessions': case 'omniauth_callbacks': if (!gon.u2f) break; diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index bd1864b8321..6d19a6d9b3a 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -217,7 +217,7 @@ window.DropzoneInput = (function() { value = e.clipboardData.getData('text/plain'); } value = value.split("\r"); - return value.first(); + return value[0]; }; const showSpinner = function(e) { diff --git a/app/assets/javascripts/extensions/array.js b/app/assets/javascripts/extensions/array.js deleted file mode 100644 index 027222f804d..00000000000 --- a/app/assets/javascripts/extensions/array.js +++ /dev/null @@ -1,11 +0,0 @@ -// TODO: remove this - -// eslint-disable-next-line no-extend-native -Array.prototype.first = function first() { - return this[0]; -}; - -// eslint-disable-next-line no-extend-native -Array.prototype.last = function last() { - return this[this.length - 1]; -}; diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index db5560fa753..8d711e3213c 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -123,11 +123,11 @@ class DropdownUtils { if (!allowMultiple && itemInExistingTokens) { updatedItem.droplab_hidden = true; - } else if (!lastKey || searchInput.split('').last() === ' ') { + } else if (!lastKey || _.last(searchInput.split('')) === ' ') { updatedItem.droplab_hidden = false; } else if (lastKey) { const split = lastKey.split(':'); - const tokenName = split[0].split(' ').last(); + const tokenName = _.last(split[0].split(' ')); const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1; updatedItem.droplab_hidden = tokenName ? match : false; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index 47cecd5b5f7..dd1c067df87 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -167,7 +167,7 @@ class FilteredSearchDropdownManager { // Eg. token = 'label:' const split = lastToken.split(':'); - const dropdownName = split[0].split(' ').last(); + const dropdownName = _.last(split[0].split(' ')); this.loadDropdown(split.length > 1 ? dropdownName : ''); } else if (lastToken) { // Token has been initialized into an object because it has a value diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 3ce8b8607ad..a31be2b0bc7 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -367,7 +367,7 @@ class FilteredSearchManager { const fragments = searchToken.split(':'); if (fragments.length > 1) { const inputValues = fragments[0].split(' '); - const tokenKey = inputValues.last(); + const tokenKey = _.last(inputValues); if (inputValues.length > 1) { inputValues.pop(); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index a807ce326a6..42092a34c2f 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -16,9 +16,6 @@ import 'mousetrap'; import 'mousetrap/plugins/pause/mousetrap-pause'; import 'vendor/fuzzaldrin-plus'; -// extensions -import './extensions/array'; - // expose common libraries as globals (TODO: remove these) window.jQuery = jQuery; window.$ = jQuery; diff --git a/spec/javascripts/abuse_reports_spec.js b/spec/javascripts/abuse_reports_spec.js index 069d857eab6..13cab81dd60 100644 --- a/spec/javascripts/abuse_reports_spec.js +++ b/spec/javascripts/abuse_reports_spec.js @@ -6,10 +6,10 @@ import '~/abuse_reports'; const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw'; const MAX_MESSAGE_LENGTH = 500; - let messages; + let $messages; const assertMaxLength = $message => expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH); - const findMessage = searchText => messages.filter( + const findMessage = searchText => $messages.filter( (index, element) => element.innerText.indexOf(searchText) > -1, ).first(); @@ -18,7 +18,7 @@ import '~/abuse_reports'; beforeEach(function () { loadFixtures(FIXTURE); this.abuseReports = new global.AbuseReports(); - messages = $('.abuse-reports .message'); + $messages = $('.abuse-reports .message'); }); it('should truncate long messages', () => { diff --git a/spec/javascripts/ajax_loading_spinner_spec.js b/spec/javascripts/ajax_loading_spinner_spec.js index 1518ae68b0d..46e072a8ebb 100644 --- a/spec/javascripts/ajax_loading_spinner_spec.js +++ b/spec/javascripts/ajax_loading_spinner_spec.js @@ -1,4 +1,3 @@ -import '~/extensions/array'; import 'jquery'; import 'jquery-ujs'; import '~/ajax_loading_spinner'; diff --git a/spec/javascripts/extensions/array_spec.js b/spec/javascripts/extensions/array_spec.js deleted file mode 100644 index b1b81b4efc2..00000000000 --- a/spec/javascripts/extensions/array_spec.js +++ /dev/null @@ -1,22 +0,0 @@ -/* eslint-disable space-before-function-paren, no-var */ - -import '~/extensions/array'; - -(function() { - describe('Array extensions', function() { - describe('first', function() { - return it('returns the first item', function() { - var arr; - arr = [0, 1, 2, 3, 4, 5]; - return expect(arr.first()).toBe(0); - }); - }); - describe('last', function() { - return it('returns the last item', function() { - var arr; - arr = [0, 1, 2, 3, 4, 5]; - return expect(arr.last()).toBe(5); - }); - }); - }); -}).call(window); diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js index 244f170ab7a..b1b3d43f241 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js @@ -1,4 +1,3 @@ -import '~/extensions/array'; import '~/filtered_search/dropdown_utils'; import '~/filtered_search/filtered_search_tokenizer'; import '~/filtered_search/filtered_search_dropdown_manager'; diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js index 9e2076dc383..5c7e9115aac 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js @@ -1,4 +1,3 @@ -import '~/extensions/array'; import '~/filtered_search/filtered_search_visual_tokens'; import '~/filtered_search/filtered_search_tokenizer'; import '~/filtered_search/filtered_search_dropdown_manager'; diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js index 1a7631994b4..69b424c3af5 100644 --- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js @@ -1,4 +1,3 @@ -import '~/extensions/array'; import '~/filtered_search/filtered_search_token_keys'; describe('Filtered Search Token Keys', () => { diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js index e4a15c83c23..585bea9b499 100644 --- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js @@ -1,4 +1,3 @@ -import '~/extensions/array'; import '~/filtered_search/filtered_search_token_keys'; import '~/filtered_search/filtered_search_tokenizer'; -- cgit v1.2.1 From f2f1b9e3dd10f1f527660e131fb307c68214246a Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Fri, 4 Aug 2017 18:01:59 +0900 Subject: Add a spec for concurrent process --- spec/features/merge_requests/pipelines_spec.rb | 95 ++++++++++++++++++-------- 1 file changed, 68 insertions(+), 27 deletions(-) diff --git a/spec/features/merge_requests/pipelines_spec.rb b/spec/features/merge_requests/pipelines_spec.rb index b3d6cf8deb4..2136f7d829c 100644 --- a/spec/features/merge_requests/pipelines_spec.rb +++ b/spec/features/merge_requests/pipelines_spec.rb @@ -1,45 +1,86 @@ require 'spec_helper' feature 'Pipelines for Merge Requests', js: true do - given(:user) { create(:user) } - given(:merge_request) { create(:merge_request) } - given(:project) { merge_request.target_project } + describe 'pipeline tab' do + given(:user) { create(:user) } + given(:merge_request) { create(:merge_request) } + given(:project) { merge_request.target_project } - before do - project.team << [user, :master] - sign_in user - end - - context 'with pipelines' do - let!(:pipeline) do - create(:ci_empty_pipeline, - project: merge_request.source_project, - ref: merge_request.source_branch, - sha: merge_request.diff_head_sha) + before do + project.team << [user, :master] + sign_in user end - before do - visit project_merge_request_path(project, merge_request) + context 'with pipelines' do + let!(:pipeline) do + create(:ci_empty_pipeline, + project: merge_request.source_project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha) + end + + before do + visit project_merge_request_path(project, merge_request) + end + + scenario 'user visits merge request pipelines tab' do + page.within('.merge-request-tabs') do + click_link('Pipelines') + end + wait_for_requests + + expect(page).to have_selector('.sssssssss') + end end - scenario 'user visits merge request pipelines tab' do - page.within('.merge-request-tabs') do - click_link('Pipelines') + context 'without pipelines' do + before do + visit project_merge_request_path(project, merge_request) end - wait_for_requests - expect(page).to have_selector('.stage-cell') + scenario 'user visits merge request page' do + page.within('.merge-request-tabs') do + expect(page).to have_no_link('Pipelines') + end + end end end - context 'without pipelines' do - before do - visit project_merge_request_path(project, merge_request) + describe 'race condition' do + given(:project) { create(:project, :repository) } + given(:user) { create(:user) } + given(:build_push_data) { { ref: 'feature', checkout_sha: TestEnv::BRANCH_SHA['feature'] } } + + given(:merge_request_params) do + { "source_branch" => "feature", "source_project_id" => project.id, + "target_branch" => "master", "target_project_id" => project.id, "title" => "A" } + end + + background do + project.add_master(user) + sign_in user end - scenario 'user visits merge request page' do - page.within('.merge-request-tabs') do - expect(page).to have_no_link('Pipelines') + context 'when pipeline and merge request were created simultaneously' do + background do + stub_ci_pipeline_to_return_yaml_file + + threads = [] + threads << Thread.new do + @merge_request = MergeRequests::CreateService.new(project, user, merge_request_params).execute + end + + threads << Thread.new do + @pipeline = Ci::CreatePipelineService.new(project, user, build_push_data).execute(:push) + end + + threads.each { |thr| thr.join } + end + + scenario 'user sees pipeline in merge request widget' do + visit project_merge_request_path(project, @merge_request) + expect(page.find(".ci-widget")).to have_content(TestEnv::BRANCH_SHA['feature']) + expect(page.find(".ci-widget")).to have_content("##{@pipeline.id}") end end end -- cgit v1.2.1 From 0c75e23fab61e34bb45343776e31cad4ea3e4ae6 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Fri, 4 Aug 2017 02:23:02 +0900 Subject: fix --- .../_pipeline_schedule.html.haml | 2 +- spec/features/projects/pipeline_schedules_spec.rb | 23 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml index 7343d6e039c..bd8c38292d6 100644 --- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml +++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml @@ -15,7 +15,7 @@ - else = s_("PipelineSchedules|None") %td.next-run-cell - - if pipeline_schedule.active? + - if pipeline_schedule.active? && pipeline_schedule.next_run_at = time_ago_with_tooltip(pipeline_schedule.real_next_run) - else = s_("PipelineSchedules|Inactive") diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb index 605415d2af4..280c8a183e6 100644 --- a/spec/features/projects/pipeline_schedules_spec.rb +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -219,6 +219,29 @@ feature 'Pipeline Schedules', :js do end end end + + context 'when active is true and next_run_at is NULL' do + background do + create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule| + pipeline_schedule.update_attribute(:cron, nil) # Consequently next_run_at will be nil + pipeline_schedule.reload + expect(pipeline_schedule.active).to be_truthy + expect(pipeline_schedule.next_run_at).to be_nil + end + + visit_pipelines_schedules + find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click + end + + scenario 'user edit and recover the problematic pipeline schedule' do + expect(find("#schedule_cron").value).to eq('') + fill_in 'schedule_cron', with: '* 1 2 3 4' + click_button 'Save pipeline schedule' + page.within('.pipeline-schedule-table-row:nth-child(1)') do + expect(page).to have_css(".next-run-cell time") + end + end + end end context 'logged in as non-member' do -- cgit v1.2.1 From 3120b463a8517516d593cb8c31cf5f63c008dee9 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 4 Aug 2017 11:17:24 +0200 Subject: Use long curl options --- doc/install/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 769c1fd3664..22aedb5403e 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -168,7 +168,7 @@ are out of date, so we'll need to install through the following commands: curl --location https://deb.nodesource.com/setup_7.x | sudo bash - sudo apt-get install -y nodejs - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - + curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list sudo apt-get update sudo apt-get install yarn -- cgit v1.2.1 From cb36c3b288a034ad4a1682d4b9d5d631f10930da Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 4 Aug 2017 11:19:20 +0200 Subject: Install yarn via apt in update guides --- doc/update/8.17-to-9.0.md | 5 ++++- doc/update/9.0-to-9.1.md | 5 ++++- doc/update/9.1-to-9.2.md | 5 ++++- doc/update/9.2-to-9.3.md | 5 ++++- doc/update/9.3-to-9.4.md | 5 ++++- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md index 6308317b1f2..4d3ababaa41 100644 --- a/doc/update/8.17-to-9.0.md +++ b/doc/update/8.17-to-9.0.md @@ -65,7 +65,10 @@ Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage JavaScript dependencies. ```bash -curl --location https://yarnpkg.com/install.sh | bash - +curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update +sudo apt-get install yarn ``` More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). diff --git a/doc/update/9.0-to-9.1.md b/doc/update/9.0-to-9.1.md index 2d597894517..2b4a7bed27f 100644 --- a/doc/update/9.0-to-9.1.md +++ b/doc/update/9.0-to-9.1.md @@ -65,7 +65,10 @@ Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage JavaScript dependencies. ```bash -curl --location https://yarnpkg.com/install.sh | bash - +curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update +sudo apt-get install yarn ``` More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). diff --git a/doc/update/9.1-to-9.2.md b/doc/update/9.1-to-9.2.md index 225a4dcc924..f38547bba1a 100644 --- a/doc/update/9.1-to-9.2.md +++ b/doc/update/9.1-to-9.2.md @@ -65,7 +65,10 @@ Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage JavaScript dependencies. ```bash -curl --location https://yarnpkg.com/install.sh | bash - +curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update +sudo apt-get install yarn ``` More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). diff --git a/doc/update/9.2-to-9.3.md b/doc/update/9.2-to-9.3.md index 910539acc70..373f43eb3bb 100644 --- a/doc/update/9.2-to-9.3.md +++ b/doc/update/9.2-to-9.3.md @@ -65,7 +65,10 @@ Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage JavaScript dependencies. ```bash -curl --location https://yarnpkg.com/install.sh | bash - +curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update +sudo apt-get install yarn ``` More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). diff --git a/doc/update/9.3-to-9.4.md b/doc/update/9.3-to-9.4.md index 9540c36e7d0..b167f0737aa 100644 --- a/doc/update/9.3-to-9.4.md +++ b/doc/update/9.3-to-9.4.md @@ -65,7 +65,10 @@ Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage JavaScript dependencies. ```bash -curl --location https://yarnpkg.com/install.sh | bash - +curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update +sudo apt-get install yarn ``` More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). -- cgit v1.2.1 From ace7d7a2b51dd8d11c1ef21eaab5cc8372762704 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Fri, 4 Aug 2017 18:19:35 +0900 Subject: Add changelog --- ...sm-35931-active-ci-pipelineschedule-have-nullified-next_run_at.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-sm-35931-active-ci-pipelineschedule-have-nullified-next_run_at.yml diff --git a/changelogs/unreleased/fix-sm-35931-active-ci-pipelineschedule-have-nullified-next_run_at.yml b/changelogs/unreleased/fix-sm-35931-active-ci-pipelineschedule-have-nullified-next_run_at.yml new file mode 100644 index 00000000000..07840205b6e --- /dev/null +++ b/changelogs/unreleased/fix-sm-35931-active-ci-pipelineschedule-have-nullified-next_run_at.yml @@ -0,0 +1,4 @@ +--- +title: Fix pipeline_schedules pages when active schedule has an abnormal state +merge_request: 13286 +author: -- cgit v1.2.1 From 26d15a89dd4602b1d80d65b29fd48e79696dfb2c Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Tue, 1 Aug 2017 16:04:35 +0300 Subject: Fix Mattermost integration --- changelogs/unreleased/mattermost_fixes.yml | 4 ++++ lib/mattermost/session.rb | 26 ++++++++++++++++++++------ spec/lib/mattermost/session_spec.rb | 7 ++++--- 3 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 changelogs/unreleased/mattermost_fixes.yml diff --git a/changelogs/unreleased/mattermost_fixes.yml b/changelogs/unreleased/mattermost_fixes.yml new file mode 100644 index 00000000000..667109a0bb4 --- /dev/null +++ b/changelogs/unreleased/mattermost_fixes.yml @@ -0,0 +1,4 @@ +--- +title: Fix Mattermost integration +merge_request: +author: diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb index 688a79c0441..ef08bd46e17 100644 --- a/lib/mattermost/session.rb +++ b/lib/mattermost/session.rb @@ -36,11 +36,12 @@ module Mattermost def with_session with_lease do - raise Mattermost::NoSessionError unless create + create begin yield self - rescue Errno::ECONNREFUSED + rescue Errno::ECONNREFUSED => e + Rails.logger.error(e.message + "\n" + e.backtrace.join("\n")) raise Mattermost::NoSessionError ensure destroy @@ -85,10 +86,12 @@ module Mattermost private def create - return unless oauth_uri - return unless token_uri + raise Mattermost::NoSessionError unless oauth_uri + raise Mattermost::NoSessionError unless token_uri @token = request_token + raise Mattermost::NoSessionError unless @token + @headers = { Authorization: "Bearer #{@token}" } @@ -106,11 +109,16 @@ module Mattermost @oauth_uri = nil response = get("/api/v3/oauth/gitlab/login", follow_redirects: false) - return unless 300 <= response.code && response.code < 400 + return unless (300...400) === response.code redirect_uri = response.headers['location'] return unless redirect_uri + oauth_cookie = parse_cookie(response) + @headers = { + Cookie: oauth_cookie.to_cookie_string + } + @oauth_uri = URI.parse(redirect_uri) end @@ -124,7 +132,7 @@ module Mattermost def request_token response = get(token_uri, follow_redirects: false) - if 200 <= response.code && response.code < 400 + if (200...400) === response.code response.headers['token'] end end @@ -156,5 +164,11 @@ module Mattermost rescue Errno::ECONNREFUSED => e raise Mattermost::ConnectionError.new(e.message) end + + def parse_cookie(response) + cookie_hash = CookieHash.new + response.get_fields('Set-Cookie').each { |c| cookie_hash.add_cookies(c) } + cookie_hash + end end end diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb index be3908e8f6a..3db19d06305 100644 --- a/spec/lib/mattermost/session_spec.rb +++ b/spec/lib/mattermost/session_spec.rb @@ -20,9 +20,10 @@ describe Mattermost::Session, type: :request do describe '#with session' do let(:location) { 'http://location.tld' } + let(:cookie_header) {'MMOAUTH=taskik8az7rq8k6rkpuas7htia; Path=/;'} let!(:stub) do WebMock.stub_request(:get, "#{mattermost_url}/api/v3/oauth/gitlab/login") - .to_return(headers: { 'location' => location }, status: 307) + .to_return(headers: { 'location' => location, 'Set-Cookie' => cookie_header }, status: 307) end context 'without oauth uri' do @@ -34,9 +35,9 @@ describe Mattermost::Session, type: :request do context 'with oauth_uri' do let!(:doorkeeper) do Doorkeeper::Application.create( - name: "GitLab Mattermost", + name: 'GitLab Mattermost', redirect_uri: "#{mattermost_url}/signup/gitlab/complete\n#{mattermost_url}/login/gitlab/complete", - scopes: "") + scopes: '') end context 'without token_uri' do -- cgit v1.2.1 From d64fd953e2e032e7fa24c8e18239f5ffeea19410 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 4 Aug 2017 03:19:05 -0700 Subject: Bump GITLAB_SHELL_VERSION and GITALY_VERSION to support unhiding refs --- GITALY_SERVER_VERSION | 2 +- GITLAB_SHELL_VERSION | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 4e8f395fa5e..1b58cc10180 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.26.0 +0.27.0 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 8a30e8f94a3..42cdd0b540f 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -5.4.0 +5.7.0 -- cgit v1.2.1 From 5d775ea39463e8b1d7f4ecb3bb3017fcf94a83c7 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 4 Aug 2017 12:29:02 +0200 Subject: Rename raw -> rugged --- app/models/project.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index d85782782aa..09b1305739c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -941,7 +941,7 @@ class Project < ActiveRecord::Base end def repo - repository.raw + repository.rugged end def url_to_repo -- cgit v1.2.1 From 1b4fd63cf26ae0f19ec7735fd56bc0df6ce07058 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Fri, 4 Aug 2017 20:18:05 +0900 Subject: Fix spec --- spec/features/merge_requests/pipelines_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/features/merge_requests/pipelines_spec.rb b/spec/features/merge_requests/pipelines_spec.rb index 2136f7d829c..44b965d5e6a 100644 --- a/spec/features/merge_requests/pipelines_spec.rb +++ b/spec/features/merge_requests/pipelines_spec.rb @@ -29,7 +29,7 @@ feature 'Pipelines for Merge Requests', js: true do end wait_for_requests - expect(page).to have_selector('.sssssssss') + expect(page).to have_selector('.stage-cell') end end @@ -66,6 +66,7 @@ feature 'Pipelines for Merge Requests', js: true do stub_ci_pipeline_to_return_yaml_file threads = [] + threads << Thread.new do @merge_request = MergeRequests::CreateService.new(project, user, merge_request_params).execute end @@ -79,6 +80,7 @@ feature 'Pipelines for Merge Requests', js: true do scenario 'user sees pipeline in merge request widget' do visit project_merge_request_path(project, @merge_request) + expect(page.find(".ci-widget")).to have_content(TestEnv::BRANCH_SHA['feature']) expect(page.find(".ci-widget")).to have_content("##{@pipeline.id}") end -- cgit v1.2.1 From 73146801e6b350c731a26dcfa68d79268fa314d4 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Fri, 4 Aug 2017 20:22:44 +0900 Subject: Fix spec --- spec/features/projects/pipeline_schedules_spec.rb | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb index 280c8a183e6..24b335a7068 100644 --- a/spec/features/projects/pipeline_schedules_spec.rb +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -224,19 +224,15 @@ feature 'Pipeline Schedules', :js do background do create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule| pipeline_schedule.update_attribute(:cron, nil) # Consequently next_run_at will be nil - pipeline_schedule.reload - expect(pipeline_schedule.active).to be_truthy - expect(pipeline_schedule.next_run_at).to be_nil end - - visit_pipelines_schedules - find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click end scenario 'user edit and recover the problematic pipeline schedule' do - expect(find("#schedule_cron").value).to eq('') + visit_pipelines_schedules + find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click fill_in 'schedule_cron', with: '* 1 2 3 4' click_button 'Save pipeline schedule' + page.within('.pipeline-schedule-table-row:nth-child(1)') do expect(page).to have_css(".next-run-cell time") end -- cgit v1.2.1 From ea2d0c4861ef957cbcfe32a189d7d399c16241ae Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 3 Aug 2017 18:11:26 +0200 Subject: Update the update guides for background migrations The introduction of background migrations means some of our requirements for online upgrades have to be adjusted / clarified. See https://gitlab.com/gitlab-org/gitlab-ce/issues/35939 for more information. --- doc/development/background_migrations.md | 9 +++++ doc/update/README.md | 68 +++++++++++++++++++++++++++----- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/doc/development/background_migrations.md b/doc/development/background_migrations.md index e67db9ff142..f83a60e49e8 100644 --- a/doc/development/background_migrations.md +++ b/doc/development/background_migrations.md @@ -7,6 +7,11 @@ storing data in a single JSON column the data is stored in a separate table. ## When To Use Background Migrations +>**Note:** +When adding background migrations _you must_ make sure they are announced in the +monthly release post along with an estimate of how long it will take to complete +the migrations. + In the vast majority of cases you will want to use a regular Rails migration instead. Background migrations should _only_ be used when migrating _data_ in tables that have so many rows this process would take hours when performed in a @@ -91,6 +96,10 @@ BackgroundMigrationWorker.perform_bulk_in(5.minutes, jobs) ## Cleaning Up +>**Note:** +Cleaning up any remaining background migrations _must_ be done in either a major +or minor release, you _must not_ do this in a patch release. + Because background migrations can take a long time you can't immediately clean things up after scheduling them. For example, you can't drop a column that's used in the migration process as this would cause jobs to fail. This means that diff --git a/doc/update/README.md b/doc/update/README.md index 22dbc7c750f..c98e20686e0 100644 --- a/doc/update/README.md +++ b/doc/update/README.md @@ -34,17 +34,67 @@ update them are in [a separate document][omnidocker]. ## Upgrading without downtime -Starting with GitLab 9.1.0 it's possible to upgrade to a newer major, minor, or patch version of GitLab -without having to take your GitLab instance offline. However, for this to work -there are the following requirements: - -1. You can only upgrade 1 minor release at a time. So from 9.1 to 9.2, not to 9.3. -2. You have to be on the most recent patch release. For example, if 9.1.15 is the last - release of 9.1 then you can safely upgrade from that version to any 9.2.x version. - However, if you are running 9.1.14 you first need to upgrade to 9.1.15. +Starting with GitLab 9.1.0 it's possible to upgrade to a newer major, minor, or +patch version of GitLab without having to take your GitLab instance offline. +However, for this to work there are the following requirements: + +1. You can only upgrade 1 minor release at a time. So from 9.1 to 9.2, not to + 9.3. 2. You have to use [post-deployment migrations](../development/post_deployment_migrations.md). -3. You are using PostgreSQL. If you are using MySQL please look at the release post to see if downtime is required. +3. You are using PostgreSQL. If you are using MySQL please look at the release + post to see if downtime is required. + +Most of the time you can safely upgrade from a patch release to the next minor +release if the patch release is not the latest. For example, upgrading from +9.1.1 to 9.2.0 should be safe even if 9.1.2 has been released. We do recommend +you check the release posts of any releases between your current and target +version just in case they include any migrations that may require you to upgrade +1 release at a time. + +Some releases may also include so called "background migrations". These +migrations are performed in the background by Sidekiq and are often used for +migrating data. Background migrations are only added in the monthly releases. + +Certain major/minor releases may require a set of background migrations to be +finished. To guarantee this such a release will process any remaining jobs +before continuing the upgrading procedure. While this won't require downtime +(if the above conditions are met) we recommend users to keep at least 1 week +between upgrading major/minor releases, allowing the background migrations to +finish. The time necessary to complete these migrations can be reduced by +increasing the number of Sidekiq workers that can process jobs in the +`background_migration` queue. + +As a rule of thumb, any database smaller than 10 GB won't take too much time to +upgrade; perhaps an hour at most per minor release. Larger databases however may +require more time, but this is highly dependent on the size of the database and +the migrations that are being performed. + +### Examples + +To help explain this, let's look at some examples. + +**Example 1:** You are running a large GitLab installation using version 9.4.2, +which is the latest patch release of 9.4. When GitLab 9.5.0 is released this +installation can be safely upgraded to 9.5.0 without requiring downtime if the +requirements mentioned above are met. You can also skip 9.5.0 and upgrade to +9.5.1 once it's released, but you **can not** upgrade straight to 9.6.0; you +_have_ to first upgrade to a 9.5.x release. + +**Example 2:** You are running a large GitLab installation using version 9.4.2, +which is the latest patch release of 9.4. GitLab 9.5 includes some background +migrations, and 10.0 will require these to be completed (processing any +remaining jobs for you). Skipping 9.5 is not possible without downtime, and due +to the background migrations would require potentially hours of downtime +depending on how long it takes for the background migrations to complete. To +work around this you will have to upgrade to 9.5.x first, then wait at least a +week before upgrading to 10.0. + +**Example 3:** You use MySQL as the database for GitLab. Any upgrade to a new +major/minor release will require downtime. If a release includes any background +migrations this could potentially lead to hours of downtime, depending on the +size of your database. To work around this you will have to use PostgreSQL and +meet the other online upgrade requirements mentioned above. ## Upgrading between editions -- cgit v1.2.1 From 9d3f27a22d0193de15cb40e5cab1c437c61eca69 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 4 Aug 2017 13:29:26 +0200 Subject: Removed commit guidelines for migrations At some point we decided this isn't really necessary, so let's get rid of it. --- doc/development/migration_style_guide.md | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 161d2544169..5aac243ae3d 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -35,13 +35,6 @@ Please don't depend on GitLab-specific code since it can change in future versions. If needed copy-paste GitLab code into the migration to make it forward compatible. -## Commit Guidelines - -Each migration **must** be added in its own commit with a descriptive commit -message. If a commit adds a migration it _should only_ include the migration and -any corresponding changes to `db/schema.rb`. This makes it easy to revert a -database migration without accidentally reverting other changes. - ## Downtime Tagging Every migration must specify if it requires downtime or not, and if it should @@ -224,9 +217,9 @@ add_column(:projects, :foo, :integer, default: 10, limit: 8) ## Timestamp column type -By default, Rails uses the `timestamp` data type that stores timestamp data without timezone information. -The `timestamp` data type is used by calling either the `add_timestamps` or the `timestamps` method. -Also Rails converts the `:datetime` data type to the `timestamp` one. +By default, Rails uses the `timestamp` data type that stores timestamp data without timezone information. +The `timestamp` data type is used by calling either the `add_timestamps` or the `timestamps` method. +Also Rails converts the `:datetime` data type to the `timestamp` one. Example: -- cgit v1.2.1 From d5cb29434a375bbbe10c4aa016bf6631bf8ede6c Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 4 Aug 2017 13:30:57 +0200 Subject: Mention schema migrations and patch releases --- doc/development/migration_style_guide.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 5aac243ae3d..9b8ab5da74e 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -35,6 +35,12 @@ Please don't depend on GitLab-specific code since it can change in future versions. If needed copy-paste GitLab code into the migration to make it forward compatible. +## Schema Changes + +Migrations that make changes to the database schema (e.g. adding a column) can +only be added in the monthly release, patch releases may only contain data +migrations _unless_ schema changes are absolutely required to solve a problem. + ## Downtime Tagging Every migration must specify if it requires downtime or not, and if it should -- cgit v1.2.1 From a92ccbcb9e0bcb1ed38c114f03af20245c2f360c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20=22BKC=22=20Carlb=C3=A4cker?= Date: Fri, 4 Aug 2017 11:42:13 +0200 Subject: Have raketask tell gitaly where to find shell --- lib/tasks/gitlab/gitaly.rake | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index 3703f9cfb5c..7d35e0df53a 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -66,6 +66,7 @@ namespace :gitlab do config = { socket_path: address.sub(%r{\Aunix:}, ''), storage: storages } config[:auth] = { token: 'secret' } if Rails.env.test? config[:'gitaly-ruby'] = { dir: File.join(Dir.pwd, 'ruby') } if gitaly_ruby + config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path } TOML.dump(config) end -- cgit v1.2.1 From 4c68f22b0bf57b307abcef62e5f9ef927b1d047e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20=22BKC=22=20Carlb=C3=A4cker?= Date: Fri, 4 Aug 2017 13:13:11 +0200 Subject: fix tests --- spec/tasks/gitlab/gitaly_rake_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index a2f4ec39d89..6d453c19fc3 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -105,6 +105,8 @@ describe 'gitlab:gitaly namespace rake task' do # Gitaly storage configuration generated from #{Gitlab.config.source} on #{Time.current.to_s(:long)} # This is in TOML format suitable for use in Gitaly's config.toml file. socket_path = "/path/to/my.socket" + [gitlab-shell] + dir = "#{Gitlab.config.gitlab_shell.path}" [[storage]] name = "default" path = "/path/to/default" -- cgit v1.2.1 From 1614eb8b0a9fb2d719e6af7fec13d76db7d99892 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Fri, 4 Aug 2017 21:44:19 +0900 Subject: Fix spec --- spec/features/merge_requests/pipelines_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/merge_requests/pipelines_spec.rb b/spec/features/merge_requests/pipelines_spec.rb index 44b965d5e6a..347ce788b36 100644 --- a/spec/features/merge_requests/pipelines_spec.rb +++ b/spec/features/merge_requests/pipelines_spec.rb @@ -52,8 +52,8 @@ feature 'Pipelines for Merge Requests', js: true do given(:build_push_data) { { ref: 'feature', checkout_sha: TestEnv::BRANCH_SHA['feature'] } } given(:merge_request_params) do - { "source_branch" => "feature", "source_project_id" => project.id, - "target_branch" => "master", "target_project_id" => project.id, "title" => "A" } + { "source_branch" => "feature", "source_project_id" => project.id, + "target_branch" => "master", "target_project_id" => project.id, "title" => "A" } end background do -- cgit v1.2.1 From 65cf702da5130f66440fd362296a34ada7b5ebc1 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 4 Aug 2017 14:50:50 +0200 Subject: Add 9.5 update guide, with Gitaly specifics --- doc/update/9.4-to-9.5.md | 352 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 352 insertions(+) create mode 100644 doc/update/9.4-to-9.5.md diff --git a/doc/update/9.4-to-9.5.md b/doc/update/9.4-to-9.5.md new file mode 100644 index 00000000000..903d3d806a3 --- /dev/null +++ b/doc/update/9.4-to-9.5.md @@ -0,0 +1,352 @@ +# From 9.4 to 9.5 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + +```bash +sudo service gitlab stop +``` + +### 2. Backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Update Ruby + +NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be +sure to upgrade your interpreter if necessary. + +You can check which version you are running with `ruby -v`. + +Download and compile Ruby: + +```bash +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz +echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz +cd ruby-2.3.3 +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. Update Node + +GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and +it has a minimum requirement of node v4.3.0. + +You can check which version you are running with `node -v`. If you are running +a version older than `v4.3.0` you will need to update to a newer version. You +can find instructions to install from community maintained packages or compile +from source at the nodejs.org website. + + + + +Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage +JavaScript dependencies. + +```bash +curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update +sudo apt-get install yarn +``` + +More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). + +### 5. Update Go + +NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go +1.5.x through 1.7.x. Be sure to upgrade your installation if necessary. + +You can check which version you are running with `go version`. + +Download and install Go: + +```bash +# Remove former Go installation folder +sudo rm -rf /usr/local/go + +curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz +echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ + sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.8.3.linux-amd64.tar.gz +``` + +### 6. Get latest code + +```bash +cd /home/git/gitlab + +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +sudo -u git -H git checkout -- locale +``` + +For GitLab Community Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 9-5-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 9-5-stable-ee +``` + +### 7. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v$( Date: Wed, 17 May 2017 18:17:15 +0200 Subject: Add a Circuitbreaker for storage paths --- app/controllers/admin/health_check_controller.rb | 7 + app/controllers/application_controller.rb | 22 ++ app/helpers/storage_health_helper.rb | 37 +++ app/models/repository.rb | 41 +++- .../admin/health_check/_failing_storages.html.haml | 15 ++ app/views/admin/health_check/show.html.haml | 27 ++- changelogs/unreleased/bvl-nfs-circuitbreaker.yml | 4 + config/gitlab.yml.example | 9 + config/initializers/1_settings.rb | 12 + config/initializers/6_validations.rb | 16 ++ config/routes/admin.rb | 4 +- doc/administration/img/failing_storage.png | Bin 0 -> 48281 bytes doc/administration/repository_storage_paths.md | 77 +++++- doc/api/repository_storage_health.md | 74 ++++++ lib/api/api.rb | 1 + lib/api/circuit_breakers.rb | 50 ++++ lib/api/entities.rb | 6 + lib/gitlab/git/repository.rb | 8 +- lib/gitlab/git/storage.rb | 22 ++ lib/gitlab/git/storage/circuit_breaker.rb | 142 +++++++++++ lib/gitlab/git/storage/forked_storage_check.rb | 59 +++++ lib/gitlab/git/storage/health.rb | 101 ++++++++ lib/gitlab/health_checks/fs_shards_check.rb | 21 +- .../admin/health_check_controller_spec.rb | 25 ++ spec/controllers/application_controller_spec.rb | 24 ++ spec/controllers/projects_controller_spec.rb | 14 ++ spec/factories/projects.rb | 6 + spec/features/admin/admin_health_check_spec.rb | 24 +- spec/helpers/storage_health_helper_spec.rb | 20 ++ spec/initializers/6_validations_spec.rb | 21 ++ spec/initializers/settings_spec.rb | 11 + .../cache/ci/project_pipeline_status_spec.rb | 14 +- spec/lib/gitlab/git/repository_spec.rb | 14 ++ .../lib/gitlab/git/storage/circuit_breaker_spec.rb | 265 +++++++++++++++++++++ .../git/storage/forked_storage_check_spec.rb | 27 +++ spec/lib/gitlab/git/storage/health_spec.rb | 85 +++++++ .../gitlab/health_checks/fs_shards_check_spec.rb | 15 ++ spec/models/repository_spec.rb | 69 +++++- spec/requests/api/circuit_breakers_spec.rb | 57 +++++ spec/support/stored_repositories.rb | 12 + 40 files changed, 1421 insertions(+), 37 deletions(-) create mode 100644 app/helpers/storage_health_helper.rb create mode 100644 app/views/admin/health_check/_failing_storages.html.haml create mode 100644 changelogs/unreleased/bvl-nfs-circuitbreaker.yml create mode 100644 doc/administration/img/failing_storage.png create mode 100644 doc/api/repository_storage_health.md create mode 100644 lib/api/circuit_breakers.rb create mode 100644 lib/gitlab/git/storage.rb create mode 100644 lib/gitlab/git/storage/circuit_breaker.rb create mode 100644 lib/gitlab/git/storage/forked_storage_check.rb create mode 100644 lib/gitlab/git/storage/health.rb create mode 100644 spec/controllers/admin/health_check_controller_spec.rb create mode 100644 spec/helpers/storage_health_helper_spec.rb create mode 100644 spec/lib/gitlab/git/storage/circuit_breaker_spec.rb create mode 100644 spec/lib/gitlab/git/storage/forked_storage_check_spec.rb create mode 100644 spec/lib/gitlab/git/storage/health_spec.rb create mode 100644 spec/requests/api/circuit_breakers_spec.rb diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb index caf4c138da8..65a17828feb 100644 --- a/app/controllers/admin/health_check_controller.rb +++ b/app/controllers/admin/health_check_controller.rb @@ -1,5 +1,12 @@ class Admin::HealthCheckController < Admin::ApplicationController def show @errors = HealthCheck::Utils.process_checks(['standard']) + @failing_storage_statuses = Gitlab::Git::Storage::Health.for_failing_storages + end + + def reset_storage_health + Gitlab::Git::Storage::CircuitBreaker.reset_all! + redirect_to admin_health_check_path, + notice: _('Git storage health information has been reset') end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d14b1dbecf6..34d948bc3d2 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -52,6 +52,15 @@ class ApplicationController < ActionController::Base head :forbidden, retry_after: Gitlab::Auth::UniqueIpsLimiter.config.unique_ips_limit_time_window end + rescue_from Gitlab::Git::Storage::Inaccessible, GRPC::Unavailable do |exception| + Raven.capture_exception(exception) if sentry_enabled? + log_exception(exception) + + headers['Retry-After'] = exception.retry_after if exception.respond_to?(:retry_after) + + render_503 + end + def redirect_back_or_default(default: root_path, options: {}) redirect_to request.referer.present? ? :back : default, options end @@ -152,6 +161,19 @@ class ApplicationController < ActionController::Base head :unprocessable_entity end + def render_503 + respond_to do |format| + format.html do + render( + file: Rails.root.join("public", "503"), + layout: false, + status: :service_unavailable + ) + end + format.any { head :service_unavailable } + end + end + def no_cache_headers response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate" response.headers["Pragma"] = "no-cache" diff --git a/app/helpers/storage_health_helper.rb b/app/helpers/storage_health_helper.rb new file mode 100644 index 00000000000..544c9efb845 --- /dev/null +++ b/app/helpers/storage_health_helper.rb @@ -0,0 +1,37 @@ +module StorageHealthHelper + def failing_storage_health_message(storage_health) + storage_name = content_tag(:strong, h(storage_health.storage_name)) + host_names = h(storage_health.failing_on_hosts.to_sentence) + translation_params = { storage_name: storage_name, + host_names: host_names, + failed_attempts: storage_health.total_failures } + + translation = n_('%{storage_name}: failed storage access attempt on host:', + '%{storage_name}: %{failed_attempts} failed storage access attempts:', + storage_health.total_failures) % translation_params + + translation.html_safe + end + + def message_for_circuit_breaker(circuit_breaker) + maximum_failures = circuit_breaker.failure_count_threshold + current_failures = circuit_breaker.failure_count + permanently_broken = circuit_breaker.circuit_broken? && current_failures >= maximum_failures + + translation_params = { number_of_failures: current_failures, + maximum_failures: maximum_failures, + number_of_seconds: circuit_breaker.failure_wait_time } + + if permanently_broken + s_("%{number_of_failures} of %{maximum_failures} failures. GitLab will not "\ + "retry automatically. Reset storage information when the problem is "\ + "resolved.") % translation_params + elsif circuit_breaker.circuit_broken? + _("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\ + "block access for %{number_of_seconds} seconds.") % translation_params + else + _("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\ + "allow access on the next attempt.") % translation_params + end + end +end diff --git a/app/models/repository.rb b/app/models/repository.rb index 4e9fe759fdc..b04d434926f 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -133,12 +133,13 @@ class Repository ref ||= root_ref args = %W( - #{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset} + log #{ref} --pretty=%H --skip #{offset} --max-count #{limit} --grep=#{query} --regexp-ignore-case ) args = args.concat(%W(-- #{path})) if path.present? - git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines + git_log_results = run_git(args).first.lines + git_log_results.map { |c| commit(c.chomp) }.compact end @@ -622,8 +623,8 @@ class Repository key = path.blank? ? "last_commit_id_for_path:#{sha}" : "last_commit_id_for_path:#{sha}:#{Digest::SHA1.hexdigest(path)}" cache.fetch(key) do - args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path}) - Gitlab::Popen.popen(args, path_to_repo).first.strip + args = %W(rev-list --max-count=1 #{sha} -- #{path}) + run_git(args).first.strip end end @@ -678,8 +679,8 @@ class Repository end def refs_contains_sha(ref_type, sha) - args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha}) - names = Gitlab::Popen.popen(args, path_to_repo).first + args = %W(#{ref_type} --contains #{sha}) + names = run_git(args).first if names.respond_to?(:split) names = names.split("\n").map(&:strip) @@ -957,15 +958,17 @@ class Repository return [] if empty_repo? || query.blank? offset = 2 - args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref}) - Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/) + args = %W(grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref}) + + run_git(args).first.scrub.split(/^--$/) end def search_files_by_name(query, ref) return [] if empty_repo? || query.blank? - args = %W(#{Gitlab.config.git.bin_path} ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)}) - Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:strip) + args = %W(ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)}) + + run_git(args).first.lines.map(&:strip) end def with_repo_branch_commit(start_repository, start_branch_name) @@ -1010,8 +1013,8 @@ class Repository end def fetch_ref(source_path, source_ref, target_ref) - args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) - Gitlab::Popen.popen(args, path_to_repo) + args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) + run_git(args) end def create_ref(ref, ref_path) @@ -1092,6 +1095,12 @@ class Repository private + def run_git(args) + circuit_breaker.perform do + Gitlab::Popen.popen([Gitlab.config.git.bin_path, *args], path_to_repo) + end + end + def blob_data_at(sha, path) blob = blob_at(sha, path) return unless blob @@ -1101,7 +1110,9 @@ class Repository end def refs_directory_exists? - File.exist?(File.join(path_to_repo, 'refs')) + circuit_breaker.perform do + File.exist?(File.join(path_to_repo, 'refs')) + end end def cache @@ -1145,4 +1156,8 @@ class Repository def initialize_raw_repository Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git') end + + def circuit_breaker + @circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(project.repository_storage) + end end diff --git a/app/views/admin/health_check/_failing_storages.html.haml b/app/views/admin/health_check/_failing_storages.html.haml new file mode 100644 index 00000000000..6830201538d --- /dev/null +++ b/app/views/admin/health_check/_failing_storages.html.haml @@ -0,0 +1,15 @@ +- if failing_storages.any? + = _('There are problems accessing Git storage: ') + %ul + - failing_storages.each do |storage_health| + %li + = failing_storage_health_message(storage_health) + %ul + - storage_health.failing_circuit_breakers.each do |circuit_breaker| + %li + #{circuit_breaker.hostname}: #{message_for_circuit_breaker(circuit_breaker)} + + = _("Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again.") + .prepend-top-10 + = button_to _("Reset git storage health information"), reset_storage_health_admin_health_check_path, + method: :post, class: 'btn btn-default' diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml index f16f59623f7..517db50b97f 100644 --- a/app/views/admin/health_check/show.html.haml +++ b/app/views/admin/health_check/show.html.haml @@ -1,22 +1,22 @@ - @no_container = true -- page_title "Health Check" +- page_title _('Health Check') +- no_errors = @errors.blank? && @failing_storage_statuses.blank? = render 'admin/monitoring/head' %div{ class: container_class } - %h3.page-title - Health Check + %h3.page-title= page_title .bs-callout.clearfix .pull-left %p - Access token is + #{ s_('HealthCheck|Access token is') } %code#health-check-token= current_application_settings.health_check_access_token .prepend-top-10 - = button_to "Reset health check access token", reset_health_check_token_admin_application_settings_path, + = button_to _("Reset health check access token"), reset_health_check_token_admin_application_settings_path, method: :put, class: 'btn btn-default', - data: { confirm: 'Are you sure you want to reset the health check token?' } + data: { confirm: _('Are you sure you want to reset the health check token?') } %p.light - Health information can be retrieved from the following endpoints. More information is available - = link_to 'here', help_page_path('user/admin_area/monitoring/health_check') + #{ _('Health information can be retrieved from the following endpoints. More information is available') } + = link_to s_('More information is available|here'), help_page_path('user/admin_area/monitoring/health_check') %ul %li %code= readiness_url(token: current_application_settings.health_check_access_token) @@ -29,14 +29,15 @@ .panel.panel-default .panel-heading Current Status: - - if @errors.blank? + - if no_errors = icon('circle', class: 'cgreen') - Healthy + #{ s_('HealthCheck|Healthy') } - else = icon('warning', class: 'cred') - Unhealthy + #{ s_('HealthCheck|Unhealthy') } .panel-body - - if @errors.blank? - No Health Problems Detected + - if no_errors + #{ s_('HealthCheck|No Health Problems Detected') } - else = @errors + = render partial: 'failing_storages', object: @failing_storage_statuses diff --git a/changelogs/unreleased/bvl-nfs-circuitbreaker.yml b/changelogs/unreleased/bvl-nfs-circuitbreaker.yml new file mode 100644 index 00000000000..151854ed31f --- /dev/null +++ b/changelogs/unreleased/bvl-nfs-circuitbreaker.yml @@ -0,0 +1,4 @@ +--- +title: Block access to failing repository storage +merge_request: 11449 +author: diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 73a68c6da1b..45ab4e1a851 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -506,6 +506,11 @@ production: &base path: /home/git/repositories/ gitaly_address: unix:/home/git/gitlab/tmp/sockets/private/gitaly.socket # TCP connections are supported too (e.g. tcp://host:port) # gitaly_token: 'special token' # Optional: override global gitaly.token for this storage. + failure_count_threshold: 10 # number of failures before stopping attempts + failure_wait_time: 30 # Seconds after an access failure before allowing access again + failure_reset_time: 1800 # Time in seconds to expire failures + storage_timeout: 5 # Time in seconds to wait before aborting a storage access attempt + ## Backup settings backup: @@ -638,6 +643,10 @@ test: default: path: tmp/tests/repositories/ gitaly_address: unix:tmp/tests/gitaly/gitaly.socket + broken: + path: tmp/tests/non-existent-repositories + gitaly_address: unix:tmp/tests/gitaly/gitaly.socket + gitaly: enabled: true token: secret diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 63f4c8c9e0a..017537f30be 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -222,6 +222,7 @@ Settings.gitlab['default_branch_protection'] ||= 2 Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil? Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost' Settings.gitlab['ssh_host'] ||= Settings.gitlab.host +Settings.gitlab['hostname'] ||= ENV['HOSTNAME'] || Socket.gethostname Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? Settings.gitlab['port'] ||= ENV['GITLAB_PORT'] || (Settings.gitlab.https ? 443 : 80) Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || '' @@ -433,6 +434,17 @@ end Settings.repositories.storages.values.each do |storage| # Expand relative paths storage['path'] = Settings.absolute(storage['path']) + # Set failure defaults + storage['failure_count_threshold'] ||= 10 + storage['failure_wait_time'] ||= 30 + storage['failure_reset_time'] ||= 1800 + storage['storage_timeout'] ||= 5 + # Set turn strings into numbers + storage['failure_count_threshold'] = storage['failure_count_threshold'].to_i + storage['failure_wait_time'] = storage['failure_wait_time'].to_i + storage['failure_reset_time'] = storage['failure_reset_time'].to_i + # We might want to have a timeout shorter than 1 second. + storage['storage_timeout'] = storage['storage_timeout'].to_f end # diff --git a/config/initializers/6_validations.rb b/config/initializers/6_validations.rb index 9e24f42d284..92ce4dd03cd 100644 --- a/config/initializers/6_validations.rb +++ b/config/initializers/6_validations.rb @@ -7,6 +7,13 @@ def find_parent_path(name, path) Gitlab.config.repositories.storages.detect do |n, rs| name != n && Pathname.new(rs['path']).realpath == parent end +rescue Errno::EIO, Errno::ENOENT => e + warning = "WARNING: couldn't verify #{path} (#{name}). "\ + "If this is an external storage, it might be offline." + message = "#{warning}\n#{e.message}" + Rails.logger.error("#{message}\n\t" + e.backtrace.join("\n\t")) + + nil end def storage_validation_error(message) @@ -29,6 +36,15 @@ def validate_storages_config if !repository_storage.is_a?(Hash) || repository_storage['path'].nil? storage_validation_error("#{name} is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example") end + + %w(failure_count_threshold failure_wait_time failure_reset_time storage_timeout).each do |setting| + # Falling back to the defaults is fine! + next if repository_storage[setting].nil? + + unless repository_storage[setting].to_f > 0 + storage_validation_error("#{setting}, for storage `#{name}` needs to be greater than 0") + end + end end end diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 5427bab93ce..c0748231813 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -67,7 +67,9 @@ namespace :admin do end resource :logs, only: [:show] - resource :health_check, controller: 'health_check', only: [:show] + resource :health_check, controller: 'health_check', only: [:show] do + post :reset_storage_health + end resource :background_jobs, controller: 'background_jobs', only: [:show] resource :system_info, controller: 'system_info', only: [:show] resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ } diff --git a/doc/administration/img/failing_storage.png b/doc/administration/img/failing_storage.png new file mode 100644 index 00000000000..82b393a58b2 Binary files /dev/null and b/doc/administration/img/failing_storage.png differ diff --git a/doc/administration/repository_storage_paths.md b/doc/administration/repository_storage_paths.md index 55a45119525..624a908b3a3 100644 --- a/doc/administration/repository_storage_paths.md +++ b/doc/administration/repository_storage_paths.md @@ -60,7 +60,7 @@ respectively. path: /mnt/cephfs/repositories ``` -1. [Restart GitLab] for the changes to take effect. +1. [Restart GitLab][restart-gitlab] for the changes to take effect. >**Note:** The [`gitlab_shell: repos_path` entry][repospath] in `gitlab.yml` will be @@ -97,9 +97,80 @@ be stored via the **Application Settings** in the Admin area. Beginning with GitLab 8.13.4, multiple paths can be chosen. New projects will be randomly placed on one of the selected paths. +## Handling failing repository storage + +> [Introduced][ce-11449] in GitLab 9.5. + +When GitLab detects access to the repositories storage fails repeatedly, it can +gracefully prevent attempts to access the storage. This might be useful when +the repositories are stored somewhere on the network. + +The configuration could look as follows: + +**For Omnibus installations** + +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + git_data_dirs({ + "default" => { + "path" => "/mnt/nfs-01/git-data", + "failure_count_threshold" => 10, + "failure_wait_time" => 30, + "failure_reset_time" => 1800, + "storage_timeout" => 5 + } + }) + ``` + +1. Save the file and [reconfigure GitLab][reconfigure-gitlab] for the changes to take effect. + +--- + +**For installations from source** + +1. Edit `config/gitlab.yml`: + + ```yaml + repositories: + storages: # You must have at least a `default` storage path. + default: + path: /home/git/repositories/ + failure_count_threshold: 10 # number of failures before stopping attempts + failure_wait_time: 30 # Seconds after last access failure before trying again + failure_reset_time: 1800 # Time in seconds to expire failures + storage_timeout: 5 # Time in seconds to wait before aborting a storage access attempt + ``` + +1. Save the file and [restart GitLab][restart-gitlab] for the changes to take effect. + + +**`failure_count_threshold`:** The number of failures of after which GitLab will +completely prevent access to the storage. The number of failures can be reset in +the admin interface: `https://gitlab.example.com/admin/health_check` or using the +[api](../api/repository_storage_health.md) to allow access to the storage again. + +**`failure_wait_time`:** When access to a storage fails. GitLab will prevent +access to the storage for the time specified here. This allows the filesystem to +recover without. + +**`failure_reset_time`:** The time in seconds GitLab will keep failure +information. When no failures occur during this time, information about the +mount is reset. + +**`storage_timeout`:** The time in seconds GitLab will try to access storage. +After this time a timeout error will be raised. + +When storage failures occur, this will be visible in the admin interface like this: + +![failing storage](img/failing_storage.png) + +To allow access to all storages, click the `Reset git storage health information` button. + [ce-4578]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4578 -[restart gitlab]: restart_gitlab.md#installations-from-source -[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure +[restart-gitlab]: restart_gitlab.md#installations-from-source +[reconfigure-gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure [backups]: ../raketasks/backup_restore.md [raketask]: https://gitlab.com/gitlab-org/gitlab-ce/blob/033e5423a2594e08a7ebcd2379bd2331f4c39032/lib/backup/repository.rb#L54-56 [repospath]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-9-stable/config/gitlab.yml.example#L457 +[ce-11449]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11449 diff --git a/doc/api/repository_storage_health.md b/doc/api/repository_storage_health.md new file mode 100644 index 00000000000..e0c0315c2d7 --- /dev/null +++ b/doc/api/repository_storage_health.md @@ -0,0 +1,74 @@ +# Circuitbreaker API + +> [Introduced][ce-11449] in GitLab 9.5. + +The Circuitbreaker API is only accessible to administrators. All requests by +guests will respond with `401 Unauthorized`, and all requests by normal users +will respond with `403 Forbidden`. + +## Repository Storages + +### Get all storage information + +Returns of all currently configured storages and their health information. + +``` +GET /circuit_breakers/repository_storage +``` + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/circuit_breakers/repository_storage +``` + +```json +[ + { + "storage_name": "default", + "failing_on_hosts": [], + "total_failures": 0 + }, + { + "storage_name": "broken", + "failing_on_hosts": [ + "web01", "worker01" + ], + "total_failures": 1 + } +] +``` + +### Get failing storages + +This returns a list of all currently failing storages. + +``` +GET /circuit_breakers/repository_storage/failing +``` + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/circuit_breakers/repository_storage/failing +``` + +```json +[ + { + "storage_name":"broken", + "failing_on_hosts":["web01", "worker01"], + "total_failures":2 + } +] +``` + +## Reset failing storage information + +Use this remove all failing storage information and allow access to the storage again. + +``` +DELETE /circuit_breakers/repository_storage +``` + +```bash +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/circuit_breakers/repository_storage +``` + +[ce-11449]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11449 diff --git a/lib/api/api.rb b/lib/api/api.rb index 982a2b88d62..94df543853b 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -95,6 +95,7 @@ module API mount ::API::Boards mount ::API::Branches mount ::API::BroadcastMessages + mount ::API::CircuitBreakers mount ::API::Commits mount ::API::CommitStatuses mount ::API::DeployKeys diff --git a/lib/api/circuit_breakers.rb b/lib/api/circuit_breakers.rb new file mode 100644 index 00000000000..118883f5ea5 --- /dev/null +++ b/lib/api/circuit_breakers.rb @@ -0,0 +1,50 @@ +module API + class CircuitBreakers < Grape::API + before { authenticated_as_admin! } + + resource :circuit_breakers do + params do + requires :type, + type: String, + desc: "The type of circuitbreaker", + values: ['repository_storage'] + end + resource ':type' do + namespace '', requirements: { type: 'repository_storage' } do + helpers do + def failing_storage_health + @failing_storage_health ||= Gitlab::Git::Storage::Health.for_failing_storages + end + + def storage_health + @failing_storage_health ||= Gitlab::Git::Storage::Health.for_all_storages + end + end + + desc 'Get all failing git storages' do + detail 'This feature was introduced in GitLab 9.5' + success Entities::RepositoryStorageHealth + end + get do + present storage_health, with: Entities::RepositoryStorageHealth + end + + desc 'Get all failing git storages' do + detail 'This feature was introduced in GitLab 9.5' + success Entities::RepositoryStorageHealth + end + get 'failing' do + present failing_storage_health, with: Entities::RepositoryStorageHealth + end + + desc 'Reset all storage failures and open circuitbreaker' do + detail 'This feature was introduced in GitLab 9.5' + end + delete do + Gitlab::Git::Storage::CircuitBreaker.reset_all! + end + end + end + end + end +end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 298831a8fdb..f25b408439a 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -954,5 +954,11 @@ module API expose :ip_address expose :submitted, as: :akismet_submitted end + + class RepositoryStorageHealth < Grape::Entity + expose :storage_name + expose :failing_on_hosts + expose :total_failures + end end end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index ffe2c8b91bb..aa3252e1df8 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -64,11 +64,17 @@ module Gitlab end def rugged - @rugged ||= Rugged::Repository.new(path, alternates: alternate_object_directories) + @rugged ||= circuit_breaker.perform do + Rugged::Repository.new(path, alternates: alternate_object_directories) + end rescue Rugged::RepositoryError, Rugged::OSError raise NoRepository.new('no repository for such path') end + def circuit_breaker + @circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(storage) + end + # Returns an Array of branch names # sorted by name ASC def branch_names diff --git a/lib/gitlab/git/storage.rb b/lib/gitlab/git/storage.rb new file mode 100644 index 00000000000..e28be4b8a38 --- /dev/null +++ b/lib/gitlab/git/storage.rb @@ -0,0 +1,22 @@ +module Gitlab + module Git + module Storage + class Inaccessible < StandardError + attr_reader :retry_after + + def initialize(message = nil, retry_after = nil) + super(message) + @retry_after = retry_after + end + end + + CircuitOpen = Class.new(Inaccessible) + + REDIS_KEY_PREFIX = 'storage_accessible:'.freeze + + def self.redis + Gitlab::Redis::SharedState + end + end + end +end diff --git a/lib/gitlab/git/storage/circuit_breaker.rb b/lib/gitlab/git/storage/circuit_breaker.rb new file mode 100644 index 00000000000..c722771e0d5 --- /dev/null +++ b/lib/gitlab/git/storage/circuit_breaker.rb @@ -0,0 +1,142 @@ +module Gitlab + module Git + module Storage + class CircuitBreaker + attr_reader :storage, + :hostname, + :storage_path, + :failure_count_threshold, + :failure_wait_time, + :failure_reset_time, + :storage_timeout + + def self.reset_all! + pattern = "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}*" + + Gitlab::Git::Storage.redis.with do |redis| + all_storage_keys = redis.scan_each(match: pattern).to_a + redis.del(*all_storage_keys) unless all_storage_keys.empty? + end + + RequestStore.delete(:circuitbreaker_cache) + end + + def self.for_storage(storage) + cached_circuitbreakers = RequestStore.fetch(:circuitbreaker_cache) do + Hash.new do |hash, storage_name| + hash[storage_name] = new(storage_name) + end + end + + cached_circuitbreakers[storage] + end + + def initialize(storage, hostname = Gitlab.config.gitlab.hostname) + @storage = storage + @hostname = hostname + + config = Gitlab.config.repositories.storages[@storage] + @storage_path = config['path'] + @failure_count_threshold = config['failure_count_threshold'] + @failure_wait_time = config['failure_wait_time'] + @failure_reset_time = config['failure_reset_time'] + @storage_timeout = config['storage_timeout'] + end + + def perform + return yield unless Feature.enabled?('git_storage_circuit_breaker') + + if circuit_broken? + raise Gitlab::Git::Storage::CircuitOpen.new("Circuit for #{storage} open", failure_wait_time) + end + + check_storage_accessible! + + yield + end + + def circuit_broken? + return false if no_failures? + + recent_failure = last_failure > failure_wait_time.seconds.ago + too_many_failures = failure_count > failure_count_threshold + + recent_failure || too_many_failures + end + + # Memoizing the `storage_available` call means we only do it once per + # request when the storage is available. + # + # When the storage appears not available, and the memoized value is `false` + # we might want to try again. + def storage_available? + @storage_available ||= Gitlab::Git::Storage::ForkedStorageCheck.storage_available?(storage_path, storage_timeout) + end + + def check_storage_accessible! + if storage_available? + track_storage_accessible + else + track_storage_inaccessible + raise Gitlab::Git::Storage::Inaccessible.new("#{storage} not accessible", failure_wait_time) + end + end + + def no_failures? + last_failure.blank? && failure_count == 0 + end + + def track_storage_inaccessible + @failure_info = [Time.now, failure_count + 1] + + Gitlab::Git::Storage.redis.with do |redis| + redis.pipelined do + redis.hset(cache_key, :last_failure, last_failure.to_i) + redis.hincrby(cache_key, :failure_count, 1) + redis.expire(cache_key, failure_reset_time) + end + end + end + + def track_storage_accessible + return if no_failures? + + @failure_info = [nil, 0] + + Gitlab::Git::Storage.redis.with do |redis| + redis.pipelined do + redis.hset(cache_key, :last_failure, nil) + redis.hset(cache_key, :failure_count, 0) + end + end + end + + def last_failure + failure_info.first + end + + def failure_count + failure_info.last + end + + def failure_info + @failure_info ||= get_failure_info + end + + def get_failure_info + last_failure, failure_count = Gitlab::Git::Storage.redis.with do |redis| + redis.hmget(cache_key, :last_failure, :failure_count) + end + + last_failure = Time.at(last_failure.to_i) if last_failure.present? + + [last_failure, failure_count.to_i] + end + + def cache_key + @cache_key ||= "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage}:#{hostname}" + end + end + end + end +end diff --git a/lib/gitlab/git/storage/forked_storage_check.rb b/lib/gitlab/git/storage/forked_storage_check.rb new file mode 100644 index 00000000000..557a3b0e61d --- /dev/null +++ b/lib/gitlab/git/storage/forked_storage_check.rb @@ -0,0 +1,59 @@ +module Gitlab + module Git + module Storage + module ForkedStorageCheck + extend self + + def storage_available?(path, timeout_seconds = 5) + status = timeout_check(path, timeout_seconds) + + status.success? + end + + def timeout_check(path, timeout_seconds) + filesystem_check_pid = check_filesystem_in_fork(path) + + deadline = timeout_seconds.seconds.from_now.utc + wait_time = 0.01 + status = nil + + while status.nil? + if deadline > Time.now.utc + sleep(wait_time) + _pid, status = Process.wait2(filesystem_check_pid, Process::WNOHANG) + else + Process.kill('KILL', filesystem_check_pid) + # Blocking wait, so we are sure the process is gone before continuing + _pid, status = Process.wait2(filesystem_check_pid) + end + end + + status + end + + # This call forks out into a process, that process will then be replaced + # With an `exec` call, since we fork out into a shell, we can create a + # child process without needing an ActiveRecord-connection. + # + # Inside the shell, we use `& wait` to fork another child. We do this + # to prevent leaving a zombie process when the parent gets killed by the + # timeout. + # + # https://stackoverflow.com/questions/27892975/what-causes-activerecord-breaking-postgres-connection-after-forking + # https://stackoverflow.com/questions/22012943/activerecordstatementinvalid-runtimeerror-the-connection-cannot-be-reused-in + def check_filesystem_in_fork(path) + fork do + STDOUT.reopen('/dev/null') + STDERR.reopen('/dev/null') + + exec("(#{test_script(path)}) & wait %1") + end + end + + def test_script(path) + "testpath=\"$(realpath #{Shellwords.escape(path)})\" && stat $testpath" + end + end + end + end +end diff --git a/lib/gitlab/git/storage/health.rb b/lib/gitlab/git/storage/health.rb new file mode 100644 index 00000000000..0a28052fd81 --- /dev/null +++ b/lib/gitlab/git/storage/health.rb @@ -0,0 +1,101 @@ +module Gitlab + module Git + module Storage + class Health + attr_reader :storage_name, :info + + def self.pattern_for_storage(storage_name) + "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage_name}:*" + end + + def self.for_all_storages + storage_names = Gitlab.config.repositories.storages.keys + results_per_storage = nil + + Gitlab::Git::Storage.redis.with do |redis| + keys_per_storage = all_keys_for_storages(storage_names, redis) + + # We need to make sure all keys are actually loaded as an array. + # Otherwise when using the enumerator of the `scan_each` within a + # second pipeline, it will be assumed unloaded, wich would make the + # result unusable inside the pipeline. + loaded_keys_per_storage = keys_per_storage.inject({}) do |loaded_keys, (storage_name, keys)| + loaded_keys[storage_name] = keys.to_a + loaded_keys + end + + results_per_storage = load_for_keys(loaded_keys_per_storage, redis) + end + + results_per_storage.map do |name, info| + info.each { |i| i[:failure_count] = i[:failure_count].value.to_i } + new(name, info) + end + end + + def self.all_keys_for_storages(storage_names, redis) + keys_per_storage = nil + + redis.pipelined do + keys_per_storage = storage_names.inject({}) do |result, storage_name| + key = pattern_for_storage(storage_name) + + result.merge(storage_name => redis.scan_each(match: key)) + end + end + + keys_per_storage + end + + def self.load_for_keys(keys_per_storage, redis) + info_for_keys = nil + + redis.pipelined do + info_for_keys = keys_per_storage.inject({}) do |result, (storage_name, keys)| + info_for_storage = keys.map do |key| + { name: key, failure_count: redis.hget(key, :failure_count) } + end + + result.merge(storage_name => info_for_storage) + end + end + + info_for_keys + end + + def self.for_failing_storages + for_all_storages.select(&:failing?) + end + + def initialize(storage_name, info) + @storage_name = storage_name + @info = info + end + + def failing_info + @failing_info ||= info.select { |info_for_host| info_for_host[:failure_count] > 0 } + end + + def failing? + failing_info.any? + end + + def failing_on_hosts + @failing_on_hosts ||= failing_info.map do |info_for_host| + info_for_host[:name].split(':').last + end + end + + def failing_circuit_breakers + @failing_circuit_breakers ||= failing_on_hosts.map do |hostname| + CircuitBreaker.new(storage_name, hostname) + end + end + + def total_failures + @total_failures ||= failing_info.sum { |info_for_host| info_for_host[: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 9e91c135956..eef97f54962 100644 --- a/lib/gitlab/health_checks/fs_shards_check.rb +++ b/lib/gitlab/health_checks/fs_shards_check.rb @@ -10,7 +10,9 @@ module Gitlab def readiness repository_storages.map do |storage_name| begin - if !storage_stat_test(storage_name) + if !storage_circuitbreaker_test(storage_name) + HealthChecks::Result.new(false, 'circuitbreaker tripped', shard: storage_name) + elsif !storage_stat_test(storage_name) HealthChecks::Result.new(false, 'cannot stat storage', shard: storage_name) else with_temp_file(storage_name) do |tmp_file_path| @@ -36,7 +38,8 @@ module Gitlab [ storage_stat_metrics(storage_name), storage_write_metrics(storage_name), - storage_read_metrics(storage_name) + storage_read_metrics(storage_name), + storage_circuitbreaker_metrics(storage_name) ].flatten end end @@ -121,6 +124,12 @@ module Gitlab file_contents == RANDOM_STRING end + def storage_circuitbreaker_test(storage_name) + Gitlab::Git::Storage::CircuitBreaker.new(storage_name).perform { "OK" } + rescue Gitlab::Git::Storage::Inaccessible + nil + end + def storage_stat_metrics(storage_name) operation_metrics(:filesystem_accessible, :filesystem_access_latency_seconds, shard: storage_name) do with_timing { storage_stat_test(storage_name) } @@ -143,6 +152,14 @@ module Gitlab end end end + + def storage_circuitbreaker_metrics(storage_name) + operation_metrics(:filesystem_circuitbreaker, + :filesystem_circuitbreaker_latency_seconds, + shard: storage_name) do + with_timing { storage_circuitbreaker_test(storage_name) } + end + end end end end diff --git a/spec/controllers/admin/health_check_controller_spec.rb b/spec/controllers/admin/health_check_controller_spec.rb new file mode 100644 index 00000000000..0b8e0c8a065 --- /dev/null +++ b/spec/controllers/admin/health_check_controller_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Admin::HealthCheckController, broken_storage: true do + let(:admin) { create(:admin) } + + before do + sign_in(admin) + end + + describe 'GET show' do + it 'loads the git storage health information' do + get :show + + expect(assigns[:failing_storage_statuses]).not_to be_nil + end + end + + describe 'POST reset_storage_health' do + it 'resets all storage health information' do + expect(Gitlab::Git::Storage::CircuitBreaker).to receive(:reset_all!) + + post :reset_storage_health + end + end +end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 1641bddea11..331903a5543 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -108,6 +108,30 @@ describe ApplicationController do end end + describe 'rescue from Gitlab::Git::Storage::Inaccessible' do + controller(described_class) do + def index + raise Gitlab::Git::Storage::Inaccessible.new('broken', 100) + end + end + + it 'renders a 503 when storage is not available' do + sign_in(create(:user)) + + get :index + + expect(response.status).to eq(503) + end + + it 'renders includes a Retry-After header' do + sign_in(create(:user)) + + get :index + + expect(response.headers['Retry-After']).to eq(100) + end + end + describe 'response format' do controller(described_class) do def index diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 34095ef6250..8ecd8b6ca71 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -107,6 +107,20 @@ describe ProjectsController do end end + context 'when the storage is not available', broken_storage: true do + let(:project) { create(:project, :broken_storage) } + before do + project.add_developer(user) + sign_in(user) + end + + it 'renders a 503' do + get :show, namespace_id: project.namespace, id: project + + expect(response).to have_http_status(503) + end + end + context "project with empty repo" do let(:empty_project) { create(:project_empty_repo, :public) } diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index be3f219e8bf..3f8e7030b1c 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -54,6 +54,12 @@ FactoryGirl.define do avatar { File.open(Rails.root.join('spec/fixtures/dk.png')) } end + trait :broken_storage do + after(:create) do |project| + project.update_column(:repository_storage, 'broken') + end + end + # Test repository - https://gitlab.com/gitlab-org/gitlab-test trait :repository do path { 'gitlabhq' } diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb index 106e7370a98..634dfd53f71 100644 --- a/spec/features/admin/admin_health_check_spec.rb +++ b/spec/features/admin/admin_health_check_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature "Admin Health Check" do +feature "Admin Health Check", feature: true, broken_storage: true do include StubENV before do @@ -55,4 +55,26 @@ feature "Admin Health Check" do expect(page).to have_content('The server is on fire') end end + + context 'with repository storage failures' do + before do + # Track a failure + Gitlab::Git::Storage::CircuitBreaker.for_storage('broken').perform { nil } rescue nil + visit admin_health_check_path + end + + it 'shows storage failure information' do + hostname = Gitlab.config.gitlab.hostname + + expect(page).to have_content('broken: failed storage access attempt on host:') + expect(page).to have_content("#{hostname}: 1 of 10 failures.") + end + + it 'allows resetting storage failures' do + click_button 'Reset git storage health information' + + expect(page).to have_content('Git storage health information has been reset') + expect(page).not_to have_content('failed storage access attempt') + end + end end diff --git a/spec/helpers/storage_health_helper_spec.rb b/spec/helpers/storage_health_helper_spec.rb new file mode 100644 index 00000000000..874498e6338 --- /dev/null +++ b/spec/helpers/storage_health_helper_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe StorageHealthHelper do + describe '#failing_storage_health_message' do + let(:health) do + Gitlab::Git::Storage::Health.new( + "", + [] + ) + end + + it 'escapes storage names' do + escaped_storage_name = '<script>alert('storage name');)</script>' + + result = helper.failing_storage_health_message(health) + + expect(result).to include(escaped_storage_name) + end + end +end diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb index 0877770c167..83283f03940 100644 --- a/spec/initializers/6_validations_spec.rb +++ b/spec/initializers/6_validations_spec.rb @@ -23,6 +23,16 @@ describe '6_validations' do end end + context 'when one of the settings is incorrect' do + before do + mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c', 'failure_count_threshold' => 'not a number' }) + end + + it 'throws an error' do + expect { validate_storages_config }.to raise_error(/failure_count_threshold/) + end + end + context 'with invalid storage names' do before do mock_storages('name with spaces' => { 'path' => 'tmp/tests/paths/a/b/c' }) @@ -84,6 +94,17 @@ describe '6_validations' do expect { validate_storages_paths }.not_to raise_error end end + + describe 'inaccessible storage' do + before do + mock_storages('foo' => { 'path' => 'tmp/tests/a/path/that/does/not/exist' }) + end + + it 'passes through with a warning' do + expect(Rails.logger).to receive(:error) + expect { validate_storages_paths }.not_to raise_error + end + end end def mock_storages(storages) diff --git a/spec/initializers/settings_spec.rb b/spec/initializers/settings_spec.rb index ebdabcf93f1..e5ec90cb8f9 100644 --- a/spec/initializers/settings_spec.rb +++ b/spec/initializers/settings_spec.rb @@ -2,6 +2,17 @@ require 'spec_helper' require_relative '../../config/initializers/1_settings' describe Settings do + describe '#repositories' do + it 'assigns the default failure attributes' do + repository_settings = Gitlab.config.repositories.storages['broken'] + + expect(repository_settings['failure_count_threshold']).to eq(10) + expect(repository_settings['failure_wait_time']).to eq(30) + expect(repository_settings['failure_reset_time']).to eq(1800) + expect(repository_settings['storage_timeout']).to eq(5) + end + end + describe '#host_without_www' do context 'URL with protocol' do it 'returns the host' do diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb index f43d89d7ccd..16704ff5e77 100644 --- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb +++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb @@ -48,8 +48,9 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do described_class.load_in_batch_for_projects([project_without_status]) end - it 'only connects to redis_cache twice' do - # Once to load, once to store in the cache + it 'only connects to redis twice' do + # Stub circuitbreaker so it doesn't count the redis connections in there + stub_circuit_breaker(project_without_status) expect(Gitlab::Redis::Cache).to receive(:with).exactly(2).and_call_original described_class.load_in_batch_for_projects([project_without_status]) @@ -301,4 +302,13 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do end end end + + def stub_circuit_breaker(project) + fake_circuitbreaker = double + allow(fake_circuitbreaker).to receive(:perform).and_yield + allow(project.repository.raw_repository) + .to receive(:circuit_breaker).and_return(fake_circuitbreaker) + allow(project.repository) + .to receive(:circuit_breaker).and_return(fake_circuitbreaker) + end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 9bfad0c9bdf..07cd6db2200 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -55,6 +55,20 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe "#rugged" do + describe 'when storage is broken', broken_storage: true do + it 'raises a storage exception when storage is not available' do + broken_repo = described_class.new('broken', 'a/path.git') + + expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Storage::Inaccessible) + end + end + + it 'raises a no repository exception when there is no repo' do + broken_repo = described_class.new('default', 'a/path.git') + + expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Repository::NoRepository) + end + context 'with no Git env stored' do before do expect(Gitlab::Git::Env).to receive(:all).and_return({}) diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb new file mode 100644 index 00000000000..479e53230bc --- /dev/null +++ b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb @@ -0,0 +1,265 @@ +require 'spec_helper' + +describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: true, broken_storage: true do + let(:circuit_breaker) { described_class.new('default') } + let(:hostname) { Gitlab.config.gitlab.hostname } + let(:cache_key) { "storage_accessible:default:#{hostname}" } + + def value_from_redis(name) + Gitlab::Git::Storage.redis.with do |redis| + redis.hmget(cache_key, name) + end.first + end + + def set_in_redis(name, value) + Gitlab::Git::Storage.redis.with do |redis| + redis.hmset(cache_key, name, value) + end.first + end + + describe '.reset_all!' do + it 'clears all entries form redis' do + set_in_redis(:failure_count, 10) + + described_class.reset_all! + + key_exists = Gitlab::Git::Storage.redis.with { |redis| redis.exists(cache_key) } + + expect(key_exists).to be_falsey + end + end + + describe '.for_storage' do + it 'only builds a single circuitbreaker per storage' do + expect(described_class).to receive(:new).once.and_call_original + + breaker = described_class.for_storage('default') + + expect(breaker).to be_a(described_class) + expect(described_class.for_storage('default')).to eq(breaker) + end + end + + describe '#initialize' do + it 'assigns the settings' do + expect(circuit_breaker.hostname).to eq(hostname) + expect(circuit_breaker.storage).to eq('default') + expect(circuit_breaker.storage_path).to eq(TestEnv.repos_path) + expect(circuit_breaker.failure_count_threshold).to eq(10) + expect(circuit_breaker.failure_wait_time).to eq(30) + expect(circuit_breaker.failure_reset_time).to eq(1800) + expect(circuit_breaker.storage_timeout).to eq(5) + end + end + + describe '#perform' do + it 'raises an exception with retry time when the circuit is open' do + allow(circuit_breaker).to receive(:circuit_broken?).and_return(true) + + expect { |b| circuit_breaker.perform(&b) } + .to raise_error(Gitlab::Git::Storage::CircuitOpen) + end + + it 'yields the block' do + expect { |b| circuit_breaker.perform(&b) } + .to yield_control + end + + it 'checks if the storage is available' do + expect(circuit_breaker).to receive(:check_storage_accessible!) + + circuit_breaker.perform { 'hello world' } + end + + it 'returns the value of the block' do + result = circuit_breaker.perform { 'return value' } + + expect(result).to eq('return value') + end + + it 'raises possible errors' do + expect { circuit_breaker.perform { raise Rugged::OSError.new('Broken') } } + .to raise_error(Rugged::OSError) + end + + context 'with the feature disabled' do + it 'returns the block without checking accessibility' do + stub_feature_flags(git_storage_circuit_breaker: false) + + expect(circuit_breaker).not_to receive(:circuit_broken?) + + result = circuit_breaker.perform { 'hello' } + + expect(result).to eq('hello') + end + end + end + + describe '#circuit_broken?' do + it 'is closed when there is no last failure' do + set_in_redis(:last_failure, nil) + set_in_redis(:failure_count, 0) + + expect(circuit_breaker.circuit_broken?).to be_falsey + end + + it 'is open when there was a recent failure' do + Timecop.freeze do + set_in_redis(:last_failure, 1.second.ago.to_f) + set_in_redis(:failure_count, 1) + + expect(circuit_breaker.circuit_broken?).to be_truthy + end + end + + it 'is open when there are to many failures' do + set_in_redis(:last_failure, 1.day.ago.to_f) + set_in_redis(:failure_count, 200) + + expect(circuit_breaker.circuit_broken?).to be_truthy + end + end + + describe '#check_storage_accessible!' do + context 'when the storage is available' do + it 'tracks that the storage was accessible an raises the error' do + expect(circuit_breaker).to receive(:track_storage_accessible) + + circuit_breaker.check_storage_accessible! + end + end + + context 'when the storage is not available' do + let(:circuit_breaker) { described_class.new('broken') } + + it 'tracks that the storage was unavailable and raises an error with retry time' do + expect(circuit_breaker).to receive(:track_storage_inaccessible) + + expect { circuit_breaker.check_storage_accessible! } + .to raise_error do |exception| + expect(exception).to be_kind_of(Gitlab::Git::Storage::Inaccessible) + expect(exception.retry_after).to eq(30) + end + end + end + end + + describe '#track_storage_inaccessible' do + around(:each) do |example| + Timecop.freeze + + example.run + + Timecop.return + end + + it 'records the failure time in redis' do + circuit_breaker.track_storage_inaccessible + + failure_time = value_from_redis(:last_failure) + + expect(Time.at(failure_time.to_i)).to be_within(1.second).of(Time.now) + end + + it 'sets the failure time on the breaker without reloading' do + circuit_breaker.track_storage_inaccessible + + expect(circuit_breaker).not_to receive(:get_failure_info) + expect(circuit_breaker.last_failure).to eq(Time.now) + end + + it 'increments the failure count in redis' do + set_in_redis(:failure_count, 10) + + circuit_breaker.track_storage_inaccessible + + expect(value_from_redis(:failure_count).to_i).to be(11) + end + + it 'increments the failure count on the breaker without reloading' do + set_in_redis(:failure_count, 10) + + circuit_breaker.track_storage_inaccessible + + expect(circuit_breaker).not_to receive(:get_failure_info) + expect(circuit_breaker.failure_count).to eq(11) + end + end + + describe '#track_storage_accessible' do + it 'sets the failure count to zero in redis' do + set_in_redis(:failure_count, 10) + + circuit_breaker.track_storage_accessible + + expect(value_from_redis(:failure_count).to_i).to be(0) + end + + it 'sets the failure count to zero on the breaker without reloading' do + set_in_redis(:failure_count, 10) + + circuit_breaker.track_storage_accessible + + expect(circuit_breaker).not_to receive(:get_failure_info) + expect(circuit_breaker.failure_count).to eq(0) + end + + it 'removes the last failure time from redis' do + set_in_redis(:last_failure, Time.now.to_i) + + circuit_breaker.track_storage_accessible + + expect(circuit_breaker).not_to receive(:get_failure_info) + expect(circuit_breaker.last_failure).to be_nil + end + + it 'removes the last failure time from the breaker without reloading' do + set_in_redis(:last_failure, Time.now.to_i) + + circuit_breaker.track_storage_accessible + + expect(value_from_redis(:last_failure)).to be_empty + end + + it 'wont connect to redis when there are no failures' do + expect(Gitlab::Git::Storage.redis).to receive(:with).once + .and_call_original + expect(circuit_breaker).to receive(:track_storage_accessible) + .and_call_original + + circuit_breaker.track_storage_accessible + end + end + + describe '#no_failures?' do + it 'is false when a failure was tracked' do + set_in_redis(:last_failure, Time.now.to_i) + set_in_redis(:failure_count, 1) + + expect(circuit_breaker.no_failures?).to be_falsey + end + end + + describe '#last_failure' do + it 'returns the last failure time' do + time = Time.parse("2017-05-26 17:52:30") + set_in_redis(:last_failure, time.to_i) + + expect(circuit_breaker.last_failure).to eq(time) + end + end + + describe '#failure_count' do + it 'returns the failure count' do + set_in_redis(:failure_count, 7) + + expect(circuit_breaker.failure_count).to eq(7) + end + end + + describe '#cache_key' do + it 'includes storage and host' do + expect(circuit_breaker.cache_key).to eq(cache_key) + end + end +end diff --git a/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb b/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb new file mode 100644 index 00000000000..a9e048b316d --- /dev/null +++ b/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Gitlab::Git::Storage::ForkedStorageCheck, skip_database_cleaner: true do + let(:existing_path) do + existing_path = TestEnv.repos_path + FileUtils.mkdir_p(existing_path) + existing_path + end + + describe '.storage_accessible?' do + it 'detects when a storage is not available' do + expect(described_class.storage_available?('/non/existant/path')).to be_falsey + end + + it 'detects when a storage is available' do + expect(described_class.storage_available?(existing_path)).to be_truthy + end + + it 'returns false when the check takes to long' do + allow(described_class).to receive(:check_filesystem_in_fork) do + fork { sleep 10 } + end + + expect(described_class.storage_available?(existing_path, 0.5)).to be_falsey + end + end +end diff --git a/spec/lib/gitlab/git/storage/health_spec.rb b/spec/lib/gitlab/git/storage/health_spec.rb new file mode 100644 index 00000000000..28b74a0581f --- /dev/null +++ b/spec/lib/gitlab/git/storage/health_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' + +describe Gitlab::Git::Storage::Health, clean_gitlab_redis_shared_state: true, broken_storage: true do + let(:host1_key) { 'storage_accessible:broken:web01' } + let(:host2_key) { 'storage_accessible:default:kiq01' } + + def set_in_redis(cache_key, value) + Gitlab::Git::Storage.redis.with do |redis| + redis.hmset(cache_key, :failure_count, value) + end.first + end + + describe '.for_failing_storages' do + it 'only includes health status for failures' do + set_in_redis(host1_key, 10) + set_in_redis(host2_key, 0) + + expect(described_class.for_failing_storages.map(&:storage_name)) + .to contain_exactly('broken') + end + end + + describe '.load_for_keys' do + let(:subject) do + results = Gitlab::Git::Storage.redis.with do |redis| + described_class.load_for_keys({ 'broken' => [host1_key] }, redis) + end + + # Make sure the `Redis#future is loaded + results.inject({}) do |result, (name, info)| + info.each { |i| i[:failure_count] = i[:failure_count].value.to_i } + + result[name] = info + + result + end + end + + it 'loads when there is no info in redis' do + expect(subject).to eq('broken' => [{ name: host1_key, failure_count: 0 }]) + end + + it 'reads the correct values for a storage from redis' do + set_in_redis(host1_key, 5) + set_in_redis(host2_key, 7) + + expect(subject).to eq('broken' => [{ name: host1_key, failure_count: 5 }]) + end + end + + describe '.for_all_storages' do + it 'loads health status for all configured storages' do + healths = described_class.for_all_storages + + expect(healths.map(&:storage_name)).to contain_exactly('default', 'broken') + end + end + + describe '#failing_info' do + it 'only contains storages that have failures' do + health = described_class.new('broken', [{ name: host1_key, failure_count: 0 }, + { name: host2_key, failure_count: 3 }]) + + expect(health.failing_info).to contain_exactly({ name: host2_key, failure_count: 3 }) + end + end + + describe '#total_failures' do + it 'sums up all the failures' do + health = described_class.new('broken', [{ name: host1_key, failure_count: 2 }, + { name: host2_key, failure_count: 3 }]) + + expect(health.total_failures).to eq(5) + end + end + + describe '#failing_on_hosts' do + it 'collects only the failing hostnames' do + health = described_class.new('broken', [{ name: host1_key, failure_count: 2 }, + { name: host2_key, failure_count: 0 }]) + + expect(health.failing_on_hosts).to contain_exactly('web01') + end + 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 8abc4320c59..8539c6deea6 100644 --- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb +++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb @@ -44,6 +44,15 @@ describe Gitlab::HealthChecks::FsShardsCheck do describe '#readiness' do subject { described_class.readiness } + context 'storage has a tripped circuitbreaker' do + let(:repository_storages) { ['broken'] } + let(:storages_paths) do + Gitlab.config.repositories.storages + end + + it { is_expected.to include(result_class.new(false, 'circuitbreaker tripped', shard: 'broken')) } + end + context 'storage points to not existing folder' do let(:storages_paths) do { @@ -51,6 +60,10 @@ describe Gitlab::HealthChecks::FsShardsCheck do }.with_indifferent_access end + before 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)) } end @@ -109,6 +122,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do expect(metrics).to include(an_object_having_attributes(name: :filesystem_access_latency_seconds, value: be >= 0)) expect(metrics).to include(an_object_having_attributes(name: :filesystem_read_latency_seconds, value: be >= 0)) expect(metrics).to include(an_object_having_attributes(name: :filesystem_write_latency_seconds, value: be >= 0)) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_circuitbreaker_latency_seconds, value: be >= 0)) end end @@ -127,6 +141,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do expect(metrics).to include(an_object_having_attributes(name: :filesystem_access_latency_seconds, value: be >= 0)) expect(metrics).to include(an_object_having_attributes(name: :filesystem_read_latency_seconds, value: be >= 0)) expect(metrics).to include(an_object_having_attributes(name: :filesystem_write_latency_seconds, value: be >= 0)) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_circuitbreaker_latency_seconds, value: be >= 0)) end it 'cleans up files used for metrics' do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 764f548be45..d077925a1cf 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1,11 +1,12 @@ require 'spec_helper' -describe Repository do +describe Repository, models: true do include RepoHelpers TestBlob = Struct.new(:path) let(:project) { create(:project, :repository) } let(:repository) { project.repository } + let(:broken_repository) { create(:project, :broken_storage).repository } let(:user) { create(:user) } let(:commit_options) do @@ -27,12 +28,26 @@ describe Repository do let(:author_email) { 'user@example.org' } let(:author_name) { 'John Doe' } + def expect_to_raise_storage_error + expect { yield }.to raise_error do |exception| + expect(exception.class).to be_in([Gitlab::Git::Storage::Inaccessible, GRPC::Unavailable]) + end + end + describe '#branch_names_contains' do subject { repository.branch_names_contains(sample_commit.id) } it { is_expected.to include('master') } it { is_expected.not_to include('feature') } it { is_expected.not_to include('fix') } + + describe 'when storage is broken', broken_storage: true do + it 'should raise a storage error' do + expect_to_raise_storage_error do + broken_repository.branch_names_contains(sample_commit.id) + end + end + end end describe '#tag_names_contains' do @@ -142,6 +157,14 @@ describe Repository do subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id } it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } + + describe 'when storage is broken', broken_storage: true do + it 'should raise a storage error' do + expect_to_raise_storage_error do + broken_repository.last_commit_for_path(sample_commit.id, '.gitignore').id + end + end + end end describe '#last_commit_id_for_path' do @@ -158,6 +181,14 @@ describe Repository do expect(cache).to receive(:fetch).with(key).and_return('c1acaa5') is_expected.to eq('c1acaa5') end + + describe 'when storage is broken', broken_storage: true do + it 'should raise a storage error' do + expect_to_raise_storage_error do + broken_repository.last_commit_id_for_path(sample_commit.id, '.gitignore') + end + end + end end describe '#commits' do @@ -196,6 +227,12 @@ describe Repository do expect(commit_ids).to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') end + + describe 'when storage is broken', broken_storage: true do + it 'should raise a storage error' do + expect_to_raise_storage_error { broken_repository.find_commits_by_message('s') } + end + end end describe '#blob_at' do @@ -521,6 +558,14 @@ describe Repository do expect(results).to match_array([]) end + describe 'when storage is broken', broken_storage: true do + it 'should raise a storage error' do + expect_to_raise_storage_error do + broken_repository.search_files_by_content('feature', 'master') + end + end + end + describe 'result' do subject { results.first } @@ -549,6 +594,22 @@ describe Repository do expect(results).to match_array([]) end + + describe 'when storage is broken', broken_storage: true do + it 'should raise a storage error' do + expect_to_raise_storage_error { broken_repository.search_files_by_name('files', 'master') } + end + end + end + + describe '#fetch_ref' do + describe 'when storage is broken', broken_storage: true do + it 'should raise a storage error' do + path = broken_repository.path_to_repo + + expect_to_raise_storage_error { broken_repository.fetch_ref(path, '1', '2') } + end + end end describe '#create_ref' do @@ -966,6 +1027,12 @@ describe Repository do expect(repository.exists?).to eq(false) end + + context 'with broken storage', broken_storage: true do + it 'should raise a storage error' do + expect_to_raise_storage_error { broken_repository.exists? } + end + end end describe '#exists?' do diff --git a/spec/requests/api/circuit_breakers_spec.rb b/spec/requests/api/circuit_breakers_spec.rb new file mode 100644 index 00000000000..76521e55994 --- /dev/null +++ b/spec/requests/api/circuit_breakers_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe API::CircuitBreakers do + let(:user) { create(:user) } + let(:admin) { create(:admin) } + + describe 'GET circuit_breakers/repository_storage' do + it 'returns a 401 for anonymous users' do + get api('/circuit_breakers/repository_storage') + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + get api('/circuit_breakers/repository_storage', user) + + expect(response).to have_http_status(403) + end + + it 'returns an Array of storages' do + expect(Gitlab::Git::Storage::Health).to receive(:for_all_storages) do + [Gitlab::Git::Storage::Health.new('broken', [{ name: 'prefix:broken:web01', failure_count: 4 }])] + end + + get api('/circuit_breakers/repository_storage', admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_kind_of(Array) + expect(json_response.first['storage_name']).to eq('broken') + expect(json_response.first['failing_on_hosts']).to eq(['web01']) + expect(json_response.first['total_failures']).to eq(4) + end + + describe 'GET circuit_breakers/repository_storage/failing' do + it 'returns an array of failing storages' do + expect(Gitlab::Git::Storage::Health).to receive(:for_failing_storages) do + [Gitlab::Git::Storage::Health.new('broken', [{ name: 'prefix:broken:web01', failure_count: 4 }])] + end + + get api('/circuit_breakers/repository_storage/failing', admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_kind_of(Array) + end + end + end + + describe 'DELETE circuit_breakers/repository_storage' do + it 'clears all circuit_breakers' do + expect(Gitlab::Git::Storage::CircuitBreaker).to receive(:reset_all!) + + delete api('/circuit_breakers/repository_storage', admin) + + expect(response).to have_http_status(204) + end + end +end diff --git a/spec/support/stored_repositories.rb b/spec/support/stored_repositories.rb index df18926d58c..f3deae0f455 100644 --- a/spec/support/stored_repositories.rb +++ b/spec/support/stored_repositories.rb @@ -2,4 +2,16 @@ RSpec.configure do |config| config.before(:each, :repository) do TestEnv.clean_test_path end + + config.before(:all, :broken_storage) do + FileUtils.rm_rf Gitlab.config.repositories.storages.broken['path'] + end + + config.before(:each, :broken_storage) do + allow(Gitlab::GitalyClient).to receive(:call) do + raise GRPC::Unavailable.new('Gitaly broken in this spec') + end + + Gitlab::Git::Storage::CircuitBreaker.reset_all! + end end -- cgit v1.2.1 From 0fa94a0dddfc38635abf49a38dbadc9b9ead4d7a Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 2 Aug 2017 08:34:41 +0200 Subject: Make sure the check works for paths with spaces. --- lib/gitlab/git/storage/forked_storage_check.rb | 2 +- .../git/storage/forked_storage_check_spec.rb | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/git/storage/forked_storage_check.rb b/lib/gitlab/git/storage/forked_storage_check.rb index 557a3b0e61d..0e9e7ba596b 100644 --- a/lib/gitlab/git/storage/forked_storage_check.rb +++ b/lib/gitlab/git/storage/forked_storage_check.rb @@ -51,7 +51,7 @@ module Gitlab end def test_script(path) - "testpath=\"$(realpath #{Shellwords.escape(path)})\" && stat $testpath" + "testpath=$(realpath #{Shellwords.escape(path)}) && stat \"$testpath\"" end end end diff --git a/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb b/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb index a9e048b316d..20cee123ede 100644 --- a/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb +++ b/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb @@ -23,5 +23,27 @@ describe Gitlab::Git::Storage::ForkedStorageCheck, skip_database_cleaner: true d expect(described_class.storage_available?(existing_path, 0.5)).to be_falsey end + + describe 'when using paths with spaces' do + let(:test_dir) { Rails.root.join('tmp', 'tests', 'storage_check') } + let(:path_with_spaces) { File.join(test_dir, 'path with spaces') } + + around do |example| + FileUtils.mkdir_p(path_with_spaces) + example.run + FileUtils.rm_r(test_dir) + end + + it 'works for paths with spaces' do + expect(described_class.storage_available?(path_with_spaces)).to be_truthy + end + + it 'works for a realpath with spaces' do + symlink_location = File.join(test_dir, 'a symlink') + FileUtils.ln_s(path_with_spaces, symlink_location) + + expect(described_class.storage_available?(symlink_location)).to be_truthy + end + end end end -- cgit v1.2.1 From 3899d07f9eb5ffa2369195c800be18e297f67613 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 3 Aug 2017 12:16:45 +0200 Subject: Move hostname to Gitlab::Environment --- config/initializers/1_settings.rb | 1 - lib/gitlab/environment.rb | 7 +++++++ lib/gitlab/git/storage/circuit_breaker.rb | 2 +- spec/features/admin/admin_health_check_spec.rb | 2 +- spec/lib/gitlab/git/storage/circuit_breaker_spec.rb | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 lib/gitlab/environment.rb diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 017537f30be..7a43bf939ea 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -222,7 +222,6 @@ Settings.gitlab['default_branch_protection'] ||= 2 Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil? Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost' Settings.gitlab['ssh_host'] ||= Settings.gitlab.host -Settings.gitlab['hostname'] ||= ENV['HOSTNAME'] || Socket.gethostname Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? Settings.gitlab['port'] ||= ENV['GITLAB_PORT'] || (Settings.gitlab.https ? 443 : 80) Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || '' diff --git a/lib/gitlab/environment.rb b/lib/gitlab/environment.rb new file mode 100644 index 00000000000..5e0dd6e7859 --- /dev/null +++ b/lib/gitlab/environment.rb @@ -0,0 +1,7 @@ +module Gitlab + module Environment + def self.hostname + @hostname ||= ENV['HOSTNAME'] || Socket.gethostname + end + end +end diff --git a/lib/gitlab/git/storage/circuit_breaker.rb b/lib/gitlab/git/storage/circuit_breaker.rb index c722771e0d5..7b040a05c42 100644 --- a/lib/gitlab/git/storage/circuit_breaker.rb +++ b/lib/gitlab/git/storage/circuit_breaker.rb @@ -31,7 +31,7 @@ module Gitlab cached_circuitbreakers[storage] end - def initialize(storage, hostname = Gitlab.config.gitlab.hostname) + def initialize(storage, hostname = Gitlab::Environment.hostname) @storage = storage @hostname = hostname diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb index 634dfd53f71..37fd3e171eb 100644 --- a/spec/features/admin/admin_health_check_spec.rb +++ b/spec/features/admin/admin_health_check_spec.rb @@ -64,7 +64,7 @@ feature "Admin Health Check", feature: true, broken_storage: true do end it 'shows storage failure information' do - hostname = Gitlab.config.gitlab.hostname + hostname = Gitlab::Environment.hostname expect(page).to have_content('broken: failed storage access attempt on host:') expect(page).to have_content("#{hostname}: 1 of 10 failures.") diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb index 479e53230bc..6fab224ad0d 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(:circuit_breaker) { described_class.new('default') } - let(:hostname) { Gitlab.config.gitlab.hostname } + let(:hostname) { Gitlab::Environment.hostname } let(:cache_key) { "storage_accessible:default:#{hostname}" } def value_from_redis(name) -- cgit v1.2.1 From 022c38e63e202e9da2608d020face6c108e47313 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 3 Aug 2017 13:28:43 +0200 Subject: Use `keys` instead of `scan_each` --- lib/gitlab/git/storage/circuit_breaker.rb | 2 +- lib/gitlab/git/storage/health.rb | 28 +++++++++------------------- spec/lib/gitlab/git/storage/health_spec.rb | 4 +++- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/lib/gitlab/git/storage/circuit_breaker.rb b/lib/gitlab/git/storage/circuit_breaker.rb index 7b040a05c42..237b4598730 100644 --- a/lib/gitlab/git/storage/circuit_breaker.rb +++ b/lib/gitlab/git/storage/circuit_breaker.rb @@ -14,7 +14,7 @@ module Gitlab pattern = "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}*" Gitlab::Git::Storage.redis.with do |redis| - all_storage_keys = redis.scan_each(match: pattern).to_a + all_storage_keys = redis.keys(pattern) redis.del(*all_storage_keys) unless all_storage_keys.empty? end diff --git a/lib/gitlab/git/storage/health.rb b/lib/gitlab/git/storage/health.rb index 0a28052fd81..2d723147f4f 100644 --- a/lib/gitlab/git/storage/health.rb +++ b/lib/gitlab/git/storage/health.rb @@ -14,17 +14,7 @@ module Gitlab Gitlab::Git::Storage.redis.with do |redis| keys_per_storage = all_keys_for_storages(storage_names, redis) - - # We need to make sure all keys are actually loaded as an array. - # Otherwise when using the enumerator of the `scan_each` within a - # second pipeline, it will be assumed unloaded, wich would make the - # result unusable inside the pipeline. - loaded_keys_per_storage = keys_per_storage.inject({}) do |loaded_keys, (storage_name, keys)| - loaded_keys[storage_name] = keys.to_a - loaded_keys - end - - results_per_storage = load_for_keys(loaded_keys_per_storage, redis) + results_per_storage = load_for_keys(keys_per_storage, redis) end results_per_storage.map do |name, info| @@ -34,13 +24,13 @@ module Gitlab end def self.all_keys_for_storages(storage_names, redis) - keys_per_storage = nil + keys_per_storage = {} redis.pipelined do - keys_per_storage = storage_names.inject({}) do |result, storage_name| - key = pattern_for_storage(storage_name) + storage_names.each do |storage_name| + pattern = pattern_for_storage(storage_name) - result.merge(storage_name => redis.scan_each(match: key)) + keys_per_storage[storage_name] = redis.keys(pattern) end end @@ -48,15 +38,15 @@ module Gitlab end def self.load_for_keys(keys_per_storage, redis) - info_for_keys = nil + info_for_keys = {} redis.pipelined do - info_for_keys = keys_per_storage.inject({}) do |result, (storage_name, keys)| - info_for_storage = keys.map do |key| + keys_per_storage.each do |storage_name, keys_future| + info_for_storage = keys_future.value.map do |key| { name: key, failure_count: redis.hget(key, :failure_count) } end - result.merge(storage_name => info_for_storage) + info_for_keys[storage_name] = info_for_storage end end diff --git a/spec/lib/gitlab/git/storage/health_spec.rb b/spec/lib/gitlab/git/storage/health_spec.rb index 28b74a0581f..2d3af387971 100644 --- a/spec/lib/gitlab/git/storage/health_spec.rb +++ b/spec/lib/gitlab/git/storage/health_spec.rb @@ -23,7 +23,9 @@ describe Gitlab::Git::Storage::Health, clean_gitlab_redis_shared_state: true, br describe '.load_for_keys' do let(:subject) do results = Gitlab::Git::Storage.redis.with do |redis| - described_class.load_for_keys({ 'broken' => [host1_key] }, redis) + fake_future = double + allow(fake_future).to receive(:value).and_return([host1_key]) + described_class.load_for_keys({ 'broken' => fake_future }, redis) end # Make sure the `Redis#future is loaded -- cgit v1.2.1 From 0dd4c306ca953e6d09a862f9641469340f8e822f Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 3 Aug 2017 15:24:43 +0200 Subject: Only track accessibility once --- lib/gitlab/git/storage/circuit_breaker.rb | 22 +++++++++----- .../lib/gitlab/git/storage/circuit_breaker_spec.rb | 35 ++++++++++++++++++++-- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/lib/gitlab/git/storage/circuit_breaker.rb b/lib/gitlab/git/storage/circuit_breaker.rb index 237b4598730..c3ef82d4e98 100644 --- a/lib/gitlab/git/storage/circuit_breaker.rb +++ b/lib/gitlab/git/storage/circuit_breaker.rb @@ -46,10 +46,6 @@ module Gitlab def perform return yield unless Feature.enabled?('git_storage_circuit_breaker') - if circuit_broken? - raise Gitlab::Git::Storage::CircuitOpen.new("Circuit for #{storage} open", failure_wait_time) - end - check_storage_accessible! yield @@ -70,14 +66,24 @@ module Gitlab # When the storage appears not available, and the memoized value is `false` # we might want to try again. def storage_available? - @storage_available ||= Gitlab::Git::Storage::ForkedStorageCheck.storage_available?(storage_path, storage_timeout) - end + return @storage_available if @storage_available - def check_storage_accessible! - if storage_available? + if @storage_available = Gitlab::Git::Storage::ForkedStorageCheck + .storage_available?(storage_path, storage_timeout) track_storage_accessible else track_storage_inaccessible + end + + @storage_available + end + + def check_storage_accessible! + if circuit_broken? + raise Gitlab::Git::Storage::CircuitOpen.new("Circuit for #{storage} is broken", failure_wait_time) + end + + unless storage_available? raise Gitlab::Git::Storage::Inaccessible.new("#{storage} not accessible", failure_wait_time) end end diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb index 6fab224ad0d..b2886628601 100644 --- a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb +++ b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb @@ -120,19 +120,48 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: end end - describe '#check_storage_accessible!' do + describe "storage_available?" do context 'when the storage is available' do it 'tracks that the storage was accessible an raises the error' do expect(circuit_breaker).to receive(:track_storage_accessible) - circuit_breaker.check_storage_accessible! + circuit_breaker.storage_available? + end + + it 'only performs the check once' do + expect(Gitlab::Git::Storage::ForkedStorageCheck) + .to receive(:storage_available?).once.and_call_original + + 2.times { circuit_breaker.storage_available? } + end + end + + context 'when storage is not available' do + let(:circuit_breaker) { described_class.new('broken') } + + it 'tracks that the storage was inaccessible' do + expect(circuit_breaker).to receive(:track_storage_inaccessible) + + circuit_breaker.storage_available? + end + end + end + + describe '#check_storage_accessible!' do + it 'raises an exception with retry time when the circuit is open' do + allow(circuit_breaker).to receive(:circuit_broken?).and_return(true) + + expect { circuit_breaker.check_storage_accessible! } + .to raise_error do |exception| + expect(exception).to be_kind_of(Gitlab::Git::Storage::CircuitOpen) + expect(exception.retry_after).to eq(30) end end context 'when the storage is not available' do let(:circuit_breaker) { described_class.new('broken') } - it 'tracks that the storage was unavailable and raises an error with retry time' do + it 'raises an error' do expect(circuit_breaker).to receive(:track_storage_inaccessible) expect { circuit_breaker.check_storage_accessible! } -- cgit v1.2.1 From 3a2aaed881f8925b08acd4204f3e130939c73946 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 3 Aug 2017 15:50:16 +0200 Subject: Use a Struct to keep track of failure info --- lib/gitlab/git/storage/circuit_breaker.rb | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/gitlab/git/storage/circuit_breaker.rb b/lib/gitlab/git/storage/circuit_breaker.rb index c3ef82d4e98..9ea9367d4b7 100644 --- a/lib/gitlab/git/storage/circuit_breaker.rb +++ b/lib/gitlab/git/storage/circuit_breaker.rb @@ -2,6 +2,8 @@ module Gitlab module Git module Storage class CircuitBreaker + FailureInfo = Struct.new(:last_failure, :failure_count) + attr_reader :storage, :hostname, :storage_path, @@ -10,6 +12,8 @@ module Gitlab :failure_reset_time, :storage_timeout + delegate :last_failure, :failure_count, to: :failure_info + def self.reset_all! pattern = "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}*" @@ -93,7 +97,7 @@ module Gitlab end def track_storage_inaccessible - @failure_info = [Time.now, failure_count + 1] + @failure_info = FailureInfo.new(Time.now, failure_count + 1) Gitlab::Git::Storage.redis.with do |redis| redis.pipelined do @@ -107,7 +111,7 @@ module Gitlab def track_storage_accessible return if no_failures? - @failure_info = [nil, 0] + @failure_info = FailureInfo.new(nil, 0) Gitlab::Git::Storage.redis.with do |redis| redis.pipelined do @@ -117,14 +121,6 @@ module Gitlab end end - def last_failure - failure_info.first - end - - def failure_count - failure_info.last - end - def failure_info @failure_info ||= get_failure_info end @@ -136,7 +132,7 @@ module Gitlab last_failure = Time.at(last_failure.to_i) if last_failure.present? - [last_failure, failure_count.to_i] + FailureInfo.new(last_failure, failure_count.to_i) end def cache_key -- cgit v1.2.1 From 4b34720c0ca8b5459cc56a4e52e11e213ab6ae9a Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 3 Aug 2017 16:22:52 +0200 Subject: Use ruby's `File.stat` to check storage availability --- lib/gitlab/git/storage/forked_storage_check.rb | 34 ++++++++++------------ .../git/storage/forked_storage_check_spec.rb | 15 ++++++++-- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/lib/gitlab/git/storage/forked_storage_check.rb b/lib/gitlab/git/storage/forked_storage_check.rb index 0e9e7ba596b..91d8241f17b 100644 --- a/lib/gitlab/git/storage/forked_storage_check.rb +++ b/lib/gitlab/git/storage/forked_storage_check.rb @@ -11,7 +11,7 @@ module Gitlab end def timeout_check(path, timeout_seconds) - filesystem_check_pid = check_filesystem_in_fork(path) + filesystem_check_pid = check_filesystem_in_process(path) deadline = timeout_seconds.seconds.from_now.utc wait_time = 0.01 @@ -31,27 +31,23 @@ module Gitlab status end - # This call forks out into a process, that process will then be replaced - # With an `exec` call, since we fork out into a shell, we can create a - # child process without needing an ActiveRecord-connection. + # This will spawn a new 2 processes to do the check: + # The outer child (waiter) will spawn another child process (stater). # - # Inside the shell, we use `& wait` to fork another child. We do this - # to prevent leaving a zombie process when the parent gets killed by the - # timeout. - # - # https://stackoverflow.com/questions/27892975/what-causes-activerecord-breaking-postgres-connection-after-forking - # https://stackoverflow.com/questions/22012943/activerecordstatementinvalid-runtimeerror-the-connection-cannot-be-reused-in - def check_filesystem_in_fork(path) - fork do - STDOUT.reopen('/dev/null') - STDERR.reopen('/dev/null') - - exec("(#{test_script(path)}) & wait %1") - end + # The stater is the process is performing the actual filesystem check + # the check might hang if the filesystem is acting up. + # In this case we will send a `KILL` to the waiter, which will still + # be responsive while the stater is hanging. + def check_filesystem_in_process(path) + spawn('ruby', '-e', ruby_check, path, [:out, :err] => '/dev/null') end - def test_script(path) - "testpath=$(realpath #{Shellwords.escape(path)}) && stat \"$testpath\"" + def ruby_check + <<~RUBY_FILESYSTEM_CHECK + inner_pid = fork { File.stat(ARGV.first) } + Process.waitpid(inner_pid) + exit $?.exitstatus + RUBY_FILESYSTEM_CHECK end end end diff --git a/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb b/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb index 20cee123ede..12366151f44 100644 --- a/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb +++ b/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb @@ -17,11 +17,20 @@ describe Gitlab::Git::Storage::ForkedStorageCheck, skip_database_cleaner: true d end it 'returns false when the check takes to long' do - allow(described_class).to receive(:check_filesystem_in_fork) do - fork { sleep 10 } + # We're forking a process here that takes too long + # It will be killed it's parent process will be killed by it's parent + # and waited for inside `Gitlab::Git::Storage::ForkedStorageCheck.timeout_check` + allow(described_class).to receive(:check_filesystem_in_process) do + Process.spawn("sleep 10") end + result = true - expect(described_class.storage_available?(existing_path, 0.5)).to be_falsey + runtime = Benchmark.realtime do + result = described_class.storage_available?(existing_path, 0.5) + end + + expect(result).to be_falsey + expect(runtime).to be < 1.0 end describe 'when using paths with spaces' do -- cgit v1.2.1 From a1be8bb8afbe77814888a6c3cba6534b950f51a5 Mon Sep 17 00:00:00 2001 From: winh Date: Thu, 3 Aug 2017 14:30:35 +0200 Subject: Make new dropdown dividers full width --- app/assets/stylesheets/framework/dropdowns.scss | 4 ++++ app/assets/stylesheets/new_nav.scss | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index bd4bd541c3a..02e0ba74158 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -728,6 +728,10 @@ @mixin new-style-dropdown { .dropdown-menu, .dropdown-menu-nav { + .divider { + margin: 6px 0; + } + li { padding: 0 1px; diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss index 1c4a84de7ec..795ee91af8b 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -312,6 +312,10 @@ header.navbar-gitlab-new { // TODO: fallback to global style .dropdown-menu { + .divider { + margin: 6px 0; + } + li { padding: 0 1px; -- cgit v1.2.1 From b7547ade672fdcb9315ae9821c1eca195d87022a Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 4 Aug 2017 21:45:14 +0800 Subject: Backport to CE for: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2483 --- .../javascripts/boards/components/modal/index.js | 4 +- .../pipelines/components/graph/graph_component.vue | 4 +- .../vue_merge_request_widget/dependencies.js | 2 +- config/application.rb | 4 +- doc/development/fe_guide/style_guide_js.md | 54 ++++++++++++++-------- spec/support/api/schema_matcher.rb | 18 ++++---- 6 files changed, 53 insertions(+), 33 deletions(-) diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js index 1d36519c75c..96af69e7a36 100644 --- a/app/assets/javascripts/boards/components/modal/index.js +++ b/app/assets/javascripts/boards/components/modal/index.js @@ -1,8 +1,8 @@ /* global ListIssue */ import Vue from 'vue'; -import queryData from '../../utils/query_data'; -import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; +import queryData from '~/boards/utils/query_data'; +import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import './header'; import './list'; import './footer'; diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 77cbaeb43ef..66bc1d1979c 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,7 +1,7 @@