diff options
105 files changed, 939 insertions, 270 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bb1c84ab918..349ea49fe8f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -322,69 +322,69 @@ setup-test-env: paths: - tmp/tests -rspec-pg 0 26: *rspec-metadata-pg -rspec-pg 1 26: *rspec-metadata-pg -rspec-pg 2 26: *rspec-metadata-pg -rspec-pg 3 26: *rspec-metadata-pg -rspec-pg 4 26: *rspec-metadata-pg -rspec-pg 5 26: *rspec-metadata-pg -rspec-pg 6 26: *rspec-metadata-pg -rspec-pg 7 26: *rspec-metadata-pg -rspec-pg 8 26: *rspec-metadata-pg -rspec-pg 9 26: *rspec-metadata-pg -rspec-pg 10 26: *rspec-metadata-pg -rspec-pg 11 26: *rspec-metadata-pg -rspec-pg 12 26: *rspec-metadata-pg -rspec-pg 13 26: *rspec-metadata-pg -rspec-pg 14 26: *rspec-metadata-pg -rspec-pg 15 26: *rspec-metadata-pg -rspec-pg 16 26: *rspec-metadata-pg -rspec-pg 17 26: *rspec-metadata-pg -rspec-pg 18 26: *rspec-metadata-pg -rspec-pg 19 26: *rspec-metadata-pg -rspec-pg 20 26: *rspec-metadata-pg -rspec-pg 21 26: *rspec-metadata-pg -rspec-pg 22 26: *rspec-metadata-pg -rspec-pg 23 26: *rspec-metadata-pg -rspec-pg 24 26: *rspec-metadata-pg -rspec-pg 25 26: *rspec-metadata-pg - -rspec-mysql 0 26: *rspec-metadata-mysql -rspec-mysql 1 26: *rspec-metadata-mysql -rspec-mysql 2 26: *rspec-metadata-mysql -rspec-mysql 3 26: *rspec-metadata-mysql -rspec-mysql 4 26: *rspec-metadata-mysql -rspec-mysql 5 26: *rspec-metadata-mysql -rspec-mysql 6 26: *rspec-metadata-mysql -rspec-mysql 7 26: *rspec-metadata-mysql -rspec-mysql 8 26: *rspec-metadata-mysql -rspec-mysql 9 26: *rspec-metadata-mysql -rspec-mysql 10 26: *rspec-metadata-mysql -rspec-mysql 11 26: *rspec-metadata-mysql -rspec-mysql 12 26: *rspec-metadata-mysql -rspec-mysql 13 26: *rspec-metadata-mysql -rspec-mysql 14 26: *rspec-metadata-mysql -rspec-mysql 15 26: *rspec-metadata-mysql -rspec-mysql 16 26: *rspec-metadata-mysql -rspec-mysql 17 26: *rspec-metadata-mysql -rspec-mysql 18 26: *rspec-metadata-mysql -rspec-mysql 19 26: *rspec-metadata-mysql -rspec-mysql 20 26: *rspec-metadata-mysql -rspec-mysql 21 26: *rspec-metadata-mysql -rspec-mysql 22 26: *rspec-metadata-mysql -rspec-mysql 23 26: *rspec-metadata-mysql -rspec-mysql 24 26: *rspec-metadata-mysql -rspec-mysql 25 26: *rspec-metadata-mysql - -spinach-pg 0 4: *spinach-metadata-pg -spinach-pg 1 4: *spinach-metadata-pg -spinach-pg 2 4: *spinach-metadata-pg -spinach-pg 3 4: *spinach-metadata-pg - -spinach-mysql 0 4: *spinach-metadata-mysql -spinach-mysql 1 4: *spinach-metadata-mysql -spinach-mysql 2 4: *spinach-metadata-mysql -spinach-mysql 3 4: *spinach-metadata-mysql +rspec-pg 0 27: *rspec-metadata-pg +rspec-pg 1 27: *rspec-metadata-pg +rspec-pg 2 27: *rspec-metadata-pg +rspec-pg 3 27: *rspec-metadata-pg +rspec-pg 4 27: *rspec-metadata-pg +rspec-pg 5 27: *rspec-metadata-pg +rspec-pg 6 27: *rspec-metadata-pg +rspec-pg 7 27: *rspec-metadata-pg +rspec-pg 8 27: *rspec-metadata-pg +rspec-pg 9 27: *rspec-metadata-pg +rspec-pg 10 27: *rspec-metadata-pg +rspec-pg 11 27: *rspec-metadata-pg +rspec-pg 12 27: *rspec-metadata-pg +rspec-pg 13 27: *rspec-metadata-pg +rspec-pg 14 27: *rspec-metadata-pg +rspec-pg 15 27: *rspec-metadata-pg +rspec-pg 16 27: *rspec-metadata-pg +rspec-pg 17 27: *rspec-metadata-pg +rspec-pg 18 27: *rspec-metadata-pg +rspec-pg 19 27: *rspec-metadata-pg +rspec-pg 20 27: *rspec-metadata-pg +rspec-pg 21 27: *rspec-metadata-pg +rspec-pg 22 27: *rspec-metadata-pg +rspec-pg 23 27: *rspec-metadata-pg +rspec-pg 24 27: *rspec-metadata-pg +rspec-pg 25 27: *rspec-metadata-pg +rspec-pg 26 27: *rspec-metadata-pg + +rspec-mysql 0 27: *rspec-metadata-mysql +rspec-mysql 1 27: *rspec-metadata-mysql +rspec-mysql 2 27: *rspec-metadata-mysql +rspec-mysql 3 27: *rspec-metadata-mysql +rspec-mysql 4 27: *rspec-metadata-mysql +rspec-mysql 5 27: *rspec-metadata-mysql +rspec-mysql 6 27: *rspec-metadata-mysql +rspec-mysql 7 27: *rspec-metadata-mysql +rspec-mysql 8 27: *rspec-metadata-mysql +rspec-mysql 9 27: *rspec-metadata-mysql +rspec-mysql 10 27: *rspec-metadata-mysql +rspec-mysql 11 27: *rspec-metadata-mysql +rspec-mysql 12 27: *rspec-metadata-mysql +rspec-mysql 13 27: *rspec-metadata-mysql +rspec-mysql 14 27: *rspec-metadata-mysql +rspec-mysql 15 27: *rspec-metadata-mysql +rspec-mysql 16 27: *rspec-metadata-mysql +rspec-mysql 17 27: *rspec-metadata-mysql +rspec-mysql 18 27: *rspec-metadata-mysql +rspec-mysql 19 27: *rspec-metadata-mysql +rspec-mysql 20 27: *rspec-metadata-mysql +rspec-mysql 21 27: *rspec-metadata-mysql +rspec-mysql 22 27: *rspec-metadata-mysql +rspec-mysql 23 27: *rspec-metadata-mysql +rspec-mysql 24 27: *rspec-metadata-mysql +rspec-mysql 25 27: *rspec-metadata-mysql +rspec-mysql 26 27: *rspec-metadata-mysql + +spinach-pg 0 3: *spinach-metadata-pg +spinach-pg 1 3: *spinach-metadata-pg +spinach-pg 2 3: *spinach-metadata-pg + +spinach-mysql 0 3: *spinach-metadata-mysql +spinach-mysql 1 3: *spinach-metadata-mysql +spinach-mysql 2 3: *spinach-metadata-mysql # Static analysis jobs .ruby-static-analysis: &ruby-static-analysis diff --git a/CHANGELOG.md b/CHANGELOG.md index 77f23981c84..248c85304a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 10.4.1 (2018-01-24) + +### Fixed (4 changes) + +- Ensure that users can reclaim a namespace or project path that is blocked by an orphaned route. !16242 +- Correctly escape UTF-8 path elements for uploads. !16560 +- Fix issues when rendering groups and their children. !16584 +- Fix bug in which projects with forks could not change visibility settings from Private to Public. !16595 + +### Performance (2 changes) + +- rework indexes on redirect_routes. +- Remove unecessary query from labels filter. + + ## 10.4.0 (2018-01-22) ### Security (8 changes, 1 of them is from the community) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 7375dee5f49..31b648bd6fa 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.72.0 +0.73.0 @@ -406,7 +406,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.74.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.76.0', require: 'gitaly' gem 'toml-rb', '~> 0.3.15', require: false diff --git a/Gemfile.lock b/Gemfile.lock index d69c532b309..1cbeab8d6b5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -285,7 +285,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.74.0) + gitaly-proto (0.76.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -1056,7 +1056,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly-proto (~> 0.74.0) + gitaly-proto (~> 0.76.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.6.2) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index d5659be28a4..89fc002d4ce 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -1,13 +1,8 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ -import notificationsDropdown from './notifications_dropdown'; -import LineHighlighter from './line_highlighter'; import MergeRequest from './merge_request'; import Flash from './flash'; -import BlobViewer from './blob/viewer/index'; import GfmAutoComplete from './gfm_auto_complete'; -import Star from './star'; import ZenMode from './zen_mode'; -import PerformanceBar from './performance_bar'; import initNotes from './init_notes'; import initIssuableSidebar from './init_issuable_sidebar'; import { convertPermissionToBoolean } from './lib/utils/common_utils'; @@ -538,6 +533,11 @@ import SearchAutocomplete from './search_autocomplete'; .then(callDefault) .catch(fail); break; + case 'dashboard:groups:index': + import('./pages/dashboard/groups/index') + .then(callDefault) + .catch(fail); + break; } switch (path[0]) { case 'sessions': @@ -621,23 +621,12 @@ import SearchAutocomplete from './search_autocomplete'; .then(callDefault) .catch(fail); break; - case 'show': - new Star(); - notificationsDropdown(); - break; case 'wikis': import('./pages/projects/wikis') .then(callDefault) .catch(fail); shortcut_handler = true; break; - case 'snippets': - if (path[2] === 'show') { - new ZenMode(); - new LineHighlighter(); - new BlobViewer(); - } - break; } break; } @@ -647,7 +636,9 @@ import SearchAutocomplete from './search_autocomplete'; } if (document.querySelector('#peek')) { - new PerformanceBar({ container: '#peek' }); + import('./performance_bar') + .then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap + .catch(fail); } }; diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 8b850765a1b..57eaac72906 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -10,7 +10,7 @@ import groupItemComponent from './components/group_item.vue'; Vue.use(Translate); -document.addEventListener('DOMContentLoaded', () => { +export default () => { const el = document.getElementById('js-groups-tree'); // Don't do anything if element doesn't exist (No groups) @@ -71,4 +71,4 @@ document.addEventListener('DOMContentLoaded', () => { }); }, }); -}); +}; diff --git a/app/assets/javascripts/groups/service/groups_service.js b/app/assets/javascripts/groups/service/groups_service.js index 639410384c2..b79ba291463 100644 --- a/app/assets/javascripts/groups/service/groups_service.js +++ b/app/assets/javascripts/groups/service/groups_service.js @@ -1,7 +1,5 @@ import Vue from 'vue'; -import VueResource from 'vue-resource'; - -Vue.use(VueResource); +import '../../vue_shared/vue_resource_interceptor'; export default class GroupsService { constructor(endpoint) { diff --git a/app/assets/javascripts/pages/dashboard/groups/index/index.js b/app/assets/javascripts/pages/dashboard/groups/index/index.js new file mode 100644 index 00000000000..8a2aae706c0 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/groups/index/index.js @@ -0,0 +1,5 @@ +import initGroupsList from '../../../../groups'; + +export default () => { + initGroupsList(); +}; diff --git a/app/assets/javascripts/pages/explore/groups/index.js b/app/assets/javascripts/pages/explore/groups/index.js index 859b073f1cb..e59c38b8bc4 100644 --- a/app/assets/javascripts/pages/explore/groups/index.js +++ b/app/assets/javascripts/pages/explore/groups/index.js @@ -1,8 +1,10 @@ import GroupsList from '~/groups_list'; import Landing from '~/landing'; +import initGroupsList from '../../../groups'; export default function () { new GroupsList(); // eslint-disable-line no-new + initGroupsList(); const landingElement = document.querySelector('.js-explore-groups-landing'); if (!landingElement) return; const exploreGroupsLanding = new Landing( diff --git a/app/assets/javascripts/pages/groups/show/index.js b/app/assets/javascripts/pages/groups/show/index.js index 45e11b64306..6ed0f010f15 100644 --- a/app/assets/javascripts/pages/groups/show/index.js +++ b/app/assets/javascripts/pages/groups/show/index.js @@ -5,6 +5,7 @@ import notificationsDropdown from '~/notifications_dropdown'; import NotificationsForm from '~/notifications_form'; import ProjectsList from '~/projects_list'; import ShortcutsNavigation from '~/shortcuts_navigation'; +import initGroupsList from '../../../groups'; export default () => { const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup'); @@ -16,4 +17,6 @@ export default () => { if (newGroupChildWrapper) { new NewGroupChild(newGroupChildWrapper); } + + initGroupsList(); }; diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index 92dc1e59651..55154cdddcb 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -5,8 +5,12 @@ import TreeView from '~/tree'; import BlobViewer from '~/blob/viewer/index'; import Activities from '~/activities'; import { ajaxGet } from '~/lib/utils/common_utils'; +import Star from '../../../star'; +import notificationsDropdown from '../../../notifications_dropdown'; export default () => { + new Star(); // eslint-disable-line no-new + notificationsDropdown(); new ShortcutsNavigation(); // eslint-disable-line no-new new NotificationsForm(); // eslint-disable-line no-new new UserCallout({ // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/snippets/show/index.js b/app/assets/javascripts/pages/projects/snippets/show/index.js index d8cf5184f8f..a3cf75c385b 100644 --- a/app/assets/javascripts/pages/projects/snippets/show/index.js +++ b/app/assets/javascripts/pages/projects/snippets/show/index.js @@ -1,7 +1,11 @@ import initNotes from '~/init_notes'; import ZenMode from '~/zen_mode'; +import LineHighlighter from '../../../../line_highlighter'; +import BlobViewer from '../../../../blob/viewer'; export default function () { + new LineHighlighter(); // eslint-disable-line no-new + new BlobViewer(); // eslint-disable-line no-new initNotes(); new ZenMode(); // eslint-disable-line no-new } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ee21d81f23e..95ad38d9230 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -147,6 +147,8 @@ class ApplicationController < ActionController::Base format.html do render file: Rails.root.join("public", "404"), layout: false, status: "404" end + # Prevent the Rails CSRF protector from thinking a missing .js file is a JavaScript file + format.js { render json: '', status: :not_found, content_type: 'application/json' } format.any { head :not_found } end end diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb index a931b456a93..16abf7bab7e 100644 --- a/app/controllers/health_controller.rb +++ b/app/controllers/health_controller.rb @@ -8,7 +8,8 @@ class HealthController < ActionController::Base Gitlab::HealthChecks::Redis::CacheCheck, Gitlab::HealthChecks::Redis::QueuesCheck, Gitlab::HealthChecks::Redis::SharedStateCheck, - Gitlab::HealthChecks::FsShardsCheck + Gitlab::HealthChecks::FsShardsCheck, + Gitlab::HealthChecks::GitalyCheck ].freeze def readiness diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index d2275139c42..98831f5be4a 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -15,6 +15,7 @@ # label_name: string # sort: string # my_reaction_emoji: string +# public_only: boolean # class IssuesFinder < IssuableFinder CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER @@ -40,7 +41,15 @@ class IssuesFinder < IssuableFinder private def init_collection - with_confidentiality_access_check + if public_only? + Issue.public_only + else + with_confidentiality_access_check + end + end + + def public_only? + params.fetch(:public_only, false) end def user_can_see_all_confidential_issues? diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 0f9ac958f95..e6a6496871a 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -170,4 +170,8 @@ module SearchHelper # Truncato's filtered_tags and filtered_attributes are not quite the same sanitize(html, tags: %w(a p ol ul li pre code)) end + + def limited_count(count, limit = 1000) + count > limit ? "#{limit}+" : count + end end diff --git a/app/models/project.rb b/app/models/project.rb index 0570bbc8ee3..e19873f64ce 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -971,9 +971,9 @@ class Project < ActiveRecord::Base hooks.hooks_for(hooks_scope).each do |hook| hook.async_execute(data, hooks_scope.to_s) end - end - SystemHooksService.new.execute_hooks(data, hooks_scope) + SystemHooksService.new.execute_hooks(data, hooks_scope) + end end def execute_services(data, hooks_scope = :push_hooks) diff --git a/app/models/repository.rb b/app/models/repository.rb index 73c4899cb9b..824e18bec78 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -20,6 +20,7 @@ class Repository attr_accessor :full_path, :disk_path, :project, :is_wiki delegate :ref_name_for_sha, to: :raw_repository + delegate :bundle_to_disk, to: :raw_repository CreateTreeError = Class.new(StandardError) diff --git a/app/views/dashboard/groups/_groups.html.haml b/app/views/dashboard/groups/_groups.html.haml index 601b6a8b1a7..db856ef7d7b 100644 --- a/app/views/dashboard/groups/_groups.html.haml +++ b/app/views/dashboard/groups/_groups.html.haml @@ -1,2 +1,4 @@ .js-groups-list-holder #js-groups-tree{ data: { hide_projects: 'true', endpoint: dashboard_groups_path(format: :json), path: dashboard_groups_path, form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } } + .loading-container.text-center + = icon('spinner spin 2x', class: 'loading-animation prepend-top-20') diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 25bf08c6c12..50f39f93283 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -3,9 +3,6 @@ - header_title "Groups", dashboard_groups_path = render 'dashboard/groups_head' -= webpack_bundle_tag 'common_vue' -= webpack_bundle_tag 'groups' - - if params[:filter].blank? && @groups.empty? = render 'shared/groups/empty_state' - else diff --git a/app/views/explore/groups/_groups.html.haml b/app/views/explore/groups/_groups.html.haml index 91149498248..ff57b39e947 100644 --- a/app/views/explore/groups/_groups.html.haml +++ b/app/views/explore/groups/_groups.html.haml @@ -1,2 +1,4 @@ .js-groups-list-holder #js-groups-tree{ data: { hide_projects: 'true', endpoint: explore_groups_path(format: :json), path: explore_groups_path, form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } } + .loading-container.text-center + = icon('spinner spin 2x', class: 'loading-animation prepend-top-20') diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index 86abdf547cc..efa8b2706da 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -2,9 +2,6 @@ - page_title "Groups" - header_title "Groups", dashboard_groups_path -= webpack_bundle_tag 'common_vue' -= webpack_bundle_tag 'groups' - - if current_user = render 'dashboard/groups_head' - else diff --git a/app/views/groups/_children.html.haml b/app/views/groups/_children.html.haml index 3afb6b2f849..742b40784d3 100644 --- a/app/views/groups/_children.html.haml +++ b/app/views/groups/_children.html.haml @@ -1,5 +1,4 @@ -= webpack_bundle_tag 'common_vue' -= webpack_bundle_tag 'groups' - .js-groups-list-holder #js-groups-tree{ data: { hide_projects: 'false', group_id: group.id, endpoint: group_children_path(group, format: :json), path: group_path(group), form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } } + .loading-container.text-center + = icon('spinner spin 2x', class: 'loading-animation prepend-top-20') diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 1597621fa78..ea13a5e6d62 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -43,7 +43,6 @@ = webpack_bundle_tag "main" = webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled = webpack_bundle_tag "test" if Rails.env.test? - = webpack_bundle_tag 'performance_bar' if performance_bar_enabled? - if content_for?(:page_specific_javascripts) = yield :page_specific_javascripts diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index d0a380516f9..93407956f56 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -4,6 +4,7 @@ - branch_label = s_('ChangeTypeActionLabel|Revert in branch') - revert_merge_request = _('Revert this merge request') - revert_commit = _('Revert this commit') + - description = s_('ChangeTypeAction|This will create a new commit in order to revert the existing changes.') - title = commit.merged_merge_request(current_user) ? revert_merge_request : revert_commit - when 'cherry-pick' - label = s_('ChangeTypeAction|Cherry-pick') @@ -17,6 +18,8 @@ %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title= title .modal-body + - if description + %p.append-bottom-20= description = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do .form-group.branch = label_tag 'start_branch', branch_label, class: 'control-label' diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index 314d8e9cb25..915e648a5d3 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -57,25 +57,24 @@ Titles and Filenames %span.badge = @search_results.snippet_titles_count - - else %li{ class: active_when(@scope == 'projects') } = link_to search_filter_path(scope: 'projects') do Projects %span.badge - = @search_results.projects_count + = limited_count(@search_results.limited_projects_count) %li{ class: active_when(@scope == 'issues') } = link_to search_filter_path(scope: 'issues') do Issues %span.badge - = @search_results.issues_count + = limited_count(@search_results.limited_issues_count) %li{ class: active_when(@scope == 'merge_requests') } = link_to search_filter_path(scope: 'merge_requests') do Merge requests %span.badge - = @search_results.merge_requests_count + = limited_count(@search_results.limited_merge_requests_count) %li{ class: active_when(@scope == 'milestones') } = link_to search_filter_path(scope: 'milestones') do Milestones %span.badge - = @search_results.milestones_count + = limited_count(@search_results.limited_milestones_count) diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 02133d09cdf..60ef44482f0 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -2,7 +2,8 @@ = render partial: "search/results/empty" - else .row-content-block - = search_entries_info(@search_objects, @scope, @search_term) + - unless @search_objects.is_a?(Kaminari::PaginatableWithoutCount) + = search_entries_info(@search_objects, @scope, @search_term) - unless @show_snippets - if @project in project #{link_to @project.name_with_namespace, [@project.namespace.becomes(Namespace), @project]} @@ -22,4 +23,4 @@ = render partial: "search/results/#{@scope.singularize}", collection: @search_objects - if @scope != 'projects' - = paginate(@search_objects, theme: 'gitlab') + = paginate_collection(@search_objects) diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index d0b9e891b82..cb21f90696f 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -1,5 +1,3 @@ -- content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('group') - parent = @group.parent - group_path = root_url - group_path << parent.full_path + '/' if parent diff --git a/changelogs/unreleased/24035-api-create-application.yml b/changelogs/unreleased/24035-api-create-application.yml new file mode 100644 index 00000000000..c583a020d9d --- /dev/null +++ b/changelogs/unreleased/24035-api-create-application.yml @@ -0,0 +1,4 @@ +--- +title: Add application create API +merge_request: 8160 +author: Nicolas Merelli @PNSalocin diff --git a/changelogs/unreleased/39917-revert-this-merge-request-text.yml b/changelogs/unreleased/39917-revert-this-merge-request-text.yml new file mode 100644 index 00000000000..9a27be1f9c6 --- /dev/null +++ b/changelogs/unreleased/39917-revert-this-merge-request-text.yml @@ -0,0 +1,5 @@ +--- +title: Changes Revert this merge request text +merge_request: 16611 +author: Jacopo Beschi @jacopo-beschi +type: changed diff --git a/changelogs/unreleased/40540-use-limit-for-global-search.yml b/changelogs/unreleased/40540-use-limit-for-global-search.yml new file mode 100644 index 00000000000..7d9612c16df --- /dev/null +++ b/changelogs/unreleased/40540-use-limit-for-global-search.yml @@ -0,0 +1,5 @@ +--- +title: Optimize search queries on the search page by setting a limit for matching records. +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/40612-cannot-change-project-visibility-from-private-even-when-owner.yml b/changelogs/unreleased/40612-cannot-change-project-visibility-from-private-even-when-owner.yml deleted file mode 100644 index 96bb59d303c..00000000000 --- a/changelogs/unreleased/40612-cannot-change-project-visibility-from-private-even-when-owner.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Fix bug in which projects with forks could not change visibility settings from - Private to Public -merge_request: 16595 -author: -type: fixed diff --git a/changelogs/unreleased/42159-utf8-uploads.yml b/changelogs/unreleased/42159-utf8-uploads.yml deleted file mode 100644 index f6eba8f28f5..00000000000 --- a/changelogs/unreleased/42159-utf8-uploads.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Correctly escape UTF-8 path elements for uploads -merge_request: 16560 -author: -type: fixed diff --git a/changelogs/unreleased/bvl-parent-preloading.yml b/changelogs/unreleased/bvl-parent-preloading.yml deleted file mode 100644 index 97c7bbb2a2a..00000000000 --- a/changelogs/unreleased/bvl-parent-preloading.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix issues when rendering groups and their children -merge_request: 16584 -author: -type: fixed diff --git a/changelogs/unreleased/dm-project-system-hooks-in-transaction.yml b/changelogs/unreleased/dm-project-system-hooks-in-transaction.yml new file mode 100644 index 00000000000..f59021c0ec9 --- /dev/null +++ b/changelogs/unreleased/dm-project-system-hooks-in-transaction.yml @@ -0,0 +1,5 @@ +--- +title: Execute system hooks after-commit when executing project hooks +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix-redirect-routes-schema.yml b/changelogs/unreleased/fix-redirect-routes-schema.yml deleted file mode 100644 index ea2b916307a..00000000000 --- a/changelogs/unreleased/fix-redirect-routes-schema.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: rework indexes on redirect_routes -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/issue_37143_2.yml b/changelogs/unreleased/issue_37143_2.yml deleted file mode 100644 index 38125f666b2..00000000000 --- a/changelogs/unreleased/issue_37143_2.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove unecessary query from labels filter -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/mk-delete-orphaned-routes-before-validation.yml b/changelogs/unreleased/mk-delete-orphaned-routes-before-validation.yml deleted file mode 100644 index 55ab318df7d..00000000000 --- a/changelogs/unreleased/mk-delete-orphaned-routes-before-validation.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Ensure that users can reclaim a namespace or project path that is blocked by - an orphaned route -merge_request: 16242 -author: -type: fixed diff --git a/changelogs/unreleased/osw-updates-merge-status-on-api-actions.yml b/changelogs/unreleased/osw-updates-merge-status-on-api-actions.yml new file mode 100644 index 00000000000..3854985e576 --- /dev/null +++ b/changelogs/unreleased/osw-updates-merge-status-on-api-actions.yml @@ -0,0 +1,5 @@ +--- +title: Return more consistent values for merge_status on MR APIs +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-add-gitaly-health-check.yml b/changelogs/unreleased/sh-add-gitaly-health-check.yml new file mode 100644 index 00000000000..32c4c5362b4 --- /dev/null +++ b/changelogs/unreleased/sh-add-gitaly-health-check.yml @@ -0,0 +1,5 @@ +--- +title: Add a gRPC health check to ensure Gitaly is up +merge_request: +author: +type: added diff --git a/config/webpack.config.js b/config/webpack.config.js index 229db11acb2..783677b5b8d 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -44,9 +44,6 @@ var config = { graphs: './graphs/graphs_bundle.js', graphs_charts: './graphs/graphs_charts.js', graphs_show: './graphs/graphs_show.js', - group: './group.js', - groups: './groups/index.js', - groups_list: './groups_list.js', help: './help/help.js', how_to_merge: './how_to_merge.js', issue_show: './issue_show/index.js', @@ -85,7 +82,6 @@ var config = { test: './test.js', two_factor_auth: './two_factor_auth.js', users: './users/index.js', - performance_bar: './performance_bar.js', webpack_runtime: './webpack.js', }, @@ -119,9 +115,9 @@ var config = { { test: /\_worker\.js$/, use: [ - { + { loader: 'worker-loader', - options: { + options: { inline: true } }, diff --git a/db/migrate/20180115201419_add_index_updated_at_to_issues.rb b/db/migrate/20180115201419_add_index_updated_at_to_issues.rb new file mode 100644 index 00000000000..a5a48fc97be --- /dev/null +++ b/db/migrate/20180115201419_add_index_updated_at_to_issues.rb @@ -0,0 +1,15 @@ +class AddIndexUpdatedAtToIssues < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :issues, :updated_at + end + + def down + remove_concurrent_index :issues, :updated_at + end +end diff --git a/db/schema.rb b/db/schema.rb index a0901833c3d..4e82a688725 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: 20180113220114) do +ActiveRecord::Schema.define(version: 20180115201419) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -886,6 +886,7 @@ ActiveRecord::Schema.define(version: 20180113220114) do 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"} + add_index "issues", ["updated_at"], name: "index_issues_on_updated_at", using: :btree add_index "issues", ["updated_by_id"], name: "index_issues_on_updated_by_id", where: "(updated_by_id IS NOT NULL)", using: :btree create_table "keys", force: :cascade do |t| diff --git a/doc/api/applications.md b/doc/api/applications.md new file mode 100644 index 00000000000..933867ed0bb --- /dev/null +++ b/doc/api/applications.md @@ -0,0 +1,37 @@ +# Applications API + +> [Introduced][ce-8160] in GitLab 10.5 + +[ce-8160]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8160 + +## Create a application + +Create a application by posting a JSON payload. + +User must be admin to do that. + +Returns `200` if the request succeeds. + +``` +POST /applications +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `name` | string | yes | The name of the application | +| `redirect_uri` | string | yes | The redirect URI of the application | +| `scopes` | string | yes | The scopes of the application | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "name=MyApplication&redirect_uri=http://redirect.uri&scopes=" https://gitlab.example.com/api/v3/applications +``` + +Example response: + +```json +{ + "application_id": "5832fc6e14300a0d962240a8144466eef4ee93ef0d218477e55f11cf12fc3737", + "secret": "ee1dd64b6adc89cf7e2c23099301ccc2c61b441064e9324d963c46902a85ec34", + "callback_url": "http://redirect.uri" +} +``` diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 06f69938aae..598a7515b01 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -110,7 +110,7 @@ future GitLab releases.** | `CI_BUILD_MANUAL` | `CI_JOB_MANUAL` | | `CI_BUILD_TOKEN` | `CI_JOB_TOKEN` | -## `.gitlab-ci.yaml` defined variables +## `.gitlab-ci.yml` defined variables >**Note:** This feature requires GitLab Runner 0.5.0 or higher and GitLab CI 7.14 or higher. diff --git a/features/support/db_cleaner.rb b/features/support/db_cleaner.rb index 8294bb1445f..31c922d23c3 100644 --- a/features/support/db_cleaner.rb +++ b/features/support/db_cleaner.rb @@ -1,6 +1,6 @@ require 'database_cleaner' -DatabaseCleaner[:active_record].strategy = :truncation +DatabaseCleaner[:active_record].strategy = :deletion Spinach.hooks.before_scenario do DatabaseCleaner.start diff --git a/lib/api/api.rb b/lib/api/api.rb index ae161efb358..f3f64244589 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -106,6 +106,7 @@ module API # Keep in alphabetical order mount ::API::AccessRequests + mount ::API::Applications mount ::API::AwardEmoji mount ::API::Boards mount ::API::Branches diff --git a/lib/api/applications.rb b/lib/api/applications.rb new file mode 100644 index 00000000000..b122cdefe4e --- /dev/null +++ b/lib/api/applications.rb @@ -0,0 +1,27 @@ +module API + # External applications API + class Applications < Grape::API + before { authenticated_as_admin! } + + resource :applications do + desc 'Create a new application' do + detail 'This feature was introduced in GitLab 10.5' + success Entities::ApplicationWithSecret + end + params do + requires :name, type: String, desc: 'Application name' + requires :redirect_uri, type: String, desc: 'Application redirect URI' + requires :scopes, type: String, desc: 'Application scopes' + end + post do + application = Doorkeeper::Application.new(declared_params) + + if application.save + present application, with: Entities::ApplicationWithSecret + else + render_validation_error! application + end + end + end + end +end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 3f4b62dc1b2..5b470bd3479 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -507,7 +507,16 @@ module API expose :work_in_progress?, as: :work_in_progress expose :milestone, using: Entities::Milestone expose :merge_when_pipeline_succeeds - expose :merge_status + + # Ideally we should deprecate `MergeRequest#merge_status` exposure and + # use `MergeRequest#mergeable?` instead (boolean). + # See https://gitlab.com/gitlab-org/gitlab-ce/issues/42344 for more + # information. + expose :merge_status do |merge_request| + # In order to avoid having a breaking change for users, we keep returning the + # expected values from MergeRequest#merge_status state machine. + merge_request.mergeable? ? 'can_be_merged' : 'cannot_be_merged' + end expose :diff_head_sha, as: :sha expose :merge_commit_sha expose :user_notes_count @@ -1157,5 +1166,15 @@ module API pages_domain end end + + class Application < Grape::Entity + expose :uid, as: :application_id + expose :redirect_uri, as: :callback_url + end + + # Use with care, this exposes the secret + class ApplicationWithSecret < Application + expose :secret + end end end diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb index 446f804124b..a7f0813bf74 100644 --- a/lib/api/v3/projects.rb +++ b/lib/api/v3/projects.rb @@ -175,7 +175,7 @@ module API end get "/search/:query", requirements: { query: /[^\/]+/ } do search_service = Search::GlobalService.new(current_user, search: params[:query]).execute - projects = search_service.objects('projects', params[:page]) + projects = search_service.objects('projects', params[:page], false) projects = projects.reorder(params[:order_by] => params[:sort]) present paginate(projects), with: ::API::V3::Entities::Project diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index f421bf69e8f..81e46028752 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -34,7 +34,7 @@ module Gitlab def raw(repository, sha) Gitlab::GitalyClient.migrate(:git_blob_raw) do |is_enabled| if is_enabled - Gitlab::GitalyClient::BlobService.new(repository).get_blob(oid: sha, limit: MAX_DATA_DISPLAY_SIZE) + repository.gitaly_blob_client.get_blob(oid: sha, limit: MAX_DATA_DISPLAY_SIZE) else rugged_raw(repository, sha, limit: MAX_DATA_DISPLAY_SIZE) end @@ -70,11 +70,19 @@ module Gitlab # Returns array of Gitlab::Git::Blob # Does not guarantee blob data will be set def batch_lfs_pointers(repository, blob_ids) - blob_ids.lazy - .select { |sha| possible_lfs_blob?(repository, sha) } - .map { |sha| rugged_raw(repository, sha, limit: LFS_POINTER_MAX_SIZE) } - .select(&:lfs_pointer?) - .force + return [] if blob_ids.empty? + + repository.gitaly_migrate(:batch_lfs_pointers) do |is_enabled| + if is_enabled + repository.gitaly_blob_client.batch_lfs_pointers(blob_ids) + else + blob_ids.lazy + .select { |sha| possible_lfs_blob?(repository, sha) } + .map { |sha| rugged_raw(repository, sha, limit: LFS_POINTER_MAX_SIZE) } + .select(&:lfs_pointer?) + .force + end + end end def binary?(data) @@ -258,7 +266,7 @@ module Gitlab Gitlab::GitalyClient.migrate(:git_blob_load_all_data) do |is_enabled| @data = begin if is_enabled - Gitlab::GitalyClient::BlobService.new(repository).get_blob(oid: id, limit: -1).data + repository.gitaly_blob_client.get_blob(oid: id, limit: -1).data else repository.lookup(id).content end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index d666362de0d..d7c712e75c5 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -1268,6 +1268,18 @@ module Gitlab success || gitlab_projects_error end + def bundle_to_disk(save_path) + gitaly_migrate(:bundle_to_disk) do |is_enabled| + if is_enabled + gitaly_repository_client.create_bundle(save_path) + else + run_git!(%W(bundle create #{save_path} --all)) + end + end + + true + end + # rubocop:disable Metrics/ParameterLists def multi_action( user, branch_name:, message:, actions:, @@ -1319,6 +1331,10 @@ module Gitlab @gitaly_remote_client ||= Gitlab::GitalyClient::RemoteService.new(self) end + def gitaly_blob_client + @gitaly_blob_client ||= Gitlab::GitalyClient::BlobService.new(self) + end + def gitaly_conflicts_client(our_commit_oid, their_commit_oid) Gitlab::GitalyClient::ConflictsService.new(self, our_commit_oid, their_commit_oid) end diff --git a/lib/gitlab/git/wiki_page.rb b/lib/gitlab/git/wiki_page.rb index a06bac4414f..669ae11a423 100644 --- a/lib/gitlab/git/wiki_page.rb +++ b/lib/gitlab/git/wiki_page.rb @@ -1,7 +1,7 @@ module Gitlab module Git class WikiPage - attr_reader :url_path, :title, :format, :path, :version, :raw_data, :name, :text_data, :historical + attr_reader :url_path, :title, :format, :path, :version, :raw_data, :name, :text_data, :historical, :formatted_data # This class is meant to be serializable so that it can be constructed # by Gitaly and sent over the network to GitLab. @@ -21,6 +21,7 @@ module Gitlab @raw_data = gollum_page.raw_data @name = gollum_page.name @historical = gollum_page.historical? + @formatted_data = gollum_page.formatted_data if gollum_page.is_a?(Gollum::Page) @version = version end diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 4507ea923b4..6bd256f57c7 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -1,6 +1,8 @@ require 'base64' require 'gitaly' +require 'grpc/health/v1/health_pb' +require 'grpc/health/v1/health_services_pb' module Gitlab module GitalyClient @@ -69,14 +71,27 @@ module Gitlab @stubs ||= {} @stubs[storage] ||= {} @stubs[storage][name] ||= begin - klass = Gitaly.const_get(name.to_s.camelcase.to_sym).const_get(:Stub) - addr = address(storage) - addr = addr.sub(%r{^tcp://}, '') if URI(addr).scheme == 'tcp' + klass = stub_class(name) + addr = stub_address(storage) klass.new(addr, :this_channel_is_insecure) end end end + def self.stub_class(name) + if name == :health_check + Grpc::Health::V1::Health::Stub + else + Gitaly.const_get(name.to_s.camelcase.to_sym).const_get(:Stub) + end + end + + def self.stub_address(storage) + addr = address(storage) + addr = addr.sub(%r{^tcp://}, '') if URI(addr).scheme == 'tcp' + addr + end + def self.clear_stubs! MUTEX.synchronize do @stubs = nil diff --git a/lib/gitlab/gitaly_client/blob_service.rb b/lib/gitlab/gitaly_client/blob_service.rb index a250eb75bd4..ee36684197b 100644 --- a/lib/gitlab/gitaly_client/blob_service.rb +++ b/lib/gitlab/gitaly_client/blob_service.rb @@ -32,6 +32,26 @@ module Gitlab binary: Gitlab::Git::Blob.binary?(data) ) end + + def batch_lfs_pointers(blob_ids) + request = Gitaly::GetLFSPointersRequest.new( + repository: @gitaly_repo, + blob_ids: blob_ids + ) + + response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_lfs_pointers, request) + + response.flat_map do |message| + message.lfs_pointers.map do |lfs_pointer| + Gitlab::Git::Blob.new( + id: lfs_pointer.oid, + size: lfs_pointer.size, + data: lfs_pointer.data, + binary: Gitlab::Git::Blob.binary?(lfs_pointer.data) + ) + end + end + end end end end diff --git a/lib/gitlab/gitaly_client/health_check_service.rb b/lib/gitlab/gitaly_client/health_check_service.rb new file mode 100644 index 00000000000..6c1213f5e20 --- /dev/null +++ b/lib/gitlab/gitaly_client/health_check_service.rb @@ -0,0 +1,19 @@ +module Gitlab + module GitalyClient + class HealthCheckService + def initialize(storage) + @storage = storage + end + + # Sends a gRPC health ping to the Gitaly server for the storage shard. + def check + request = Grpc::Health::V1::HealthCheckRequest.new + response = GitalyClient.call(@storage, :health_check, :check, request, timeout: GitalyClient.fast_timeout) + + { success: response&.status == :SERVING } + rescue GRPC::BadStatus => e + { success: false, message: e.to_s } + end + end + end +end diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index 12016aee2a6..654a3c314f1 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -161,6 +161,23 @@ module Gitlab return response.error.b, 1 end end + + def create_bundle(save_path) + request = Gitaly::CreateBundleRequest.new(repository: @gitaly_repo) + response = GitalyClient.call( + @storage, + :repository_service, + :create_bundle, + request, + timeout: GitalyClient.default_timeout + ) + + File.open(save_path, 'wb') do |f| + response.each do |message| + f.write(message.data) + end + end + end end end end diff --git a/lib/gitlab/health_checks/gitaly_check.rb b/lib/gitlab/health_checks/gitaly_check.rb new file mode 100644 index 00000000000..11416c002e3 --- /dev/null +++ b/lib/gitlab/health_checks/gitaly_check.rb @@ -0,0 +1,53 @@ +module Gitlab + module HealthChecks + class GitalyCheck + extend BaseAbstractCheck + + METRIC_PREFIX = 'gitaly_health_check'.freeze + + class << self + def readiness + repository_storages.map do |storage_name| + check(storage_name) + end + end + + def metrics + repository_storages.flat_map do |storage_name| + result, elapsed = with_timing { check(storage_name) } + labels = { shard: storage_name } + + [ + metric("#{metric_prefix}_success", successful?(result) ? 1 : 0, **labels), + metric("#{metric_prefix}_latency_seconds", elapsed, **labels) + ].flatten + end + end + + def check(storage_name) + serv = Gitlab::GitalyClient::HealthCheckService.new(storage_name) + result = serv.check + HealthChecks::Result.new(result[:success], result[:message], shard: storage_name) + end + + private + + def metric_prefix + METRIC_PREFIX + end + + def successful?(result) + result[:success] + end + + def repository_storages + storages.keys + end + + def storages + Gitlab.config.repositories.storages + end + end + end + end +end diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index dd5d35feab9..25399f307f2 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -11,10 +11,6 @@ module Gitlab untar_with_options(archive: archive, dir: dir, options: 'zxf') end - def git_bundle(repo_path:, bundle_path:) - execute(%W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all)) - end - def git_clone_bundle(repo_path:, bundle_path:) execute(%W(#{git_bin_path} clone --bare -- #{bundle_path} #{repo_path})) Gitlab::Git::Repository.create_hooks(repo_path, File.expand_path(Gitlab.config.gitlab_shell.hooks_path)) diff --git a/lib/gitlab/import_export/repo_saver.rb b/lib/gitlab/import_export/repo_saver.rb index a7028a32570..695462c7dd2 100644 --- a/lib/gitlab/import_export/repo_saver.rb +++ b/lib/gitlab/import_export/repo_saver.rb @@ -21,7 +21,7 @@ module Gitlab def bundle_to_disk mkdir_p(@shared.export_path) - git_bundle(repo_path: path_to_repo, bundle_path: @full_path) + @project.repository.bundle_to_disk(@full_path) rescue => e @shared.error(e) false diff --git a/lib/gitlab/import_export/wiki_repo_saver.rb b/lib/gitlab/import_export/wiki_repo_saver.rb index 1e6722a7bba..5fa2e101e29 100644 --- a/lib/gitlab/import_export/wiki_repo_saver.rb +++ b/lib/gitlab/import_export/wiki_repo_saver.rb @@ -10,7 +10,7 @@ module Gitlab def bundle_to_disk(full_path) mkdir_p(@shared.export_path) - git_bundle(repo_path: path_to_repo, bundle_path: full_path) + @wiki.repository.bundle_to_disk(full_path) rescue => e @shared.error(e) false diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 7771b15069b..4823f703ba4 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -20,7 +20,7 @@ module Gitlab when 'commits' Kaminari.paginate_array(commits).page(page).per(per_page) else - super + super(scope, page, false) end end diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 70b639501fd..7362514167f 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -40,19 +40,21 @@ module Gitlab @default_project_filter = default_project_filter end - def objects(scope, page = nil) - case scope - when 'projects' - projects.page(page).per(per_page) - when 'issues' - issues.page(page).per(per_page) - when 'merge_requests' - merge_requests.page(page).per(per_page) - when 'milestones' - milestones.page(page).per(per_page) - else - Kaminari.paginate_array([]).page(page).per(per_page) - end + def objects(scope, page = nil, without_count = true) + collection = case scope + when 'projects' + projects.page(page).per(per_page) + when 'issues' + issues.page(page).per(per_page) + when 'merge_requests' + merge_requests.page(page).per(per_page) + when 'milestones' + milestones.page(page).per(per_page) + else + Kaminari.paginate_array([]).page(page).per(per_page) + end + + without_count ? collection.without_count : collection end def projects_count @@ -71,18 +73,46 @@ module Gitlab @milestones_count ||= milestones.count end + def limited_projects_count + @limited_projects_count ||= projects.limit(count_limit).count + end + + def limited_issues_count + return @limited_issues_count if @limited_issues_count + + # By default getting limited count (e.g. 1000+) is fast on issuable + # collections except for issues, where filtering both not confidential + # and confidential issues user has access to, is too complex. + # It's faster to try to fetch all public issues first, then only + # if necessary try to fetch all issues. + sum = issues(public_only: true).limit(count_limit).count + @limited_issues_count = sum < count_limit ? issues.limit(count_limit).count : sum + end + + def limited_merge_requests_count + @limited_merge_requests_count ||= merge_requests.limit(count_limit).count + end + + def limited_milestones_count + @limited_milestones_count ||= milestones.limit(count_limit).count + end + def single_commit_result? false end + def count_limit + 1001 + end + private def projects limit_projects.search(query) end - def issues - issues = IssuesFinder.new(current_user).execute + def issues(finder_params = {}) + issues = IssuesFinder.new(current_user, finder_params).execute unless default_project_filter issues = issues.where(project_id: project_ids_relation) end @@ -94,13 +124,13 @@ module Gitlab issues.full_search(query) end - issues.order('updated_at DESC') + issues.reorder('updated_at DESC') end def milestones milestones = Milestone.where(project_id: project_ids_relation) milestones = milestones.search(query) - milestones.order('updated_at DESC') + milestones.reorder('updated_at DESC') end def merge_requests @@ -116,7 +146,7 @@ module Gitlab merge_requests.full_search(query) end - merge_requests.order('updated_at DESC') + merge_requests.reorder('updated_at DESC') end def default_scope diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb index b85f70e450e..4f86b3e8f73 100644 --- a/lib/gitlab/snippet_search_results.rb +++ b/lib/gitlab/snippet_search_results.rb @@ -16,7 +16,7 @@ module Gitlab when 'snippet_blobs' snippet_blobs.page(page).per(per_page) else - super + super(scope, nil, false) end end diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb index 4f575613848..f8c4db1403c 100644 --- a/spec/features/global_search_spec.rb +++ b/spec/features/global_search_spec.rb @@ -22,7 +22,7 @@ feature 'Global search' do click_button "Go" select_filter("Issues") - expect(page).to have_selector('.gl-pagination .page', count: 2) + expect(page).to have_selector('.gl-pagination .next') end end end diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb index 53706ef84bc..c7cfd01f588 100644 --- a/spec/features/issues/spam_issues_spec.rb +++ b/spec/features/issues/spam_issues_spec.rb @@ -34,6 +34,9 @@ describe 'New issue', :js do click_button 'Submit issue' + # reCAPTCHA alerts when it can't contact the server, so just accept it and move on + page.driver.browser.switch_to.alert.accept + # it is impossible to test recaptcha automatically and there is no possibility to fill in recaptcha # recaptcha verification is skipped in test environment and it always returns true expect(page).not_to have_content('issue title') diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb index 590210d44ef..3e83a549682 100644 --- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb +++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb @@ -108,7 +108,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do it 'shows resolved discussion when toggled' do find(".timeline-content .discussion[data-discussion-id='#{note.discussion_id}'] .discussion-toggle-button").click - expect(page.find(".timeline-content #note_#{note.noteable_id}")).to be_visible + expect(page.find(".timeline-content #note_#{note.id}")).to be_visible end end diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index d62c2966a8b..0afe09d87bc 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -10,9 +10,10 @@ describe('Pipelines table in Commits and Merge requests', () => { preloadFixtures(jsonFixtureName); beforeEach(() => { - PipelinesTable = Vue.extend(pipelinesTable); const pipelines = getJSONFixture(jsonFixtureName).pipelines; - pipeline = pipelines.find(p => p.id === 1); + + PipelinesTable = Vue.extend(pipelinesTable); + pipeline = pipelines.find(p => p.user !== null && p.commit !== null); }); describe('successful request', () => { diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js index a9126d2f4e9..b3cbf9aba48 100644 --- a/spec/javascripts/pipelines/pipelines_table_row_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js @@ -24,9 +24,10 @@ describe('Pipelines Table Row', () => { beforeEach(() => { const pipelines = getJSONFixture(jsonFixtureName).pipelines; - pipeline = pipelines.find(p => p.id === 1); - pipelineWithoutAuthor = pipelines.find(p => p.id === 2); - pipelineWithoutCommit = pipelines.find(p => p.id === 3); + + pipeline = pipelines.find(p => p.user !== null && p.commit !== null); + pipelineWithoutAuthor = pipelines.find(p => p.user == null && p.commit !== null); + pipelineWithoutCommit = pipelines.find(p => p.user == null && p.commit == null); }); afterEach(() => { diff --git a/spec/javascripts/pipelines/pipelines_table_spec.js b/spec/javascripts/pipelines/pipelines_table_spec.js index ca2f9163313..4fc3c08145e 100644 --- a/spec/javascripts/pipelines/pipelines_table_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_spec.js @@ -11,9 +11,10 @@ describe('Pipelines Table', () => { preloadFixtures(jsonFixtureName); beforeEach(() => { - PipelinesTableComponent = Vue.extend(pipelinesTableComp); const pipelines = getJSONFixture(jsonFixtureName).pipelines; - pipeline = pipelines.find(p => p.id === 1); + + PipelinesTableComponent = Vue.extend(pipelinesTableComp); + pipeline = pipelines.find(p => p.user !== null && p.commit !== null); }); describe('table', () => { 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 index d21183b668b..c8df6dd2118 100644 --- 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 @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :truncate, :migration, schema: 20171114162227 do +describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :migration, schema: 20171114162227 do let(:merge_request_diffs) { table(:merge_request_diffs) } let(:merge_requests) { table(:merge_requests) } diff --git a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb index 7b5a00c6111..021e1d14b18 100644 --- a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder do +describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder, :delete do let(:migration) { described_class.new } before do @@ -8,7 +8,7 @@ describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder do end describe '#perform' do - it 'renames the path of system-uploads', :truncate do + it 'renames the path of system-uploads' do upload = create(:upload, model: create(:project), path: 'uploads/system/project/avatar.jpg') migration.perform('uploads/system/', 'uploads/-/system/') diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb index 596cc435bd9..cc7cb3f23fd 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :truncate do +describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :delete do let(:migration) { FakeRenameReservedPathMigrationV1.new } let(:subject) { described_class.new(['the-path'], migration) } diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb index 1143182531f..f31475dbd71 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :truncate do +describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :delete do let(:migration) { FakeRenameReservedPathMigrationV1.new } let(:subject) { described_class.new(['the-path'], migration) } let(:namespace) { create(:group, name: 'the-path') } diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb index e850b5cd6a4..0958144643b 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :truncate do +describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :delete do let(:migration) { FakeRenameReservedPathMigrationV1.new } let(:subject) { described_class.new(['the-path'], migration) } let(:project) do diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb index 7695b95dc57..1d31f96159c 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb @@ -13,7 +13,7 @@ shared_examples 'renames child namespaces' do |type| end end -describe Gitlab::Database::RenameReservedPathsMigration::V1, :truncate do +describe Gitlab::Database::RenameReservedPathsMigration::V1, :delete do let(:subject) { FakeRenameReservedPathMigrationV1.new } before do diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index 8706c89c147..168207552ff 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -260,29 +260,42 @@ describe Gitlab::Git::Blob, seed_helper: true do ) end - it 'returns a list of Gitlab::Git::Blob' do - blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id]) + shared_examples 'fetching batch of LFS pointers' do + it 'returns a list of Gitlab::Git::Blob' do + blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id]) - expect(blobs.count).to eq(1) - expect(blobs).to all( be_a(Gitlab::Git::Blob) ) - end + expect(blobs.count).to eq(1) + expect(blobs).to all( be_a(Gitlab::Git::Blob) ) + end - it 'silently ignores tree objects' do - blobs = described_class.batch_lfs_pointers(repository, [tree_object.oid]) + it 'silently ignores tree objects' do + blobs = described_class.batch_lfs_pointers(repository, [tree_object.oid]) - expect(blobs).to eq([]) - end + expect(blobs).to eq([]) + end + + it 'silently ignores non lfs objects' do + blobs = described_class.batch_lfs_pointers(repository, [non_lfs_blob.id]) - it 'silently ignores non lfs objects' do - blobs = described_class.batch_lfs_pointers(repository, [non_lfs_blob.id]) + expect(blobs).to eq([]) + end + + it 'avoids loading large blobs into memory' do + # This line could call `lookup` on `repository`, so do here before mocking. + non_lfs_blob_id = non_lfs_blob.id + + expect(repository).not_to receive(:lookup) - expect(blobs).to eq([]) + described_class.batch_lfs_pointers(repository, [non_lfs_blob_id]) + end end - it 'avoids loading large blobs into memory' do - expect(repository).not_to receive(:lookup) + context 'when Gitaly batch_lfs_pointers is enabled' do + it_behaves_like 'fetching batch of LFS pointers' + end - described_class.batch_lfs_pointers(repository, [non_lfs_blob.id]) + context 'when Gitaly batch_lfs_pointers is disabled', :disable_gitaly do + it_behaves_like 'fetching batch of LFS pointers' end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index aec7cde6df8..36ca3980de9 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1926,6 +1926,34 @@ describe Gitlab::Git::Repository, seed_helper: true do it { expect(subject.repository_relative_path).to eq(repository.relative_path) } end + describe '#bundle_to_disk' do + shared_examples 'bundling to disk' do + let(:save_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") } + + after do + FileUtils.rm_rf(save_path) + end + + it 'saves a bundle to disk' do + repository.bundle_to_disk(save_path) + + success = system( + *%W(#{Gitlab.config.git.bin_path} -C #{repository.path} bundle verify #{save_path}), + [:out, :err] => '/dev/null' + ) + expect(success).to be true + end + end + + context 'when Gitaly bundle_to_disk feature is enabled' do + it_behaves_like 'bundling to disk' + end + + context 'when Gitaly bundle_to_disk feature is disabled', :disable_gitaly do + it_behaves_like 'bundling to disk' + end + end + context 'gitlab_projects commands' do let(:gitlab_projects) { repository.gitlab_projects } let(:timeout) { Gitlab.config.gitlab_shell.git_timeout } diff --git a/spec/lib/gitlab/gitaly_client/health_check_service_spec.rb b/spec/lib/gitlab/gitaly_client/health_check_service_spec.rb new file mode 100644 index 00000000000..2c7e5eb5787 --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/health_check_service_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::HealthCheckService do + let(:project) { create(:project) } + let(:storage_name) { project.repository_storage } + + subject { described_class.new(storage_name) } + + describe '#check' do + it 'successfully sends a health check request' do + expect(Gitlab::GitalyClient).to receive(:call).with( + storage_name, + :health_check, + :check, + instance_of(Grpc::Health::V1::HealthCheckRequest), + timeout: Gitlab::GitalyClient.fast_timeout).and_call_original + + expect(subject.check).to eq({ success: true }) + end + + it 'receives an unsuccessful health check request' do + expect_any_instance_of(Grpc::Health::V1::Health::Stub) + .to receive(:check) + .and_return(double(status: false)) + + expect(subject.check).to eq({ success: false }) + end + + it 'gracefully handles gRPC error' do + expect(Gitlab::GitalyClient).to receive(:call).with( + storage_name, + :health_check, + :check, + instance_of(Grpc::Health::V1::HealthCheckRequest), + timeout: Gitlab::GitalyClient.fast_timeout) + .and_raise(GRPC::Unavailable.new('Connection refused')) + + expect(subject.check).to eq({ success: false, message: '14:Connection refused' }) + end + end +end diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index 309b7338ef0..81bcd8c28ed 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -3,6 +3,31 @@ require 'spec_helper' # We stub Gitaly in `spec/support/gitaly.rb` for other tests. We don't want # those stubs while testing the GitalyClient itself. describe Gitlab::GitalyClient, skip_gitaly_mock: true do + describe '.stub_class' do + it 'returns the gRPC health check stub' do + expect(described_class.stub_class(:health_check)).to eq(::Grpc::Health::V1::Health::Stub) + end + + it 'returns a Gitaly stub' do + expect(described_class.stub_class(:ref_service)).to eq(::Gitaly::RefService::Stub) + end + end + + describe '.stub_address' do + it 'returns the same result after being called multiple times' do + address = 'localhost:9876' + prefixed_address = "tcp://#{address}" + + allow(Gitlab.config.repositories).to receive(:storages).and_return({ + 'default' => { 'gitaly_address' => prefixed_address } + }) + + 2.times do + expect(described_class.stub_address('default')).to eq('localhost:9876') + end + end + end + describe '.stub' do # Notice that this is referring to gRPC "stubs", not rspec stubs before do diff --git a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb new file mode 100644 index 00000000000..724beefff69 --- /dev/null +++ b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe Gitlab::HealthChecks::GitalyCheck do + let(:result_class) { Gitlab::HealthChecks::Result } + let(:repository_storages) { ['default'] } + + before do + allow(described_class).to receive(:repository_storages) { repository_storages } + end + + describe '#readiness' do + subject { described_class.readiness } + + before do + expect(Gitlab::GitalyClient::HealthCheckService).to receive(:new).and_return(gitaly_check) + end + + context 'Gitaly server is up' do + let(:gitaly_check) { double(check: { success: true }) } + + it { is_expected.to eq([result_class.new(true, nil, shard: 'default')]) } + end + + context 'Gitaly server is down' do + let(:gitaly_check) { double(check: { success: false, message: 'Connection refused' }) } + + it { is_expected.to eq([result_class.new(false, 'Connection refused', shard: 'default')]) } + end + end + + describe '#metrics' do + subject { described_class.metrics } + + before do + expect(Gitlab::GitalyClient::HealthCheckService).to receive(:new).and_return(gitaly_check) + end + + context 'Gitaly server is up' do + let(:gitaly_check) { double(check: { success: true }) } + + it 'provides metrics' do + expect(subject).to all(have_attributes(labels: { shard: 'default' })) + expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_success', value: 1)) + expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_latency_seconds', value: be >= 0)) + end + end + + context 'Gitaly server is down' do + let(:gitaly_check) { double(check: { success: false, message: 'Connection refused' }) } + + it 'provides metrics' do + expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_success', value: 0)) + expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_latency_seconds', value: be >= 0)) + end + end + end +end diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index b5a9ac570e6..17b48b3d062 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -19,6 +19,12 @@ describe Gitlab::SearchResults do project.add_developer(user) end + describe '#objects' do + it 'returns without_page collection by default' do + expect(results.objects('projects')).to be_kind_of(Kaminari::PaginatableWithoutCount) + end + end + describe '#projects_count' do it 'returns the total amount of projects' do expect(results.projects_count).to eq(1) @@ -43,6 +49,58 @@ describe Gitlab::SearchResults do end end + context "when count_limit is lower than total amount" do + before do + allow(results).to receive(:count_limit).and_return(1) + end + + describe '#limited_projects_count' do + it 'returns the limited amount of projects' do + create(:project, name: 'foo2') + + expect(results.limited_projects_count).to eq(1) + end + end + + describe '#limited_merge_requests_count' do + it 'returns the limited amount of merge requests' do + create(:merge_request, :simple, source_project: project, title: 'foo2') + + expect(results.limited_merge_requests_count).to eq(1) + end + end + + describe '#limited_milestones_count' do + it 'returns the limited amount of milestones' do + create(:milestone, project: project, title: 'foo2') + + expect(results.limited_milestones_count).to eq(1) + end + end + + describe '#limited_issues_count' do + it 'runs single SQL query to get the limited amount of issues' do + create(:milestone, project: project, title: 'foo2') + + expect(results).to receive(:issues).with(public_only: true).and_call_original + expect(results).not_to receive(:issues).with(no_args).and_call_original + + expect(results.limited_issues_count).to eq(1) + end + end + end + + context "when count_limit is higher than total amount" do + describe '#limited_issues_count' do + it 'runs multiple queries to get the limited amount of issues' do + expect(results).to receive(:issues).with(public_only: true).and_call_original + expect(results).to receive(:issues).with(no_args).and_call_original + + expect(results.limited_issues_count).to eq(1) + end + end + end + it 'includes merge requests from source and target projects' do forked_project = fork_project(project, user) merge_request_2 = create(:merge_request, target_project: project, source_project: forked_project, title: 'foo') diff --git a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb index 84c2e9f7e52..63defcb39bf 100644 --- a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb +++ b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170508170547_add_head_pipeline_for_each_merge_request.rb') -describe AddHeadPipelineForEachMergeRequest, :truncate do +describe AddHeadPipelineForEachMergeRequest, :delete do include ProjectForksHelper let(:migration) { described_class.new } diff --git a/spec/migrations/calculate_conv_dev_index_percentages_spec.rb b/spec/migrations/calculate_conv_dev_index_percentages_spec.rb index 597d8eab51c..f3a46025376 100644 --- a/spec/migrations/calculate_conv_dev_index_percentages_spec.rb +++ b/spec/migrations/calculate_conv_dev_index_percentages_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170803090603_calculate_conv_dev_index_percentages.rb') -describe CalculateConvDevIndexPercentages, truncate: true do +describe CalculateConvDevIndexPercentages, :delete do let(:migration) { described_class.new } let!(:conv_dev_index) do create(:conversational_development_index_metric, diff --git a/spec/migrations/fix_wrongly_renamed_routes_spec.rb b/spec/migrations/fix_wrongly_renamed_routes_spec.rb index 78f8ab7512d..543cf55f076 100644 --- a/spec/migrations/fix_wrongly_renamed_routes_spec.rb +++ b/spec/migrations/fix_wrongly_renamed_routes_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170518231126_fix_wrongly_renamed_routes.rb') -describe FixWronglyRenamedRoutes, :truncate, :migration do +describe FixWronglyRenamedRoutes, :migration do let(:subject) { described_class.new } let(:namespaces_table) { table(:namespaces) } let(:projects_table) { table(:projects) } diff --git a/spec/migrations/migrate_issues_to_ghost_user_spec.rb b/spec/migrations/migrate_issues_to_ghost_user_spec.rb index cfd4021fbac..ff0d44e1ed2 100644 --- a/spec/migrations/migrate_issues_to_ghost_user_spec.rb +++ b/spec/migrations/migrate_issues_to_ghost_user_spec.rb @@ -8,10 +8,10 @@ describe MigrateIssuesToGhostUser, :migration do let(:users) { table(:users) } before do - projects.create!(name: 'gitlab') + project = projects.create!(name: 'gitlab') user = users.create(email: 'test@example.com') - issues.create(title: 'Issue 1', author_id: nil, project_id: 1) - issues.create(title: 'Issue 2', author_id: user.id, project_id: 1) + issues.create(title: 'Issue 1', author_id: nil, project_id: project.id) + issues.create(title: 'Issue 2', author_id: user.id, project_id: project.id) end context 'when ghost user exists' do diff --git a/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb b/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb index 063829be546..a17c9c72bde 100644 --- a/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb +++ b/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170324160416_migrate_user_activities_to_users_last_activity_on.rb') -describe MigrateUserActivitiesToUsersLastActivityOn, :clean_gitlab_redis_shared_state, :truncate do +describe MigrateUserActivitiesToUsersLastActivityOn, :clean_gitlab_redis_shared_state, :delete do let(:migration) { described_class.new } let!(:user_active_1) { create(:user) } let!(:user_active_2) { create(:user) } diff --git a/spec/migrations/migrate_user_project_view_spec.rb b/spec/migrations/migrate_user_project_view_spec.rb index 5e16769d63a..31d16e17d7b 100644 --- a/spec/migrations/migrate_user_project_view_spec.rb +++ b/spec/migrations/migrate_user_project_view_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170406142253_migrate_user_project_view.rb') -describe MigrateUserProjectView, :truncate do +describe MigrateUserProjectView, :delete do let(:migration) { described_class.new } let!(:user) { create(:user, project_view: 'readme') } diff --git a/spec/migrations/remove_duplicate_mr_events_spec.rb b/spec/migrations/remove_duplicate_mr_events_spec.rb index e393374028f..e51872239ad 100644 --- a/spec/migrations/remove_duplicate_mr_events_spec.rb +++ b/spec/migrations/remove_duplicate_mr_events_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170815060945_remove_duplicate_mr_events.rb') -describe RemoveDuplicateMrEvents, truncate: true do +describe RemoveDuplicateMrEvents, :delete do let(:migration) { described_class.new } describe '#up' do diff --git a/spec/migrations/rename_more_reserved_project_names_spec.rb b/spec/migrations/rename_more_reserved_project_names_spec.rb index ae3a4cb9b29..75310075cc5 100644 --- a/spec/migrations/rename_more_reserved_project_names_spec.rb +++ b/spec/migrations/rename_more_reserved_project_names_spec.rb @@ -5,8 +5,8 @@ require Rails.root.join('db', 'post_migrate', '20170313133418_rename_more_reserv # This migration uses multiple threads, and thus different transactions. This # means data created in this spec may not be visible to some threads. To work -# around this we use the TRUNCATE cleaning strategy. -describe RenameMoreReservedProjectNames, truncate: true do +# around this we use the DELETE cleaning strategy. +describe RenameMoreReservedProjectNames, :delete do let(:migration) { described_class.new } let!(:project) { create(:project) } diff --git a/spec/migrations/rename_reserved_project_names_spec.rb b/spec/migrations/rename_reserved_project_names_spec.rb index 462f4c08d63..e6555b1fe6b 100644 --- a/spec/migrations/rename_reserved_project_names_spec.rb +++ b/spec/migrations/rename_reserved_project_names_spec.rb @@ -5,8 +5,8 @@ require Rails.root.join('db', 'post_migrate', '20161221153951_rename_reserved_pr # This migration uses multiple threads, and thus different transactions. This # means data created in this spec may not be visible to some threads. To work -# around this we use the TRUNCATE cleaning strategy. -describe RenameReservedProjectNames, truncate: true do +# around this we use the DELETE cleaning strategy. +describe RenameReservedProjectNames, :delete do let(:migration) { described_class.new } let!(:project) { create(:project) } diff --git a/spec/migrations/rename_users_with_renamed_namespace_spec.rb b/spec/migrations/rename_users_with_renamed_namespace_spec.rb index 1e9aab3d9a1..cbc0ebeb44d 100644 --- a/spec/migrations/rename_users_with_renamed_namespace_spec.rb +++ b/spec/migrations/rename_users_with_renamed_namespace_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170518200835_rename_users_with_renamed_namespace.rb') -describe RenameUsersWithRenamedNamespace, truncate: true do +describe RenameUsersWithRenamedNamespace, :delete do it 'renames a user that had their namespace renamed to the namespace path' do other_user = create(:user, username: 'kodingu') other_user1 = create(:user, username: 'api0') diff --git a/spec/migrations/update_retried_for_ci_build_spec.rb b/spec/migrations/update_retried_for_ci_build_spec.rb index 3742b4dafe5..ccb77766b84 100644 --- a/spec/migrations/update_retried_for_ci_build_spec.rb +++ b/spec/migrations/update_retried_for_ci_build_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170503004427_update_retried_for_ci_build.rb') -describe UpdateRetriedForCiBuild, truncate: true do +describe UpdateRetriedForCiBuild, :delete do let(:pipeline) { create(:ci_pipeline) } let!(:build_old) { create(:ci_build, pipeline: pipeline, name: 'test') } let!(:build_new) { create(:ci_build, pipeline: pipeline, name: 'test') } diff --git a/spec/models/concerns/avatarable_spec.rb b/spec/models/concerns/avatarable_spec.rb index cbdc438be0b..3696e6f62fd 100644 --- a/spec/models/concerns/avatarable_spec.rb +++ b/spec/models/concerns/avatarable_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' describe Avatarable do - subject { create(:project, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) } + set(:project) { create(:project, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) } let(:gitlab_host) { "https://gitlab.example.com" } let(:relative_url_root) { "/gitlab" } - let(:asset_host) { "https://gitlab-assets.example.com" } + let(:asset_host) { 'https://gitlab-assets.example.com' } before do stub_config_setting(base_url: gitlab_host) @@ -15,29 +15,32 @@ describe Avatarable do describe '#avatar_path' do using RSpec::Parameterized::TableSyntax - where(:has_asset_host, :visibility_level, :only_path, :avatar_path) do - true | Project::PRIVATE | true | [gitlab_host, relative_url_root, subject.avatar.url] - true | Project::PRIVATE | false | [gitlab_host, relative_url_root, subject.avatar.url] - true | Project::INTERNAL | true | [gitlab_host, relative_url_root, subject.avatar.url] - true | Project::INTERNAL | false | [gitlab_host, relative_url_root, subject.avatar.url] - true | Project::PUBLIC | true | [subject.avatar.url] - true | Project::PUBLIC | false | [asset_host, subject.avatar.url] - false | Project::PRIVATE | true | [relative_url_root, subject.avatar.url] - false | Project::PRIVATE | false | [gitlab_host, relative_url_root, subject.avatar.url] - false | Project::INTERNAL | true | [relative_url_root, subject.avatar.url] - false | Project::INTERNAL | false | [gitlab_host, relative_url_root, subject.avatar.url] - false | Project::PUBLIC | true | [relative_url_root, subject.avatar.url] - false | Project::PUBLIC | false | [gitlab_host, relative_url_root, subject.avatar.url] + where(:has_asset_host, :visibility_level, :only_path, :avatar_path_prefix) do + true | Project::PRIVATE | true | [gitlab_host, relative_url_root] + true | Project::PRIVATE | false | [gitlab_host, relative_url_root] + true | Project::INTERNAL | true | [gitlab_host, relative_url_root] + true | Project::INTERNAL | false | [gitlab_host, relative_url_root] + true | Project::PUBLIC | true | [] + true | Project::PUBLIC | false | [asset_host] + false | Project::PRIVATE | true | [relative_url_root] + false | Project::PRIVATE | false | [gitlab_host, relative_url_root] + false | Project::INTERNAL | true | [relative_url_root] + false | Project::INTERNAL | false | [gitlab_host, relative_url_root] + false | Project::PUBLIC | true | [relative_url_root] + false | Project::PUBLIC | false | [gitlab_host, relative_url_root] end with_them do before do - allow(ActionController::Base).to receive(:asset_host).and_return(has_asset_host ? asset_host : nil) - subject.visibility_level = visibility_level + allow(ActionController::Base).to receive(:asset_host) { has_asset_host && asset_host } + + project.visibility_level = visibility_level end + let(:avatar_path) { (avatar_path_prefix + [project.avatar.url]).join } + it 'returns the expected avatar path' do - expect(subject.avatar_path(only_path: only_path)).to eq(avatar_path.join) + expect(project.avatar_path(only_path: only_path)).to eq(avatar_path) end end end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 6aa0e7f49c3..c64cdf8f812 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -488,7 +488,7 @@ describe Member do member.accept_invite!(user) end - it "refreshes user's authorized projects", :truncate do + it "refreshes user's authorized projects", :delete do project = member.source expect(user.authorized_projects).not_to include(project) @@ -523,7 +523,7 @@ describe Member do end end - describe "destroying a record", :truncate do + describe "destroying a record", :delete do it "refreshes user's authorized projects" do project = create(:project, :private) user = create(:user) diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb index 41e2ab20d69..1fccf92627a 100644 --- a/spec/models/project_group_link_spec.rb +++ b/spec/models/project_group_link_spec.rb @@ -30,7 +30,7 @@ describe ProjectGroupLink do end end - describe "destroying a record", :truncate do + describe "destroying a record", :delete do it "refreshes group users' authorized projects" do project = create(:project, :private) group = create(:group) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 4d10df410ab..31dcb543cbd 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -3228,5 +3228,22 @@ describe Project do project = build(:project) project.execute_hooks({ data: 'data' }, :merge_request_hooks) end + + it 'executes the system hooks when inside a transaction' do + allow_any_instance_of(WebHookService).to receive(:execute) + + create(:system_hook, merge_requests_events: true) + + project = build(:project) + + # Ideally, we'd test that `WebHookWorker.jobs.size` increased by 1, + # but since the entire spec run takes place in a transaction, we never + # actually get to the `after_commit` hook that queues these jobs. + expect do + project.transaction do + project.execute_hooks({ data: 'data' }, :merge_request_hooks) + end + end.not_to raise_error # Sidekiq::Worker::EnqueueFromTransactionError + end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 762cec9b95e..594f23718da 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1569,7 +1569,7 @@ describe User do it { is_expected.to eq([private_group]) } end - describe '#authorized_projects', :truncate do + describe '#authorized_projects', :delete do context 'with a minimum access level' do it 'includes projects for which the user is an owner' do user = create(:user) diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index ea75434e399..cc9d79da708 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -386,6 +386,17 @@ describe WikiPage do end end + describe '#formatted_content' do + it 'returns processed content of the page', :disable_gitaly do + subject.create({ title: "RDoc", content: "*bold*", format: "rdoc" }) + page = wiki.find_page('RDoc') + + expect(page.formatted_content).to eq("\n<p><strong>bold</strong></p>\n") + + destroy_page('RDoc') + end + end + private def remove_temp_repo(path) diff --git a/spec/requests/api/applications_spec.rb b/spec/requests/api/applications_spec.rb new file mode 100644 index 00000000000..f56bc932f40 --- /dev/null +++ b/spec/requests/api/applications_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' + +describe API::Applications, :api do + include ApiHelpers + + let(:admin_user) { create(:user, admin: true) } + let(:user) { create(:user, admin: false) } + + describe 'POST /applications' do + context 'authenticated and authorized user' do + it 'creates and returns an OAuth application' do + expect do + post api('/applications', admin_user), name: 'application_name', redirect_uri: 'http://application.url', scopes: '' + end.to change { Doorkeeper::Application.count }.by 1 + + application = Doorkeeper::Application.find_by(name: 'application_name', redirect_uri: 'http://application.url') + + expect(response).to have_http_status 201 + expect(json_response).to be_a Hash + expect(json_response['application_id']).to eq application.uid + expect(json_response['secret']).to eq application.secret + expect(json_response['callback_url']).to eq application.redirect_uri + end + + it 'does not allow creating an application with the wrong redirect_uri format' do + expect do + post api('/applications', admin_user), name: 'application_name', redirect_uri: 'wrong_url_format', scopes: '' + end.not_to change { Doorkeeper::Application.count } + + expect(response).to have_http_status 400 + expect(json_response).to be_a Hash + expect(json_response['message']['redirect_uri'][0]).to eq('must be an absolute URI.') + end + + it 'does not allow creating an application without a name' do + expect do + post api('/applications', admin_user), redirect_uri: 'http://application.url', scopes: '' + end.not_to change { Doorkeeper::Application.count } + + expect(response).to have_http_status 400 + expect(json_response).to be_a Hash + expect(json_response['error']).to eq('name is missing') + end + + it 'does not allow creating an application without a redirect_uri' do + expect do + post api('/applications', admin_user), name: 'application_name', scopes: '' + end.not_to change { Doorkeeper::Application.count } + + expect(response).to have_http_status 400 + expect(json_response).to be_a Hash + expect(json_response['error']).to eq('redirect_uri is missing') + end + + it 'does not allow creating an application without scopes' do + expect do + post api('/applications', admin_user), name: 'application_name', redirect_uri: 'http://application.url' + end.not_to change { Doorkeeper::Application.count } + + expect(response).to have_http_status 400 + expect(json_response).to be_a Hash + expect(json_response['error']).to eq('scopes is missing') + end + end + + context 'authorized user without authorization' do + it 'does not create application' do + expect do + post api('/applications', user), name: 'application_name', redirect_uri: 'http://application.url', scopes: '' + end.not_to change { Doorkeeper::Application.count } + + expect(response).to have_http_status 403 + end + end + + context 'non-authenticated user' do + it 'does not create application' do + expect do + post api('/applications'), name: 'application_name', redirect_uri: 'http://application.url' + end.not_to change { Doorkeeper::Application.count } + + expect(response).to have_http_status 401 + end + end + end +end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index f3c98fa5416..388c9d63c7b 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -297,9 +297,11 @@ describe Issues::MoveService do end context 'project issue hooks' do - let(:hook) { create(:project_hook, project: old_project, issues_events: true) } + let!(:hook) { create(:project_hook, project: old_project, issues_events: true) } it 'executes project issue hooks' do + allow_any_instance_of(WebHookService).to receive(:execute) + # Ideally, we'd test that `WebHookWorker.jobs.size` increased by 1, # but since the entire spec run takes place in a transaction, we never # actually get to the `after_commit` hook that queues these jobs. diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index edaee03ea6c..1809ae1d141 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -1,10 +1,26 @@ +require 'database_cleaner/active_record/deletion' + +module FakeInformationSchema + # Work around a bug in DatabaseCleaner when using the deletion strategy: + # https://github.com/DatabaseCleaner/database_cleaner/issues/347 + # + # On MySQL, if the information schema is said to exist, we use an inaccurate + # row count leading to some tables not being cleaned when they should + def information_schema_exists?(_connection) + false + end +end + +DatabaseCleaner::ActiveRecord::Deletion.prepend(FakeInformationSchema) + RSpec.configure do |config| + # Ensure all sequences are reset at the start of the suite run config.before(:suite) do DatabaseCleaner.clean_with(:truncation) end config.append_after(:context) do - DatabaseCleaner.clean_with(:truncation, cache_tables: false) + DatabaseCleaner.clean_with(:deletion, cache_tables: false) end config.before(:each) do @@ -12,15 +28,15 @@ RSpec.configure do |config| end config.before(:each, :js) do - DatabaseCleaner.strategy = :truncation + DatabaseCleaner.strategy = :deletion end - config.before(:each, :truncate) do - DatabaseCleaner.strategy = :truncation + config.before(:each, :delete) do + DatabaseCleaner.strategy = :deletion end config.before(:each, :migration) do - DatabaseCleaner.strategy = :truncation, { cache_tables: false } + DatabaseCleaner.strategy = :deletion, { cache_tables: false } end config.before(:each) do diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb index fa94aa2ae3d..c8662d41769 100644 --- a/spec/support/features/discussion_comments_shared_example.rb +++ b/spec/support/features/discussion_comments_shared_example.rb @@ -143,15 +143,17 @@ shared_examples 'discussion comments' do |resource_name| end if resource_name == 'merge request' + let(:note_id) { find("#{comments_selector} .note", match: :first)['data-note-id'] } + it 'shows resolved discussion when toggled' do click_button "Resolve discussion" - expect(page).to have_selector('.note-row-1', visible: true) + expect(page).to have_selector(".note-row-#{note_id}", visible: true) refresh click_button "Toggle discussion" - expect(page).to have_selector('.note-row-1', visible: true) + expect(page).to have_selector(".note-row-#{note_id}", visible: true) end end end diff --git a/spec/uploaders/job_artifact_uploader_spec.rb b/spec/uploaders/job_artifact_uploader_spec.rb index 14fd5f3600f..98a4373e9d0 100644 --- a/spec/uploaders/job_artifact_uploader_spec.rb +++ b/spec/uploaders/job_artifact_uploader_spec.rb @@ -8,7 +8,7 @@ describe JobArtifactUploader do describe '#store_dir' do subject { uploader.store_dir } - let(:path) { "#{job_artifact.created_at.utc.strftime('%Y_%m_%d')}/#{job_artifact.project_id}/#{job_artifact.id}" } + let(:path) { "#{job_artifact.created_at.utc.strftime('%Y_%m_%d')}/#{job_artifact.job_id}/#{job_artifact.id}" } context 'when using local storage' do it { is_expected.to start_with(local_path) } @@ -45,7 +45,7 @@ describe JobArtifactUploader do it { is_expected.to start_with(local_path) } it { is_expected.to include("/#{job_artifact.created_at.utc.strftime('%Y_%m_%d')}/") } - it { is_expected.to include("/#{job_artifact.project_id}/") } + it { is_expected.to include("/#{job_artifact.job_id}/#{job_artifact.id}/") } it { is_expected.to end_with("ci_build_artifacts.zip") } end end |