From 031caf212607282f06b9c080a7f55e3049b2c896 Mon Sep 17 00:00:00 2001 From: sujay patel Date: Thu, 13 Jun 2019 01:33:21 +0530 Subject: [PATCH 001/195] Adding order by to list runner jobs api. --- app/finders/runner_jobs_finder.rb | 19 +++++++- .../51794-add-ordering-to-runner-jobs-api.yml | 5 +++ doc/api/runners.md | 2 + lib/api/runners.rb | 2 + spec/finders/runner_jobs_finder_spec.rb | 22 ++++++++++ spec/requests/api/runners_spec.rb | 44 +++++++++++++++++++ 6 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml diff --git a/app/finders/runner_jobs_finder.rb b/app/finders/runner_jobs_finder.rb index 4fca4ec94f3..5e2c779cef8 100644 --- a/app/finders/runner_jobs_finder.rb +++ b/app/finders/runner_jobs_finder.rb @@ -3,6 +3,8 @@ class RunnerJobsFinder attr_reader :runner, :params + ALLOWED_INDEXED_COLUMNS = %w[id].freeze + def initialize(runner, params = {}) @runner = runner @params = params @@ -11,7 +13,7 @@ class RunnerJobsFinder def execute items = @runner.builds items = by_status(items) - items + sort_items(items) end private @@ -23,4 +25,19 @@ class RunnerJobsFinder items.where(status: params[:status]) end # rubocop: enable CodeReuse/ActiveRecord + + # rubocop: disable CodeReuse/ActiveRecord + def sort_items(items) + return items unless ALLOWED_INDEXED_COLUMNS.include?(params[:order_by]) + + order_by = params[:order_by] + sort = if params[:sort].match?(/\A(ASC|DESC)\z/i) + params[:sort] + else + :desc + end + + items.order(order_by => sort) + end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml b/changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml new file mode 100644 index 00000000000..908a132688c --- /dev/null +++ b/changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml @@ -0,0 +1,5 @@ +--- +title: Add order_by and sort params to list runner jobs api +merge_request: 29629 +author: Sujay Patel +type: added diff --git a/doc/api/runners.md b/doc/api/runners.md index 2d91428d1c1..b5d25bf23a1 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -291,6 +291,8 @@ GET /runners/:id/jobs |-----------|---------|----------|---------------------| | `id` | integer | yes | The ID of a runner | | `status` | string | no | Status of the job; one of: `running`, `success`, `failed`, `canceled` | +| `order_by`| string | no | Order jobs by `id`. | +| `sort` | string | no | Sort jobs in `asc` or `desc` order (default: `desc`) | ``` curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/runners/1/jobs?status=running" diff --git a/lib/api/runners.rb b/lib/api/runners.rb index f3fea463e7f..c2d371b6867 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -115,6 +115,8 @@ module API params do requires :id, type: Integer, desc: 'The ID of the runner' optional :status, type: String, desc: 'Status of the job', values: Ci::Build::AVAILABLE_STATUSES + optional :order_by, type: String, desc: 'Order by `id` or not', values: RunnerJobsFinder::ALLOWED_INDEXED_COLUMNS + optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Sort by asc (ascending) or desc (descending)' use :pagination end get ':id/jobs' do diff --git a/spec/finders/runner_jobs_finder_spec.rb b/spec/finders/runner_jobs_finder_spec.rb index 97304170c4e..01f45a37ba8 100644 --- a/spec/finders/runner_jobs_finder_spec.rb +++ b/spec/finders/runner_jobs_finder_spec.rb @@ -35,5 +35,27 @@ describe RunnerJobsFinder do end end end + + context 'when order_by and sort are specified' do + context 'when order_by id and sort is asc' do + let(:params) { { order_by: 'id', sort: 'asc' } } + let!(:jobs) { create_list(:ci_build, 2, runner: runner, project: project, user: create(:user)) } + + it 'sorts as id: :asc' do + is_expected.to eq(jobs.sort_by(&:id)) + end + end + end + + context 'when order_by is specified and sort is not specified' do + context 'when order_by id and sort is not specified' do + let(:params) { { order_by: 'id' } } + let!(:jobs) { create_list(:ci_build, 2, runner: runner, project: project, user: create(:user)) } + + it 'sorts as id: :desc' do + is_expected.to eq(jobs.sort_by(&:id).reverse) + end + end + end end end diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index 5548e3fd01a..f5ce3a3570e 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -584,6 +584,34 @@ describe API::Runners do end end + context 'when valid order_by is provided' do + context 'when sort order is not specified' do + it 'return jobs in descending order' do + get api("/runners/#{project_runner.id}/jobs?order_by=id", admin) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(2) + expect(json_response.first).to include('id' => job_5.id) + end + end + + context 'when sort order is specified as asc' do + it 'return jobs sorted in ascending order' do + get api("/runners/#{project_runner.id}/jobs?order_by=id&sort=asc", admin) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(2) + expect(json_response.first).to include('id' => job_4.id) + end + end + end + context 'when invalid status is provided' do it 'return 400' do get api("/runners/#{project_runner.id}/jobs?status=non-existing", admin) @@ -591,6 +619,22 @@ describe API::Runners do expect(response).to have_gitlab_http_status(400) end end + + context 'when invalid order_by is provided' do + it 'return 400' do + get api("/runners/#{project_runner.id}/jobs?order_by=non-existing", admin) + + expect(response).to have_gitlab_http_status(400) + end + end + + context 'when invalid sort is provided' do + it 'return 400' do + get api("/runners/#{project_runner.id}/jobs?sort=non-existing", admin) + + expect(response).to have_gitlab_http_status(400) + end + end end context "when runner doesn't exist" do From f7a92ff52822267d834c1c2d2d8ecf367a294e17 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Thu, 6 Jun 2019 14:52:36 -0300 Subject: [PATCH 002/195] Add GetCommitSignatures feature flag Adds feature flag for GetCommitSignatures which got ported to go. More info: https://gitlab.com/gitlab-org/gitaly/merge_requests/1283 --- lib/feature/gitaly.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/feature/gitaly.rb b/lib/feature/gitaly.rb index d7a8f8a0b9e..67c0b902c0c 100644 --- a/lib/feature/gitaly.rb +++ b/lib/feature/gitaly.rb @@ -8,7 +8,12 @@ class Feature # CATFILE_CACHE sets an incorrect example CATFILE_CACHE = 'catfile-cache'.freeze - SERVER_FEATURE_FLAGS = [CATFILE_CACHE].freeze + SERVER_FEATURE_FLAGS = + [ + CATFILE_CACHE, + 'get_commit_signatures'.freeze + ].freeze + DEFAULT_ON_FLAGS = Set.new([CATFILE_CACHE]).freeze class << self From 36e73eff4e6a56ff7e3b1c078f42ae664754eaa7 Mon Sep 17 00:00:00 2001 From: Nathan Friend Date: Fri, 28 Jun 2019 16:27:47 -0300 Subject: [PATCH 003/195] Allow (Haml) tooltip delay customization via localStorage --- app/assets/javascripts/main.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 9f30a989295..2d32e6a7fbb 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -119,11 +119,15 @@ function deferredInitialisation() { .catch(() => {}); } + const glTooltipDelay = localStorage.getItem('gl-tooltip-delay'); + const delay = glTooltipDelay ? JSON.parse(glTooltipDelay) : 0; + // Initialize tooltips $body.tooltip({ selector: '.has-tooltip, [data-toggle="tooltip"]', trigger: 'hover', boundary: 'viewport', + delay, }); // Initialize popovers From e549a7fb1f364395c20522e5395e22a2bf434ed0 Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Mon, 1 Jul 2019 18:49:53 +0900 Subject: [PATCH 004/195] Update mixin-deep to 1.3.2 To address a Prototype Pollution vulnerability, which exists in `mixin-deep` package, versions `>=2.0.0 <2.0.1 || <1.3.2` (CVE-2019-10746). - Diff: https://github.com/jonschlinkert/mixin-deep/compare/1.3.1...1.3.2 - Synk ID: https://app.snyk.io/vuln/SNYK-JS-MIXINDEEP-450212 Signed-off-by: Takuya Noguchi --- changelogs/unreleased/63945-update-mixin-deep-to-1-3-2.yml | 5 +++++ yarn.lock | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/63945-update-mixin-deep-to-1-3-2.yml diff --git a/changelogs/unreleased/63945-update-mixin-deep-to-1-3-2.yml b/changelogs/unreleased/63945-update-mixin-deep-to-1-3-2.yml new file mode 100644 index 00000000000..a0ef34f3700 --- /dev/null +++ b/changelogs/unreleased/63945-update-mixin-deep-to-1-3-2.yml @@ -0,0 +1,5 @@ +--- +title: Update mixin-deep to 1.3.2 +merge_request: 30223 +author: Takuya Noguchi +type: other diff --git a/yarn.lock b/yarn.lock index 07b4e20fc5f..901f7fbd6fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7534,9 +7534,9 @@ mississippi@^3.0.0: through2 "^2.0.0" mixin-deep@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" - integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ== + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== dependencies: for-in "^1.0.2" is-extendable "^1.0.1" From 8f27b0b1109b84bacd482a75b9b7092ee5fd3660 Mon Sep 17 00:00:00 2001 From: Sarah Daily Date: Mon, 1 Jul 2019 20:09:07 +0000 Subject: [PATCH 005/195] Add relevant webcast to drive traffic and MQLs --- doc/user/project/clusters/index.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index c6ee168bad0..581fa66d58c 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -5,6 +5,9 @@ Connect your project to Google Kubernetes Engine (GKE) or an existing Kubernetes cluster in a few steps. +NOTE: **Scalable app deployment with GitLab and Google Cloud Platform** +[Watch the webcast](https://about.gitlab.com/webcast/scalable-app-deploy/) and learn how to spin up a Kubernetes cluster managed by Google Cloud Platform (GCP) in a few clicks. + ## Overview With one or more Kubernetes clusters associated to your project, you can use From 4d02fb67ad942ccd910db80d57d015ec9a81a7d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarka=20Ko=C5=A1anov=C3=A1?= Date: Fri, 28 Jun 2019 19:09:31 +0200 Subject: [PATCH 006/195] Update label note to support scoped labels notes - port of EE change --- app/models/label_note.rb | 18 +++++++++++++----- app/models/resource_label_event.rb | 7 +++++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/app/models/label_note.rb b/app/models/label_note.rb index d6814f4a948..ba5f1f82a81 100644 --- a/app/models/label_note.rb +++ b/app/models/label_note.rb @@ -62,19 +62,27 @@ class LabelNote < Note end def note_text(html: false) - added = labels_str('added', label_refs_by_action('add', html)) - removed = labels_str('removed', label_refs_by_action('remove', html)) + added = labels_str(label_refs_by_action('add', html), prefix: 'added', suffix: added_suffix) + removed = labels_str(label_refs_by_action('remove', html), prefix: removed_prefix) [added, removed].compact.join(' and ') end + def removed_prefix + 'removed' + end + + def added_suffix + '' + end + # returns string containing added/removed labels including # count of deleted labels: # # added ~1 ~2 + 1 deleted label # added 3 deleted labels # added ~1 ~2 labels - def labels_str(prefix, label_refs) + def labels_str(label_refs, prefix: '', suffix: '') existing_refs = label_refs.select { |ref| ref.present? }.sort refs_str = existing_refs.empty? ? nil : existing_refs.join(' ') @@ -84,9 +92,9 @@ class LabelNote < Note return unless refs_str || deleted_str label_list_str = [refs_str, deleted_str].compact.join(' + ') - suffix = 'label'.pluralize(deleted > 0 ? deleted : existing_refs.count) + suffix += ' label'.pluralize(deleted > 0 ? deleted : existing_refs.count) - "#{prefix} #{label_list_str} #{suffix}" + "#{prefix} #{label_list_str} #{suffix.squish}" end def label_refs_by_action(action, html) diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb index f2c7cb6a65d..ad08f4763ae 100644 --- a/app/models/resource_label_event.rb +++ b/app/models/resource_label_event.rb @@ -36,10 +36,9 @@ class ResourceLabelEvent < ApplicationRecord issue || merge_request end - # create same discussion id for all actions with the same user and time def discussion_id(resource = nil) strong_memoize(:discussion_id) do - Digest::SHA1.hexdigest([self.class.name, created_at, user_id].join("-")) + Digest::SHA1.hexdigest(discussion_id_key.join("-")) end end @@ -121,4 +120,8 @@ class ResourceLabelEvent < ApplicationRecord def resource_parent issuable.project || issuable.group end + + def discussion_id_key + [self.class.name, created_at, user_id] + end end From 4000d0cc3799182113adeda5e15e4252a7bf0bdb Mon Sep 17 00:00:00 2001 From: Maxim Efimov Date: Tue, 2 Jul 2019 16:12:55 +0000 Subject: [PATCH 007/195] Fix incorrect link for prometheus AWS CloudWatch exporter --- doc/user/project/integrations/prometheus_library/cloudwatch.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/project/integrations/prometheus_library/cloudwatch.md b/doc/user/project/integrations/prometheus_library/cloudwatch.md index 66f1b587070..01da7e00d74 100644 --- a/doc/user/project/integrations/prometheus_library/cloudwatch.md +++ b/doc/user/project/integrations/prometheus_library/cloudwatch.md @@ -18,7 +18,7 @@ The [Prometheus service](../prometheus.md) must be enabled. ## Configuring Prometheus to monitor for Cloudwatch metrics -To get started with Cloudwatch monitoring, you should install and configure the [Cloudwatch exporter](https://github.com/hnlq715/nginx-vts-exporter) which retrieves and parses the specified Cloudwatch metrics and translates them into a Prometheus monitoring endpoint. +To get started with Cloudwatch monitoring, you should install and configure the [Cloudwatch exporter](https://github.com/prometheus/cloudwatch_exporter) which retrieves and parses the specified Cloudwatch metrics and translates them into a Prometheus monitoring endpoint. Right now, the only AWS resource supported is the Elastic Load Balancer, whose Cloudwatch metrics can be found [here](http://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-cloudwatch-metrics.html). From 7edeedc700cdb825aa885b53ceca8ebc145c1b23 Mon Sep 17 00:00:00 2001 From: sujay patel Date: Thu, 13 Jun 2019 01:33:21 +0530 Subject: [PATCH 008/195] Adding order by to list runner jobs api. --- app/finders/runner_jobs_finder.rb | 22 ++++++++++- .../51794-add-ordering-to-runner-jobs-api.yml | 5 +++ doc/api/runners.md | 2 + spec/finders/runner_jobs_finder_spec.rb | 37 +++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml diff --git a/app/finders/runner_jobs_finder.rb b/app/finders/runner_jobs_finder.rb index 4fca4ec94f3..f1ee1d38255 100644 --- a/app/finders/runner_jobs_finder.rb +++ b/app/finders/runner_jobs_finder.rb @@ -3,6 +3,8 @@ class RunnerJobsFinder attr_reader :runner, :params + ALLOWED_INDEXED_COLUMNS = %w[id created_at].freeze + def initialize(runner, params = {}) @runner = runner @params = params @@ -11,7 +13,7 @@ class RunnerJobsFinder def execute items = @runner.builds items = by_status(items) - items + sort_items(items) end private @@ -23,4 +25,22 @@ class RunnerJobsFinder items.where(status: params[:status]) end # rubocop: enable CodeReuse/ActiveRecord + + # rubocop: disable CodeReuse/ActiveRecord + def sort_items(items) + order_by = if ALLOWED_INDEXED_COLUMNS.include?(params[:order_by]) + params[:order_by] + else + :id + end + + sort = if params[:sort] =~ /\A(ASC|DESC)\z/i + params[:sort] + else + :desc + end + + items.order(order_by => sort) + end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml b/changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml new file mode 100644 index 00000000000..6af61d7b145 --- /dev/null +++ b/changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml @@ -0,0 +1,5 @@ +--- +title: 51794-add-order-by-to-list-runner-jobs-api +merge_request: +author: Sujay Patel +type: added diff --git a/doc/api/runners.md b/doc/api/runners.md index 2d91428d1c1..b70b954031b 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -291,6 +291,8 @@ GET /runners/:id/jobs |-----------|---------|----------|---------------------| | `id` | integer | yes | The ID of a runner | | `status` | string | no | Status of the job; one of: `running`, `success`, `failed`, `canceled` | +| `order_by`| string | no | Order jobs by `id` or `created_at` (default: id) | +| `sort` | string | no | Sort jobs in `asc` or `desc` order (default: `desc`) | ``` curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/runners/1/jobs?status=running" diff --git a/spec/finders/runner_jobs_finder_spec.rb b/spec/finders/runner_jobs_finder_spec.rb index 97304170c4e..9ed6f50ddfb 100644 --- a/spec/finders/runner_jobs_finder_spec.rb +++ b/spec/finders/runner_jobs_finder_spec.rb @@ -35,5 +35,42 @@ describe RunnerJobsFinder do end end end + + context 'when order_by and sort are specified' do + context 'when order_by created_at' do + let(:params) { { order_by: 'created_at', sort: 'asc' } } + let!(:jobs) { Array.new(2) { create(:ci_build, runner: runner, project: project, user: create(:user)) } } + + it 'sorts as created_at: :asc' do + is_expected.to match_array(jobs) + end + + context 'when sort is invalid' do + let(:params) { { order_by: 'created_at', sort: 'invalid_sort' } } + + it 'sorts as created_at: :desc' do + is_expected.to eq(jobs.sort_by { |p| -p.user.id }) + end + end + end + + context 'when order_by is invalid' do + let(:params) { { order_by: 'invalid_column', sort: 'asc' } } + let!(:jobs) { Array.new(2) { create(:ci_build, runner: runner, project: project, user: create(:user)) } } + + it 'sorts as id: :asc' do + is_expected.to eq(jobs.sort_by { |p| p.id }) + end + end + + context 'when both are nil' do + let(:params) { { order_by: nil, sort: nil } } + let!(:jobs) { Array.new(2) { create(:ci_build, runner: runner, project: project, user: create(:user)) } } + + it 'sorts as id: :desc' do + is_expected.to eq(jobs.sort_by { |p| -p.id }) + end + end + end end end From d48ee86053acabf4d52effc2a6a2ba714926821d Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 3 Jul 2019 00:13:00 -0700 Subject: [PATCH 009/195] Make Housekeeping button do a full garbage collection Previously the Housekeeping button and API would use the counter of last pushes to determine whether to do a full garbage collection, or whether to do one of the less comprehensive tasks: a full repack, incremental pack, or ref pack. This was confusing behavior, since a project owner might have to click the button dozens of times before a full GC would be initiated. This commit forces a full GC each time this is initiated. Note that the `ExclusiveLease` in `HousekeepingService` prevents users from clicking on the button more than once a day. Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/63349 --- app/controllers/projects_controller.rb | 2 +- changelogs/unreleased/sh-fix-issue-63349.yml | 5 +++ lib/api/projects.rb | 2 +- spec/controllers/projects_controller_spec.rb | 47 ++++++++++++++++++++ spec/requests/api/projects_spec.rb | 2 +- 5 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/sh-fix-issue-63349.yml diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 12db493978b..9d2f64280a3 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -182,7 +182,7 @@ class ProjectsController < Projects::ApplicationController end def housekeeping - ::Projects::HousekeepingService.new(@project).execute + ::Projects::HousekeepingService.new(@project, :gc).execute redirect_to( project_path(@project), diff --git a/changelogs/unreleased/sh-fix-issue-63349.yml b/changelogs/unreleased/sh-fix-issue-63349.yml new file mode 100644 index 00000000000..0e51a6b7b20 --- /dev/null +++ b/changelogs/unreleased/sh-fix-issue-63349.yml @@ -0,0 +1,5 @@ +--- +title: Make Housekeeping button do a full garbage collection +merge_request: 30289 +author: +type: fixed diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 1e14c77b5be..a7d62014509 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -474,7 +474,7 @@ module API authorize_admin_project begin - ::Projects::HousekeepingService.new(user_project).execute + ::Projects::HousekeepingService.new(user_project, :gc).execute rescue ::Projects::HousekeepingService::LeaseTaken => error conflict!(error.message) end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 8d2412f97ef..4e1cac67d23 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -318,6 +318,53 @@ describe ProjectsController do end end + describe '#housekeeping' do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + let(:housekeeping) { Projects::HousekeepingService.new(project) } + + context 'when authenticated as owner' do + before do + group.add_owner(user) + sign_in(user) + + allow(Projects::HousekeepingService).to receive(:new).with(project, :gc).and_return(housekeeping) + end + + it 'forces a full garbage collection' do + expect(housekeeping).to receive(:execute).once + + post :housekeeping, + params: { + namespace_id: project.namespace.path, + id: project.path + } + + expect(response).to have_gitlab_http_status(302) + end + end + + context 'when authenticated as developer' do + let(:developer) { create(:user) } + + before do + group.add_developer(developer) + end + + it 'does not execute housekeeping' do + expect(housekeeping).not_to receive(:execute) + + post :housekeeping, + params: { + namespace_id: project.namespace.path, + id: project.path + } + + expect(response).to have_gitlab_http_status(302) + end + end + end + describe "#update" do render_views diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 5f7d2fa6d9c..c67412a44c1 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -2428,7 +2428,7 @@ describe API::Projects do let(:housekeeping) { Projects::HousekeepingService.new(project) } before do - allow(Projects::HousekeepingService).to receive(:new).with(project).and_return(housekeeping) + allow(Projects::HousekeepingService).to receive(:new).with(project, :gc).and_return(housekeeping) end context 'when authenticated as owner' do From abed4d0ce3c73f549d7e70d516926f85fb4a1940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 3 Jul 2019 09:59:03 +0000 Subject: [PATCH 010/195] Rename Release groups in issue_workflow.md Based on https://about.gitlab.com/handbook/product/categories/#release-stage: - 'core release' -> 'progressive delivery' - 'supporting capabilities' -> 'release management' --- doc/development/contributing/issue_workflow.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md index f1015f56106..f4fa0caeb01 100644 --- a/doc/development/contributing/issue_workflow.md +++ b/doc/development/contributing/issue_workflow.md @@ -154,8 +154,8 @@ The current group labels are: * ~"group::ci and runner" * ~"group::testing" * ~"group::package" -* ~"group::core release" -* ~"group::supporting capabilities" +* ~"group::progressive delivery" +* ~"group::release management" * ~"group::autodevops and kubernetes" * ~"group::serverless and paas" * ~"group::apm" From 93182f4f06fa35d83a8b1467a846aa123ad82e3e Mon Sep 17 00:00:00 2001 From: Sanad Liaquat Date: Wed, 3 Jul 2019 15:12:53 +0500 Subject: [PATCH 011/195] Raise error on api call failure --- qa/qa/tools/generate_perf_testdata.rb | 63 ++++++++++++++++++++------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/qa/qa/tools/generate_perf_testdata.rb b/qa/qa/tools/generate_perf_testdata.rb index b0477951967..1b053622f1b 100644 --- a/qa/qa/tools/generate_perf_testdata.rb +++ b/qa/qa/tools/generate_perf_testdata.rb @@ -196,6 +196,7 @@ module QA project_path = "#{@group_name}%2F#{@project_name}" branch_name = "branch_with_many_commits-#{SecureRandom.hex(8)}" file_name = "file_for_many_commits.txt" + create_a_branch_api_req(branch_name, project_path) create_a_new_file_api_req(file_name, branch_name, project_path, "Initial commit for new file", "Initial file content") create_mr_response = create_a_merge_request_api_req(project_path, branch_name, "master", "MR with many commits-#{SecureRandom.hex(8)}") @@ -203,7 +204,7 @@ module QA 100.times do |i| update_file_api_req(file_name, branch_name, project_path, Faker::Lorem.sentences(5).join(" "), Faker::Lorem.sentences(500).join("\n")) end - STDOUT.puts "Created an MR with many commits: #{@urls[:mr_with_many_commits]}" + STDOUT.puts "Using branch: #{branch_name}, created an MR with many commits: #{@urls[:mr_with_many_commits]}" end private @@ -211,56 +212,88 @@ module QA # API Requests def create_a_discussion_on_issue_api_req(project_path_or_id, issue_id, body) - post Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/issues/#{issue_id}/discussions").url, "body=\"#{body}\"" + call_api(expected_response_code: 201) do + post Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/issues/#{issue_id}/discussions").url, "body=\"#{body}\"" + end end def update_a_discussion_on_issue_api_req(project_path_or_id, mr_iid, discussion_id, resolved_status) - put Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/merge_requests/#{mr_iid}/discussions/#{discussion_id}").url, "resolved=#{resolved_status}" + call_api(expected_response_code: 200) do + put Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/merge_requests/#{mr_iid}/discussions/#{discussion_id}").url, "resolved=#{resolved_status}" + end end def create_a_discussion_on_mr_api_req(project_path_or_id, mr_iid, body) - post Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/merge_requests/#{mr_iid}/discussions").url, - "body=\"#{body}\"" + call_api(expected_response_code: 201) do + post Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/merge_requests/#{mr_iid}/discussions").url, "body=\"#{body}\"" + end end def create_a_label_api_req(project_path_or_id, name, color) - post Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/labels").url, "name=#{name}&color=#{color}" + call_api(expected_response_code: 201) do + post Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/labels").url, "name=#{name}&color=#{color}" + end end def create_a_todo_api_req(project_path_or_id, issue_id) - post Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/issues/#{issue_id}/todo").url, nil + call_api(expected_response_code: 201) do + post Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/issues/#{issue_id}/todo").url, nil + end end def create_an_issue_api_req(project_path_or_id, title, description) - post Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/issues").url, "title=#{title}&description=#{description}" + call_api(expected_response_code: 201) do + post Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/issues").url, "title=#{title}&description=#{description}" + end end def update_an_issue_api_req(project_path_or_id, issue_id, description, labels_list) - put Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/issues/#{issue_id}").url, "description=#{description}&labels=#{labels_list}" + call_api(expected_response_code: 200) do + put Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/issues/#{issue_id}").url, "description=#{description}&labels=#{labels_list}" + end end def create_a_project_api_req(project_name, group_id, visibility) - post Runtime::API::Request.new(@api_client, "/projects").url, "name=#{project_name}&namespace_id=#{group_id}&visibility=#{visibility}" + call_api(expected_response_code: 201) do + post Runtime::API::Request.new(@api_client, "/projects").url, "name=#{project_name}&namespace_id=#{group_id}&visibility=#{visibility}" + end end def create_a_group_api_req(group_name, visibility) - post Runtime::API::Request.new(@api_client, "/groups").url, "name=#{group_name}&path=#{group_name}&visibility=#{visibility}" + call_api(expected_response_code: 201) do + post Runtime::API::Request.new(@api_client, "/groups").url, "name=#{group_name}&path=#{group_name}&visibility=#{visibility}" + end end def create_a_branch_api_req(branch_name, project_path_or_id) - post Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/repository/branches").url, "branch=#{branch_name}&ref=master" + call_api(expected_response_code: 201) do + post Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/repository/branches").url, "branch=#{branch_name}&ref=master" + end end def create_a_new_file_api_req(file_path, branch_name, project_path_or_id, commit_message, content) - post Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/repository/files/#{file_path}").url, "branch=#{branch_name}&commit_message=\"#{commit_message}\"&content=\"#{content}\"" + call_api(expected_response_code: 201) do + post Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/repository/files/#{file_path}").url, "branch=#{branch_name}&commit_message=\"#{commit_message}\"&content=\"#{content}\"" + end end def create_a_merge_request_api_req(project_path_or_id, source_branch, target_branch, mr_title) - post Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/merge_requests").url, "source_branch=#{source_branch}&target_branch=#{target_branch}&title=#{mr_title}" + call_api(expected_response_code: 201) do + post Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/merge_requests").url, "source_branch=#{source_branch}&target_branch=#{target_branch}&title=#{mr_title}" + end end def update_file_api_req(file_path, branch_name, project_path_or_id, commit_message, content) - put Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/repository/files/#{file_path}").url, "branch=#{branch_name}&commit_message=\"#{commit_message}\"&content=\"#{content}\"" + call_api(expected_response_code: 200) do + put Runtime::API::Request.new(@api_client, "/projects/#{project_path_or_id}/repository/files/#{file_path}").url, "branch=#{branch_name}&commit_message=\"#{commit_message}\"&content=\"#{content}\"" + end + end + + def call_api(expected_response_code: 200) + response = yield + raise "API call failed with response code: #{response.code} and body: #{response.body}" unless response.code == expected_response_code + + response end end end From 9671ca19de430e49b6cb2a51d2405c640dfddd16 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 3 Jul 2019 12:27:06 +0200 Subject: [PATCH 012/195] Re-align CE and EE API docs --- doc/api/group_boards.md | 22 ++++++++++++++++++++-- doc/api/users.md | 33 ++++++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/doc/api/group_boards.md b/doc/api/group_boards.md index 4bee05a128a..a677a9c9a33 100644 --- a/doc/api/group_boards.md +++ b/doc/api/group_boards.md @@ -27,7 +27,16 @@ Example response: [ { "id": 1, - "group_id": 5, + "name:": "group issue board", + "group": { + "id": 5, + "name": "Documentcloud", + "web_url": "http://example.com/groups/documentcloud" + }, + "milestone": { + "id": 12 + "title": "10.0" + }, "lists" : [ { "id" : 1, @@ -136,7 +145,16 @@ Example response: ```json { "id": 1, - "group_id": 5, + "name:": "group issue board", + "group": { + "id": 5, + "name": "Documentcloud", + "web_url": "http://example.com/groups/documentcloud" + }, + "milestone": { + "id": 12 + "title": "10.0" + }, "lists" : [ { "id" : 1, diff --git a/doc/api/users.md b/doc/api/users.md index 5615dcdd307..6be097e6364 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -147,6 +147,24 @@ GET /users ] ``` +Users on GitLab [Silver or higher](https://about.gitlab.com/pricing/) will also see +the `group_saml` provider option: + +```json +[ + { + "id": 1, + ... + "identities": [ + {"provider": "github", "extern_uid": "2435223452345"}, + {"provider": "bitbucket", "extern_uid": "john.smith"}, + {"provider": "google_oauth2", "extern_uid": "8776128412476123468721346"}, + {"provider": "group_saml", "extern_uid": "123789", "saml_provider_id": 10} + ], + ... + } +] + You can lookup users by external UID and provider: ``` @@ -260,14 +278,13 @@ Example Responses: "can_create_project": true, "two_factor_enabled": true, "external": false, - "private_profile": false, - "shared_runners_minutes_limit": 133 - "extra_shared_runners_minutes_limit": 133 + "private_profile": false } ``` Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see -the `shared_runners_minutes_limit` and `extra_shared_runners_minutes_limit` parameters: **[STARTER]** +the `shared_runners_minutes_limit` and `extra_shared_runners_minutes_limit` parameters. +Users on GitLab Silver will also see the `group_saml` option: ```json { @@ -275,6 +292,12 @@ the `shared_runners_minutes_limit` and `extra_shared_runners_minutes_limit` para "username": "john_smith", "shared_runners_minutes_limit": 133, "extra_shared_runners_minutes_limit": 133 + "identities": [ + {"provider": "github", "extern_uid": "2435223452345"}, + {"provider": "bitbucket", "extern_uid": "john.smith"}, + {"provider": "google_oauth2", "extern_uid": "8776128412476123468721346"}, + {"provider": "group_saml", "extern_uid": "123789", "saml_provider_id": 10} + ], ... } ``` @@ -1285,4 +1308,4 @@ Example response: Please note that `last_activity_at` is deprecated, please use `last_activity_on`. -[gemojione-index]: https://github.com/jonathanwiesel/gemojione/blob/master/config/index.json \ No newline at end of file +[gemojione-index]: https://github.com/jonathanwiesel/gemojione/blob/master/config/index.json From 8b809837f44bbebdac65eebadb09a45fb60c6a65 Mon Sep 17 00:00:00 2001 From: charlieablett Date: Sun, 16 Jun 2019 22:12:56 +1200 Subject: [PATCH 013/195] Enumerate fields with Gitaly calls - Add a complexity of 1 if Gitaly is called at least once - Add an error notification if `calls_gitaly` isn't right for a particular field --- app/graphql/types/base_field.rb | 23 +++++++- app/graphql/types/project_type.rb | 2 +- app/graphql/types/tree/tree_type.rb | 4 +- spec/graphql/types/base_field_spec.rb | 81 +++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 4 deletions(-) diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index dd0d9105df6..ee23146f711 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -4,21 +4,30 @@ module Types class BaseField < GraphQL::Schema::Field prepend Gitlab::Graphql::Authorize + attr_reader :calls_gitaly + DEFAULT_COMPLEXITY = 1 def initialize(*args, **kwargs, &block) + @calls_gitaly = !!kwargs.delete(:calls_gitaly) kwargs[:complexity] ||= field_complexity(kwargs[:resolver_class]) super(*args, **kwargs, &block) end + def base_complexity + complexity = DEFAULT_COMPLEXITY + complexity += 1 if @calls_gitaly + complexity + end + private def field_complexity(resolver_class) if resolver_class field_resolver_complexity else - DEFAULT_COMPLEXITY + base_complexity end end @@ -45,5 +54,17 @@ module Types complexity.to_i end end + + def calls_gitaly_check + # Will inform you if :calls_gitaly should be true or false based on the number of Gitaly calls + # involved with the request. + if @calls_gitaly && Gitlab::GitalyClient.get_request_count == 0 + raise "Gitaly is called for field '#{name}' - please add `calls_gitaly: true` to the field declaration" + elsif !@calls_gitaly && Gitlab::GitalyClient.get_request_count > 0 + raise "Gitaly not called for field '#{name}' - please remove `calls_gitaly: true` from the field declaration" + end + rescue => e + Gitlab::Sentry.track_exception(e) + end end end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index c25688ab043..87d5351f80f 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -26,7 +26,7 @@ module Types field :web_url, GraphQL::STRING_TYPE, null: true field :star_count, GraphQL::INT_TYPE, null: false - field :forks_count, GraphQL::INT_TYPE, null: false + field :forks_count, GraphQL::INT_TYPE, null: false, calls_gitaly: true # 4 times field :created_at, Types::TimeType, null: true field :last_activity_at, Types::TimeType, null: true diff --git a/app/graphql/types/tree/tree_type.rb b/app/graphql/types/tree/tree_type.rb index b947713074e..2023abc13f9 100644 --- a/app/graphql/types/tree/tree_type.rb +++ b/app/graphql/types/tree/tree_type.rb @@ -15,9 +15,9 @@ module Types Gitlab::Graphql::Representation::TreeEntry.decorate(obj.trees, obj.repository) end - field :submodules, Types::Tree::SubmoduleType.connection_type, null: false + field :submodules, Types::Tree::SubmoduleType.connection_type, null: false, calls_gitaly: true - field :blobs, Types::Tree::BlobType.connection_type, null: false, resolve: -> (obj, args, ctx) do + field :blobs, Types::Tree::BlobType.connection_type, null: false, calls_gitaly: true, resolve: -> (obj, args, ctx) do Gitlab::Graphql::Representation::TreeEntry.decorate(obj.blobs, obj.repository) end # rubocop: enable Graphql/AuthorizeTypes diff --git a/spec/graphql/types/base_field_spec.rb b/spec/graphql/types/base_field_spec.rb index 0d3c3e37daf..ebdfa3eaf4d 100644 --- a/spec/graphql/types/base_field_spec.rb +++ b/spec/graphql/types/base_field_spec.rb @@ -22,6 +22,24 @@ describe Types::BaseField do expect(field.to_graphql.complexity).to eq 1 end + describe '#base_complexity' do + context 'with no gitaly calls' do + it 'defaults to 1' do + field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true) + + expect(field.base_complexity).to eq 1 + end + end + + context 'with a gitaly call' do + it 'adds 1 to the default value' do + field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true) + + expect(field.base_complexity).to eq 2 + end + end + end + it 'has specified value' do field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, complexity: 12) @@ -52,5 +70,68 @@ describe Types::BaseField do end end end + + context 'calls_gitaly' do + context 'for fields with a resolver' do + it 'adds 1 if true' do + field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true) + + expect(field.to_graphql.complexity).to eq 2 + end + end + + context 'for fields without a resolver' do + it 'adds 1 if true' do + field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true) + + expect(field.to_graphql.complexity).to eq 2 + end + end + + it 'defaults to false' do + field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true) + + expect(field.base_complexity).to eq Types::BaseField::DEFAULT_COMPLEXITY + end + + it 'is overridden by declared complexity value' do + field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true, complexity: 12) + + expect(field.to_graphql.complexity).to eq 12 + end + end + + describe '#calls_gitaly_check' do + let(:gitaly_field) { described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true) } + let(:no_gitaly_field) { described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: false) } + + context 'if there are no Gitaly calls' do + before do + allow(Gitlab::GitalyClient).to receive(:get_request_count).and_return(0) + end + + it 'does not raise an error if calls_gitaly is false' do + expect { no_gitaly_field.send(:calls_gitaly_check) }.not_to raise_error + end + + it 'raises an error if calls_gitaly: true appears' do + expect { gitaly_field.send(:calls_gitaly_check) }.to raise_error(/please add `calls_gitaly: true`/) + end + end + + context 'if there is at least 1 Gitaly call' do + before do + allow(Gitlab::GitalyClient).to receive(:get_request_count).and_return(1) + end + + it 'does not raise an error if calls_gitaly is true' do + expect { gitaly_field.send(:calls_gitaly_check) }.not_to raise_error + end + + it 'raises an error if calls_gitaly is not decalared' do + expect { no_gitaly_field.send(:calls_gitaly_check) }.to raise_error(/please remove `calls_gitaly: true`/) + end + end + end end end From c99c30fdd6f3adf4fb29aad4b10e265be69d2d67 Mon Sep 17 00:00:00 2001 From: charlieablett Date: Wed, 19 Jun 2019 14:37:54 +0200 Subject: [PATCH 014/195] Remove potentially noisy warning - If Gitaly calls are missing, it could be due to a conditional and may just become noise --- app/graphql/types/base_field.rb | 2 -- spec/graphql/types/base_field_spec.rb | 4 ---- 2 files changed, 6 deletions(-) diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index ee23146f711..95db116d6f9 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -60,8 +60,6 @@ module Types # involved with the request. if @calls_gitaly && Gitlab::GitalyClient.get_request_count == 0 raise "Gitaly is called for field '#{name}' - please add `calls_gitaly: true` to the field declaration" - elsif !@calls_gitaly && Gitlab::GitalyClient.get_request_count > 0 - raise "Gitaly not called for field '#{name}' - please remove `calls_gitaly: true` from the field declaration" end rescue => e Gitlab::Sentry.track_exception(e) diff --git a/spec/graphql/types/base_field_spec.rb b/spec/graphql/types/base_field_spec.rb index ebdfa3eaf4d..d7360872508 100644 --- a/spec/graphql/types/base_field_spec.rb +++ b/spec/graphql/types/base_field_spec.rb @@ -127,10 +127,6 @@ describe Types::BaseField do it 'does not raise an error if calls_gitaly is true' do expect { gitaly_field.send(:calls_gitaly_check) }.not_to raise_error end - - it 'raises an error if calls_gitaly is not decalared' do - expect { no_gitaly_field.send(:calls_gitaly_check) }.to raise_error(/please remove `calls_gitaly: true`/) - end end end end From f4890d90782ad42a802b89c2a17c83bf9fb9d123 Mon Sep 17 00:00:00 2001 From: charlieablett Date: Fri, 21 Jun 2019 16:20:00 +0200 Subject: [PATCH 015/195] Alert if `calls_gitaly` declaration missing - Move `calls_gitaly_check` to public - Add instrumentation for flagging missing CallsGitaly declarations - Wrap resolver proc in before-and-after Gitaly counts to get the net Gitaly call count for the resolver. --- app/graphql/gitlab_schema.rb | 1 + app/graphql/types/base_field.rb | 22 +++++++------ lib/gitlab/graphql/authorize.rb | 2 +- lib/gitlab/graphql/calls_gitaly.rb | 15 +++++++++ .../graphql/calls_gitaly/instrumentation.rb | 30 ++++++++++++++++++ spec/graphql/gitlab_schema_spec.rb | 4 +++ spec/graphql/types/base_field_spec.rb | 18 +++-------- spec/requests/api/graphql_spec.rb | 31 +++++++++++++++++++ 8 files changed, 99 insertions(+), 24 deletions(-) create mode 100644 lib/gitlab/graphql/calls_gitaly.rb create mode 100644 lib/gitlab/graphql/calls_gitaly/instrumentation.rb diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index 5615909c4ec..152ebb930e2 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -13,6 +13,7 @@ class GitlabSchema < GraphQL::Schema use BatchLoader::GraphQL use Gitlab::Graphql::Authorize use Gitlab::Graphql::Present + use Gitlab::Graphql::CallsGitaly use Gitlab::Graphql::Connections use Gitlab::Graphql::GenericTracing diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index 95db116d6f9..64bc7e6474f 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -21,6 +21,18 @@ module Types complexity end + def calls_gitaly_check(calls) + return if @calls_gitaly + + # Will inform you if :calls_gitaly should be true or false based on the number of Gitaly calls + # involved with the request. + if calls > 0 + raise "Gitaly is called for field '#{name}' - please add `calls_gitaly: true` to the field declaration" + end + rescue => e + Gitlab::Sentry.track_exception(e) + end + private def field_complexity(resolver_class) @@ -54,15 +66,5 @@ module Types complexity.to_i end end - - def calls_gitaly_check - # Will inform you if :calls_gitaly should be true or false based on the number of Gitaly calls - # involved with the request. - if @calls_gitaly && Gitlab::GitalyClient.get_request_count == 0 - raise "Gitaly is called for field '#{name}' - please add `calls_gitaly: true` to the field declaration" - end - rescue => e - Gitlab::Sentry.track_exception(e) - end end end diff --git a/lib/gitlab/graphql/authorize.rb b/lib/gitlab/graphql/authorize.rb index f8d0208e275..e83b567308b 100644 --- a/lib/gitlab/graphql/authorize.rb +++ b/lib/gitlab/graphql/authorize.rb @@ -8,7 +8,7 @@ module Gitlab extend ActiveSupport::Concern def self.use(schema_definition) - schema_definition.instrument(:field, Instrumentation.new, after_built_ins: true) + schema_definition.instrument(:field, Gitlab::Graphql::Authorize::Instrumentation.new, after_built_ins: true) end end end diff --git a/lib/gitlab/graphql/calls_gitaly.rb b/lib/gitlab/graphql/calls_gitaly.rb new file mode 100644 index 00000000000..f75941e269f --- /dev/null +++ b/lib/gitlab/graphql/calls_gitaly.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + # Allow fields to declare permissions their objects must have. The field + # will be set to nil unless all required permissions are present. + module CallsGitaly + extend ActiveSupport::Concern + + def self.use(schema_definition) + schema_definition.instrument(:field, Gitlab::Graphql::CallsGitaly::Instrumentation.new, after_built_ins: true) + end + end + end +end diff --git a/lib/gitlab/graphql/calls_gitaly/instrumentation.rb b/lib/gitlab/graphql/calls_gitaly/instrumentation.rb new file mode 100644 index 00000000000..ca54e12c049 --- /dev/null +++ b/lib/gitlab/graphql/calls_gitaly/instrumentation.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + module CallsGitaly + class Instrumentation + # Check if any `calls_gitaly: true` declarations need to be added + def instrument(_type, field) + type_object = field.metadata[:type_class] + return field unless type_object && type_object.respond_to?(:calls_gitaly_check) + + old_resolver_proc = field.resolve_proc + wrapped_proc = gitaly_wrapped_resolve(old_resolver_proc, type_object) + field.redefine { resolve(wrapped_proc) } + end + + def gitaly_wrapped_resolve(old_resolver_proc, type_object) + proc do |parent_typed_object, args, ctx| + previous_gitaly_call_count = Gitlab::GitalyClient.get_request_count + + old_resolver_proc.call(parent_typed_object, args, ctx) + + current_gitaly_call_count = Gitlab::GitalyClient.get_request_count + type_object.calls_gitaly_check(current_gitaly_call_count - previous_gitaly_call_count) + end + end + end + end + end +end diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb index d36e428a8ee..93b86b9b812 100644 --- a/spec/graphql/gitlab_schema_spec.rb +++ b/spec/graphql/gitlab_schema_spec.rb @@ -21,6 +21,10 @@ describe GitlabSchema do expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::Present::Instrumentation)) end + it 'enables using gitaly call checker' do + expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::CallsGitaly::Instrumentation)) + end + it 'has the base mutation' do expect(described_class.mutation).to eq(::Types::MutationType.to_graphql) end diff --git a/spec/graphql/types/base_field_spec.rb b/spec/graphql/types/base_field_spec.rb index d7360872508..0be83ea60c4 100644 --- a/spec/graphql/types/base_field_spec.rb +++ b/spec/graphql/types/base_field_spec.rb @@ -106,26 +106,18 @@ describe Types::BaseField do let(:no_gitaly_field) { described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: false) } context 'if there are no Gitaly calls' do - before do - allow(Gitlab::GitalyClient).to receive(:get_request_count).and_return(0) - end - it 'does not raise an error if calls_gitaly is false' do - expect { no_gitaly_field.send(:calls_gitaly_check) }.not_to raise_error - end - - it 'raises an error if calls_gitaly: true appears' do - expect { gitaly_field.send(:calls_gitaly_check) }.to raise_error(/please add `calls_gitaly: true`/) + expect { no_gitaly_field.send(:calls_gitaly_check, 0) }.not_to raise_error end end context 'if there is at least 1 Gitaly call' do - before do - allow(Gitlab::GitalyClient).to receive(:get_request_count).and_return(1) + it 'does not raise an error if calls_gitaly is true' do + expect { gitaly_field.send(:calls_gitaly_check, 1) }.not_to raise_error end - it 'does not raise an error if calls_gitaly is true' do - expect { gitaly_field.send(:calls_gitaly_check) }.not_to raise_error + it 'raises an error if calls_gitaly: is false or not defined' do + expect { no_gitaly_field.send(:calls_gitaly_check, 1) }.to raise_error(/please add `calls_gitaly: true`/) end end end diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb index 656d6f8b50b..d78b17827a6 100644 --- a/spec/requests/api/graphql_spec.rb +++ b/spec/requests/api/graphql_spec.rb @@ -131,4 +131,35 @@ describe 'GraphQL' do end end end + + describe 'testing for Gitaly calls' do + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + let(:query) do + graphql_query_for('project', { 'fullPath' => project.full_path }, %w(forksCount)) + end + + before do + project.add_developer(user) + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: user) + end + end + + context 'when Gitaly is called' do + before do + allow(Gitlab::GitalyClient).to receive(:get_request_count).and_return(1, 2) + end + + it "logs a warning that the 'calls_gitaly' field declaration is missing" do + expect(Gitlab::Sentry).to receive(:track_exception).once + + post_graphql(query, current_user: user) + end + end + end end From a11fe5de4408595cc8b2b091cbbb76e423c98f34 Mon Sep 17 00:00:00 2001 From: charlieablett Date: Wed, 26 Jun 2019 22:42:25 +1200 Subject: [PATCH 016/195] Wrap proc properly in gitaly call counts - Add `calls_gitaly: true` to some fields missing (hey, it works!) - Clarify proc wrapping - Add kwargs argument to `mount_mutation` --- app/graphql/types/base_field.rb | 5 ++--- app/graphql/types/merge_request_type.rb | 2 +- app/graphql/types/mutation_type.rb | 2 +- .../types/permission_types/merge_request.rb | 4 ++-- app/graphql/types/project_type.rb | 2 +- app/graphql/types/repository_type.rb | 6 +++--- .../graphql/calls_gitaly/instrumentation.rb | 17 +++++++++-------- lib/gitlab/graphql/mount_mutation.rb | 5 +++-- 8 files changed, 22 insertions(+), 21 deletions(-) diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index 64bc7e6474f..42c7eb6b485 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -23,12 +23,11 @@ module Types def calls_gitaly_check(calls) return if @calls_gitaly + return if calls < 1 # Will inform you if :calls_gitaly should be true or false based on the number of Gitaly calls # involved with the request. - if calls > 0 - raise "Gitaly is called for field '#{name}' - please add `calls_gitaly: true` to the field declaration" - end + raise "Gitaly is called for field '#{name}' #{"on type #{owner.name} " if owner}- please add `calls_gitaly: true` to the field declaration" rescue => e Gitlab::Sentry.track_exception(e) end diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 577ccd48ef8..6734d4761c2 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -43,7 +43,7 @@ module Types field :allow_collaboration, GraphQL::BOOLEAN_TYPE, null: true field :should_be_rebased, GraphQL::BOOLEAN_TYPE, method: :should_be_rebased?, null: false field :rebase_commit_sha, GraphQL::STRING_TYPE, null: true - field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false + field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false, calls_gitaly: true field :merge_commit_message, GraphQL::STRING_TYPE, method: :default_merge_commit_message, null: true, deprecation_reason: "Renamed to defaultMergeCommitMessage" field :default_merge_commit_message, GraphQL::STRING_TYPE, null: true field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 6ef1d816b7c..bc5fb709522 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -9,6 +9,6 @@ module Types mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Remove mount_mutation Mutations::AwardEmojis::Toggle - mount_mutation Mutations::MergeRequests::SetWip + mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true end end diff --git a/app/graphql/types/permission_types/merge_request.rb b/app/graphql/types/permission_types/merge_request.rb index 13995d3ea8f..d877fc177d2 100644 --- a/app/graphql/types/permission_types/merge_request.rb +++ b/app/graphql/types/permission_types/merge_request.rb @@ -10,8 +10,8 @@ module Types abilities :read_merge_request, :admin_merge_request, :update_merge_request, :create_note - permission_field :push_to_source_branch, method: :can_push_to_source_branch? - permission_field :remove_source_branch, method: :can_remove_source_branch? + permission_field :push_to_source_branch, method: :can_push_to_source_branch?, calls_gitaly: true + permission_field :remove_source_branch, method: :can_remove_source_branch?, calls_gitaly: true permission_field :cherry_pick_on_current_merge_request, method: :can_cherry_pick_on_current_merge_request? permission_field :revert_on_current_merge_request, method: :can_revert_on_current_merge_request? end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 87d5351f80f..13be71c26ee 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -40,7 +40,7 @@ module Types field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true field :merge_requests_ff_only_enabled, GraphQL::BOOLEAN_TYPE, null: true - field :avatar_url, GraphQL::STRING_TYPE, null: true, resolve: -> (project, args, ctx) do + field :avatar_url, GraphQL::STRING_TYPE, null: true, calls_gitaly: true, resolve: -> (project, args, ctx) do project.avatar_url(only_path: false) end diff --git a/app/graphql/types/repository_type.rb b/app/graphql/types/repository_type.rb index 5987467e1ea..b024eca61fc 100644 --- a/app/graphql/types/repository_type.rb +++ b/app/graphql/types/repository_type.rb @@ -6,9 +6,9 @@ module Types authorize :download_code - field :root_ref, GraphQL::STRING_TYPE, null: true - field :empty, GraphQL::BOOLEAN_TYPE, null: false, method: :empty? + field :root_ref, GraphQL::STRING_TYPE, null: true, calls_gitaly: true + field :empty, GraphQL::BOOLEAN_TYPE, null: false, method: :empty?, calls_gitaly: true field :exists, GraphQL::BOOLEAN_TYPE, null: false, method: :exists? - field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver + field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true end end diff --git a/lib/gitlab/graphql/calls_gitaly/instrumentation.rb b/lib/gitlab/graphql/calls_gitaly/instrumentation.rb index ca54e12c049..08e98028755 100644 --- a/lib/gitlab/graphql/calls_gitaly/instrumentation.rb +++ b/lib/gitlab/graphql/calls_gitaly/instrumentation.rb @@ -10,18 +10,19 @@ module Gitlab return field unless type_object && type_object.respond_to?(:calls_gitaly_check) old_resolver_proc = field.resolve_proc - wrapped_proc = gitaly_wrapped_resolve(old_resolver_proc, type_object) - field.redefine { resolve(wrapped_proc) } - end - def gitaly_wrapped_resolve(old_resolver_proc, type_object) - proc do |parent_typed_object, args, ctx| + gitaly_wrapped_resolve = -> (typed_object, args, ctx) do previous_gitaly_call_count = Gitlab::GitalyClient.get_request_count - - old_resolver_proc.call(parent_typed_object, args, ctx) - + result = old_resolver_proc.call(typed_object, args, ctx) current_gitaly_call_count = Gitlab::GitalyClient.get_request_count type_object.calls_gitaly_check(current_gitaly_call_count - previous_gitaly_call_count) + result + rescue => e + ap "#{e.message}" + end + + field.redefine do + resolve(gitaly_wrapped_resolve) end end end diff --git a/lib/gitlab/graphql/mount_mutation.rb b/lib/gitlab/graphql/mount_mutation.rb index 9048967d4e1..b10e963170a 100644 --- a/lib/gitlab/graphql/mount_mutation.rb +++ b/lib/gitlab/graphql/mount_mutation.rb @@ -6,11 +6,12 @@ module Gitlab extend ActiveSupport::Concern class_methods do - def mount_mutation(mutation_class) + def mount_mutation(mutation_class, **custom_kwargs) # Using an underscored field name symbol will make `graphql-ruby` # standardize the field name field mutation_class.graphql_name.underscore.to_sym, - mutation: mutation_class + mutation: mutation_class, + **custom_kwargs end end end From cf1b0d10bcdde69f05695a2e9a0d380c6badb6d1 Mon Sep 17 00:00:00 2001 From: charlieablett Date: Fri, 28 Jun 2019 13:24:47 +1200 Subject: [PATCH 017/195] Address reviewer comments - Add 1 for all fields that call Gitaly (with resolvers or without) - Clarify comment regarding Gitaly call alert - Expose predicate `calls_gitaly?` instead of ivar --- app/graphql/types/base_field.rb | 16 +++------- .../graphql/calls_gitaly/instrumentation.rb | 16 +++++++--- spec/graphql/types/base_field_spec.rb | 27 +++-------------- .../calls_gitaly/instrumentation_spec.rb | 29 +++++++++++++++++++ spec/requests/api/graphql_spec.rb | 2 +- 5 files changed, 50 insertions(+), 40 deletions(-) create mode 100644 spec/lib/gitlab/graphql/calls_gitaly/instrumentation_spec.rb diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index 42c7eb6b485..6b377e88e16 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -4,8 +4,6 @@ module Types class BaseField < GraphQL::Schema::Field prepend Gitlab::Graphql::Authorize - attr_reader :calls_gitaly - DEFAULT_COMPLEXITY = 1 def initialize(*args, **kwargs, &block) @@ -17,19 +15,12 @@ module Types def base_complexity complexity = DEFAULT_COMPLEXITY - complexity += 1 if @calls_gitaly + complexity += 1 if calls_gitaly? complexity end - def calls_gitaly_check(calls) - return if @calls_gitaly - return if calls < 1 - - # Will inform you if :calls_gitaly should be true or false based on the number of Gitaly calls - # involved with the request. - raise "Gitaly is called for field '#{name}' #{"on type #{owner.name} " if owner}- please add `calls_gitaly: true` to the field declaration" - rescue => e - Gitlab::Sentry.track_exception(e) + def calls_gitaly? + @calls_gitaly end private @@ -51,6 +42,7 @@ module Types proc do |ctx, args, child_complexity| # Resolvers may add extra complexity depending on used arguments complexity = child_complexity + self.resolver&.try(:resolver_complexity, args, child_complexity: child_complexity).to_i + complexity += 1 if calls_gitaly? field_defn = to_graphql diff --git a/lib/gitlab/graphql/calls_gitaly/instrumentation.rb b/lib/gitlab/graphql/calls_gitaly/instrumentation.rb index 08e98028755..e2733a1416f 100644 --- a/lib/gitlab/graphql/calls_gitaly/instrumentation.rb +++ b/lib/gitlab/graphql/calls_gitaly/instrumentation.rb @@ -7,7 +7,7 @@ module Gitlab # Check if any `calls_gitaly: true` declarations need to be added def instrument(_type, field) type_object = field.metadata[:type_class] - return field unless type_object && type_object.respond_to?(:calls_gitaly_check) + return field unless type_object && type_object.respond_to?(:calls_gitaly?) old_resolver_proc = field.resolve_proc @@ -15,16 +15,24 @@ module Gitlab previous_gitaly_call_count = Gitlab::GitalyClient.get_request_count result = old_resolver_proc.call(typed_object, args, ctx) current_gitaly_call_count = Gitlab::GitalyClient.get_request_count - type_object.calls_gitaly_check(current_gitaly_call_count - previous_gitaly_call_count) + calls_gitaly_check(type_object, current_gitaly_call_count - previous_gitaly_call_count) result - rescue => e - ap "#{e.message}" end field.redefine do resolve(gitaly_wrapped_resolve) end end + + def calls_gitaly_check(type_object, calls) + return if type_object.calls_gitaly? + return if calls < 1 + + # Will inform you if there needs to be `calls_gitaly: true` as a kwarg in the field declaration + # if there is at least 1 Gitaly call involved with the field resolution. + error = RuntimeError.new("Gitaly is called for field '#{type_object.name}' on #{type_object.owner.try(:name)} - please add `calls_gitaly: true` to the field declaration") + Gitlab::Sentry.track_exception(error) + end end end end diff --git a/spec/graphql/types/base_field_spec.rb b/spec/graphql/types/base_field_spec.rb index 0be83ea60c4..10913e530cf 100644 --- a/spec/graphql/types/base_field_spec.rb +++ b/spec/graphql/types/base_field_spec.rb @@ -74,9 +74,11 @@ describe Types::BaseField do context 'calls_gitaly' do context 'for fields with a resolver' do it 'adds 1 if true' do - field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true) + with_gitaly_field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, null: true, calls_gitaly: true) + without_gitaly_field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, null: true) + base_result = without_gitaly_field.to_graphql.complexity.call({}, {}, 2) - expect(field.to_graphql.complexity).to eq 2 + expect(with_gitaly_field.to_graphql.complexity.call({}, {}, 2)).to eq base_result + 1 end end @@ -100,26 +102,5 @@ describe Types::BaseField do expect(field.to_graphql.complexity).to eq 12 end end - - describe '#calls_gitaly_check' do - let(:gitaly_field) { described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true) } - let(:no_gitaly_field) { described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: false) } - - context 'if there are no Gitaly calls' do - it 'does not raise an error if calls_gitaly is false' do - expect { no_gitaly_field.send(:calls_gitaly_check, 0) }.not_to raise_error - end - end - - context 'if there is at least 1 Gitaly call' do - it 'does not raise an error if calls_gitaly is true' do - expect { gitaly_field.send(:calls_gitaly_check, 1) }.not_to raise_error - end - - it 'raises an error if calls_gitaly: is false or not defined' do - expect { no_gitaly_field.send(:calls_gitaly_check, 1) }.to raise_error(/please add `calls_gitaly: true`/) - end - end - end end end diff --git a/spec/lib/gitlab/graphql/calls_gitaly/instrumentation_spec.rb b/spec/lib/gitlab/graphql/calls_gitaly/instrumentation_spec.rb new file mode 100644 index 00000000000..92d200bfd4e --- /dev/null +++ b/spec/lib/gitlab/graphql/calls_gitaly/instrumentation_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::Graphql::CallsGitaly::Instrumentation do + subject { described_class.new } + + context 'when considering complexity' do + describe '#calls_gitaly_check' do + let(:gitaly_field) { Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true) } + let(:no_gitaly_field) { Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, calls_gitaly: false) } + + context 'if there are no Gitaly calls' do + it 'does not raise an error if calls_gitaly is false' do + expect { subject.send(:calls_gitaly_check, no_gitaly_field, 0) }.not_to raise_error + end + end + + context 'if there is at least 1 Gitaly call' do + it 'does not raise an error if calls_gitaly is true' do + expect { subject.send(:calls_gitaly_check, gitaly_field, 1) }.not_to raise_error + end + + it 'raises an error if calls_gitaly: is false or not defined' do + expect { subject.send(:calls_gitaly_check, no_gitaly_field, 1) }.to raise_error(/please add `calls_gitaly: true`/) + end + end + end + end +end diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb index d78b17827a6..67371cb35b6 100644 --- a/spec/requests/api/graphql_spec.rb +++ b/spec/requests/api/graphql_spec.rb @@ -137,7 +137,7 @@ describe 'GraphQL' do let(:user) { create(:user) } let(:query) do - graphql_query_for('project', { 'fullPath' => project.full_path }, %w(forksCount)) + graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id)) end before do From 1e225340190187c96960e138ab218504165cb351 Mon Sep 17 00:00:00 2001 From: Sanad Liaquat Date: Wed, 3 Jul 2019 18:22:55 +0500 Subject: [PATCH 018/195] Return created group id --- qa/qa/tools/generate_perf_testdata.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qa/qa/tools/generate_perf_testdata.rb b/qa/qa/tools/generate_perf_testdata.rb index 1b053622f1b..26bcb2fe958 100644 --- a/qa/qa/tools/generate_perf_testdata.rb +++ b/qa/qa/tools/generate_perf_testdata.rb @@ -59,8 +59,8 @@ module QA group_search_response = create_a_group_api_req(@group_name, @visibility) group = JSON.parse(group_search_response.body) @urls[:group_page] = group["web_url"] - group["id"] STDOUT.puts "Created a group: #{@urls[:group_page]}" + group["id"] end def create_project(group_id) From c3cf460f34649661a3e22fbb6417d57e0873bc43 Mon Sep 17 00:00:00 2001 From: Aurelien Date: Wed, 3 Jul 2019 13:28:01 +0000 Subject: [PATCH 019/195] Minor spelling errors fixed --- doc/user/project/settings/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index 2bf8d4dfe7b..01763c49207 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -9,7 +9,7 @@ to your project's homepage and clicking **Settings**. ## General settings -Under a project's general settings you can find everything concerning the +Under a project's general settings, you can find everything concerning the functionality of a project. ### General project settings @@ -26,7 +26,7 @@ Set up your project's access, [visibility](../../../public_access/public_access. ![projects sharing permissions](img/sharing_and_permissions_settings.png) -If Issues are disabled, or you can't access Issues because you're not a project member, then Lables and Milestones +If Issues are disabled, or you can't access Issues because you're not a project member, then Labels and Milestones links will be missing from the sidebar UI. You can still access them with direct links if you can access Merge Requests. This is deliberate, if you can see @@ -96,7 +96,7 @@ To rename a repository: 1. Hit **Rename project**. Remember that this can have unintended side effects since everyone with the -old URL will not be able to push or pull. Read more about what happens with the +old URL will not be able to push or pull. Read more about what happens with the [redirects when renaming repositories](../index.md#redirects-when-changing-repository-paths). #### Transferring an existing project into another namespace From eaf58debac14474dc7ad5e5a6a6fc88da0af1370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 3 Jul 2019 16:02:03 +0200 Subject: [PATCH 020/195] Only save Peek session in Redis when Peek is enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- config/initializers/peek.rb | 3 +++ .../redis_adapter_when_peek_enabled.rb | 12 ++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb diff --git a/config/initializers/peek.rb b/config/initializers/peek.rb index cb108416b10..d492b60705d 100644 --- a/config/initializers/peek.rb +++ b/config/initializers/peek.rb @@ -42,3 +42,6 @@ class PEEK_DB_CLIENT end PEEK_DB_VIEW.prepend ::Gitlab::PerformanceBar::PeekQueryTracker + +require 'peek/adapters/redis' +Peek::Adapters::Redis.prepend ::Gitlab::PerformanceBar::RedisAdapterWhenPeekEnabled diff --git a/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb b/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb new file mode 100644 index 00000000000..2d997760c46 --- /dev/null +++ b/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Adapted from https://github.com/peek/peek/blob/master/lib/peek/adapters/redis.rb +module Gitlab + module PerformanceBar + module RedisAdapterWhenPeekEnabled + def save + super unless ::Peek.request_id.blank? + end + end + end +end From 564acca1119b78b0d86f1d214c2d4deebfa09e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C5=82gorzata=20Ksionek?= Date: Wed, 29 May 2019 10:11:27 +0200 Subject: [PATCH 021/195] Add salesforce logo --- app/assets/images/auth_buttons/salesforce_64.png | Bin 0 -> 8774 bytes app/helpers/auth_helper.rb | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 app/assets/images/auth_buttons/salesforce_64.png diff --git a/app/assets/images/auth_buttons/salesforce_64.png b/app/assets/images/auth_buttons/salesforce_64.png new file mode 100644 index 0000000000000000000000000000000000000000..c8a86a0c5153da2802a38009c2e9781d86b4ad23 GIT binary patch literal 8774 zcmeHMc|4TuyC1S;ClOi3SW1kUVGJ{oWhmQNvP8CKc`yva7`u`pYbZ)eB$TL>?6QVP z)+|Mftks)+jnoa?|ohO^W2|_wJ&q#7<^yOPB zySE^1))kzko%AlYcTjQRy_#PT`*Il9eEIxv=s?Y2O?BporOCvrzG2(WecrMU6f&Q3 z)-}3JKFWC4LakX>)A1^{@gISQEk?vpv%W69q1JW!t~Cf++39MWgX?Kdd8BqXYTS_B zSUwoDBTH1#jwd-jgSV)3O_loS%ayw-i%YLXHP1%OZ@8?^H_d*l-Rn-xsa!pFvN_J5 zbxfokKK@xdkSXS!x#?nTQQ+$P^)+JsVgBd)o?qfwF`1P{Zopp0u}=j7Pw#y+BEuF% zPxB)JT@i=Q)Z2tTuU#|Iq*1M%WAzhMgwgcT}yLQd3DJvAqyGkBs z1*^nDbg;}!w#Mk)3-|Q0XAHAa{gmx#9ne)llLM8hkY;*;Nvf=UT#Plj*Yob3$>Rlm z#~r4jB{2!WpmB<=gJ+E@+5GxL%xa*Sdw0IkBjMfmhD!K8sP?wzaEtnv^tfH|g)n(v zR(RBj+F0N^aIa+HfQhY>*D}vwz-#i9Q`zUL;Tc$;r1iU;6@f~roJpy^My6*oDX@Is z(Vmsm0$77UR#O(l-&n@*P9XM2KVFmWN7QE{8 zTD)iTc12WIvLD-p z@`3HAd-t!~p3Zyy=)IW5bo;scx*kKNDxViZn@YJXPFqXH4hiLa7B8sU-YvmiGM?xgQ5=iQ>f875+QCR;yJfy+`GVxb}zll#)~ zNoS=GzQoE{#V?u)iX5%FFL?ZpL_~`IN$32TOZd5Se#DPgRF|s;9zVqPrLZ4~HL^lI zksRiHpGj>at)g6-&t7mv4O58CXcm#kxUYd6<&XA!X!#wM4|erblECHNJE6j#B+=9r=(5BRv4+-gt4? zrfQhqFQu34KztY^%QBMlQpVGvRjpFQ823`X(0nA@5puo*1Cp1&`P_BVg&gKmIH6?obTD+VXF#jnsByym}E)l01uiH|bU2(h>UC_~X>bcxaw9qpp6!rLY zOyYd1d_XSJYVIn_V1z_S(3JxSS9t*Ad_;&B~lU!VWoifKv zxzPA9k#67rICWp7>NU#Pq&nrylf3$Mjl0RmD|I;e`SabKPIkQvA?59QgJ9jI+lG9$ z6PQn07}#87-^D{yDb$bhwCgL7nV3%WQn=kaRo!cTU^Brs#iGbKR>tqfIiE~|`$4A( z)Nv>=EHbn3(n!e7yVj~Cfg_p9Y0f!f$THr$-zMf7&UBc9Z=b|I$4=?A6uSXme<#t5|;QKa^!N)8bHQ$O!} zj0E3?@pD8>r-L4(6an2w8JsE4lWJ+S7gwHOFSwP)gX%~=LH ze0I6;T2ag3W^T8&65&%t))ho#jd*jOWPurGho%r9`fA^q-Ntvxvs?}j%i?*Br$8zT zQ5O>w8`gJT!H$fQg0Ed9{HtX{+_kI7p|=#6@`K*`vptY= zUeg|sZVJJLtf$ufBvl$Ni&QmfdrOPk4M2=*-+5rKj!3rgRE*y(t%yzZiHU;N#fxz$ zIX1ew)Lb}}>Pu|w6b(d{>GaP{h` z;JPQo1$1Oj-j!3<#N7>6^{ybtzu-Zgy6- zFw_z0Nza9{8L?A6B|ul(Y? zj3Ens?!4GByO7-`PiE7icD#7eK|maE3ZY$ro7UEJ-)NtqeR)v}S&qL|SkAjh6XB!B)3SIy&?*@YYN!2USb8?3oCUOQ)U_3YY%#G;O_n_F(!1j(rfV2jv> zhwS;{qA`^4T`%satGZI;u$Z;!Pq-|6(*XuipK zarNHlx&f@9AAZLahYmX2{X;)iP__ntN$<2SV~@o%Coa%zXa82G9d zzVuzoqNJBq{Tr8&cIqadT5l2v#1u)!Vl9lY*x#S+8Bg>XCogCjKG6|xcCpI4X~P~L zqe03y6V^8nFXc4I=l6)Rl`vJ_13G&sDbIt;K{8}H5q&-(6+FrakC*iWn zF}JMT^5|>VCc3IWqxtk{L(M1LFXWx~We%0_bZ#^W*rwWy`i)wqC>wrl^=}?i?_^D5 z%@jqvxPu<$cxdO2&x^9}>5JJHn^SPhsy2DvZsKvXy`qiXSmxKL_ZhX1oBXe04Now| zCxi8LS@-U5IGkX6U5HCzC~0Gj-KkDrIm&VGiSa8(M8!yheBapXs!JDkjX59H;4P`s z=zI$2A7cGv^YqK1fQ{9$bOmu}TL?zp9GRD>%1AkbaScR05zJHrMbZnXfzrISAnUhKp6<2!t^X{z=XlB^m@VI)6IYYH!o~(Md2PMgz&14+8*Gfx|VRa22Qu8uqI_V-$z` z!`j>Lr-}?cVL^Bw7(y8i^YZ$Og&*A@;7@=5(!$T0apZ*^1^lS~G$LRS0C>}7es$_Y z@%Q`Hr@tSt)wLZrh2#oj1hwt?SDPb7IEz1Qwq$f8d--fzY@vTel8ArceEeyYZ48MB z11NwOBM?6ZGvY6JI@$Hl1^UZ;wnqL-APjeZ@c#w zkf=m53A6nZPShX(D544!P9PAWNCXK9#jBDCP*pV~fvE0^c6C)J{z7Hs?MKIZ6M-!% z2Dvhs!2?IB!BMJe>QGm_IsuAA5eQHMQUic$005e#p-w`f5x-KH)5wfUz*ByWYKw}* zpi+es)d>hZ5=ug-BcMnXI39{d!AVdQ0fi^15{P&-0>2&2);eHxEsV4vD$4Lbdn_n; zx+|6Dr3Jy^iC}Z9KS!*|UVs%Hzoi;N4W)uWsi`9oD7c2Y3i3~oEkN^QWcd~;0mImPIRGKxFO3{LB z1qmb3Xq^A zk}95&$7n|Kx#Hna1RjM%pw&<)I702O?0!^NdLW(#=(;g-fU(Yu+S*=c@ctj^r|?&J zpgXXYTW|y#3Rj21Rjm;yjG7t-iBN>AVBm1bk0l3d!nStV->0m(wGZKNm|sQF+}fuw zMqAlvJ!YUWOurGp!dIZ|1W?)7);4T zz}t`d??V3-@?2nt~&s5o}rvJz5XA1s5dSFohwAvg;qY{+0rN3;a)Z{h!Ij`RB14@Mb*x1u_oRZC9I<83%GUg2@qm(D$v^ zjhfpjjGmo7hW36Skci;co5?M!ckBODcDfPHfPIX82d}zP8k$ab zpa|LQIbDZVtsb7%;}T*PGQ2C0FU+);10^lQeZWv(pAc4mg~KkDS%I08`Q}=^oVlezEgtfOldoHBLqtMrcW>i79WCk@k?^#yL8G0ac3-?6f*A))Rx zP<^nwa!MXK`%*YbdD>%XPIjTV)_wothak;bw0mYW)pr_5a7Sc++WlPosHhZE;VI+W zPEVaUmcQA)(>0FG7JRB=`Z38VkGa0L5@~md3?8=0M$VT=USEbKiv7P%B?8esPHViA*a%<4=)SPONP1z zxH;SROLP-bm^_oX<&ulbCk#e;W%7)?`r#HVMr~u3eFFPQ(#YB6r>vAhZu0jS!29-1 zieh|AUx{OVONvX5N6yT%DmkxR2JRi2Mz z4q0QpwKD9#5qMgAVm^{nQYZhTENj3~I%R#DHR$o?F29aKjDjboap$ZROwe@l*JE|^a zrKXARtM3+3ozAuztkw-~v~0N6GG*H4=v=o_epiB*^PA#*wtdXS&N{heEDFmk;gfRb zYEyR%%UdMB01s5T=C7^lJvLI^MITt*b?16>NkraxR3ympj-5`LvpKl$ZLHkvIfLwaI|NVksrQk8+oRTd~syHGk6+l`X(CY6ZLF?<{Umw@rx!` z$ds&Gu(c^TSe8Zv)_}CE>rMEC$||Ki=Ahcu2_|mmZbVOlr$?MHi-KB6m~g|p3?_Az z>ZT)Ex;YkFmGz*=tDINtdg^+@x>_b?RfpH(-Nyk&ddK!B-{T^TWer*+R=QqhR{!bjCVB89I8N}|p~miG@XFcO zdr!^7ALVn^g^7#~Cl$_+j+P!eAD>hgG5Fn#yQ=sJOV0UF6&SLG$ za}w!ReYO)9vrZmzp0)0|Rk&|CJs}&WYh1lswJxb39y=x)Hq3gOM0;ltSrAPS+qj1Q zO?PRIJ3xr%HuyH=t&Lh)9;KrTYshJ;BT#7=4Aa9sDT8qMiVb5T0q?QVk1%@;OhWwc emx<#Sc8!c|_m2IOqKuslWMp8be_!v!ng0MA2y~(V literal 0 HcmV?d00001 diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 076976175a9..31c4b27273b 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module AuthHelper - PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze + PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq salesforce).freeze LDAP_PROVIDER = /\Aldap/.freeze def ldap_enabled? From 55ed4a405e814453f2f1722214e61deffd799c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C5=82gorzata=20Ksionek?= Date: Wed, 29 May 2019 10:25:47 +0200 Subject: [PATCH 022/195] Add changelog entry --- changelogs/unreleased/add-salesforce-logo.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/add-salesforce-logo.yml diff --git a/changelogs/unreleased/add-salesforce-logo.yml b/changelogs/unreleased/add-salesforce-logo.yml new file mode 100644 index 00000000000..13766821b88 --- /dev/null +++ b/changelogs/unreleased/add-salesforce-logo.yml @@ -0,0 +1,5 @@ +--- +title: Add salesforce logo for salesforce SSO +merge_request: 28857 +author: +type: changed From def94f5043abe57f453094fa570407e5b806c48a Mon Sep 17 00:00:00 2001 From: Maneschi Romain Date: Wed, 3 Jul 2019 16:09:51 +0000 Subject: [PATCH 023/195] Add Grafana to Admin > Monitoring menu when enabled --- app/helpers/application_settings_helper.rb | 2 ++ .../application_settings/_grafana.html.haml | 17 +++++++++++++++++ .../metrics_and_profiling.html.haml | 11 +++++++++++ app/views/layouts/nav/sidebar/_admin.html.haml | 5 +++++ ...05-grafanaInAdminSettingsMonitoringMenu.yml | 5 +++++ .../20190617123615_add_grafana_to_settings.rb | 18 ++++++++++++++++++ ...190624123615_add_grafana_url_to_settings.rb | 18 ++++++++++++++++++ db/schema.rb | 2 ++ .../performance/grafana_configuration.md | 15 +++++++++++++++ lib/api/settings.rb | 2 ++ locale/gitlab.pot | 18 ++++++++++++++++++ 11 files changed, 113 insertions(+) create mode 100644 app/views/admin/application_settings/_grafana.html.haml create mode 100644 changelogs/unreleased/61005-grafanaInAdminSettingsMonitoringMenu.yml create mode 100644 db/migrate/20190617123615_add_grafana_to_settings.rb create mode 100644 db/migrate/20190624123615_add_grafana_url_to_settings.rb diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index a7a4e945a99..4bf9b708401 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -187,6 +187,8 @@ module ApplicationSettingsHelper :gitaly_timeout_default, :gitaly_timeout_medium, :gitaly_timeout_fast, + :grafana_enabled, + :grafana_url, :gravatar_enabled, :hashed_storage_enabled, :help_page_hide_commercial_content, diff --git a/app/views/admin/application_settings/_grafana.html.haml b/app/views/admin/application_settings/_grafana.html.haml new file mode 100644 index 00000000000..b6e02bde895 --- /dev/null +++ b/app/views/admin/application_settings/_grafana.html.haml @@ -0,0 +1,17 @@ += form_for @application_setting, url: admin_application_settings_path(anchor: 'js-grafana-settings'), html: { class: 'fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + %p + = _("Add a Grafana button in the admin sidebar, monitoring section, to access a variety of statistics on the health and performance of GitLab.") + = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/grafana_configuration.md') + .form-group + .form-check + = f.check_box :grafana_enabled, class: 'form-check-input' + = f.label :grafana_enabled, class: 'form-check-label' do + = _('Enable access to Grafana') + .form-group + = f.label :grafana_url, _('Grafana URL'), class: 'label-bold' + = f.text_field :grafana_url, class: 'form-control', placeholder: '/-/grafana' + + = f.submit _('Save changes'), class: "btn btn-success" diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml index 01d61beaf53..55a48da8342 100644 --- a/app/views/admin/application_settings/metrics_and_profiling.html.haml +++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml @@ -24,6 +24,17 @@ .settings-content = render 'prometheus' +%section.settings.as-grafana.no-animate#js-grafana-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Metrics - Grafana') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Enable and configure Grafana.') + .settings-content + = render 'grafana' + %section.settings.qa-performance-bar-settings.as-performance-bar.no-animate#js-performance-bar-settings{ class: ('expanded' if expanded_by_default?) } .settings-header %h4 diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index 83fe871285a..87133c7ba22 100644 --- a/app/views/layouts/nav/sidebar/_admin.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml @@ -81,6 +81,11 @@ = link_to admin_requests_profiles_path, title: _('Requests Profiles') do %span = _('Requests Profiles') + - if Gitlab::CurrentSettings.current_application_settings.grafana_enabled? + = nav_link do + = link_to Gitlab::CurrentSettings.current_application_settings.grafana_url, target: '_blank', title: _('Metrics Dashboard') do + %span + = _('Metrics Dashboard') = render_if_exists 'layouts/nav/ee/admin/new_monitoring_sidebar' = nav_link(controller: :broadcast_messages) do diff --git a/changelogs/unreleased/61005-grafanaInAdminSettingsMonitoringMenu.yml b/changelogs/unreleased/61005-grafanaInAdminSettingsMonitoringMenu.yml new file mode 100644 index 00000000000..3ee512f3448 --- /dev/null +++ b/changelogs/unreleased/61005-grafanaInAdminSettingsMonitoringMenu.yml @@ -0,0 +1,5 @@ +--- +title: Adds link to Grafana in Admin > Monitoring settings when grafana is enabled in config +merge_request: 28937 +author: Romain Maneschi +type: added diff --git a/db/migrate/20190617123615_add_grafana_to_settings.rb b/db/migrate/20190617123615_add_grafana_to_settings.rb new file mode 100644 index 00000000000..f9c6f4d883e --- /dev/null +++ b/db/migrate/20190617123615_add_grafana_to_settings.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddGrafanaToSettings < ActiveRecord::Migration[5.1] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_column_with_default(:application_settings, :grafana_enabled, :boolean, + default: false, allow_null: false) + end + + def down + remove_column(:application_settings, :grafana_enabled) + end +end diff --git a/db/migrate/20190624123615_add_grafana_url_to_settings.rb b/db/migrate/20190624123615_add_grafana_url_to_settings.rb new file mode 100644 index 00000000000..61efe64a7a1 --- /dev/null +++ b/db/migrate/20190624123615_add_grafana_url_to_settings.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddGrafanaUrlToSettings < ActiveRecord::Migration[5.1] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_column_with_default(:application_settings, :grafana_url, :string, + default: '/-/grafana', allow_null: false) + end + + def down + remove_column(:application_settings, :grafana_url) + end +end diff --git a/db/schema.rb b/db/schema.rb index 32a25f643ce..4bcc8b5f1d7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -193,6 +193,7 @@ ActiveRecord::Schema.define(version: 20190628185004) do t.string "required_instance_ci_template" t.boolean "dns_rebinding_protection_enabled", default: true, null: false t.boolean "default_project_deletion_protection", default: false, null: false + t.boolean "grafana_enabled", default: false, null: false t.boolean "lock_memberships_to_ldap", default: false, null: false t.text "help_text" t.boolean "elasticsearch_indexing", default: false, null: false @@ -226,6 +227,7 @@ ActiveRecord::Schema.define(version: 20190628185004) do t.boolean "elasticsearch_limit_indexing", default: false, null: false t.string "geo_node_allowed_ips", default: "0.0.0.0/0, ::/0" t.boolean "time_tracking_limit_to_hours", default: false, null: false + t.string "grafana_url", default: "/-/grafana", null: false t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id", using: :btree t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id", using: :btree t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree diff --git a/doc/administration/monitoring/performance/grafana_configuration.md b/doc/administration/monitoring/performance/grafana_configuration.md index 187fb2f73a1..51b0d78681d 100644 --- a/doc/administration/monitoring/performance/grafana_configuration.md +++ b/doc/administration/monitoring/performance/grafana_configuration.md @@ -103,6 +103,21 @@ repository for more information on this process. [grafana-dashboards]: https://gitlab.com/gitlab-org/grafana-dashboards +## Integration with GitLab UI + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/61005) in GitLab 12.1. + +If you have set up Grafana, you can enable a link to access it easily from the sidebar: + +1. Go to the admin area under **Settings > Metrics and profiling** + and expand "Metrics - Grafana". +1. Check the "Enable access to Grafana" checkbox. +1. If Grafana is enabled through Omnibus GitLab and on the same server, + leave "Grafana URL" unchanged. In any other case, enter the full URL + path of the Grafana instance. +1. Click **Save changes**. +1. The new link will be available in the admin area under **Monitoring > Metrics Dashboard**. + --- Read more on: diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 3c5c1a9fd5f..4275d911708 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -55,6 +55,8 @@ module API optional :gitaly_timeout_default, type: Integer, desc: 'Default Gitaly timeout, in seconds. Set to 0 to disable timeouts.' optional :gitaly_timeout_fast, type: Integer, desc: 'Gitaly fast operation timeout, in seconds. Set to 0 to disable timeouts.' optional :gitaly_timeout_medium, type: Integer, desc: 'Medium Gitaly timeout, in seconds. Set to 0 to disable timeouts.' + optional :grafana_enabled, type: Boolean, desc: 'Enable Grafana' + optional :grafana_url, type: String, desc: 'Grafana URL' optional :gravatar_enabled, type: Boolean, desc: 'Flag indicating if the Gravatar service is enabled' optional :help_page_hide_commercial_content, type: Boolean, desc: 'Hide marketing-related entries from help' optional :help_page_support_url, type: String, desc: 'Alternate support URL for help page' diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 0738df663fb..8a4f57c5b13 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -605,6 +605,9 @@ msgstr "" msgid "Add a GPG key" msgstr "" +msgid "Add a Grafana button in the admin sidebar, monitoring section, to access a variety of statistics on the health and performance of GitLab." +msgstr "" + msgid "Add a bullet list" msgstr "" @@ -3923,9 +3926,15 @@ msgstr "" msgid "Enable HTML emails" msgstr "" +msgid "Enable access to Grafana" +msgstr "" + msgid "Enable access to the Performance Bar for a given group." msgstr "" +msgid "Enable and configure Grafana." +msgstr "" + msgid "Enable and configure InfluxDB metrics." msgstr "" @@ -4933,6 +4942,9 @@ msgstr "" msgid "Got it!" msgstr "" +msgid "Grafana URL" +msgstr "" + msgid "Grant access" msgstr "" @@ -6391,12 +6403,18 @@ msgstr "" msgid "Metrics" msgstr "" +msgid "Metrics - Grafana" +msgstr "" + msgid "Metrics - Influx" msgstr "" msgid "Metrics - Prometheus" msgstr "" +msgid "Metrics Dashboard" +msgstr "" + msgid "Metrics and profiling" msgstr "" From d0b76d065289b50a14b151f45fbb2718e8a50f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 3 Jul 2019 15:56:07 +0200 Subject: [PATCH 024/195] Cache PerformanceBar.allowed_user_ids list locally and in Redis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- lib/gitlab/performance_bar.rb | 28 ++++++++++++++++++------- spec/lib/gitlab/performance_bar_spec.rb | 27 +++++++++++++++++++++++- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/lib/gitlab/performance_bar.rb b/lib/gitlab/performance_bar.rb index 4b0c7b5c7f8..07439d8e011 100644 --- a/lib/gitlab/performance_bar.rb +++ b/lib/gitlab/performance_bar.rb @@ -3,7 +3,8 @@ module Gitlab module PerformanceBar ALLOWED_USER_IDS_KEY = 'performance_bar_allowed_user_ids:v2'.freeze - EXPIRY_TIME = 5.minutes + EXPIRY_TIME_L1_CACHE = 1.minute + EXPIRY_TIME_L2_CACHE = 5.minutes def self.enabled?(user = nil) return true if Rails.env.development? @@ -19,20 +20,31 @@ module Gitlab # rubocop: disable CodeReuse/ActiveRecord def self.allowed_user_ids - Rails.cache.fetch(ALLOWED_USER_IDS_KEY, expires_in: EXPIRY_TIME) do - group = Group.find_by_id(allowed_group_id) + l1_cache_backend.fetch(ALLOWED_USER_IDS_KEY, expires_in: EXPIRY_TIME_L1_CACHE) do + l2_cache_backend.fetch(ALLOWED_USER_IDS_KEY, expires_in: EXPIRY_TIME_L2_CACHE) do + group = Group.find_by_id(allowed_group_id) - if group - GroupMembersFinder.new(group).execute.pluck(:user_id) - else - [] + if group + GroupMembersFinder.new(group).execute.pluck(:user_id) + else + [] + end end end end # rubocop: enable CodeReuse/ActiveRecord def self.expire_allowed_user_ids_cache - Rails.cache.delete(ALLOWED_USER_IDS_KEY) + l1_cache_backend.delete(ALLOWED_USER_IDS_KEY) + l2_cache_backend.delete(ALLOWED_USER_IDS_KEY) + end + + def self.l1_cache_backend + Gitlab::ThreadMemoryCache.cache_backend + end + + def self.l2_cache_backend + Rails.cache end end end diff --git a/spec/lib/gitlab/performance_bar_spec.rb b/spec/lib/gitlab/performance_bar_spec.rb index f480376acb4..ee3c571c9c0 100644 --- a/spec/lib/gitlab/performance_bar_spec.rb +++ b/spec/lib/gitlab/performance_bar_spec.rb @@ -3,17 +3,42 @@ require 'spec_helper' describe Gitlab::PerformanceBar do shared_examples 'allowed user IDs are cached' do before do - # Warm the Redis cache + # Warm the caches described_class.enabled?(user) end it 'caches the allowed user IDs in cache', :use_clean_rails_memory_store_caching do expect do + expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original + expect(described_class.l2_cache_backend).not_to receive(:fetch) expect(described_class.enabled?(user)).to be_truthy end.not_to exceed_query_limit(0) end + + it 'caches the allowed user IDs in L1 cache for 1 minute', :use_clean_rails_memory_store_caching do + Timecop.travel 2.minutes do + expect do + expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original + expect(described_class.l2_cache_backend).to receive(:fetch).and_call_original + expect(described_class.enabled?(user)).to be_truthy + end.not_to exceed_query_limit(0) + end + end + + it 'caches the allowed user IDs in L2 cache for 5 minutes', :use_clean_rails_memory_store_caching do + Timecop.travel 6.minutes do + expect do + expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original + expect(described_class.l2_cache_backend).to receive(:fetch).and_call_original + expect(described_class.enabled?(user)).to be_truthy + end.not_to exceed_query_limit(2) + end + end end + it { expect(described_class.l1_cache_backend).to eq(Gitlab::ThreadMemoryCache.cache_backend) } + it { expect(described_class.l2_cache_backend).to eq(Rails.cache) } + describe '.enabled?' do let(:user) { create(:user) } From 9af6dd3e8d1579d55759de49032a78f277162f66 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 3 Jul 2019 17:56:17 +0000 Subject: [PATCH 025/195] Put a failed example from appearance_spec in quarantine --- spec/models/appearance_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb index 209d138f956..75d850623f4 100644 --- a/spec/models/appearance_spec.rb +++ b/spec/models/appearance_spec.rb @@ -21,7 +21,7 @@ describe Appearance do end end - context 'with uploads' do + context 'with uploads', :quarantine do it_behaves_like 'model with uploads', false do let(:model_object) { create(:appearance, :with_logo) } let(:upload_attribute) { :logo } From 3b924c13460d4afe26c386633241d1362d10e824 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 3 Jul 2019 11:27:16 -0700 Subject: [PATCH 026/195] Fix order-dependent spec failure in appearance_spec.rb When file_mover_spec.rb ran, it would initialize fog-aws with `Fog::AWS::Storage::Real` service instead of `Fog::AWS::Storage::Mock` because `Fog.mock!` was not called. Ensure that we use `stub_uploads_object_storage` to prevent that from happening. Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/64083 --- spec/models/appearance_spec.rb | 2 +- spec/uploaders/file_mover_spec.rb | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb index 75d850623f4..209d138f956 100644 --- a/spec/models/appearance_spec.rb +++ b/spec/models/appearance_spec.rb @@ -21,7 +21,7 @@ describe Appearance do end end - context 'with uploads', :quarantine do + context 'with uploads' do it_behaves_like 'model with uploads', false do let(:model_object) { create(:appearance, :with_logo) } let(:upload_attribute) { :logo } diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb index a9e03f3d4e5..5ee0a10f38d 100644 --- a/spec/uploaders/file_mover_spec.rb +++ b/spec/uploaders/file_mover_spec.rb @@ -85,8 +85,7 @@ describe FileMover do context 'when tmp uploader is not local storage' do before do - allow(PersonalFileUploader).to receive(:object_store_enabled?) { true } - tmp_uploader.object_store = ObjectStorage::Store::REMOTE + stub_uploads_object_storage(uploader: PersonalFileUploader) allow_any_instance_of(PersonalFileUploader).to receive(:file_storage?) { false } end From ee685e228a4e1eac4567287aaa84ad27db2face1 Mon Sep 17 00:00:00 2001 From: Walmyr Lima Date: Wed, 3 Jul 2019 19:12:41 +0200 Subject: [PATCH 027/195] Backport EE MR that improves end-to-end tests https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/14533 --- .../collapse_comments_in_discussions_spec.rb | 26 ++++++++++++------- .../issue/filter_issue_comments_spec.rb | 23 +++++++++------- .../2_plan/issue/issue_suggestions_spec.rb | 2 +- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb index 4478ea41662..2101311f065 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb @@ -9,27 +9,33 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) - Resource::Issue.fabricate_via_browser_ui! do |issue| + issue = Resource::Issue.fabricate_via_api! do |issue| issue.title = issue_title end + issue.visit! + expect(page).to have_content(issue_title) Page::Project::Issue::Show.perform do |show_page| - show_page.select_all_activities_filter - show_page.start_discussion("My first discussion") - expect(show_page).to have_content("My first discussion") + my_first_discussion = "My first discussion" + my_first_reply = "My First Reply" + one_reply = "1 reply" - show_page.reply_to_discussion("My First Reply") - expect(show_page).to have_content("My First Reply") + show_page.select_all_activities_filter + show_page.start_discussion(my_first_discussion) + expect(show_page).to have_content(my_first_discussion) + + show_page.reply_to_discussion(my_first_reply) + expect(show_page).to have_content(my_first_reply) show_page.collapse_replies - expect(show_page).to have_content("1 reply") - expect(show_page).not_to have_content("My First Reply") + expect(show_page).to have_content(one_reply) + expect(show_page).not_to have_content(my_first_reply) show_page.expand_replies - expect(show_page).to have_content("My First Reply") - expect(show_page).not_to have_content("1 reply") + expect(show_page).to have_content(my_first_reply) + expect(show_page).not_to have_content(one_reply) end end end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb index ad2773b41ac..301836f5ce8 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb @@ -9,28 +9,33 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Resource::Issue.fabricate_via_browser_ui! do |issue| + issue = Resource::Issue.fabricate_via_api! do |issue| issue.title = issue_title end + issue.visit! + expect(page).to have_content(issue_title) Page::Project::Issue::Show.perform do |show_page| - show_page.comment('/confidential', filter: :comments_only) - show_page.comment('My own comment', filter: :comments_only) + my_own_comment = "My own comment" + made_the_issue_confidential = "made the issue confidential" - expect(show_page).not_to have_content("made the issue confidential") - expect(show_page).to have_content("My own comment") + show_page.comment('/confidential', filter: :comments_only) + show_page.comment(my_own_comment, filter: :comments_only) + + expect(show_page).not_to have_content(made_the_issue_confidential) + expect(show_page).to have_content(my_own_comment) show_page.select_all_activities_filter - expect(show_page).to have_content("made the issue confidential") - expect(show_page).to have_content("My own comment") + expect(show_page).to have_content(made_the_issue_confidential) + expect(show_page).to have_content(my_own_comment) show_page.select_history_only_filter - expect(show_page).to have_content("made the issue confidential") - expect(show_page).not_to have_content("My own comment") + expect(show_page).to have_content(made_the_issue_confidential) + expect(show_page).not_to have_content(my_own_comment) end end end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb index 530fc684437..24dcb32f63f 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb @@ -14,7 +14,7 @@ module QA resource.description = 'project for issue suggestions' end - Resource::Issue.fabricate_via_browser_ui! do |issue| + Resource::Issue.fabricate_via_api! do |issue| issue.title = issue_title issue.project = project end From 76b0518f334090294d4ad7db7be64846f6018e80 Mon Sep 17 00:00:00 2001 From: Heinrich Lee Yu Date: Fri, 21 Jun 2019 05:01:14 +0800 Subject: [PATCH 028/195] Use #filename when generating upload URLs We don't need to find the filename from the remote URL --- app/uploaders/file_uploader.rb | 2 +- app/uploaders/personal_file_uploader.rb | 2 +- .../53357-fix-plus-in-upload-file-names.yml | 5 ++ spec/uploaders/file_uploader_spec.rb | 68 +++++++++++-------- 4 files changed, 46 insertions(+), 31 deletions(-) create mode 100644 changelogs/unreleased/53357-fix-plus-in-upload-file-names.yml diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 1c7582533ad..b326b266017 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -203,6 +203,6 @@ class FileUploader < GitlabUploader end def secure_url - File.join('/uploads', @secret, file.filename) + File.join('/uploads', @secret, filename) end end diff --git a/app/uploaders/personal_file_uploader.rb b/app/uploaders/personal_file_uploader.rb index b43162f0935..1ac69601d18 100644 --- a/app/uploaders/personal_file_uploader.rb +++ b/app/uploaders/personal_file_uploader.rb @@ -93,6 +93,6 @@ class PersonalFileUploader < FileUploader end def secure_url - File.join('/', base_dir, secret, file.filename) + File.join('/', base_dir, secret, filename) end end diff --git a/changelogs/unreleased/53357-fix-plus-in-upload-file-names.yml b/changelogs/unreleased/53357-fix-plus-in-upload-file-names.yml new file mode 100644 index 00000000000..c11d74bd4fe --- /dev/null +++ b/changelogs/unreleased/53357-fix-plus-in-upload-file-names.yml @@ -0,0 +1,5 @@ +--- +title: Fix broken URLs for uploads with a plus in the filename +merge_request: 29915 +author: +type: fixed diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index 185c62491ce..04206de3dc6 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -184,40 +184,37 @@ describe FileUploader do end end - describe '#cache!' do - subject do + context 'when remote file is used' do + let(:temp_file) { Tempfile.new("test") } + + let!(:fog_connection) do + stub_uploads_object_storage(described_class) + end + + let(:filename) { "my file.txt" } + let(:uploaded_file) do + UploadedFile.new(temp_file.path, filename: filename, remote_id: "test/123123") + end + + let!(:fog_file) do + fog_connection.directories.new(key: 'uploads').files.create( + key: 'tmp/uploads/test/123123', + body: 'content' + ) + end + + before do + FileUtils.touch(temp_file) + uploader.store!(uploaded_file) end - context 'when remote file is used' do - let(:temp_file) { Tempfile.new("test") } - - let!(:fog_connection) do - stub_uploads_object_storage(described_class) - end - - let(:uploaded_file) do - UploadedFile.new(temp_file.path, filename: "my file.txt", remote_id: "test/123123") - end - - let!(:fog_file) do - fog_connection.directories.new(key: 'uploads').files.create( - key: 'tmp/uploads/test/123123', - body: 'content' - ) - end - - before do - FileUtils.touch(temp_file) - end - - after do - FileUtils.rm_f(temp_file) - end + after do + FileUtils.rm_f(temp_file) + end + describe '#cache!' do it 'file is stored remotely in permament location with sanitized name' do - subject - expect(uploader).to be_exists expect(uploader).not_to be_cached expect(uploader).not_to be_file_storage @@ -228,5 +225,18 @@ describe FileUploader do expect(uploader.object_store).to eq(described_class::Store::REMOTE) end end + + describe '#to_h' do + subject { uploader.to_h } + + let(:filename) { 'my+file.txt' } + + it 'generates URL using original file name instead of filename returned by object storage' do + # GCS returns a URL with a `+` instead of `%2B` + allow(uploader.file).to receive(:url).and_return('https://storage.googleapis.com/gitlab-test-uploads/@hashed/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b/64c5065e62100b1a12841644256a98be/my+file.txt') + + expect(subject[:url]).to end_with(filename) + end + end end end From 9dc1850524b8c3ea784ed61b40c8b6be5ffa8831 Mon Sep 17 00:00:00 2001 From: Cynthia Ng Date: Wed, 3 Jul 2019 20:02:07 +0000 Subject: [PATCH 029/195] Update link to page instead of redirect --- doc/user/project/deploy_boards.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/project/deploy_boards.md b/doc/user/project/deploy_boards.md index 175384bc985..0d51e8ae19a 100644 --- a/doc/user/project/deploy_boards.md +++ b/doc/user/project/deploy_boards.md @@ -125,6 +125,6 @@ version of your application. [kube-service]: integrations/kubernetes.md "Kubernetes project service" [review apps]: ../../ci/review_apps/index.md "Review Apps documentation" [variables]: ../../ci/variables/README.md "GitLab CI variables" -[autodeploy]: ../../ci/autodeploy/index.md "GitLab Autodeploy" +[autodeploy]: ../../topics/autodevops/index.md#auto-deploy "GitLab Autodeploy" [kube-image]: https://gitlab.com/gitlab-examples/kubernetes-deploy/container_registry "Kubernetes deploy Container Registry" [runners]: ../../ci/runners/README.md From 49b5ef5c3110ec25b65e20c75c3f98dbb2c8dfea Mon Sep 17 00:00:00 2001 From: Reuben Pereira Date: Wed, 3 Jul 2019 20:02:17 +0000 Subject: [PATCH 030/195] Change occurrence of Sidekiq::Testing.inline! - Change it to perform_enqueued_jobs --- .../migrations/backfill_store_project_full_path_in_repo_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb b/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb index 34f4a36d63d..65a918d5440 100644 --- a/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb +++ b/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb @@ -13,7 +13,7 @@ describe BackfillStoreProjectFullPathInRepo, :migration do subject(:migration) { described_class.new } around do |example| - Sidekiq::Testing.inline! do + perform_enqueued_jobs do example.run end end From a08209ffa35a29cd84271895389b4537dee92e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Tue, 2 Jul 2019 19:40:21 +0200 Subject: [PATCH 031/195] Limit amount of JUnit tests returned Currently, we do not cap amount of tests returned to frontend, thus in some extreme cases we can see a MBs of data stored in Redis. This adds an upper limit of 100 tests per-suite. We will continue showing the total counters correctly, but we will limit amount of tests that will be presented. --- app/serializers/test_suite_comparer_entity.rb | 29 +++++- .../ci/compare_reports_base_service.rb | 6 +- .../limit-amount-of-tests-returned.yml | 5 + .../user_sees_merge_widget_spec.rb | 62 ++++++++++--- .../ci/reports/test_reports_comparer_spec.rb | 8 +- .../ci/reports/test_suite_comparer_spec.rb | 23 ++--- spec/serializers/test_case_entity_spec.rb | 2 +- .../test_reports_comparer_entity_spec.rb | 8 +- .../test_reports_comparer_serializer_spec.rb | 8 +- .../test_suite_comparer_entity_spec.rb | 92 ++++++++++++++++--- .../test_reports/test_reports_helper.rb | 34 +++---- 11 files changed, 189 insertions(+), 88 deletions(-) create mode 100644 changelogs/unreleased/limit-amount-of-tests-returned.yml diff --git a/app/serializers/test_suite_comparer_entity.rb b/app/serializers/test_suite_comparer_entity.rb index 9fa3a897ebe..d402a4d5718 100644 --- a/app/serializers/test_suite_comparer_entity.rb +++ b/app/serializers/test_suite_comparer_entity.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true class TestSuiteComparerEntity < Grape::Entity + DEFAULT_MAX_TESTS = 100 + DEFAULT_MIN_TESTS = 10 + expose :name expose :total_status, as: :status @@ -10,7 +13,27 @@ class TestSuiteComparerEntity < Grape::Entity expose :failed_count, as: :failed end - expose :new_failures, using: TestCaseEntity - expose :resolved_failures, using: TestCaseEntity - expose :existing_failures, using: TestCaseEntity + # rubocop: disable CodeReuse/ActiveRecord + expose :new_failures, using: TestCaseEntity do |suite| + suite.new_failures.take(max_tests) + end + + expose :existing_failures, using: TestCaseEntity do |suite| + suite.existing_failures.take( + max_tests(suite.new_failures)) + end + + expose :resolved_failures, using: TestCaseEntity do |suite| + suite.resolved_failures.take( + max_tests(suite.new_failures, suite.existing_failures)) + end + + private + + def max_tests(*used) + return Integer::MAX unless Feature.enabled?(:ci_limit_test_reports_size, default_enabled: true) + + [DEFAULT_MAX_TESTS - used.map(&:count).sum, DEFAULT_MIN_TESTS].max + end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/services/ci/compare_reports_base_service.rb b/app/services/ci/compare_reports_base_service.rb index d5625857599..6c2d80d8f45 100644 --- a/app/services/ci/compare_reports_base_service.rb +++ b/app/services/ci/compare_reports_base_service.rb @@ -8,7 +8,7 @@ module Ci status: :parsed, key: key(base_pipeline, head_pipeline), data: serializer_class - .new(project: project) + .new(**serializer_params) .represent(comparer).as_json } rescue Gitlab::Ci::Parsers::ParserError => e @@ -40,6 +40,10 @@ module Ci raise NotImplementedError end + def serializer_params + { project: project } + end + def get_report(pipeline) raise NotImplementedError end diff --git a/changelogs/unreleased/limit-amount-of-tests-returned.yml b/changelogs/unreleased/limit-amount-of-tests-returned.yml new file mode 100644 index 00000000000..0e80a64b6b7 --- /dev/null +++ b/changelogs/unreleased/limit-amount-of-tests-returned.yml @@ -0,0 +1,5 @@ +--- +title: Limit amount of JUnit tests returned +merge_request: 30274 +author: +type: performance diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index 733e8aa3eba..30e30751693 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -519,6 +519,8 @@ describe 'Merge request > User sees merge widget', :js do end before do + allow_any_instance_of(TestSuiteComparerEntity) + .to receive(:max_tests).and_return(2) allow_any_instance_of(MergeRequest) .to receive(:has_test_reports?).and_return(true) allow_any_instance_of(MergeRequest) @@ -551,7 +553,7 @@ describe 'Merge request > User sees merge widget', :js do expect(page).to have_content('rspec found no changed test results out of 1 total test') expect(page).to have_content('junit found 1 failed test result out of 1 total test') expect(page).to have_content('New') - expect(page).to have_content('subtractTest') + expect(page).to have_content('addTest') end end end @@ -562,7 +564,7 @@ describe 'Merge request > User sees merge widget', :js do click_button 'Expand' within(".js-report-section-container") do - click_button 'subtractTest' + click_button 'addTest' expect(page).to have_content('6.66') expect(page).to have_content(sample_java_failed_message.gsub!(/\s+/, ' ').strip) @@ -596,7 +598,7 @@ describe 'Merge request > User sees merge widget', :js do expect(page).to have_content('rspec found 1 failed test result out of 1 total test') expect(page).to have_content('junit found no changed test results out of 1 total test') expect(page).not_to have_content('New') - expect(page).to have_content('Test#sum when a is 2 and b is 2 returns summary') + expect(page).to have_content('Test#sum when a is 1 and b is 3 returns summary') end end end @@ -607,7 +609,7 @@ describe 'Merge request > User sees merge widget', :js do click_button 'Expand' within(".js-report-section-container") do - click_button 'Test#sum when a is 2 and b is 2 returns summary' + click_button 'Test#sum when a is 1 and b is 3 returns summary' expect(page).to have_content('2.22') expect(page).to have_content(sample_rspec_failed_message.gsub!(/\s+/, ' ').strip) @@ -628,13 +630,7 @@ describe 'Merge request > User sees merge widget', :js do let(:head_reports) do Gitlab::Ci::Reports::TestReports.new.tap do |reports| reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) - reports.get_suite('junit').add_test_case(create_test_case_java_resolved) - end - end - - let(:create_test_case_java_resolved) do - create_test_case_java_failed.tap do |test_case| - test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) + reports.get_suite('junit').add_test_case(create_test_case_java_success) end end @@ -646,7 +642,7 @@ describe 'Merge request > User sees merge widget', :js do within(".js-report-section-container") do expect(page).to have_content('rspec found no changed test results out of 1 total test') expect(page).to have_content('junit found 1 fixed test result out of 1 total test') - expect(page).to have_content('subtractTest') + expect(page).to have_content('addTest') end end end @@ -657,15 +653,53 @@ describe 'Merge request > User sees merge widget', :js do click_button 'Expand' within(".js-report-section-container") do - click_button 'subtractTest' + click_button 'addTest' - expect(page).to have_content('6.66') + expect(page).to have_content('5.55') end end end end end + context 'properly truncates the report' do + let(:base_reports) do + Gitlab::Ci::Reports::TestReports.new.tap do |reports| + 10.times do |index| + reports.get_suite('rspec').add_test_case( + create_test_case_rspec_failed(index)) + reports.get_suite('junit').add_test_case( + create_test_case_java_success(index)) + end + end + end + + let(:head_reports) do + Gitlab::Ci::Reports::TestReports.new.tap do |reports| + 10.times do |index| + reports.get_suite('rspec').add_test_case( + create_test_case_rspec_failed(index)) + reports.get_suite('junit').add_test_case( + create_test_case_java_failed(index)) + end + end + end + + it 'shows test reports summary which includes the resolved failure' do + within(".js-reports-container") do + click_button 'Expand' + + expect(page).to have_content('Test summary contained 20 failed test results out of 20 total tests') + within(".js-report-section-container") do + expect(page).to have_content('rspec found 10 failed test results out of 10 total tests') + expect(page).to have_content('junit found 10 failed test results out of 10 total tests') + + expect(page).to have_content('Test#sum when a is 1 and b is 3 returns summary', count: 2) + end + end + end + end + def comparer Gitlab::Ci::Reports::TestReportsComparer.new(base_reports, head_reports) end diff --git a/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb index 71c61e0345f..36582204cc1 100644 --- a/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_reports_comparer_spec.rb @@ -74,17 +74,11 @@ describe Gitlab::Ci::Reports::TestReportsComparer do subject { comparer.resolved_count } context 'when there is a resolved test case in head suites' do - let(:create_test_case_java_resolved) do - create_test_case_java_failed.tap do |test_case| - test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) - end - end - before do base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) base_reports.get_suite('junit').add_test_case(create_test_case_java_failed) head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) - head_reports.get_suite('junit').add_test_case(create_test_case_java_resolved) + head_reports.get_suite('junit').add_test_case(create_test_case_java_success) end it 'returns the correct count' do diff --git a/spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb index 6ab16e5518d..579b3e6fd24 100644 --- a/spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb @@ -10,12 +10,6 @@ describe Gitlab::Ci::Reports::TestSuiteComparer do let(:test_case_success) { create_test_case_rspec_success } let(:test_case_failed) { create_test_case_rspec_failed } - let(:test_case_resolved) do - create_test_case_rspec_failed.tap do |test_case| - test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) - end - end - describe '#new_failures' do subject { comparer.new_failures } @@ -44,7 +38,7 @@ describe Gitlab::Ci::Reports::TestSuiteComparer do context 'when head sutie has a success test case which failed in base' do before do base_suite.add_test_case(test_case_failed) - head_suite.add_test_case(test_case_resolved) + head_suite.add_test_case(test_case_success) end it 'does not return the failed test case' do @@ -81,7 +75,7 @@ describe Gitlab::Ci::Reports::TestSuiteComparer do context 'when head sutie has a success test case which failed in base' do before do base_suite.add_test_case(test_case_failed) - head_suite.add_test_case(test_case_resolved) + head_suite.add_test_case(test_case_success) end it 'does not return the failed test case' do @@ -126,11 +120,11 @@ describe Gitlab::Ci::Reports::TestSuiteComparer do context 'when head sutie has a success test case which failed in base' do before do base_suite.add_test_case(test_case_failed) - head_suite.add_test_case(test_case_resolved) + head_suite.add_test_case(test_case_success) end it 'does not return the resolved test case' do - is_expected.to eq([test_case_resolved]) + is_expected.to eq([test_case_success]) end it 'returns the correct resolved count' do @@ -156,13 +150,8 @@ describe Gitlab::Ci::Reports::TestSuiteComparer do context 'when there are a new failure and an existing failure' do let(:test_case_1_success) { create_test_case_rspec_success } - let(:test_case_2_failed) { create_test_case_rspec_failed } - - let(:test_case_1_failed) do - create_test_case_rspec_success.tap do |test_case| - test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_FAILED) - end - end + let(:test_case_1_failed) { create_test_case_rspec_failed } + let(:test_case_2_failed) { create_test_case_rspec_failed('case2') } before do base_suite.add_test_case(test_case_1_success) diff --git a/spec/serializers/test_case_entity_spec.rb b/spec/serializers/test_case_entity_spec.rb index 986c9feb07b..cc5f086ca4e 100644 --- a/spec/serializers/test_case_entity_spec.rb +++ b/spec/serializers/test_case_entity_spec.rb @@ -24,7 +24,7 @@ describe TestCaseEntity do it 'contains correct test case details' do expect(subject[:status]).to eq('failed') - expect(subject[:name]).to eq('Test#sum when a is 2 and b is 2 returns summary') + expect(subject[:name]).to eq('Test#sum when a is 1 and b is 3 returns summary') expect(subject[:classname]).to eq('spec.test_spec') expect(subject[:execution_time]).to eq(2.22) end diff --git a/spec/serializers/test_reports_comparer_entity_spec.rb b/spec/serializers/test_reports_comparer_entity_spec.rb index 59c058fe368..4a951bbbde4 100644 --- a/spec/serializers/test_reports_comparer_entity_spec.rb +++ b/spec/serializers/test_reports_comparer_entity_spec.rb @@ -53,13 +53,7 @@ describe TestReportsComparerEntity do base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) base_reports.get_suite('junit').add_test_case(create_test_case_java_failed) head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) - head_reports.get_suite('junit').add_test_case(create_test_case_java_resolved) - end - - let(:create_test_case_java_resolved) do - create_test_case_java_failed.tap do |test_case| - test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) - end + head_reports.get_suite('junit').add_test_case(create_test_case_java_success) end it 'contains correct compared test reports details' do diff --git a/spec/serializers/test_reports_comparer_serializer_spec.rb b/spec/serializers/test_reports_comparer_serializer_spec.rb index 9ea86c0dd83..62dc6f486c5 100644 --- a/spec/serializers/test_reports_comparer_serializer_spec.rb +++ b/spec/serializers/test_reports_comparer_serializer_spec.rb @@ -44,13 +44,7 @@ describe TestReportsComparerSerializer do base_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) base_reports.get_suite('junit').add_test_case(create_test_case_java_failed) head_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) - head_reports.get_suite('junit').add_test_case(create_test_case_java_resolved) - end - - let(:create_test_case_java_resolved) do - create_test_case_java_failed.tap do |test_case| - test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) - end + head_reports.get_suite('junit').add_test_case(create_test_case_java_success) end it 'matches the schema' do diff --git a/spec/serializers/test_suite_comparer_entity_spec.rb b/spec/serializers/test_suite_comparer_entity_spec.rb index f61331f53a0..4b2cca2c68c 100644 --- a/spec/serializers/test_suite_comparer_entity_spec.rb +++ b/spec/serializers/test_suite_comparer_entity_spec.rb @@ -11,16 +11,10 @@ describe TestSuiteComparerEntity do let(:test_case_success) { create_test_case_rspec_success } let(:test_case_failed) { create_test_case_rspec_failed } - let(:test_case_resolved) do - create_test_case_rspec_failed.tap do |test_case| - test_case.instance_variable_set("@status", Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) - end - end - describe '#as_json' do subject { entity.as_json } - context 'when head sutie has a newly failed test case which does not exist in base' do + context 'when head suite has a newly failed test case which does not exist in base' do before do base_suite.add_test_case(test_case_success) head_suite.add_test_case(test_case_failed) @@ -41,7 +35,7 @@ describe TestSuiteComparerEntity do end end - context 'when head sutie still has a failed test case which failed in base' do + context 'when head suite still has a failed test case which failed in base' do before do base_suite.add_test_case(test_case_failed) head_suite.add_test_case(test_case_failed) @@ -62,10 +56,10 @@ describe TestSuiteComparerEntity do end end - context 'when head sutie has a success test case which failed in base' do + context 'when head suite has a success test case which failed in base' do before do base_suite.add_test_case(test_case_failed) - head_suite.add_test_case(test_case_resolved) + head_suite.add_test_case(test_case_success) end it 'contains correct compared test suite details' do @@ -74,13 +68,83 @@ describe TestSuiteComparerEntity do expect(subject[:summary]).to include(total: 1, resolved: 1, failed: 0) expect(subject[:new_failures]).to be_empty subject[:resolved_failures].first.tap do |resolved_failure| - expect(resolved_failure[:status]).to eq(test_case_resolved.status) - expect(resolved_failure[:name]).to eq(test_case_resolved.name) - expect(resolved_failure[:execution_time]).to eq(test_case_resolved.execution_time) - expect(resolved_failure[:system_output]).to eq(test_case_resolved.system_output) + expect(resolved_failure[:status]).to eq(test_case_success.status) + expect(resolved_failure[:name]).to eq(test_case_success.name) + expect(resolved_failure[:execution_time]).to eq(test_case_success.execution_time) + expect(resolved_failure[:system_output]).to eq(test_case_success.system_output) end expect(subject[:existing_failures]).to be_empty end end + + context 'limits amount of tests returned' do + before do + stub_const('TestSuiteComparerEntity::DEFAULT_MAX_TESTS', 2) + stub_const('TestSuiteComparerEntity::DEFAULT_MIN_TESTS', 1) + end + + context 'prefers new over existing and resolved' do + before do + 3.times { add_new_failure } + 3.times { add_existing_failure } + 3.times { add_resolved_failure } + end + + it 'returns 2 new failures, and 1 of resolved and existing' do + expect(subject[:summary]).to include(total: 9, resolved: 3, failed: 6) + expect(subject[:new_failures].count).to eq(2) + expect(subject[:existing_failures].count).to eq(1) + expect(subject[:resolved_failures].count).to eq(1) + end + end + + context 'prefers existing over resolved' do + before do + 3.times { add_existing_failure } + 3.times { add_resolved_failure } + end + + it 'returns 2 existing failures, and 1 resolved' do + expect(subject[:summary]).to include(total: 6, resolved: 3, failed: 3) + expect(subject[:new_failures].count).to eq(0) + expect(subject[:existing_failures].count).to eq(2) + expect(subject[:resolved_failures].count).to eq(1) + end + end + + context 'limits amount of resolved' do + before do + 3.times { add_resolved_failure } + end + + it 'returns 2 resolved failures' do + expect(subject[:summary]).to include(total: 3, resolved: 3, failed: 0) + expect(subject[:new_failures].count).to eq(0) + expect(subject[:existing_failures].count).to eq(0) + expect(subject[:resolved_failures].count).to eq(2) + end + end + + private + + def add_new_failure + failed_case = create_test_case_rspec_failed(SecureRandom.hex) + head_suite.add_test_case(failed_case) + end + + def add_existing_failure + failed_case = create_test_case_rspec_failed(SecureRandom.hex) + base_suite.add_test_case(failed_case) + head_suite.add_test_case(failed_case) + end + + def add_resolved_failure + case_name = SecureRandom.hex + failed_case = create_test_case_rspec_failed(case_name) + success_case = create_test_case_rspec_success(case_name) + base_suite.add_test_case(failed_case) + head_suite.add_test_case(success_case) + end + end end end diff --git a/spec/support/test_reports/test_reports_helper.rb b/spec/support/test_reports/test_reports_helper.rb index 45c6e04dbf3..6840fb9a860 100644 --- a/spec/support/test_reports/test_reports_helper.rb +++ b/spec/support/test_reports/test_reports_helper.rb @@ -1,36 +1,36 @@ module TestReportsHelper - def create_test_case_rspec_success + def create_test_case_rspec_success(name = 'test_spec') Gitlab::Ci::Reports::TestCase.new( name: 'Test#sum when a is 1 and b is 3 returns summary', - classname: 'spec.test_spec', + classname: "spec.#{name}", file: './spec/test_spec.rb', execution_time: 1.11, status: Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) end - def create_test_case_rspec_failed + def create_test_case_rspec_failed(name = 'test_spec') Gitlab::Ci::Reports::TestCase.new( - name: 'Test#sum when a is 2 and b is 2 returns summary', - classname: 'spec.test_spec', + name: 'Test#sum when a is 1 and b is 3 returns summary', + classname: "spec.#{name}", file: './spec/test_spec.rb', execution_time: 2.22, system_output: sample_rspec_failed_message, status: Gitlab::Ci::Reports::TestCase::STATUS_FAILED) end - def create_test_case_rspec_skipped + def create_test_case_rspec_skipped(name = 'test_spec') Gitlab::Ci::Reports::TestCase.new( name: 'Test#sum when a is 3 and b is 3 returns summary', - classname: 'spec.test_spec', + classname: "spec.#{name}", file: './spec/test_spec.rb', execution_time: 3.33, status: Gitlab::Ci::Reports::TestCase::STATUS_SKIPPED) end - def create_test_case_rspec_error + def create_test_case_rspec_error(name = 'test_spec') Gitlab::Ci::Reports::TestCase.new( name: 'Test#sum when a is 4 and b is 4 returns summary', - classname: 'spec.test_spec', + classname: "spec.#{name}", file: './spec/test_spec.rb', execution_time: 4.44, status: Gitlab::Ci::Reports::TestCase::STATUS_ERROR) @@ -48,34 +48,34 @@ module TestReportsHelper EOF end - def create_test_case_java_success + def create_test_case_java_success(name = 'addTest') Gitlab::Ci::Reports::TestCase.new( - name: 'addTest', + name: name, classname: 'CalculatorTest', execution_time: 5.55, status: Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS) end - def create_test_case_java_failed + def create_test_case_java_failed(name = 'addTest') Gitlab::Ci::Reports::TestCase.new( - name: 'subtractTest', + name: name, classname: 'CalculatorTest', execution_time: 6.66, system_output: sample_java_failed_message, status: Gitlab::Ci::Reports::TestCase::STATUS_FAILED) end - def create_test_case_java_skipped + def create_test_case_java_skipped(name = 'addTest') Gitlab::Ci::Reports::TestCase.new( - name: 'multiplyTest', + name: name, classname: 'CalculatorTest', execution_time: 7.77, status: Gitlab::Ci::Reports::TestCase::STATUS_SKIPPED) end - def create_test_case_java_error + def create_test_case_java_error(name = 'addTest') Gitlab::Ci::Reports::TestCase.new( - name: 'divideTest', + name: name, classname: 'CalculatorTest', execution_time: 8.88, status: Gitlab::Ci::Reports::TestCase::STATUS_ERROR) From b7ebf1b531ee75658f1ccb37019304b2edb051c5 Mon Sep 17 00:00:00 2001 From: Mark Lapierre Date: Wed, 3 Jul 2019 05:01:57 +0000 Subject: [PATCH 032/195] Update docs environment:action:stop GIT_STRATEGY Notes that `GIT_STRATEGY` should be set to `none` so that the job doesn't fail when triggered automatically when the branch is deleted. --- doc/ci/yaml/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 2759f1c5160..3e564e4244c 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -973,6 +973,8 @@ review_app: stop_review_app: stage: deploy + variables: + GIT_STRATEGY: none script: make delete-app when: manual environment: @@ -987,6 +989,10 @@ Once the `review_app` job is successfully finished, it will trigger the set it up to `manual` so it will need a [manual action](#whenmanual) via GitLab's web interface in order to run. +Also in the example, `GIT_STRATEGY` is set to `none` so that GitLab Runner won’t +try to check out the code after the branch is deleted when the `stop_review_app` +job is [automatically triggered](../environments.md#automatically-stopping-an-environment). + The `stop_review_app` job is **required** to have the following keywords defined: - `when` - [reference](#when) From 2db7c5762b41acbcbd71bc4bd5a8aa3d90e1a383 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 2 Jul 2019 11:19:30 -0700 Subject: [PATCH 033/195] Cache Flipper feature flags in L1 and L2 caches In https://gitlab.com/gitlab-com/gl-infra/production/issues/928, we saw a significant amount of network traffic and CPU usage due to Redis checking feature flags via Flipper. Since these flags are hit with every request, the overhead becomes significant. To alleviate Redis overhead, we now cache the data in the following way: * L1: A thread-local memory store for 1 minute * L2: Redis for 1 hour --- .../sh-cache-flipper-checks-in-memory.yml | 5 ++ lib/feature.rb | 23 ++++++- spec/lib/feature_spec.rb | 62 +++++++++++++++++++ 3 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/sh-cache-flipper-checks-in-memory.yml diff --git a/changelogs/unreleased/sh-cache-flipper-checks-in-memory.yml b/changelogs/unreleased/sh-cache-flipper-checks-in-memory.yml new file mode 100644 index 00000000000..125b6244d80 --- /dev/null +++ b/changelogs/unreleased/sh-cache-flipper-checks-in-memory.yml @@ -0,0 +1,5 @@ +--- +title: Cache Flipper feature flags in L1 and L2 caches +merge_request: 30276 +author: +type: performance diff --git a/lib/feature.rb b/lib/feature.rb index 22420e95ea2..e28333aa58e 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -103,10 +103,27 @@ class Feature feature_class: FlipperFeature, gate_class: FlipperGate) + # Redis L2 cache + redis_cache_adapter = + Flipper::Adapters::ActiveSupportCacheStore.new( + active_record_adapter, + l2_cache_backend, + expires_in: 1.hour) + + # Thread-local L1 cache: use a short timeout since we don't have a + # way to expire this cache all at once Flipper::Adapters::ActiveSupportCacheStore.new( - active_record_adapter, - Rails.cache, - expires_in: 1.hour) + redis_cache_adapter, + l1_cache_backend, + expires_in: 1.minute) + end + + def l1_cache_backend + Gitlab::ThreadMemoryCache.cache_backend + end + + def l2_cache_backend + Rails.cache end end diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb index 403e0785d1b..127463a57e8 100644 --- a/spec/lib/feature_spec.rb +++ b/spec/lib/feature_spec.rb @@ -144,6 +144,68 @@ describe Feature do expect(described_class.enabled?(:enabled_feature_flag)).to be_truthy end + it { expect(described_class.l1_cache_backend).to eq(Gitlab::ThreadMemoryCache.cache_backend) } + it { expect(described_class.l2_cache_backend).to eq(Rails.cache) } + + it 'caches the status in L1 and L2 caches', + :request_store, :use_clean_rails_memory_store_caching do + described_class.enable(:enabled_feature_flag) + flipper_key = "flipper/v1/feature/enabled_feature_flag" + + expect(described_class.l2_cache_backend) + .to receive(:fetch) + .once + .with(flipper_key, expires_in: 1.hour) + .and_call_original + + expect(described_class.l1_cache_backend) + .to receive(:fetch) + .once + .with(flipper_key, expires_in: 1.minute) + .and_call_original + + 2.times do + expect(described_class.enabled?(:enabled_feature_flag)).to be_truthy + end + end + + context 'cached feature flag', :request_store do + let(:flag) { :some_feature_flag } + + before do + described_class.flipper.memoize = false + described_class.enabled?(flag) + end + + it 'caches the status in L1 cache for the first minute' do + expect do + expect(described_class.l1_cache_backend).to receive(:fetch).once.and_call_original + expect(described_class.l2_cache_backend).not_to receive(:fetch) + expect(described_class.enabled?(flag)).to be_truthy + end.not_to exceed_query_limit(0) + end + + it 'caches the status in L2 cache after 2 minutes' do + Timecop.travel 2.minutes do + expect do + expect(described_class.l1_cache_backend).to receive(:fetch).once.and_call_original + expect(described_class.l2_cache_backend).to receive(:fetch).once.and_call_original + expect(described_class.enabled?(flag)).to be_truthy + end.not_to exceed_query_limit(0) + end + end + + it 'fetches the status after an hour' do + Timecop.travel 61.minutes do + expect do + expect(described_class.l1_cache_backend).to receive(:fetch).once.and_call_original + expect(described_class.l2_cache_backend).to receive(:fetch).once.and_call_original + expect(described_class.enabled?(flag)).to be_truthy + end.not_to exceed_query_limit(1) + end + end + end + context 'with an individual actor' do CustomActor = Struct.new(:flipper_id) From 4efc8574cb45369797cf902d4e9676506a6e4cf7 Mon Sep 17 00:00:00 2001 From: Marcel Amirault Date: Thu, 4 Jul 2019 00:55:35 +0000 Subject: [PATCH 034/195] Fix notes and update links to issues doc After review, heading was changed and links to it needed to be updated, and minor tweaks to the issues docs such as note formatting --- doc/README.md | 2 +- doc/administration/incoming_email.md | 2 +- doc/administration/index.md | 2 +- doc/administration/issue_closing_pattern.md | 4 +- doc/customization/issue_closing.md | 4 +- .../contributing/merge_request_workflow.md | 2 +- doc/gitlab-basics/README.md | 2 +- doc/gitlab-basics/create-issue.md | 4 +- doc/intro/README.md | 4 +- doc/user/project/cycle_analytics.md | 2 +- .../project/issues/automatic_issue_closing.md | 64 +----- doc/user/project/issues/closing_issues.md | 62 +----- doc/user/project/issues/create_new_issue.md | 107 +-------- .../project/issues/crosslinking_issues.md | 23 +- doc/user/project/issues/csv_import.md | 29 +-- doc/user/project/issues/deleting_issues.md | 16 +- doc/user/project/issues/due_dates.md | 33 +-- doc/user/project/issues/index.md | 97 ++++++--- .../project/issues/issue_data_and_actions.md | 2 +- doc/user/project/issues/managing_issues.md | 204 ++++++++++++++++++ doc/user/project/issues/moving_issues.md | 38 +--- doc/user/project/issues/similar_issues.md | 19 +- doc/user/project/merge_requests/index.md | 2 +- doc/user/project/repository/branches/index.md | 2 +- doc/user/project/repository/web_editor.md | 3 +- doc/workflow/README.md | 2 +- 26 files changed, 352 insertions(+), 379 deletions(-) create mode 100644 doc/user/project/issues/managing_issues.md diff --git a/doc/README.md b/doc/README.md index 489c8117b9d..5eaa998a7b8 100644 --- a/doc/README.md +++ b/doc/README.md @@ -111,7 +111,7 @@ The following documentation relates to the DevOps **Plan** stage: | [Discussions](user/discussions/index.md) | Threads, comments, and resolvable discussions in issues, commits, and merge requests. | | [Due Dates](user/project/issues/due_dates.md) | Keep track of issue deadlines. | | [Epics](user/group/epics/index.md) **[ULTIMATE]** | Tracking groups of issues that share a theme. | -| [Issues](user/project/issues/index.md), including [confidential issues](user/project/issues/confidential_issues.md),
[issue and merge request templates](user/project/description_templates.md),
and [moving issues](user/project/issues/moving_issues.md) | Project issues, restricting access to issues, create templates for submitting new issues and merge requests, and moving issues between projects. | +| [Issues](user/project/issues/index.md), including [confidential issues](user/project/issues/confidential_issues.md),
[issue and merge request templates](user/project/description_templates.md),
and [moving issues](user/project/issues/managing_issues.md#moving-issues) | Project issues, restricting access to issues, create templates for submitting new issues and merge requests, and moving issues between projects. | | [Labels](user/project/labels.md) | Categorize issues or merge requests with descriptive labels. | | [Milestones](user/project/milestones/index.md) | Set milestones for delivery of issues and merge requests, with optional due date. | | [Project Issue Board](user/project/issue_board.md) | Display issues on a Scrum or Kanban board. | diff --git a/doc/administration/incoming_email.md b/doc/administration/incoming_email.md index 8271c579f5b..84a34ae7d6e 100644 --- a/doc/administration/incoming_email.md +++ b/doc/administration/incoming_email.md @@ -4,7 +4,7 @@ GitLab has several features based on receiving incoming emails: - [Reply by Email](reply_by_email.md): allow GitLab users to comment on issues and merge requests by replying to notification emails. -- [New issue by email](../user/project/issues/create_new_issue.md#new-issue-via-email): +- [New issue by email](../user/project/issues/managing_issues.md#new-issue-via-email): allow GitLab users to create a new issue by sending an email to a user-specific email address. - [New merge request by email](../user/project/merge_requests/index.md#create-new-merge-requests-by-email): diff --git a/doc/administration/index.md b/doc/administration/index.md index 602eecb9746..f480d18ea00 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -117,7 +117,7 @@ Learn how to install, configure, update, and maintain your GitLab instance. - Instances. **[PREMIUM ONLY]** - [Auditor users](auditor_users.md): Users with read-only access to all projects, groups, and other resources on the GitLab instance. **[PREMIUM ONLY]** - [Incoming email](incoming_email.md): Configure incoming emails to allow - users to [reply by email](reply_by_email.md), create [issues by email](../user/project/issues/create_new_issue.md#new-issue-via-email) and + users to [reply by email](reply_by_email.md), create [issues by email](../user/project/issues/managing_issues.md#new-issue-via-email) and [merge requests by email](../user/project/merge_requests/index.md#create-new-merge-requests-by-email), and to enable [Service Desk](../user/project/service_desk.md). - [Postfix for incoming email](reply_by_email_postfix_setup.md): Set up a basic Postfix mail server with IMAP authentication on Ubuntu for incoming diff --git a/doc/administration/issue_closing_pattern.md b/doc/administration/issue_closing_pattern.md index 160da47c780..9c352096ecc 100644 --- a/doc/administration/issue_closing_pattern.md +++ b/doc/administration/issue_closing_pattern.md @@ -1,4 +1,4 @@ -# Issue closing pattern +# Issue closing pattern **[CORE ONLY]** >**Note:** This is the administration documentation. @@ -46,4 +46,4 @@ Because Rubular doesn't understand `%{issue_ref}`, you can replace this by [gitlab.yml.example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example [reconfigure]: restart_gitlab.md#omnibus-gitlab-reconfigure [restart]: restart_gitlab.md#installations-from-source -[user documentation]: ../user/project/issues/automatic_issue_closing.md +[user documentation]: ../user/project/issues/managing_issues.md#closing-issues-automatically diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md index 680c51e7524..9333f55ca9c 100644 --- a/doc/customization/issue_closing.md +++ b/doc/customization/issue_closing.md @@ -1,3 +1,5 @@ --- -redirect_to: '../user/project/issues/automatic_issue_closing.md' +redirect_to: '../user/project/issues/managing_issues.md#closing-issues-automatically' --- + +This document was moved to [another location](../user/project/issues/managing_issues.md#closing-issues-automatically). diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md index 6064f59ed10..ce5d9786e6e 100644 --- a/doc/development/contributing/merge_request_workflow.md +++ b/doc/development/contributing/merge_request_workflow.md @@ -65,7 +65,7 @@ request is as follows: 1. If you are contributing documentation, choose `Documentation` from the "Choose a template" menu and fill in the description according to the template. 1. Mention the issue(s) your merge request solves, using the `Solves #XXX` or - `Closes #XXX` syntax to [auto-close](../../user/project/issues/automatic_issue_closing.md) + `Closes #XXX` syntax to [auto-close](../../user/project/issues/managing_issues.md#closing-issues-automatically) the issue(s) once the merge request is merged. 1. If you're allowed to (Core team members, for example), set a relevant milestone and [labels](issue_workflow.md). diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md index 0c268eff9f1..fd16047b8e4 100644 --- a/doc/gitlab-basics/README.md +++ b/doc/gitlab-basics/README.md @@ -22,7 +22,7 @@ The following are guides to basic GitLab functionality: - [Fork a project](fork-project.md), to duplicate projects so they can be worked on in parallel. - [Add a file](add-file.md), to add new files to a project's repository. - [Add an image](add-image.md), to add new images to a project's repository. -- [Create an issue](../user/project/issues/create_new_issue.md), to start collaborating within a project. +- [Create an issue](../user/project/issues/managing_issues.md#create-a-new-issue), to start collaborating within a project. - [Create a merge request](add-merge-request.md), to request changes made in a branch be merged into a project's repository. - See how these features come together in the [GitLab Flow introduction video](https://youtu.be/InKNIvky2KE) and [GitLab Flow page](../workflow/gitlab_flow.md). diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md index 6e2a09fc030..5fa5f1bf2e2 100644 --- a/doc/gitlab-basics/create-issue.md +++ b/doc/gitlab-basics/create-issue.md @@ -1,5 +1,5 @@ --- -redirect_to: '../user/project/issues/index.md#issue-actions' +redirect_to: '../user/project/issues/index.md#viewing-and-managing-issues' --- -This document was moved to [another location](../user/project/issues/index.md#issue-actions). +This document was moved to [another location](../user/project/issues/index.md#viewing-and-managing-issues). diff --git a/doc/intro/README.md b/doc/intro/README.md index d9c733d4285..9a8cd925e48 100644 --- a/doc/intro/README.md +++ b/doc/intro/README.md @@ -15,7 +15,7 @@ Create projects and groups. Create issues, labels, milestones, cast your vote, and review issues. -- [Create an issue](../user/project/issues/create_new_issue.md) +- [Create an issue](../user/project/issues/managing_issues.md#create-a-new-issue) - [Assign labels to issues](../user/project/labels.md) - [Use milestones as an overview of your project's tracker](../user/project/milestones/index.md) - [Use voting to express your like/dislike to issues and merge requests](../workflow/award_emoji.md) @@ -26,7 +26,7 @@ Create merge requests and review code. - [Fork a project and contribute to it](../workflow/forking_workflow.md) - [Create a new merge request](../gitlab-basics/add-merge-request.md) -- [Automatically close issues from merge requests](../user/project/issues/automatic_issue_closing.md) +- [Automatically close issues from merge requests](../user/project/issues/managing_issues.md#closing-issues-automatically) - [Automatically merge when pipeline succeeds](../user/project/merge_requests/merge_when_pipeline_succeeds.md) - [Revert any commit](../user/project/merge_requests/revert_changes.md) - [Cherry-pick any commit](../user/project/merge_requests/cherry_pick_changes.md) diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md index dc97a44fd68..5d36e1d4be3 100644 --- a/doc/user/project/cycle_analytics.md +++ b/doc/user/project/cycle_analytics.md @@ -156,6 +156,6 @@ Learn more about Cycle Analytics in the following resources: [environment]: ../../ci/yaml/README.md#environment [GitLab flow]: ../../workflow/gitlab_flow.md [idea to production]: https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab -[issue closing pattern]: issues/automatic_issue_closing.md +[issue closing pattern]: issues/managing_issues.md#closing-issues-automatically [permissions]: ../permissions.md [yml]: ../../ci/yaml/README.md diff --git a/doc/user/project/issues/automatic_issue_closing.md b/doc/user/project/issues/automatic_issue_closing.md index c3e06b219ff..dab79327d6a 100644 --- a/doc/user/project/issues/automatic_issue_closing.md +++ b/doc/user/project/issues/automatic_issue_closing.md @@ -1,63 +1,5 @@ -# Automatic issue closing - ->**Notes:** -> -> - This is the user docs. In order to change the default issue closing pattern, -> follow the steps in the [administration docs]. -> - For performance reasons, automatic issue closing is disabled for the very -> first push from an existing repository. - -When a commit or merge request resolves one or more issues, it is possible to -automatically have these issues closed when the commit or merge request lands -in the project's default branch. - -If a commit message or merge request description contains a sentence matching -a certain regular expression, all issues referenced from the matched text will -be closed. This happens when the commit is pushed to a project's -[**default** branch](../repository/branches/index.md#default-branch), or when a -commit or merge request is merged into it. - -## Default closing pattern value - -When not specified, the default issue closing pattern as shown below will be -used: - -```bash -((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)|[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+) -``` - -Note that `%{issue_ref}` is a complex regular expression defined inside GitLab's -source code that can match references to: - -- A local issue (`#123`). -- A cross-project issue (`group/project#123`). -- A link to an issue - (`https://gitlab.example.com/group/project/issues/123`). - +--- +redirect_to: 'managing_issues.md#closing-issues-automatically' --- -This translates to the following keywords: - -- Close, Closes, Closed, Closing, close, closes, closed, closing -- Fix, Fixes, Fixed, Fixing, fix, fixes, fixed, fixing -- Resolve, Resolves, Resolved, Resolving, resolve, resolves, resolved, resolving -- Implement, Implements, Implemented, Implementing, implement, implements, implemented, implementing - ---- - -For example the following commit message: - -``` -Awesome commit message - -Fix #20, Fixes #21 and Closes group/otherproject#22. -This commit is also related to #17 and fixes #18, #19 -and https://gitlab.example.com/group/otherproject/issues/23. -``` - -will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed -to, as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as -it does not match the pattern. It works with multi-line commit messages as well -as one-liners when used with `git commit -m`. - -[administration docs]: ../../../administration/issue_closing_pattern.md +This document was moved to [another location](managing_issues.md#closing-issues-automatically). diff --git a/doc/user/project/issues/closing_issues.md b/doc/user/project/issues/closing_issues.md index 1d88745af9f..04f1c8e1a4a 100644 --- a/doc/user/project/issues/closing_issues.md +++ b/doc/user/project/issues/closing_issues.md @@ -1,59 +1,5 @@ -# Closing Issues +--- +redirect_to: 'managing_issues.md#closing-issues' +--- -Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues. - -## Directly - -Whenever you decide that's no longer need for that issue, -close the issue using the close button: - -![close issue - button](img/button_close_issue.png) - -## Via Merge Request - -When a merge request resolves the discussion over an issue, you can -make it close that issue(s) when merged. - -All you need is to use a [keyword](automatic_issue_closing.md) -accompanying the issue number, add to the description of that MR. - -In this example, the keyword "closes" prefixing the issue number will create a relationship -in such a way that the merge request will close the issue when merged. - -Mentioning various issues in the same line also works for this purpose: - -```md -Closes #333, #444, #555 and #666 -``` - -If the issue is in a different repository rather then the MR's, -add the full URL for that issue(s): - -```md -Closes #333, #444, and https://gitlab.com///issues/ -``` - -All the following keywords will produce the same behaviour: - -- Close, Closes, Closed, Closing, close, closes, closed, closing -- Fix, Fixes, Fixed, Fixing, fix, fixes, fixed, fixing -- Resolve, Resolves, Resolved, Resolving, resolve, resolves, resolved, resolving - -![merge request closing issue when merged](img/merge_request_closes_issue.png) - -If you use any other word before the issue number, the issue and the MR will -link to each other, but the MR will NOT close the issue(s) when merged. - -![mention issues in MRs - closing and related](img/closing_and_related_issues.png) - -## From the Issue Board - -You can close an issue from [Issue Boards](../issue_board.md) by dragging an issue card -from its list and dropping into **Closed**. - -![close issue from the Issue Board](img/close_issue_from_board.gif) - -## Customizing the issue closing pattern - -Alternatively, a GitLab **administrator** can -[customize the issue closing pattern](../../../administration/issue_closing_pattern.md). +This document was moved to [another location](managing_issues.md#closing-issues). diff --git a/doc/user/project/issues/create_new_issue.md b/doc/user/project/issues/create_new_issue.md index c2916c79876..8eec29716c1 100644 --- a/doc/user/project/issues/create_new_issue.md +++ b/doc/user/project/issues/create_new_issue.md @@ -1,104 +1,5 @@ -# Create a new Issue +--- +redirect_to: 'managing_issues.md#create-a-new-issue' +--- -Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues. - -When you create a new issue, you'll be prompted to fill in -the information illustrated on the image below. - -![New issue from the issues list](img/new_issue.png) - -Read through the [issue data and actions documentation](issue_data_and_actions.md#parts-of-an-issue) -to understand these fields one by one. - -## New issue from the Issue Tracker - -Navigate to your **Project's Dashboard** > **Issues** > **New Issue** to create a new issue: - -![New issue from the issue list view](img/new_issue_from_tracker_list.png) - -## New issue from an opened issue - -From an **opened issue** in your project, click **New Issue** to create a new -issue in the same project: - -![New issue from an open issue](img/new_issue_from_open_issue.png) - -## New issue from the project's dashboard - -From your **Project's Dashboard**, click the plus sign (**+**) to open a dropdown -menu with a few options. Select **New Issue** to create an issue in that project: - -![New issue from a project's dashboard](img/new_issue_from_projects_dashboard.png) - -## New issue from the Issue Board - -From an Issue Board, create a new issue by clicking on the plus sign (**+**) on the top of a list. -It opens a new issue for that project labeled after its respective list. - -![From the issue board](img/new_issue_from_issue_board.png) - -## New issue via email - -At the bottom of a project's Issues List page, a link to **Email a new issue to this project** -is displayed if your GitLab instance has [incoming email](../../../administration/incoming_email.md) configured. - -![Bottom of a project issues page](img/new_issue_from_email.png) - -When you click this link, an email address is displayed which belongs to you for creating issues in this project. -You can save this address as a contact in your email client for easy acceess. - -CAUTION: **Caution:** -This is a private email address, generated just for you. **Keep it to yourself**, -as anyone who gets ahold of it can create issues or merge requests as if they -were you. If the address is compromised, or you'd like it to be regenerated for -any reason, click **Email a new issue to this project** again and click the reset link. - -Sending an email to this address will create a new issue on your behalf for -this project, where: - -- The email subject becomes the issue title. -- The email body becomes the issue description. -- [Markdown](../../markdown.md) and [quick actions](../quick_actions.md) are supported. - -NOTE: **Note:** -In GitLab 11.7, we updated the format of the generated email address. -However the older format is still supported, allowing existing aliases -or contacts to continue working._ - -## New issue via Service Desk **[PREMIUM]** - -Enable [Service Desk](../service_desk.md) to your project and offer email support. -By doing so, when your customer sends a new email, a new issue can be created in -the appropriate project and followed up from there. - -## New issue from the group-level Issue Tracker - -Head to the Group dashboard and click "Issues" in the sidebar to visit the Issue Tracker -for all projects in your Group. Select the project you'd like to add an issue for -using the dropdown button at the top-right of the page. - -![Select project to create issue](img/select_project_from_group_level_issue_tracker.png) - -We'll keep track of the project you selected most recently, and use it as the default -for your next visit. This should save you a lot of time and clicks, if you mostly -create issues for the same project. - -![Create issue from group-level issue tracker](img/create_issue_from_group_level_issue_tracker.png) - -## New issue via URL with prefilled fields - -You can link directly to the new issue page for a given project, with prefilled -field values using query string parameters in a URL. This is useful for embedding -a URL in an external HTML page, and also certain scenarios where you want the user to -create an issue with certain fields prefilled. - -The title, description, and description template fields can be prefilled using -this method. The description and description template fields cannot be pre-entered -in the same URL (since a description template just populates the description field). - -Follow these examples to form your new issue URL with prefilled fields. - -- For a new issue in the GitLab Community Edition project with a pre-entered title - and a pre-entered description, the URL would be `https://gitlab.com/gitlab-org/gitlab-ce/issues/new?issue[title]=Validate%20new%20concept&issue[description]=Research%20idea` -- For a new issue in the GitLab Community Edition project with a pre-entered title - and a pre-entered description template, the URL would be `https://gitlab.com/gitlab-org/gitlab-ce/issues/new?issue[title]=Validate%20new%20concept&issuable_template=Research%20proposal` +This document was moved to [another location](managing_issues.md#create-a-new-issue). diff --git a/doc/user/project/issues/crosslinking_issues.md b/doc/user/project/issues/crosslinking_issues.md index ff5b1f2ce50..93dc2a2e4ca 100644 --- a/doc/user/project/issues/crosslinking_issues.md +++ b/doc/user/project/issues/crosslinking_issues.md @@ -25,9 +25,8 @@ git commit -m "this is my commit message. Related to https://gitlab.com/ **Note:** A permission level of `Developer` or higher is required to import issues. +NOTE: **Note:** A permission level of [Developer](../../permissions.md), or higher, is required +to import issues. ## Prepare for the import @@ -24,34 +25,38 @@ To import issues: 1. Select the file and click the **Import issues** button. The file is processed in the background and a notification email is sent -to you once the import is completed. +to you once the import is complete. ## CSV file format -### Header row +Sample CSV file data: CSV files must contain a header row where the first column header is `title` and the second is `description`. If additional columns are present, they will be ignored. -### Column separator +### Header row -The column separator is automatically detected from the header row. +CSV files must contain a header row beginning with at least two columns, `title` and +`description`, in that order. If additional columns are present, they will be ignored. -Supported separator characters are: commas (`,`), semicolons (`;`), and tabs (`\t`). +### Separators -### Row separator +The column separator is automatically detected from the header row. Supported separator +characters are: commas (`,`), semicolons (`;`), and tabs (`\t`). -Lines ending in either `CRLF` or `LF` are supported. +The row separator can be either `CRLF` or `LF`. ### Quote character -The double-quote (`"`) character is used to quote fields so you can use the column separator within a field. To insert -a double-quote (`"`) within a quoted field, use two double-quote characters in succession, i.e. `""`. +The double-quote (`"`) character is used to quote fields, enabling the use of the column +separator within a field (see the third line in the [sample CSV](#csv-file-format)). +To insert a double-quote (`"`) within a quoted field, use two double-quote characters +in succession, i.e. `""`. ### Data rows -After the header row, succeeding rows must follow the same column order. The issue title is required while the -description is optional. +After the header row, succeeding rows must follow the same column order. The issue +title is required while the description is optional. ### File size diff --git a/doc/user/project/issues/deleting_issues.md b/doc/user/project/issues/deleting_issues.md index 536a0de8974..e50259e0dcf 100644 --- a/doc/user/project/issues/deleting_issues.md +++ b/doc/user/project/issues/deleting_issues.md @@ -1,13 +1,5 @@ -# Deleting Issues +--- +redirect_to: 'managing_issues.md#deleting-issues' +--- -> [Introduced][ce-2982] in GitLab 8.6 - -Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues. - -You can delete an issue by editing it and clicking on the delete button. - -![delete issue - button](img/delete_issue.png) - ->**Note:** Only [project owners](../../permissions.md) can delete issues. - -[ce-2982]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2982 \ No newline at end of file +This document was moved to [another location](managing_issues.md#deleting-issues). diff --git a/doc/user/project/issues/due_dates.md b/doc/user/project/issues/due_dates.md index 987c16dfab6..be577b3f24c 100644 --- a/doc/user/project/issues/due_dates.md +++ b/doc/user/project/issues/due_dates.md @@ -4,30 +4,32 @@ Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues. -Due dates can be used in issues to keep track of deadlines and make sure -features are shipped on time. Due dates require at least [Reporter permissions](../../permissions.md#project-members-permissions) -to be able to edit them. On the contrary, they can be seen by everybody. +Due dates can be used in issues to keep track of deadlines and make sure features are +shipped on time. Users must have at least [Reporter permissions](../../permissions.md) +to be able to edit them, but they can be seen by everybody with permission to view +the issue. ## Setting a due date -When creating or editing an issue, you can see the due date field from where -a calendar will appear to help you choose the date you want. To remove it, -select the date text and delete it. +When creating or editing an issue, you can click in the **due date** field and a calendar +will appear to help you choose the date you want. To remove the date, select the date +text and delete it. The date is related to the server's timezone, not the timezone of +the user setting the due date. ![Create a due date](img/due_dates_create.png) -A quicker way to set a due date is via the issue sidebar. Simply expand the -sidebar and select **Edit** to pick a due date or remove the existing one. +You can also set a due date via the issue sidebar. Expand the +sidebar and click **Edit** to pick a due date or remove the existing one. Changes are saved immediately. ![Edit a due date via the sidebar](img/due_dates_edit_sidebar.png) ## Making use of due dates -Issues that have a due date can be distinctively seen in the issue tracker +Issues that have a due date can be easily seen in the issue tracker, displaying a date next to them. Issues where the date is overdue will have the icon and the date colored red. You can sort issues by those that are -_Due soon_ or _Due later_ from the dropdown menu in the right. +`Due soon` or `Due later` from the dropdown menu on the right. ![Issues with due dates in the issues index page](img/due_dates_issues_index_page.png) @@ -36,14 +38,13 @@ Due dates also appear in your [todos list](../../../workflow/todos.md). ![Issues with due dates in the todos](img/due_dates_todos.png) The day before an open issue is due, an email will be sent to all participants -of the issue. Both the due date and the day before are calculated using the -server's timezone. +of the issue. Like the due date, the "day before the due date" is determined by the +server's timezone, ignoring the participants' timezones. Issues with due dates can also be exported as an iCalendar feed. The URL of the -feed can be added to calendar applications. The feed is accessible by clicking -on the _Subscribe to calendar_ button on the following pages: +feed can be added to many calendar applications. The feed is accessible by clicking +on the **Subscribe to calendar** button on the following pages: -- on the **Assigned Issues** page that is linked on the right-hand side of the - GitLab header +- on the **Assigned Issues** page that is linked on the right-hand side of the GitLab header - on the **Project Issues** page - on the **Group Issues** page diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md index 4acbb4cc3f6..86c2f2f3959 100644 --- a/doc/user/project/issues/index.md +++ b/doc/user/project/issues/index.md @@ -6,8 +6,9 @@ Issues are the fundamental medium for collaborating on ideas and planning work i The GitLab issue tracker is an advanced tool for collaboratively developing ideas, solving problems, and planning work. -Issues can allow you, your team, and your collaborators to share and discuss proposals before and during their implementation. -However, they can be used for a variety of other purposes, customized to your needs and workflow. +Issues can allow you, your team, and your collaborators to share and discuss proposals +before, and during, their implementation. However, they can be used for a variety of +other purposes, customized to your needs and workflow. Issues are always associated with a specific project, but if you have multiple projects in a group, you can also view all the issues collectively at the group level. @@ -17,13 +18,15 @@ you can also view all the issues collectively at the group level. - Discussing the implementation of a new idea - Tracking tasks and work status - Accepting feature proposals, questions, support requests, or bug reports -- Elaborating new code implementations +- Elaborating on new code implementations -See also the blog post "[Always start a discussion with an issue](https://about.gitlab.com/2016/03/03/start-with-an-issue/)". +See also [Always start a discussion with an issue](https://about.gitlab.com/2016/03/03/start-with-an-issue/). ## Parts of an issue -Issues contain a variety of content and metadata, enabling a large range of flexibility in how they are used. Each issue can contain the following attributes, though some items may remain unset. +Issues contain a variety of content and metadata, enabling a large range of flexibility +in how they are used. Each issue can contain the following attributes, though not all items +must be set. @@ -70,23 +73,36 @@ Issues contain a variety of content and metadata, enabling a large range of flex ## Viewing and managing issues -While you can view and manage the full detail of an issue at its URL, you can also work with multiple issues at a time using the Issues List, Issue Boards, Epics **[ULTIMATE]**, and issue references. +While you can view and manage the full details of an issue on the [issue page](#issue-page), +you can also work with multiple issues at a time using the [Issues List](#issues-list), +[Issue Boards](#issue-boards), Issue references, and [Epics](#epics-ultimate)**[ULTIMATE]**. + +Key actions for Issues include: + +- [Creating issues](managing_issues.md#create-a-new-issue) +- [Moving issues](managing_issues.md#moving-issues) +- [Closing issues](managing_issues.md#closing-issues) +- [Deleting issues](managing_issues.md#deleting-issues) ### Issue page ![Issue view](img/issues_main_view.png) -On an issue’s page, you can view all aspects of the issue, and you can also modify them if you you have the necessary [permissions](../../permissions.md). - -For more information, see the [Issue Data and Actions](issue_data_and_actions.md) page. +On an issue's page, you can view [all aspects of the issue](issue_data_and_actions.md), +and modify them if you you have the necessary [permissions](../../permissions.md). ### Issues list ![Project issues list view](img/project_issues_list_view.png) -On the Issues List, you can view all issues in the current project, or from multiple projects when opening the Issues List from the higher-level group context. Filter the issue list by [any search query](../../search/index.md#issues-and-merge-requests-per-project) and/or specific metadata, such as label(s), assignees(s), status, and more. From this view, you can also make certain changes [in bulk](../bulk_editing.md) to the displayed issues. +On the Issues List, you can view all issues in the current project, or from multiple +projects when opening the Issues List from the higher-level group context. Filter the +issue list with a [search query](../../search/index.md#issues-and-merge-requests-per-project), +including specific metadata, such as label(s), assignees(s), status, and more. From this +view, you can also make certain changes [in bulk](../bulk_editing.md) to the displayed issues. -For more information on interacting with Issues, see the [Issue Data and Actions](issue_data_and_actions.md) page. +For more information, see the [Issue Data and Actions](issue_data_and_actions.md) page +for a rundown of all the fields and information in an issue. For sorting by issue priority, see [Label Priority](../labels.md#label-priority). @@ -94,44 +110,55 @@ For sorting by issue priority, see [Label Priority](../labels.md#label-priority) ![Issue board](img/issue_board.png) -Issue boards are Kanban boards with columns that display issues based on their labels or their assignees**[PREMIUM]**. They offer the flexibility to manage issues using highly customizable workflows. +[Issue boards](../issue_board.md) are Kanban boards with columns that display issues based on their labels +or their assignees**[PREMIUM]**. They offer the flexibility to manage issues using +highly customizable workflows. -You can reorder issues within a column, or drag an issue card to another column; its associated label or assignee will change to match that of the new column. The entire board can also be filtered to only include issues from a certain milestone or an overarching label. - -For more information, see the [Issue Boards](../issue_board.md) page. +You can reorder issues within a column. If you drag an issue card to another column, its +associated label or assignee will change to match that of the new column. The entire +board can also be filtered to only include issues from a certain milestone or an overarching +label. ### Epics **[ULTIMATE]** -Epics let you manage your portfolio of projects more efficiently and with less effort by tracking groups of issues that share a theme, across projects and milestones. - -For more information, see the [Epics](../../group/epics/index.md) page. +[Epics](../../group/epics/index.md) let you manage your portfolio of projects more +efficiently and with less effort by tracking groups of issues that share a theme, across +projects and milestones. ### Related issues **[STARTER]** -You can mark two issues as related, so that when viewing each one, the other is always listed in its Related Issues section. This can help display important context, such as past work, dependencies, or duplicates. - -For more information, see [Related Issues](related_issues.md). +You can mark two issues as related, so that when viewing one, the other is always +listed in its [Related Issues](related_issues.md) section. This can help display important +context, such as past work, dependencies, or duplicates. ### Crosslinking issues -When you reference an issue from another issue or merge request by including its URL or ID, the referenced issue displays a message in the Activity stream about the reference, with a link to the other issue or MR. +You can [crosslink issues](crosslinking_issues.md) by referencing an issue from another +issue or merge request by including its URL or ID. The referenced issue displays a +message in the Activity stream about the reference, with a link to the other issue or MR. -For more information, see [Crosslinking issues](crosslinking_issues.md). +### Similar issues -## Issue actions +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22866) in GitLab 11.6. + +To prevent duplication of issues for the same topic, GitLab searches for similar issues +when new issues are being created. + +When typing in the title in the new issue form, GitLab searches titles and descriptions +across all issues the user has access to in the current project. Up 5 similar issues, +sorted by most recently updated, are displayed below the title box. Note that this feature +requires [GraphQL](../../../api/graphql/index.md) to be enabled. + +![Similar issues](img/similar_issues.png) + +## Other Issue actions -- [Create an issue](create_new_issue.md) - [Create an issue from a template](../../project/description_templates.md#using-the-templates) -- [Close an issue](closing_issues.md) -- [Move an issue](moving_issues.md) -- [Delete an issue](deleting_issues.md) -- [Create a merge request from an issue](issue_data_and_actions.md#22-create-merge-request) - -## Advanced issue management - -- [Bulk edit issues](../bulk_editing.md) - From the Issues List, select multiple issues in order to change their status, assignee, milestone, or labels in bulk. +- [Set a due date](due_dates.md) +- [Bulk edit issues](../bulk_editing.md) - From the Issues List, select multiple issues + in order to change their status, assignee, milestone, or labels in bulk. - [Import issues](csv_import.md) - [Export issues](csv_export.md) **[STARTER]** - [Issues API](../../../api/issues.md) -- Configure an [external issue tracker](../../../integration/external-issue-tracker.md) such as Jira, Redmine, - or Bugzilla. +- Configure an [external issue tracker](../../../integration/external-issue-tracker.md) + such as Jira, Redmine, or Bugzilla. diff --git a/doc/user/project/issues/issue_data_and_actions.md b/doc/user/project/issues/issue_data_and_actions.md index 2103f331aa2..9898cd6cf15 100644 --- a/doc/user/project/issues/issue_data_and_actions.md +++ b/doc/user/project/issues/issue_data_and_actions.md @@ -48,7 +48,7 @@ which you can click to mark that issue as done (which will be reflected in the T #### 3. Assignee An issue can be assigned to yourself, another person, or [many people](#31-multiple-assignees-STARTER). -The assignee(s) can be changed as much as needed. The idea is that the assignees are +The assignee(s) can be changed as often as needed. The idea is that the assignees are responsible for that issue until it's reassigned to someone else to take it from there. When assigned to someone, it will appear in their assigned issues list. diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md new file mode 100644 index 00000000000..663bacf4a45 --- /dev/null +++ b/doc/user/project/issues/managing_issues.md @@ -0,0 +1,204 @@ +# Managing Issues + +[GitLab Issues](index.md) are the fundamental medium for collaborating on ideas and +planning work in GitLab. [Creating](#create-a-new-issue), [moving](#moving-issues), +[closing](#closing-issues), and [deleting](#deleting-issues) are key actions that +you can do with issues. + +## Create a new Issue + +When you create a new issue, you'll be prompted to fill in the [data and fields of the issue](issue_data_and_actions.md#parts-of-an-issue), as illustrated below. + +![New issue from the issues list](img/new_issue.png) + +### Accessing the new Issue form + +There are many ways to get to the new Issue form from within a project: + +- Navigate to your **Project's Dashboard** > **Issues** > **New Issue**: + + ![New issue from the issue list view](img/new_issue_from_tracker_list.png) + +- From an **opened issue** in your project, click **New Issue** to create a new + issue in the same project: + + ![New issue from an open issue](img/new_issue_from_open_issue.png) + +- From your **Project's Dashboard**, click the plus sign (**+**) to open a dropdown + menu with a few options. Select **New Issue** to create an issue in that project: + + ![New issue from a project's dashboard](img/new_issue_from_projects_dashboard.png) + +- From an **Issue Board**, create a new issue by clicking on the plus sign (**+**) at the top of a list. + It opens a new issue for that project, pre-labeled with its respective list. + + ![From the issue board](img/new_issue_from_issue_board.png) + +### New issue from the group-level Issue Tracker + +Go to the Group dashboard and click "Issues" in the sidebar to visit the Issue Tracker +for all projects in your Group. Select the project you'd like to add an issue for +using the dropdown button at the top-right of the page. + +![Select project to create issue](img/select_project_from_group_level_issue_tracker.png) + +We'll keep track of the project you selected most recently, and use it as the default +for your next visit. This should save you a lot of time and clicks, if you mostly +create issues for the same project. + +![Create issue from group-level issue tracker](img/create_issue_from_group_level_issue_tracker.png) + +### New issue via Service Desk **[PREMIUM]** + +Enable [Service Desk](../service_desk.md) for your project and offer email support. +By doing so, when your customer sends a new email, a new issue can be created in +the appropriate project and followed up from there. + +### New issue via email + +A link to **Email a new issue to this project** is displayed at the bottom of a project's +**Issues List** page, if your GitLab instance has [incoming email](../../../administration/incoming_email.md) +configured. + +![Bottom of a project issues page](img/new_issue_from_email.png) + +When you click this link, an email address is generated and displayed, which should be used +by **you only**, to create issues in this project. You can save this address as a +contact in your email client for easy acceess. + +CAUTION: **Caution:** +This is a private email address, generated just for you. **Keep it to yourself**, +as anyone who knows it can create issues or merge requests as if they +were you. If the address is compromised, or you'd like it to be regenerated for +any reason, click **Email a new issue to this project** again and click the reset link. + +Sending an email to this address will create a new issue in your name for +this project, where: + +- The email subject becomes the issue title. +- The email body becomes the issue description. +- [Markdown](../../markdown.md) and [quick actions](../quick_actions.md) are supported. + +NOTE: **Note:** +In GitLab 11.7, we updated the format of the generated email address. However the +older format is still supported, allowing existing aliases or contacts to continue working. + +### New issue via URL with prefilled fields + +You can link directly to the new issue page for a given project, with prefilled +field values using query string parameters in a URL. This is useful for embedding +a URL in an external HTML page, and also certain scenarios where you want the user to +create an issue with certain fields prefilled. + +The title, description, and description template fields can be prefilled using +this method. You cannot pre-fill both the description and description template fields +in the same URL (since a description template also populates the description field). + +Follow these examples to form your new issue URL with prefilled fields. + +- For a new issue in the GitLab Community Edition project with a pre-filled title + and a pre-filled description, the URL would be `https://gitlab.com/gitlab-org/gitlab-ce/issues/new?issue[title]=Validate%20new%20concept&issue[description]=Research%20idea` +- For a new issue in the GitLab Community Edition project with a pre-filled title + and a pre-filled description template, the URL would be `https://gitlab.com/gitlab-org/gitlab-ce/issues/new?issue[title]=Validate%20new%20concept&issuable_template=Research%20proposal` + +## Moving Issues + +Moving an issue will copy it to a new location (project), and close it in the old project, +but it will not be deleted. There will also be a system note added to both issues +indicating where it came from and went to. + +The "Move issue" button is at the bottom of the right-sidebar when viewing the issue. + +![move issue - button](img/sidebar_move_issue.png) + +## Closing Issues + +When you decide that an issue is resolved, or no longer needed, you can close the issue +using the close button: + +![close issue - button](img/button_close_issue.png) + +You can also close an issue from the [Issue Boards](../issue_board.md) by dragging an issue card +from its list and dropping it into the **Closed** list. + +![close issue from the Issue Board](img/close_issue_from_board.gif) + +### Closing issues automatically + +NOTE: **Note:** +For performance reasons, automatic issue closing is disabled for the very first +push from an existing repository. + +When a commit or merge request resolves one or more issues, it is possible to have +these issues closed automatically when the commit or merge request reaches the project's +default branch. + +If a commit message or merge request description contains text matching a [defined pattern](#default-closing-pattern), +all issues referenced in the matched text will be closed. This happens when the commit +is pushed to a project's [**default** branch](../repository/branches/index.md#default-branch), +or when a commit or merge request is merged into it. + +For example, if `Closes #4, #6, Related to #5` is included in a Merge Request +description, issues `#4` and `#6` will close automatically when the MR is merged, but not `#5`. +Using `Related to` flags `#5` as a [related issue](related_issues.md), +but it will not close automatically. + +![merge request closing issue when merged](img/merge_request_closes_issue.png) + +If the issue is in a different repository than the MR, add the full URL for the issue(s): + +```md +Closes #4, #6, and https://gitlab.com///issues/ +``` + +#### Default closing pattern + +When not specified, the default issue closing pattern as shown below will be used: + +```bash +((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)|[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+) +``` + +This translates to the following keywords: + +- Close, Closes, Closed, Closing, close, closes, closed, closing +- Fix, Fixes, Fixed, Fixing, fix, fixes, fixed, fixing +- Resolve, Resolves, Resolved, Resolving, resolve, resolves, resolved, resolving +- Implement, Implements, Implemented, Implementing, implement, implements, implemented, implementing + +Note that `%{issue_ref}` is a complex regular expression defined inside GitLab's +source code that can match references to: + +- A local issue (`#123`). +- A cross-project issue (`group/project#123`). +- A link to an issue (`https://gitlab.example.com/group/project/issues/123`). + +For example the following commit message: + +``` +Awesome commit message + +Fix #20, Fixes #21 and Closes group/otherproject#22. +This commit is also related to #17 and fixes #18, #19 +and https://gitlab.example.com/group/otherproject/issues/23. +``` + +will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed to, +as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as it does +not match the pattern. It works with multi-line commit messages as well as one-liners +when used from the command line with `git commit -m`. + +#### Customizing the issue closing pattern **[CORE ONLY]** + +In order to change the default issue closing pattern, you must edit the +[`gitlab.rb` or `gitlab.yml` file](../../../administration/issue_closing_pattern.md) +of your installation. + +## Deleting Issues + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2982) in GitLab 8.6 + +Users with [project owner permission](../../permissions.md) can delete an issue by +editing it and clicking on the delete button. + +![delete issue - button](img/delete_issue.png) diff --git a/doc/user/project/issues/moving_issues.md b/doc/user/project/issues/moving_issues.md index 8aac2c01444..8331f865b83 100644 --- a/doc/user/project/issues/moving_issues.md +++ b/doc/user/project/issues/moving_issues.md @@ -1,35 +1,5 @@ -# Moving Issues - -Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues. - -Moving an issue will close it and duplicate it on the specified project. -There will also be a system note added to both issues indicating where it came from or went to. - -You can move an issue with the "Move issue" button at the bottom of the right-sidebar when viewing the issue. - -![move issue - button](img/sidebar_move_issue.png) - -## Troubleshooting - -### Moving Issues in Bulk - -If you have advanced technical skills you can also bulk move all the issues from one project to another in the rails console. The below script will move all the issues from one project to another that are not in status **closed**. - -To access rails console run `sudo gitlab-rails console` on the GitLab server and run the below script. Please be sure to change **project**, **admin_user** and **target_project** to your values. We do also recommend [creating a backup](https://docs.gitlab.com/ee/raketasks/backup_restore.html#creating-a-backup-of-the-gitlab-system) before attempting any changes in the console. - -```ruby -project = Project.find_by_full_path('full path of the project where issues are moved from') -issues = project.issues -admin_user = User.find_by_username('username of admin user') # make sure user has permissions to move the issues -target_project = Project.find_by_full_path('full path of target project where issues moved to') - -issues.each do |issue| - if issue.state != "closed" && issue.moved_to.nil? - Issues::MoveService.new(project, admin_user).execute(issue, target_project) - else - puts "issue with id: #{issue.id} and title: #{issue.title} was not moved" - end -end; nil - -``` +--- +redirect_to: 'managing_issues.md#moving-issues' +--- +This document was moved to [another location](managing_issues.md#moving-issues). diff --git a/doc/user/project/issues/similar_issues.md b/doc/user/project/issues/similar_issues.md index e90ecd88ec6..9cbac53ee41 100644 --- a/doc/user/project/issues/similar_issues.md +++ b/doc/user/project/issues/similar_issues.md @@ -1,16 +1,5 @@ -# Similar issues +--- +redirect_to: 'index.md#similar-issues' +--- -> [Introduced][ce-22866] in GitLab 11.6. - -Similar issues suggests issues that are similar when new issues are being created. -This features requires [GraphQL] to be enabled. - -![Similar issues](img/similar_issues.png) - -You can see the similar issues when typing in the title in the new issue form. -This searches both titles and descriptions across all issues the user has access -to in the current project. It then displays the first 5 issues sorted by most -recently updated. - -[GraphQL]: ../../../api/graphql/index.md -[ce-22866]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22866 +This document was moved to [another location](index.md#similar-issues). diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 447b338928c..169b10572b0 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -23,7 +23,7 @@ With GitLab merge requests, you can: - Build, test, and deploy your code in a per-branch basis with built-in [GitLab CI/CD](../../../ci/README.md) - Prevent the merge request from being merged before it's ready with [WIP MRs](#work-in-progress-merge-requests) - View the deployment process through [Pipeline Graphs](../../../ci/pipelines.md#visualizing-pipelines) -- [Automatically close the issue(s)](../../project/issues/closing_issues.md#via-merge-request) that originated the implementation proposed in the merge request +- [Automatically close the issue(s)](../../project/issues/managing_issues.md#closing-issues-automatically) that originated the implementation proposed in the merge request - Assign it to any registered user, and change the assignee how many times you need - Assign a [milestone](../../project/milestones/index.md) and track the development of a broader implementation - Organize your issues and merge requests consistently throughout the project with [labels](../../project/labels.md) diff --git a/doc/user/project/repository/branches/index.md b/doc/user/project/repository/branches/index.md index 13e4f2ce163..a81c9197ec1 100644 --- a/doc/user/project/repository/branches/index.md +++ b/doc/user/project/repository/branches/index.md @@ -21,7 +21,7 @@ branch for your project. You can choose another branch to be your project's default under your project's **Settings > Repository**. The default branch is the branch affected by the -[issue closing pattern](../../issues/automatic_issue_closing.md), +[issue closing pattern](../../issues/managing_issues.md#closing-issues-automatically), which means that _an issue will be closed when a merge request is merged to the **default branch**_. diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md index ce9d23bf911..253e5374f52 100644 --- a/doc/user/project/repository/web_editor.md +++ b/doc/user/project/repository/web_editor.md @@ -114,7 +114,7 @@ If your [project is already configured with a deployment service][project-servic After the branch is created, you can edit files in the repository to fix the issue. When a merge request is created based on the newly created branch, -the description field will automatically display the [issue closing pattern] +the description field will automatically display the [issue closing pattern](../issues/managing_issues.md#closing-issues-automatically) `Closes #ID`, where `ID` the ID of the issue. This will close the issue once the merge request is merged. @@ -181,4 +181,3 @@ through the web editor, you can choose to use another of your linked email addresses from the **User Settings > Edit Profile** page. [ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808 -[issue closing pattern]: ../issues/automatic_issue_closing.md diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 40e2486ace5..45bd8c29a48 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -4,7 +4,7 @@ comments: false # Workflow -- [Automatic issue closing](../user/project/issues/automatic_issue_closing.md) +- [Automatic issue closing](../user/project/issues/managing_issues.md#closing-issues-automatically) - [Change your time zone](timezone.md) - [Cycle Analytics](../user/project/cycle_analytics.md) - [Description templates](../user/project/description_templates.md) From 93be669e16b71a00362187a690e573209d80d960 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Wed, 26 Jun 2019 19:29:35 +0700 Subject: [PATCH 035/195] Refactor pipeline errors_message Use the shared method in Ci::Pipeline --- app/models/ci/pipeline.rb | 4 ++++ app/services/ci/create_pipeline_service.rb | 2 +- spec/models/ci/pipeline_spec.rb | 24 ++++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index fd5aa216174..20ca4a9ab24 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -790,6 +790,10 @@ module Ci stages.find_by!(name: name) end + def error_messages + errors ? errors.full_messages.to_sentence : "" + end + private def ci_yaml_from_repo diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index c17712355af..cdcc4b15bea 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -65,7 +65,7 @@ module Ci def execute!(*args, &block) execute(*args, &block).tap do |pipeline| unless pipeline.persisted? - raise CreateError, pipeline.errors.full_messages.join(',') + raise CreateError, pipeline.error_messages end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 55cea48b641..e24bbc39761 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -2998,4 +2998,28 @@ describe Ci::Pipeline, :mailer do end end end + + describe '#error_messages' do + subject { pipeline.error_messages } + + before do + pipeline.valid? + end + + context 'when pipeline has errors' do + let(:pipeline) { build(:ci_pipeline, sha: nil, ref: nil) } + + it 'returns the full error messages' do + is_expected.to eq("Sha can't be blank and Ref can't be blank") + end + end + + context 'when pipeline does not have errors' do + let(:pipeline) { build(:ci_pipeline) } + + it 'returns empty string' do + is_expected.to be_empty + end + end + end end From 824ec018a1f9bafe3d7b5ff5f7db2a04d3cb4993 Mon Sep 17 00:00:00 2001 From: Simon Knox Date: Wed, 3 Jul 2019 00:51:32 +1000 Subject: [PATCH 036/195] Use gl-empty-state for monitor charts Move a unit test to jest and use snapshot tests --- .../monitoring/components/dashboard.vue | 6 ++ .../monitoring/components/empty_state.vue | 65 +++++------ locale/gitlab.pot | 6 +- .../dashboard_state_spec.js.snap | 37 +++++++ .../monitoring/dashboard_state_spec.js | 43 ++++++++ .../monitoring/dashboard_state_spec.js | 101 ------------------ 6 files changed, 117 insertions(+), 141 deletions(-) create mode 100644 spec/frontend/monitoring/__snapshots__/dashboard_state_spec.js.snap create mode 100644 spec/frontend/monitoring/dashboard_state_spec.js delete mode 100644 spec/javascripts/monitoring/dashboard_state_spec.js diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 2cbda8ea05d..ed25a6e3684 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -124,6 +124,11 @@ export default { required: false, default: '', }, + smallEmptyState: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -386,6 +391,7 @@ export default { :empty-loading-svg-path="emptyLoadingSvgPath" :empty-no-data-svg-path="emptyNoDataSvgPath" :empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath" + :compact="smallEmptyState" /> diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue index a3c6de14aa4..1bb40447a3e 100644 --- a/app/assets/javascripts/monitoring/components/empty_state.vue +++ b/app/assets/javascripts/monitoring/components/empty_state.vue @@ -1,7 +1,11 @@ diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8a4f57c5b13..7e5e2f8df7b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3995,6 +3995,9 @@ msgstr "" msgid "Enforce DNS rebinding attack protection" msgstr "" +msgid "Ensure connectivity is available from the GitLab server to the Prometheus server" +msgstr "" + msgid "Enter at least three characters to search" msgstr "" @@ -8330,9 +8333,6 @@ msgstr "" msgid "ProjectsDropdown|This feature requires browser localStorage support" msgstr "" -msgid "Prometheus server" -msgstr "" - msgid "PrometheusService|%{exporters} with %{metrics} were found" msgstr "" diff --git a/spec/frontend/monitoring/__snapshots__/dashboard_state_spec.js.snap b/spec/frontend/monitoring/__snapshots__/dashboard_state_spec.js.snap new file mode 100644 index 00000000000..5f24bab600c --- /dev/null +++ b/spec/frontend/monitoring/__snapshots__/dashboard_state_spec.js.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EmptyState shows gettingStarted state 1`] = ` + +`; + +exports[`EmptyState shows loading state 1`] = ` + +`; + +exports[`EmptyState shows unableToConnect state 1`] = ` + +`; diff --git a/spec/frontend/monitoring/dashboard_state_spec.js b/spec/frontend/monitoring/dashboard_state_spec.js new file mode 100644 index 00000000000..950422911eb --- /dev/null +++ b/spec/frontend/monitoring/dashboard_state_spec.js @@ -0,0 +1,43 @@ +import { shallowMount } from '@vue/test-utils'; +import EmptyState from '~/monitoring/components/empty_state.vue'; + +function createComponent(props) { + return shallowMount(EmptyState, { + propsData: { + ...props, + settingsPath: '/settingsPath', + clustersPath: '/clustersPath', + documentationPath: '/documentationPath', + emptyGettingStartedSvgPath: '/path/to/getting-started.svg', + emptyLoadingSvgPath: '/path/to/loading.svg', + emptyNoDataSvgPath: '/path/to/no-data.svg', + emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg', + }, + }); +} + +describe('EmptyState', () => { + it('shows gettingStarted state', () => { + const wrapper = createComponent({ + selectedState: 'gettingStarted', + }); + + expect(wrapper.element).toMatchSnapshot(); + }); + + it('shows loading state', () => { + const wrapper = createComponent({ + selectedState: 'loading', + }); + + expect(wrapper.element).toMatchSnapshot(); + }); + + it('shows unableToConnect state', () => { + const wrapper = createComponent({ + selectedState: 'unableToConnect', + }); + + expect(wrapper.element).toMatchSnapshot(); + }); +}); diff --git a/spec/javascripts/monitoring/dashboard_state_spec.js b/spec/javascripts/monitoring/dashboard_state_spec.js deleted file mode 100644 index 6b2be83aa8c..00000000000 --- a/spec/javascripts/monitoring/dashboard_state_spec.js +++ /dev/null @@ -1,101 +0,0 @@ -import Vue from 'vue'; -import EmptyState from '~/monitoring/components/empty_state.vue'; -import { statePaths } from './mock_data'; - -function createComponent(props) { - const Component = Vue.extend(EmptyState); - - return new Component({ - propsData: { - ...props, - settingsPath: statePaths.settingsPath, - clustersPath: statePaths.clustersPath, - documentationPath: statePaths.documentationPath, - emptyGettingStartedSvgPath: '/path/to/getting-started.svg', - emptyLoadingSvgPath: '/path/to/loading.svg', - emptyNoDataSvgPath: '/path/to/no-data.svg', - emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg', - }, - }).$mount(); -} - -function getTextFromNode(component, selector) { - return component.$el.querySelector(selector).firstChild.nodeValue.trim(); -} - -describe('EmptyState', () => { - describe('Computed props', () => { - it('currentState', () => { - const component = createComponent({ - selectedState: 'gettingStarted', - }); - - expect(component.currentState).toBe(component.states.gettingStarted); - }); - - it('showButtonDescription returns a description with a link for the unableToConnect state', () => { - const component = createComponent({ - selectedState: 'unableToConnect', - }); - - expect(component.showButtonDescription).toEqual(true); - }); - - it('showButtonDescription returns the description without a link for any other state', () => { - const component = createComponent({ - selectedState: 'loading', - }); - - expect(component.showButtonDescription).toEqual(false); - }); - }); - - it('should show the gettingStarted state', () => { - const component = createComponent({ - selectedState: 'gettingStarted', - }); - - expect(component.$el.querySelector('svg')).toBeDefined(); - expect(getTextFromNode(component, '.state-title')).toEqual( - component.states.gettingStarted.title, - ); - - expect(getTextFromNode(component, '.state-description')).toEqual( - component.states.gettingStarted.description, - ); - - expect(getTextFromNode(component, '.btn-success')).toEqual( - component.states.gettingStarted.buttonText, - ); - }); - - it('should show the loading state', () => { - const component = createComponent({ - selectedState: 'loading', - }); - - expect(component.$el.querySelector('svg')).toBeDefined(); - expect(getTextFromNode(component, '.state-title')).toEqual(component.states.loading.title); - expect(getTextFromNode(component, '.state-description')).toEqual( - component.states.loading.description, - ); - - expect(getTextFromNode(component, '.btn-success')).toEqual(component.states.loading.buttonText); - }); - - it('should show the unableToConnect state', () => { - const component = createComponent({ - selectedState: 'unableToConnect', - }); - - expect(component.$el.querySelector('svg')).toBeDefined(); - expect(getTextFromNode(component, '.state-title')).toEqual( - component.states.unableToConnect.title, - ); - - expect(component.$el.querySelector('.state-description a')).toBeDefined(); - expect(getTextFromNode(component, '.btn-success')).toEqual( - component.states.unableToConnect.buttonText, - ); - }); -}); From 4e75c7135a4c5309c02a6335de583cd097cc72c7 Mon Sep 17 00:00:00 2001 From: Ramya Authappan Date: Thu, 4 Jul 2019 04:21:09 +0000 Subject: [PATCH 037/195] E2E Test to check XSS issue in @mentions autocomplete --- qa/qa/page/project/show.rb | 1 + qa/qa/page/project/sub_menus/ci_cd.rb | 2 + qa/qa/page/project/sub_menus/issues.rb | 2 + qa/qa/page/project/sub_menus/operations.rb | 2 + qa/qa/page/project/sub_menus/repository.rb | 2 + qa/qa/page/project/sub_menus/settings.rb | 2 + .../issue/check_mentions_for_xss_spec.rb | 43 +++++++++++++++++++ 7 files changed, 54 insertions(+) create mode 100644 qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 1a9a2fd413f..9fd668f812b 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -5,6 +5,7 @@ module QA module Project class Show < Page::Base include Page::Component::ClonePanel + include Page::Project::SubMenus::Settings view 'app/views/layouts/header/_new_dropdown.haml' do element :new_menu_toggle diff --git a/qa/qa/page/project/sub_menus/ci_cd.rb b/qa/qa/page/project/sub_menus/ci_cd.rb index adae2ce08c4..2f0bc8b9ba6 100644 --- a/qa/qa/page/project/sub_menus/ci_cd.rb +++ b/qa/qa/page/project/sub_menus/ci_cd.rb @@ -5,6 +5,8 @@ module QA module Project module SubMenus module CiCd + include Page::Project::SubMenus::Common + def self.included(base) base.class_eval do view 'app/views/layouts/nav/sidebar/_project.html.haml' do diff --git a/qa/qa/page/project/sub_menus/issues.rb b/qa/qa/page/project/sub_menus/issues.rb index f81e4f34909..8fb8fa06346 100644 --- a/qa/qa/page/project/sub_menus/issues.rb +++ b/qa/qa/page/project/sub_menus/issues.rb @@ -5,6 +5,8 @@ module QA module Project module SubMenus module Issues + include Page::Project::SubMenus::Common + def self.included(base) base.class_eval do view 'app/views/layouts/nav/sidebar/_project.html.haml' do diff --git a/qa/qa/page/project/sub_menus/operations.rb b/qa/qa/page/project/sub_menus/operations.rb index 24a99a9464c..d266cb21417 100644 --- a/qa/qa/page/project/sub_menus/operations.rb +++ b/qa/qa/page/project/sub_menus/operations.rb @@ -5,6 +5,8 @@ module QA module Project module SubMenus module Operations + include Page::Project::SubMenus::Common + def self.included(base) base.class_eval do view 'app/views/layouts/nav/sidebar/_project.html.haml' do diff --git a/qa/qa/page/project/sub_menus/repository.rb b/qa/qa/page/project/sub_menus/repository.rb index 4cc73a6b25a..c53d805c61d 100644 --- a/qa/qa/page/project/sub_menus/repository.rb +++ b/qa/qa/page/project/sub_menus/repository.rb @@ -5,6 +5,8 @@ module QA module Project module SubMenus module Repository + include Page::Project::SubMenus::Common + def self.included(base) base.class_eval do view 'app/views/layouts/nav/sidebar/_project.html.haml' do diff --git a/qa/qa/page/project/sub_menus/settings.rb b/qa/qa/page/project/sub_menus/settings.rb index 88b45ec55ae..1cd39fcff58 100644 --- a/qa/qa/page/project/sub_menus/settings.rb +++ b/qa/qa/page/project/sub_menus/settings.rb @@ -5,6 +5,8 @@ module QA module Project module SubMenus module Settings + include Page::Project::SubMenus::Common + def self.included(base) base.class_eval do view 'app/views/layouts/nav/sidebar/_project.html.haml' do diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb new file mode 100644 index 00000000000..013cea0a40e --- /dev/null +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module QA + context 'Plan' do + describe 'check xss occurence in @mentions in issues' do + let(:issue_title) { 'issue title' } + + it 'user mentions a user in comment' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) + + user = Resource::User.fabricate! do |user| + user.name = "eve Date: Thu, 4 Jul 2019 06:24:39 +0000 Subject: [PATCH 038/195] Resolve proclems with issues documentation Change csv import section to bullet list, add back missing bulk import section, and resolve minor issues raised in https://gitlab.com/gitlab-org/gitlab-ce/issues/64102 --- doc/user/project/issues/csv_import.md | 54 ++++++++-------------- doc/user/project/issues/due_dates.md | 4 +- doc/user/project/issues/index.md | 2 +- doc/user/project/issues/managing_issues.md | 25 +++++++++- 4 files changed, 46 insertions(+), 39 deletions(-) diff --git a/doc/user/project/issues/csv_import.md b/doc/user/project/issues/csv_import.md index e1e58e5ab24..cc0d5ac9028 100644 --- a/doc/user/project/issues/csv_import.md +++ b/doc/user/project/issues/csv_import.md @@ -29,42 +29,22 @@ to you once the import is complete. ## CSV file format -Sample CSV file data: +When importing issues from a CSV file, it must be formatted in a certain way: -CSV files must contain a header row where the first column header is `title` and the second is `description`. -If additional columns are present, they will be ignored. +- **header row:** CSV files must contain a header row where the first column header + is `title` and the second is `description`. If additional columns are present, they + will be ignored. +- **separators:** The column separator is automatically detected from the header row. + Supported separator characters are: commas (`,`), semicolons (`;`), and tabs (`\t`). + The row separator can be either `CRLF` or `LF`. +- **double-quote character:** The double-quote (`"`) character is used to quote fields, + enabling the use of the column separator within a field (see the third line in the + sample CSV data below). To insert a double-quote (`"`) within a quoted + field, use two double-quote characters in succession, i.e. `""`. +- **data rows:** After the header row, succeeding rows must follow the same column + order. The issue title is required while the description is optional. -### Header row - -CSV files must contain a header row beginning with at least two columns, `title` and -`description`, in that order. If additional columns are present, they will be ignored. - -### Separators - -The column separator is automatically detected from the header row. Supported separator -characters are: commas (`,`), semicolons (`;`), and tabs (`\t`). - -The row separator can be either `CRLF` or `LF`. - -### Quote character - -The double-quote (`"`) character is used to quote fields, enabling the use of the column -separator within a field (see the third line in the [sample CSV](#csv-file-format)). -To insert a double-quote (`"`) within a quoted field, use two double-quote characters -in succession, i.e. `""`. - -### Data rows - -After the header row, succeeding rows must follow the same column order. The issue -title is required while the description is optional. - -### File size - -The limit depends on the configuration value of Max Attachment Size for the GitLab instance. - -For GitLab.com, it is set to 10 MB. - -## Sample data +Sample CSV data: ```csv title,description @@ -72,3 +52,9 @@ My Issue Title,My Issue Description Another Title,"A description, with a comma" "One More Title","One More Description" ``` + +### File size + +The limit depends on the configuration value of Max Attachment Size for the GitLab instance. + +For GitLab.com, it is set to 10 MB. diff --git a/doc/user/project/issues/due_dates.md b/doc/user/project/issues/due_dates.md index be577b3f24c..bd3298497d2 100644 --- a/doc/user/project/issues/due_dates.md +++ b/doc/user/project/issues/due_dates.md @@ -39,10 +39,10 @@ Due dates also appear in your [todos list](../../../workflow/todos.md). The day before an open issue is due, an email will be sent to all participants of the issue. Like the due date, the "day before the due date" is determined by the -server's timezone, ignoring the participants' timezones. +server's timezone. Issues with due dates can also be exported as an iCalendar feed. The URL of the -feed can be added to many calendar applications. The feed is accessible by clicking +feed can be added to calendar applications. The feed is accessible by clicking on the **Subscribe to calendar** button on the following pages: - on the **Assigned Issues** page that is linked on the right-hand side of the GitLab header diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md index 86c2f2f3959..f69b841e908 100644 --- a/doc/user/project/issues/index.md +++ b/doc/user/project/issues/index.md @@ -144,7 +144,7 @@ message in the Activity stream about the reference, with a link to the other iss To prevent duplication of issues for the same topic, GitLab searches for similar issues when new issues are being created. -When typing in the title in the new issue form, GitLab searches titles and descriptions +When typing in the title in the **New Issue** page, GitLab searches titles and descriptions across all issues the user has access to in the current project. Up 5 similar issues, sorted by most recently updated, are displayed below the title box. Note that this feature requires [GraphQL](../../../api/graphql/index.md) to be enabled. diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md index 663bacf4a45..91dbf0d848e 100644 --- a/doc/user/project/issues/managing_issues.md +++ b/doc/user/project/issues/managing_issues.md @@ -111,6 +111,27 @@ The "Move issue" button is at the bottom of the right-sidebar when viewing the i ![move issue - button](img/sidebar_move_issue.png) +### Moving Issues in Bulk + +If you have advanced technical skills you can also bulk move all the issues from one project to another in the rails console. The below script will move all the issues from one project to another that are not in status **closed**. + +To access rails console run `sudo gitlab-rails console` on the GitLab server and run the below script. Please be sure to change **project**, **admin_user** and **target_project** to your values. We do also recommend [creating a backup](https://docs.gitlab.com/ee/raketasks/backup_restore.html#creating-a-backup-of-the-gitlab-system) before attempting any changes in the console. + +```ruby +project = Project.find_by_full_path('full path of the project where issues are moved from') +issues = project.issues +admin_user = User.find_by_username('username of admin user') # make sure user has permissions to move the issues +target_project = Project.find_by_full_path('full path of target project where issues moved to') + +issues.each do |issue| + if issue.state != "closed" && issue.moved_to.nil? + Issues::MoveService.new(project, admin_user).execute(issue, target_project) + else + puts "issue with id: #{issue.id} and title: #{issue.title} was not moved" + end +end; nil +``` + ## Closing Issues When you decide that an issue is resolved, or no longer needed, you can close the issue @@ -190,7 +211,7 @@ when used from the command line with `git commit -m`. #### Customizing the issue closing pattern **[CORE ONLY]** -In order to change the default issue closing pattern, you must edit the +In order to change the default issue closing pattern, GitLab administrators must edit the [`gitlab.rb` or `gitlab.yml` file](../../../administration/issue_closing_pattern.md) of your installation. @@ -199,6 +220,6 @@ of your installation. > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2982) in GitLab 8.6 Users with [project owner permission](../../permissions.md) can delete an issue by -editing it and clicking on the delete button. +editing it and clicking on the delete button. ![delete issue - button](img/delete_issue.png) From df3a0f261360b319d553c7122ac1e7abe6099de0 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Wed, 26 Jun 2019 18:58:24 +0700 Subject: [PATCH 039/195] Fix MWPS system notes shows inconsistent sha Fix the system note service --- .../auto_merge/merge_when_pipeline_succeeds_service.rb | 2 +- app/services/system_note_service.rb | 4 ++-- spec/services/auto_merge/base_service_spec.rb | 5 +++++ .../auto_merge/merge_when_pipeline_succeeds_service_spec.rb | 5 ++++- spec/services/system_note_service_spec.rb | 2 +- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb index c41073a73e9..cde8c19e8fc 100644 --- a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb +++ b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb @@ -5,7 +5,7 @@ module AutoMerge def execute(merge_request) super do if merge_request.saved_change_to_auto_merge_enabled? - SystemNoteService.merge_when_pipeline_succeeds(merge_request, project, current_user, merge_request.diff_head_commit) + SystemNoteService.merge_when_pipeline_succeeds(merge_request, project, current_user, merge_request.actual_head_pipeline.sha) end end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 237ddbcf2c2..4783417ad6d 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -221,8 +221,8 @@ module SystemNoteService end # Called when 'merge when pipeline succeeds' is executed - def merge_when_pipeline_succeeds(noteable, project, author, last_commit) - body = "enabled an automatic merge when the pipeline for #{last_commit.to_reference(project)} succeeds" + def merge_when_pipeline_succeeds(noteable, project, author, sha) + body = "enabled an automatic merge when the pipeline for #{sha} succeeds" create_note(NoteSummary.new(noteable, project, author, body, action: 'merge')) end diff --git a/spec/services/auto_merge/base_service_spec.rb b/spec/services/auto_merge/base_service_spec.rb index cd08e0b6f32..24cb63a0d61 100644 --- a/spec/services/auto_merge/base_service_spec.rb +++ b/spec/services/auto_merge/base_service_spec.rb @@ -59,6 +59,11 @@ describe AutoMerge::BaseService do context 'when strategy is merge when pipeline succeeds' do let(:service) { AutoMerge::MergeWhenPipelineSucceedsService.new(project, user) } + before do + pipeline = build(:ci_pipeline) + allow(merge_request).to receive(:actual_head_pipeline) { pipeline } + end + it 'sets the auto merge strategy' do subject diff --git a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb index a20bf8e17e4..5e84ef052ce 100644 --- a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb +++ b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb @@ -64,8 +64,11 @@ describe AutoMerge::MergeWhenPipelineSucceedsService do end it 'creates a system note' do + pipeline = build(:ci_pipeline) + allow(merge_request).to receive(:actual_head_pipeline) { pipeline } + note = merge_request.notes.last - expect(note.note).to match %r{enabled an automatic merge when the pipeline for (\w+/\w+@)?\h{8}} + expect(note.note).to match "enabled an automatic merge when the pipeline for #{pipeline.sha}" end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 2a2547f9400..9f60e49290e 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -332,7 +332,7 @@ describe SystemNoteService do create(:merge_request, source_project: project, target_project: project) end - subject { described_class.merge_when_pipeline_succeeds(noteable, project, author, noteable.diff_head_commit) } + subject { described_class.merge_when_pipeline_succeeds(noteable, project, author, pipeline.sha) } it_behaves_like 'a system note' do let(:action) { 'merge' } From 152a100a188a7b7abfdaba27ceec8d3156fb6ab5 Mon Sep 17 00:00:00 2001 From: Pirate Praveen Date: Thu, 4 Jul 2019 09:06:06 +0200 Subject: [PATCH 040/195] Update net-ssh gem to ~> 5.2 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 1264d75eac6..a37b65f459c 100644 --- a/Gemfile +++ b/Gemfile @@ -419,7 +419,7 @@ gem 'vmstat', '~> 2.3.0' gem 'sys-filesystem', '~> 1.1.6' # SSH host key support -gem 'net-ssh', '~> 5.0' +gem 'net-ssh', '~> 5.2' gem 'sshkey', '~> 2.0' # Required for ED25519 SSH host key support diff --git a/Gemfile.lock b/Gemfile.lock index 5b648d43137..8b548c08f92 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -515,7 +515,7 @@ GEM mysql2 (0.4.10) nakayoshi_fork (0.0.4) net-ldap (0.16.0) - net-ssh (5.0.1) + net-ssh (5.2.0) netrc (0.11.0) nio4r (2.3.1) nokogiri (1.10.3) @@ -1142,7 +1142,7 @@ DEPENDENCIES mysql2 (~> 0.4.10) nakayoshi_fork (~> 0.0.4) net-ldap - net-ssh (~> 5.0) + net-ssh (~> 5.2) nokogiri (~> 1.10.3) oauth2 (~> 1.4) octokit (~> 4.9) From aed3fadb556a120ad4b4efda173c460d0af9bded Mon Sep 17 00:00:00 2001 From: Alexander Tanayno Date: Thu, 4 Jul 2019 07:24:11 +0000 Subject: [PATCH 041/195] Replace the variable DOCKER_AUTH_LOGIN with DOCKER_AUTH_CONFIG --- doc/ci/docker/using_docker_images.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index e012f4f8595..816bd35018a 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -489,17 +489,17 @@ runtime. ### Using statically-defined credentials There are two approaches that you can take in order to access a private registry. Both require setting the environment variable -`DOCKER_AUTH_LOGIN` with appropriate authentication info. +`DOCKER_AUTH_CONFIG` with appropriate authentication info. 1. Per-job: To configure one job to access a private registry, add - `DOCKER_AUTH_LOGIN` as a job variable. + `DOCKER_AUTH_CONFIG` as a job variable. 1. Per-runner: To configure a Runner so all its jobs can access a - private registry, add `DOCKER_AUTH_LOGIN` to the environment in the + private registry, add `DOCKER_AUTH_CONFIG` to the environment in the Runner's configuration. See below for examples of each. -#### Determining your `DOCKER_AUTH_LOGIN` data +#### Determining your `DOCKER_AUTH_CONFIG` data As an example, let's assume that you want to use the `registry.example.com:5000/private/image:latest` image which is private and requires you to login into a private container registry. From c1d09cfdfdcdcc2b36450b262826b18acdfd698e Mon Sep 17 00:00:00 2001 From: Tomislav Nikic Date: Thu, 4 Jul 2019 09:44:35 +0200 Subject: [PATCH 042/195] Removing Quarantine tag after testing Tested the two tests inside the suite, both passed and thus are being dequarantined. --- .../merge_request/view_merge_request_diff_patch_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb index db33c6330ff..9e48ee7ca2a 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true module QA - # https://gitlab.com/gitlab-org/quality/staging/issues/55 - context 'Create', :quarantine do + context 'Create' do describe 'Download merge request patch and diff' do before(:context) do Runtime::Browser.visit(:gitlab, Page::Main::Login) From 9ef0c8559de925d0a72a3fe421d95209c2b81d8f Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Wed, 3 Jul 2019 14:46:13 +0100 Subject: [PATCH 043/195] Clarify documentation of Gitlab::SidekiqStatus The "running" state is ambiguous. Clarify that it covers both enqueued and actually running jobs. --- lib/gitlab/sidekiq_status.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb index 583a970bf4e..0f890a12134 100644 --- a/lib/gitlab/sidekiq_status.rb +++ b/lib/gitlab/sidekiq_status.rb @@ -53,14 +53,14 @@ module Gitlab self.num_running(job_ids).zero? end - # Returns true if the given job is running + # Returns true if the given job is running or enqueued. # # job_id - The Sidekiq job ID to check. def self.running?(job_id) num_running([job_id]) > 0 end - # Returns the number of jobs that are running. + # Returns the number of jobs that are running or enqueued. # # job_ids - The Sidekiq job IDs to check. def self.num_running(job_ids) @@ -81,7 +81,7 @@ module Gitlab # job_ids - The Sidekiq job IDs to check. # # Returns an array of true or false indicating job completion. - # true = job is still running + # true = job is still running or enqueued # false = job completed def self.job_status(job_ids) keys = job_ids.map { |jid| key_for(jid) } From 381468d0cc6e5b528a4b2207c0a534569035a73f Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Fri, 21 Jun 2019 17:56:47 +0100 Subject: [PATCH 044/195] Allow asynchronous rebase operations to be monitored This MR introduces tracking of the `rebase_jid` for merge requests. As with `merge_ongoing?`, `rebase_in_progress?` will now return true if a rebase is proceeding in sidekiq. After one release, we should remove the Gitaly-based lookup of rebases. It is much better to track this kind of thing via the database. --- .../projects/merge_requests_controller.rb | 2 +- app/models/merge_request.rb | 28 ++++- app/services/merge_requests/rebase_service.rb | 4 +- app/workers/rebase_worker.rb | 2 + .../unreleased/54117-transactional-rebase.yml | 5 + ...0621151636_add_merge_request_rebase_jid.rb | 9 ++ db/schema.rb | 1 + doc/api/merge_requests.md | 10 +- lib/api/api.rb | 5 +- lib/api/merge_requests.rb | 3 +- lib/gitlab/import_export/import_export.yml | 1 + spec/models/merge_request_spec.rb | 115 ++++++++++++++---- spec/requests/api/merge_requests_spec.rb | 13 ++ .../merge_requests/rebase_service_spec.rb | 28 +++-- 14 files changed, 188 insertions(+), 38 deletions(-) create mode 100644 changelogs/unreleased/54117-transactional-rebase.yml create mode 100644 db/migrate/20190621151636_add_merge_request_rebase_jid.rb diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 7ee8e0ea8f8..2aa2508be16 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -201,7 +201,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def rebase - RebaseWorker.perform_async(@merge_request.id, current_user.id) + @merge_request.rebase_async(current_user.id) head :ok end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 8391d526d18..e96e26cc773 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -223,7 +223,13 @@ class MergeRequest < ApplicationRecord end def rebase_in_progress? - strong_memoize(:rebase_in_progress) do + (rebase_jid.present? && Gitlab::SidekiqStatus.running?(rebase_jid)) || + gitaly_rebase_in_progress? + end + + # TODO: remove the Gitaly lookup after v12.1, when rebase_jid will be reliable + def gitaly_rebase_in_progress? + strong_memoize(:gitaly_rebase_in_progress) do # The source project can be deleted next false unless source_project @@ -389,6 +395,26 @@ class MergeRequest < ApplicationRecord update_column(:merge_jid, jid) end + # Set off a rebase asynchronously, atomically updating the `rebase_jid` of + # the MR so that the status of the operation can be tracked. + def rebase_async(user_id) + transaction do + lock! + + raise ActiveRecord::StaleObjectError if !open? || rebase_in_progress? + + # Although there is a race between setting rebase_jid here and clearing it + # in the RebaseWorker, it can't do any harm since we check both that the + # attribute is set *and* that the sidekiq job is still running. So a JID + # for a completed RebaseWorker is equivalent to a nil JID. + jid = Sidekiq::Worker.skipping_transaction_check do + RebaseWorker.perform_async(id, user_id) + end + + update_column(:rebase_jid, jid) + end + end + def merge_participants participants = [author] diff --git a/app/services/merge_requests/rebase_service.rb b/app/services/merge_requests/rebase_service.rb index 4b9921c28ba..8d3b9b05819 100644 --- a/app/services/merge_requests/rebase_service.rb +++ b/app/services/merge_requests/rebase_service.rb @@ -15,7 +15,7 @@ module MergeRequests end def rebase - if merge_request.rebase_in_progress? + if merge_request.gitaly_rebase_in_progress? log_error('Rebase task canceled: Another rebase is already in progress', save_message_on_model: true) return false end @@ -27,6 +27,8 @@ module MergeRequests log_error(REBASE_ERROR, save_message_on_model: true) log_error(e.message) false + ensure + merge_request.update_column(:rebase_jid, nil) end end end diff --git a/app/workers/rebase_worker.rb b/app/workers/rebase_worker.rb index a6baebc1443..8d06adcd993 100644 --- a/app/workers/rebase_worker.rb +++ b/app/workers/rebase_worker.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# The RebaseWorker must be wrapped in important concurrency code, so should only +# be scheduled via MergeRequest#rebase_async class RebaseWorker include ApplicationWorker diff --git a/changelogs/unreleased/54117-transactional-rebase.yml b/changelogs/unreleased/54117-transactional-rebase.yml new file mode 100644 index 00000000000..d0c93114c49 --- /dev/null +++ b/changelogs/unreleased/54117-transactional-rebase.yml @@ -0,0 +1,5 @@ +--- +title: Allow asynchronous rebase operations to be monitored +merge_request: 29940 +author: +type: fixed diff --git a/db/migrate/20190621151636_add_merge_request_rebase_jid.rb b/db/migrate/20190621151636_add_merge_request_rebase_jid.rb new file mode 100644 index 00000000000..1fed5690ead --- /dev/null +++ b/db/migrate/20190621151636_add_merge_request_rebase_jid.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddMergeRequestRebaseJid < ActiveRecord::Migration[5.1] + DOWNTIME = false + + def change + add_column :merge_requests, :rebase_jid, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 4bcc8b5f1d7..9cc45bb1e47 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1989,6 +1989,7 @@ ActiveRecord::Schema.define(version: 20190628185004) do t.boolean "allow_maintainer_to_push" t.integer "state_id", limit: 2 t.integer "approvals_before_merge" + t.string "rebase_jid" t.index ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree t.index ["author_id"], name: "index_merge_requests_on_author_id", using: :btree t.index ["created_at"], name: "index_merge_requests_on_created_at", using: :btree diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 69db9f97a35..5a20db4b647 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -1485,8 +1485,14 @@ PUT /projects/:id/merge_requests/:merge_request_iid/rebase curl --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/projects/76/merge_requests/1/rebase ``` -This is an asynchronous request. The API will return an empty `202 Accepted` -response if the request is enqueued successfully. +This is an asynchronous request. The API will return a `202 Accepted` response +if the request is enqueued successfully, with a response containing: + +```json +{ + "rebase_in_progress": true +} +``` You can poll the [Get single MR](#get-single-mr) endpoint with the `include_rebase_in_progress` parameter to check the status of the diff --git a/lib/api/api.rb b/lib/api/api.rb index 20f8c637274..42499c5b41e 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -52,7 +52,10 @@ module API rack_response({ 'message' => '404 Not found' }.to_json, 404) end - rescue_from ::Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError do + rescue_from( + ::ActiveRecord::StaleObjectError, + ::Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError + ) do rack_response({ 'message' => '409 Conflict: Resource lock' }.to_json, 409) end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 6b8c1a2c0e8..64ee82cd775 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -429,9 +429,10 @@ module API authorize_push_to_merge_request!(merge_request) - RebaseWorker.perform_async(merge_request.id, current_user.id) + merge_request.rebase_async(current_user.id) status :accepted + present rebase_in_progress: merge_request.rebase_in_progress? end desc 'List issues that will be closed on merge' do diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index a0fb051e806..01437c67fa9 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -160,6 +160,7 @@ excluded_attributes: - :milestone_id - :ref_fetched - :merge_jid + - :rebase_jid - :latest_merge_request_diff_id award_emoji: - :awardable_id diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index a2547755510..fe6d68aff3f 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -7,6 +7,8 @@ describe MergeRequest do include ProjectForksHelper include ReactiveCachingHelpers + using RSpec::Parameterized::TableSyntax + subject { create(:merge_request) } describe 'associations' do @@ -1996,6 +1998,47 @@ describe MergeRequest do end end + describe '#rebase_async' do + let(:merge_request) { create(:merge_request) } + let(:user_id) { double(:user_id) } + let(:rebase_jid) { 'rebase-jid' } + + subject(:execute) { merge_request.rebase_async(user_id) } + + it 'atomically enqueues a RebaseWorker job and updates rebase_jid' do + expect(RebaseWorker) + .to receive(:perform_async) + .with(merge_request.id, user_id) + .and_return(rebase_jid) + + expect(merge_request).to receive(:lock!).and_call_original + + execute + + expect(merge_request.rebase_jid).to eq(rebase_jid) + end + + it 'refuses to enqueue a job if a rebase is in progress' do + merge_request.update_column(:rebase_jid, rebase_jid) + + expect(RebaseWorker).not_to receive(:perform_async) + expect(Gitlab::SidekiqStatus) + .to receive(:running?) + .with(rebase_jid) + .and_return(true) + + expect { execute }.to raise_error(ActiveRecord::StaleObjectError) + end + + it 'refuses to enqueue a job if the MR is not open' do + merge_request.update_column(:state, 'foo') + + expect(RebaseWorker).not_to receive(:perform_async) + + expect { execute }.to raise_error(ActiveRecord::StaleObjectError) + end + end + describe '#mergeable?' do let(:project) { create(:project) } @@ -2946,40 +2989,64 @@ describe MergeRequest do end describe '#rebase_in_progress?' do - shared_examples 'checking whether a rebase is in progress' do - let(:repo_path) do - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - subject.source_project.repository.path - end + where(:rebase_jid, :jid_valid, :result) do + 'foo' | true | true + 'foo' | false | false + '' | true | false + nil | true | false + end + + with_them do + let(:merge_request) { create(:merge_request) } + + subject { merge_request.rebase_in_progress? } + + it do + # Stub out the legacy gitaly implementation + allow(merge_request).to receive(:gitaly_rebase_in_progress?) { false } + + allow(Gitlab::SidekiqStatus).to receive(:running?).with(rebase_jid) { jid_valid } + + merge_request.rebase_jid = rebase_jid + + is_expected.to eq(result) end - let(:rebase_path) { File.join(repo_path, "gitlab-worktree", "rebase-#{subject.id}") } + end + end - before do - system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{rebase_path} master)) + describe '#gitaly_rebase_in_progress?' do + let(:repo_path) do + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + subject.source_project.repository.path end + end + let(:rebase_path) { File.join(repo_path, "gitlab-worktree", "rebase-#{subject.id}") } - it 'returns true when there is a current rebase directory' do - expect(subject.rebase_in_progress?).to be_truthy - end + before do + system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{rebase_path} master)) + end - it 'returns false when there is no rebase directory' do - FileUtils.rm_rf(rebase_path) + it 'returns true when there is a current rebase directory' do + expect(subject.rebase_in_progress?).to be_truthy + end - expect(subject.rebase_in_progress?).to be_falsey - end + it 'returns false when there is no rebase directory' do + FileUtils.rm_rf(rebase_path) - it 'returns false when the rebase directory has expired' do - time = 20.minutes.ago.to_time - File.utime(time, time, rebase_path) + expect(subject.rebase_in_progress?).to be_falsey + end - expect(subject.rebase_in_progress?).to be_falsey - end + it 'returns false when the rebase directory has expired' do + time = 20.minutes.ago.to_time + File.utime(time, time, rebase_path) - it 'returns false when the source project has been removed' do - allow(subject).to receive(:source_project).and_return(nil) + expect(subject.rebase_in_progress?).to be_falsey + end - expect(subject.rebase_in_progress?).to be_falsey - end + it 'returns false when the source project has been removed' do + allow(subject).to receive(:source_project).and_return(nil) + + expect(subject.rebase_in_progress?).to be_falsey end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index a82ecb4fd63..ced853caab4 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -2033,6 +2033,9 @@ describe API::MergeRequests do expect(response).to have_gitlab_http_status(202) expect(RebaseWorker.jobs.size).to eq(1) + + expect(merge_request.reload).to be_rebase_in_progress + expect(json_response['rebase_in_progress']).to be(true) end it 'returns 403 if the user cannot push to the branch' do @@ -2043,6 +2046,16 @@ describe API::MergeRequests do expect(response).to have_gitlab_http_status(403) end + + it 'returns 409 if a rebase is already in progress' do + Sidekiq::Testing.fake! do + merge_request.rebase_async(user.id) + + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/rebase", user) + end + + expect(response).to have_gitlab_http_status(409) + end end describe 'Time tracking' do diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb index 7e2f03d1097..ee9caaf2f47 100644 --- a/spec/services/merge_requests/rebase_service_spec.rb +++ b/spec/services/merge_requests/rebase_service_spec.rb @@ -6,10 +6,12 @@ describe MergeRequests::RebaseService do include ProjectForksHelper let(:user) { create(:user) } + let(:rebase_jid) { 'fake-rebase-jid' } let(:merge_request) do - create(:merge_request, + create :merge_request, source_branch: 'feature_conflict', - target_branch: 'master') + target_branch: 'master', + rebase_jid: rebase_jid end let(:project) { merge_request.project } let(:repository) { project.repository.raw } @@ -23,11 +25,11 @@ describe MergeRequests::RebaseService do describe '#execute' do context 'when another rebase is already in progress' do before do - allow(merge_request).to receive(:rebase_in_progress?).and_return(true) + allow(merge_request).to receive(:gitaly_rebase_in_progress?).and_return(true) end it 'saves the error message' do - subject.execute(merge_request) + service.execute(merge_request) expect(merge_request.reload.merge_error).to eq 'Rebase task canceled: Another rebase is already in progress' end @@ -36,6 +38,13 @@ describe MergeRequests::RebaseService do expect(service.execute(merge_request)).to match(status: :error, message: described_class::REBASE_ERROR) end + + it 'clears rebase_jid' do + expect { service.execute(merge_request) } + .to change { merge_request.rebase_jid } + .from(rebase_jid) + .to(nil) + end end shared_examples 'sequence of failure and success' do @@ -43,14 +52,19 @@ describe MergeRequests::RebaseService do allow(repository).to receive(:gitaly_operation_client).and_raise('Something went wrong') service.execute(merge_request) + merge_request.reload - expect(merge_request.reload.merge_error).to eq described_class::REBASE_ERROR + expect(merge_request.reload.merge_error).to eq(described_class::REBASE_ERROR) + expect(merge_request.rebase_jid).to eq(nil) allow(repository).to receive(:gitaly_operation_client).and_call_original + merge_request.update!(rebase_jid: rebase_jid) service.execute(merge_request) + merge_request.reload - expect(merge_request.reload.merge_error).to eq nil + expect(merge_request.merge_error).to eq(nil) + expect(merge_request.rebase_jid).to eq(nil) end end @@ -72,7 +86,7 @@ describe MergeRequests::RebaseService do it 'saves a generic error message' do subject.execute(merge_request) - expect(merge_request.reload.merge_error).to eq described_class::REBASE_ERROR + expect(merge_request.reload.merge_error).to eq(described_class::REBASE_ERROR) end it 'returns an error' do From 8ac2c3ef434db3b6437811b7a198a086cd155a38 Mon Sep 17 00:00:00 2001 From: Marcel Amirault Date: Thu, 4 Jul 2019 08:22:41 +0000 Subject: [PATCH 045/195] Clean up EE api docs that were merged to CE Many small fixes to api docs which were merged from EE to CE, and tables cleaned up, as noted in issue https://gitlab.com/gitlab-org/gitlab-ce/issues/64072 --- doc/api/group_boards.md | 32 +++---- doc/api/groups.md | 153 ++++++++++++++++--------------- doc/api/merge_requests.md | 8 +- doc/api/namespaces.md | 2 +- doc/api/notification_settings.md | 5 +- doc/api/pipeline_schedules.md | 12 +-- doc/api/projects.md | 25 +++-- doc/api/settings.md | 77 ++++++++-------- doc/api/users.md | 127 +++++++++++++------------ 9 files changed, 231 insertions(+), 210 deletions(-) diff --git a/doc/api/group_boards.md b/doc/api/group_boards.md index a677a9c9a33..4157b25477f 100644 --- a/doc/api/group_boards.md +++ b/doc/api/group_boards.md @@ -5,7 +5,7 @@ Every API call to group boards must be authenticated. If a user is not a member of a group and the group is private, a `GET` request will result in `404` status code. -## Group Board +## List all group issue boards in a group Lists Issue Boards in the given group. @@ -71,8 +71,7 @@ Example response: ``` Users on GitLab [Premium, Silver, or higher](https://about.gitlab.com/pricing/) will see -different parameters, due to the ability to have multiple group boards. Refer to the table -above to see what enpoint(s) belong to each tier. +different parameters, due to the ability to have multiple group boards. Example response: @@ -123,9 +122,9 @@ Example response: ] ``` -## Single board +## Single group issue board -Gets a single board. +Gets a single group issue board. ``` GET /groups/:id/boards/:board_id @@ -188,7 +187,7 @@ Example response: ``` Users on GitLab [Premium, Silver, or higher](https://about.gitlab.com/pricing/) will see -different parameters, due to the ability to have multiple group boards: +different parameters, due to the ability to have multiple group issue boards.s Example response: @@ -237,7 +236,7 @@ Example response: } ``` -## Create a Group Issue Board **[PREMIUM]** +## Create a group issue board **[PREMIUM]** Creates a Group Issue Board. @@ -301,9 +300,9 @@ Example response: } ``` -## Update a Group Issue Board **[PREMIUM]** +## Update a group issue board **[PREMIUM]** -> [Introduced][ee-5954] in GitLab 11.1. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/5954) in GitLab 11.1. Updates a Group Issue Board. @@ -321,7 +320,6 @@ PUT /groups/:id/boards/:board_id | `labels` | string | no | Comma-separated list of label names which the board should be scoped to | | `weight` | integer | no | The weight range from 0 to 9, to which the board should be scoped to | - ```bash curl --request PUT --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/groups/5/boards/1?name=new_name&milestone_id=44&assignee_id=1&labels=GroupLabel&weight=4 ``` @@ -370,7 +368,7 @@ Example response: } ``` -## Delete a Group Issue Board **[PREMIUM]** +## Delete a group issue board **[PREMIUM]** Deletes a Group Issue Board. @@ -387,7 +385,7 @@ DELETE /groups/:id/boards/:board_id curl --request DELETE --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/groups/5/boards/1 ``` -## List board lists +## List group issue board lists Get a list of the board's lists. Does not include `open` and `closed` lists @@ -439,7 +437,7 @@ Example response: ] ``` -## Single board list +## Single group issue board list Get a single board list. @@ -471,7 +469,7 @@ Example response: } ``` -## New board list +## New group issue board list Creates a new Issue Board list. @@ -503,7 +501,7 @@ Example response: } ``` -## Edit board list +## Edit group issue board list Updates an existing Issue Board list. This call is used to change list position. @@ -536,7 +534,7 @@ Example response: } ``` -## Delete a board list +## Delete a group issue board list Only for admins and group owners. Soft deletes the board list in question. @@ -553,5 +551,3 @@ DELETE /groups/:id/boards/:board_id/lists/:list_id ```bash curl --request DELETE --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/groups/5/boards/1/lists/1 ``` - -[ee-5954]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/5954 diff --git a/doc/api/groups.md b/doc/api/groups.md index 80a2fb8e4d9..e1bf296bc41 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -7,17 +7,17 @@ authentication, only public groups are returned. Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `skip_groups` | array of integers | no | Skip the group IDs passed | -| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for admin); Attributes `owned` and `min_access_level` have precedence | -| `search` | string | no | Return the list of authorized groups matching the search criteria | -| `order_by` | string | no | Order groups by `name`, `path` or `id`. Default is `name` | -| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` | -| `statistics` | boolean | no | Include group statistics (admins only) | -| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) | -| `owned` | boolean | no | Limit to groups explicitly owned by the current user | -| `min_access_level` | integer | no | Limit to groups where current user has at least this [access level](members.md) | +| Attribute | Type | Required | Description | +| ------------------------ | ----------------- | -------- | ---------- | +| `skip_groups` | array of integers | no | Skip the group IDs passed | +| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for admin); Attributes `owned` and `min_access_level` have precedence | +| `search` | string | no | Return the list of authorized groups matching the search criteria | +| `order_by` | string | no | Order groups by `name`, `path` or `id`. Default is `name` | +| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` | +| `statistics` | boolean | no | Include group statistics (admins only) | +| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) | +| `owned` | boolean | no | Limit to groups explicitly owned by the current user | +| `min_access_level` | integer | no | Limit to groups where current user has at least this [access level](members.md) | ``` GET /groups @@ -94,18 +94,18 @@ When accessed without authentication, only public groups are returned. Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) of the parent group | -| `skip_groups` | array of integers | no | Skip the group IDs passed | -| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for admin); Attributes `owned` and `min_access_level` have precedence | -| `search` | string | no | Return the list of authorized groups matching the search criteria | -| `order_by` | string | no | Order groups by `name`, `path` or `id`. Default is `name` | -| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` | -| `statistics` | boolean | no | Include group statistics (admins only) | -| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) | -| `owned` | boolean | no | Limit to groups explicitly owned by the current user | -| `min_access_level` | integer | no | Limit to groups where current user has at least this [access level](members.md) | +| Attribute | Type | Required | Description | +| ------------------------ | ----------------- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) of the parent group | +| `skip_groups` | array of integers | no | Skip the group IDs passed | +| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for admin); Attributes `owned` and `min_access_level` have preceden | +| `search` | string | no | Return the list of authorized groups matching the search criteria | +| `order_by` | string | no | Order groups by `name`, `path` or `id`. Default is `name` | +| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` | +| `statistics` | boolean | no | Include group statistics (admins only) | +| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) | +| `owned` | boolean | no | Limit to groups explicitly owned by the current user | +| `min_access_level` | integer | no | Limit to groups where current user has at least this [access level](members.md) | ``` GET /groups/:id/subgroups @@ -142,22 +142,22 @@ GET /groups/:id/projects Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | -| `archived` | boolean | no | Limit by archived status | -| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` | -| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` | -| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | -| `search` | string | no | Return list of authorized projects matching the search criteria | -| `simple` | boolean | no | Return only the ID, URL, name, and path of each project | -| `owned` | boolean | no | Limit by projects owned by the current user | -| `starred` | boolean | no | Limit by projects starred by the current user | -| `with_issues_enabled` | boolean | no | Limit by projects with issues feature enabled. Default is `false` | -| `with_merge_requests_enabled` | boolean | no | Limit by projects with merge requests feature enabled. Default is `false` | -| `with_shared` | boolean | no | Include projects shared to this group. Default is `true` | -| `include_subgroups` | boolean | no | Include projects in subgroups of this group. Default is `false` | -| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) | +| Attribute | Type | Required | Description | +| ----------------------------- | -------------- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | +| `archived` | boolean | no | Limit by archived status | +| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` | +| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` | +| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | +| `search` | string | no | Return list of authorized projects matching the search criteria | +| `simple` | boolean | no | Return only the ID, URL, name, and path of each project | +| `owned` | boolean | no | Limit by projects owned by the current user | +| `starred` | boolean | no | Limit by projects starred by the current user | +| `with_issues_enabled` | boolean | no | Limit by projects with issues feature enabled. Default is `false` | +| `with_merge_requests_enabled` | boolean | no | Limit by projects with merge requests feature enabled. Default is `false` | +| `with_shared` | boolean | no | Include projects shared to this group. Default is `true` | +| `include_subgroups` | boolean | no | Include projects in subgroups of this group. Default is `false` | +| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) | Example response: @@ -214,11 +214,11 @@ GET /groups/:id Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | -| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) | -| `with_projects` | boolean | no | Include details from projects that belong to the specified group (defaults to `true`). | +| Attribute | Type | Required | Description | +| ------------------------ | -------------- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only). | +| `with_projects` | boolean | no | Include details from projects that belong to the specified group (defaults to `true`). | ```bash curl --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/groups/4 @@ -378,11 +378,16 @@ Example response: Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see the `shared_runners_minutes_limit` and `extra_shared_runners_minutes_limit` parameters: -Additional response parameters: **[STARTER]** +Additional response parameters: ```json +{ + "id": 4, + "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.", "shared_runners_minutes_limit": 133, "extra_shared_runners_minutes_limit": 133, + ... +} ``` When adding the parameter `with_projects=false`, projects will not be returned. @@ -420,17 +425,17 @@ POST /groups Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `name` | string | yes | The name of the group | -| `path` | string | yes | The path of the group | -| `description` | string | no | The group's description | -| `visibility` | string | no | The group's visibility. Can be `private`, `internal`, or `public`. | -| `lfs_enabled` | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group | -| `request_access_enabled` | boolean | no | Allow users to request member access. | -| `parent_id` | integer | no | The parent group ID for creating nested group. | -| `shared_runners_minutes_limit` | integer | no | **[STARTER ONLY]** Pipeline minutes quota for this group. | -| `extra_shared_runners_minutes_limit` | integer | no | **[STARTER ONLY]** Extra pipeline minutes quota for this group. | +| Attribute | Type | Required | Description | +| ------------------------------------ | ------- | -------- | ----------- | +| `name` | string | yes | The name of the group. | +| `path` | string | yes | The path of the group. | +| `description` | string | no | The group's description. | +| `visibility` | string | no | The group's visibility. Can be `private`, `internal`, or `public`. | +| `lfs_enabled` | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. | +| `request_access_enabled` | boolean | no | Allow users to request member access. | +| `parent_id` | integer | no | The parent group ID for creating nested group. | +| `shared_runners_minutes_limit` | integer | no | **[STARTER ONLY]** Pipeline minutes quota for this group. | +| `extra_shared_runners_minutes_limit` | integer | no | **[STARTER ONLY]** Extra pipeline minutes quota for this group. | ## Transfer project to group @@ -442,10 +447,10 @@ POST /groups/:id/projects/:project_id Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | -| `project_id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| Attribute | Type | Required | Description | +| ------------ | -------------- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | +| `project_id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | ## Update group @@ -455,20 +460,20 @@ Updates the project group. Only available to group owners and administrators. PUT /groups/:id ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the group | -| `name` | string | no | The name of the group | -| `path` | string | no | The path of the group | -| `description` | string | no | The description of the group | -| `membership_lock` | boolean | no | **[STARTER]** Prevent adding new members to project membership within this group | -| `share_with_group_lock` | boolean | no | Prevent sharing a project with another group within this group | -| `visibility` | string | no | The visibility level of the group. Can be `private`, `internal`, or `public`. | -| `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group | -| `request_access_enabled` | boolean | no | Allow users to request member access. | -| `file_template_project_id` | integer | no | **[PREMIUM]** The ID of a project to load custom file templates from | -| `shared_runners_minutes_limit` | integer | no | **[STARTER ONLY]** Pipeline minutes quota for this group | -| `extra_shared_runners_minutes_limit` | integer | no | **[STARTER ONLY]** Extra pipeline minutes quota for this group | +| Attribute | Type | Required | Description | +| ------------------------------------ | ------- | -------- | ----------- | +| `id` | integer | yes | The ID of the group. | +| `name` | string | no | The name of the group. | +| `path` | string | no | The path of the group. | +| `description` | string | no | The description of the group. | +| `membership_lock` | boolean | no | **[STARTER]** Prevent adding new members to project membership within this group. | +| `share_with_group_lock` | boolean | no | Prevent sharing a project with another group within this group. | +| `visibility` | string | no | The visibility level of the group. Can be `private`, `internal`, or `public`. | +| `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. | +| `request_access_enabled` | boolean | no | Allow users to request member access. | +| `file_template_project_id` | integer | no | **[PREMIUM]** The ID of a project to load custom file templates from. | +| `shared_runners_minutes_limit` | integer | no | **[STARTER ONLY]** Pipeline minutes quota for this group. | +| `extra_shared_runners_minutes_limit` | integer | no | **[STARTER ONLY]** Extra pipeline minutes quota for this group. | ```bash curl --request PUT --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/groups/5?name=Experimental" diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 69db9f97a35..4a00ab66446 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -850,9 +850,9 @@ If `approvals_before_merge` **[STARTER]** is not provided, it inherits the value target project. If it is provided, then the following conditions must hold in order for it to take effect: -1. The target project's `approvals_before_merge` must be greater than zero. (A - value of zero disables approvals for that project.) -2. The provided value of `approvals_before_merge` must be greater than the +1. The target project's `approvals_before_merge` must be greater than zero. A + value of zero disables approvals for that project. +1. The provided value of `approvals_before_merge` must be greater than the target project's `approvals_before_merge`. ```json @@ -1296,7 +1296,7 @@ the `approvals_before_merge` parameter: ## Merge to default merge ref path -Merge the changes between the merge request source and target branches into `refs/merge-requests/:iid/merge` +Merge the changes between the merge request source and target branches into `refs/merge-requests/:iid/merge` ref, of the target project repository, if possible. This ref will have the state the target branch would have if a regular merge action was taken. diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md index b354e2b9ab2..5db7035fd90 100644 --- a/doc/api/namespaces.md +++ b/doc/api/namespaces.md @@ -68,7 +68,7 @@ the `plan` parameter associated with a namespace: ] ``` -**Note**: Only group maintainers/owners are presented with `members_count_with_descendants`, as well as `plan` **[BRONZE ONLY]**. +NOTE: **Note:** Only group maintainers/owners are presented with `members_count_with_descendants`, as well as `plan` **[BRONZE ONLY]**. ## Search for namespace diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md index 0342622f384..ccc1cccf7a4 100644 --- a/doc/api/notification_settings.md +++ b/doc/api/notification_settings.md @@ -1,8 +1,8 @@ # Notification settings API ->**Note:** This feature was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5632) in GitLab 8.12. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5632) in GitLab 8.12. -**Valid notification levels** +## Valid notification levels The notification levels are defined in the `NotificationSetting.level` model enumeration. Currently, these levels are recognized: @@ -33,7 +33,6 @@ If the `custom` level is used, specific email events can be controlled. Availabl - `success_pipeline` - `new_epic` **[ULTIMATE]** - ## Global notification settings Get current notification settings and email address. diff --git a/doc/api/pipeline_schedules.md b/doc/api/pipeline_schedules.md index 470e55425f8..acf1ac90315 100644 --- a/doc/api/pipeline_schedules.md +++ b/doc/api/pipeline_schedules.md @@ -108,9 +108,9 @@ POST /projects/:id/pipeline_schedules | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `description` | string | yes | The description of pipeline schedule | | `ref` | string | yes | The branch/tag name will be triggered | -| `cron ` | string | yes | The cron (e.g. `0 1 * * *`) ([Cron syntax](https://en.wikipedia.org/wiki/Cron)) | -| `cron_timezone ` | string | no | The timezone supported by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) (default: `'UTC'`) | -| `active ` | boolean | no | The activation of pipeline schedule. If false is set, the pipeline schedule will deactivated initially (default: `true`) | +| `cron` | string | yes | The cron (e.g. `0 1 * * *`) ([Cron syntax](https://en.wikipedia.org/wiki/Cron)) | +| `cron_timezone` | string | no | The timezone supported by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) (default: `'UTC'`) | +| `active` | boolean | no | The activation of pipeline schedule. If false is set, the pipeline schedule will deactivated initially (default: `true`) | ```sh curl --request POST --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form description="Build packages" --form ref="master" --form cron="0 1 * * 5" --form cron_timezone="UTC" --form active="true" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules" @@ -153,9 +153,9 @@ PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id | `pipeline_schedule_id` | integer | yes | The pipeline schedule id | | `description` | string | no | The description of pipeline schedule | | `ref` | string | no | The branch/tag name will be triggered | -| `cron ` | string | no | The cron (e.g. `0 1 * * *`) ([Cron syntax](https://en.wikipedia.org/wiki/Cron)) | -| `cron_timezone ` | string | no | The timezone supported by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) or `TZInfo::Timezone` (e.g. `America/Los_Angeles`) | -| `active ` | boolean | no | The activation of pipeline schedule. If false is set, the pipeline schedule will deactivated initially. | +| `cron` | string | no | The cron (e.g. `0 1 * * *`) ([Cron syntax](https://en.wikipedia.org/wiki/Cron)) | +| `cron_timezone` | string | no | The timezone supported by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) or `TZInfo::Timezone` (e.g. `America/Los_Angeles`) | +| `active` | boolean | no | The activation of pipeline schedule. If false is set, the pipeline schedule will deactivated initially. | ```sh curl --request PUT --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form cron="0 2 * * *" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13" diff --git a/doc/api/projects.md b/doc/api/projects.md index b8ccf25581e..e07d6ce9a42 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -55,8 +55,8 @@ GET /projects | `with_issues_enabled` | boolean | no | Limit by enabled issues feature | | `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature | | `with_programming_language` | string | no | Limit by projects which use the given programming language | -| `wiki_checksum_failed` | boolean | no | **[PREMIUM]** Limit projects where the wiki checksum calculation has failed *([Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2)* | -| `repository_checksum_failed` | boolean | no | **[PREMIUM]** Limit projects where the repository checksum calculation has failed *([Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2)* | +| `wiki_checksum_failed` | boolean | no | **[PREMIUM]** Limit projects where the wiki checksum calculation has failed ([Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2) | +| `repository_checksum_failed` | boolean | no | **[PREMIUM]** Limit projects where the repository checksum calculation has failed ([Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2) | | `min_access_level` | integer | no | Limit by current user minimal [access level](members.md) | When `simple=true` or the user is unauthenticated this returns something like: @@ -734,7 +734,7 @@ POST /projects | `mirror_trigger_builds` | boolean | no | **[STARTER]** Pull mirroring triggers builds | | `initialize_with_readme` | boolean | no | `false` by default | ->**Note**: If your HTTP repository is not publicly accessible, +NOTE: **Note:** If your HTTP repository is not publicly accessible, add authentication information to the URL: `https://username:password@gitlab.company.com/group/project.git` where `password` is a public access key with the `api` scope enabled. @@ -779,7 +779,7 @@ POST /projects/user/:user_id | `mirror` | boolean | no | **[STARTER]** Enables pull mirroring in a project | | `mirror_trigger_builds` | boolean | no | **[STARTER]** Pull mirroring triggers builds | ->**Note**: If your HTTP repository is not publicly accessible, +NOTE: **Note:** If your HTTP repository is not publicly accessible, add authentication information to the URL: `https://username:password@gitlab.company.com/group/project.git` where `password` is a public access key with the `api` scope enabled. @@ -828,7 +828,7 @@ PUT /projects/:id | `mirror_overwrites_diverged_branches` | boolean | no | **[STARTER]** Pull mirror overwrites diverged branches | | `packages_enabled` | boolean | no | **[PREMIUM ONLY]** Enable or disable packages repository feature | ->**Note**: If your HTTP repository is not publicly accessible, +NOTE: **Note:** If your HTTP repository is not publicly accessible, add authentication information to the URL: `https://username:password@gitlab.company.com/group/project.git` where `password` is a public access key with the `api` scope enabled. @@ -1354,7 +1354,7 @@ Example response: ## Remove project -Removes a project including all associated resources (issues, merge requests etc.) +Removes a project including all associated resources (issues, merge requests etc). ``` DELETE /projects/:id @@ -1643,10 +1643,17 @@ GET /projects/:id/push_rule } ``` -The following attributes are restricted to certain plans, and will not appear if -you do not have access to those features: +Users on GitLab [Premium, Silver, or higher](https://about.gitlab.com/pricing/) will also see +the `commit_committer_check` parameter: -* `commit_committer_check` only available on **[PREMIUM]** +```json +{ + "id": 1, + "project_id": 3, + "commit_committer_check": false + ... +} +``` ### Add project push rule diff --git a/doc/api/settings.md b/doc/api/settings.md index 876a5a75590..0f46662a8ae 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -68,11 +68,16 @@ Example response: ``` Users on GitLab [Premium or Ultimate](https://about.gitlab.com/pricing/) may also see -the `file_template_project_id` or the `geo_node_allowed_ips` parameters: **[PREMIUM ONLY]** +the `file_template_project_id` or the `geo_node_allowed_ips` parameters: ```json +{ + "id" : 1, + "signup_enabled" : true, "file_template_project_id": 1, "geo_node_allowed_ips": "0.0.0.0/0, ::/0" + ... +} ``` ## Change application settings @@ -169,12 +174,12 @@ are listed in the descriptions of the relevant settings. | `after_sign_up_text` | string | no | Text shown to the user after signing up | | `akismet_api_key` | string | required by: `akismet_enabled` | API key for akismet spam protection. | | `akismet_enabled` | boolean | no | (**If enabled, requires:** `akismet_api_key`) Enable or disable akismet spam protection. | -| `allow_group_owners_to_manage_ldap` | boolean | no | **[Premium]** Set to `true` to allow group owners to manage LDAP | +| `allow_group_owners_to_manage_ldap` | boolean | no | **[PREMIUM]** Set to `true` to allow group owners to manage LDAP | | `allow_local_requests_from_hooks_and_services` | boolean | no | Allow requests to the local network from hooks and services. | | `authorized_keys_enabled` | boolean | no | By default, we write to the `authorized_keys` file to support Git over SSH without additional configuration. GitLab can be optimized to authenticate SSH keys via the database file. Only disable this if you have configured your OpenSSH server to use the AuthorizedKeysCommand. | | `auto_devops_domain` | string | no | Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages. | | `auto_devops_enabled` | boolean | no | Enable Auto DevOps for projects by default. It will automatically build, test, and deploy applications based on a predefined CI/CD configuration. | -| `check_namespace_plan` | boolean | no | **[Premium]** Enabling this will make only licensed EE features available to projects if the project namespace's plan includes the feature or if the project is public. | +| `check_namespace_plan` | boolean | no | **[PREMIUM]** Enabling this will make only licensed EE features available to projects if the project namespace's plan includes the feature or if the project is public. | | `clientside_sentry_dsn` | string | required by: `clientside_sentry_enabled` | Clientside Sentry Data Source Name. | | `clientside_sentry_enabled` | boolean | no | (**If enabled, requires:** `clientside_sentry_dsn`) Enable Sentry error reporting for the client side. | | `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes. | @@ -191,31 +196,31 @@ are listed in the descriptions of the relevant settings. | `dsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded DSA key. Default is `0` (no restriction). `-1` disables DSA keys. | | `ecdsa_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ECDSA key. Default is `0` (no restriction). `-1` disables ECDSA keys. | | `ed25519_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ED25519 key. Default is `0` (no restriction). `-1` disables ED25519 keys. | -| `elasticsearch_aws` | boolean | no | **[Premium]** Enable the use of AWS hosted Elasticsearch | -| `elasticsearch_aws_access_key` | string | no | **[Premium]** AWS IAM access key | -| `elasticsearch_aws_region` | string | no | **[Premium]** The AWS region the elasticsearch domain is configured | -| `elasticsearch_aws_secret_access_key` | string | no | **[Premium]** AWS IAM secret access key | -| `elasticsearch_experimental_indexer` | boolean | no | **[Premium]** Use the experimental elasticsearch indexer. More info: | -| `elasticsearch_indexing` | boolean | no | **[Premium]** Enable Elasticsearch indexing | -| `elasticsearch_search` | boolean | no | **[Premium]** Enable Elasticsearch search | -| `elasticsearch_url` | string | no | **[Premium]** The url to use for connecting to Elasticsearch. Use a comma-separated list to support cluster (e.g., `http://localhost:9200, http://localhost:9201"`). If your Elasticsearch instance is password protected, pass the `username:password` in the URL (e.g., `http://:@:9200/`). | -| `elasticsearch_limit_indexing` | boolean | no | **[Premium]** Limit Elasticsearch to index certain namespaces and projects | -| `elasticsearch_project_ids` | array of integers | no | **[Premium]** The projects to index via Elasticsearch if `elasticsearch_limit_indexing` is enabled. | -| `elasticsearch_namespace_ids` | array of integers | no | **[Premium]** The namespaces to index via Elasticsearch if `elasticsearch_limit_indexing` is enabled. | -| `email_additional_text` | string | no | **[Premium]** Additional text added to the bottom of every email for legal/auditing/compliance reasons | +| `elasticsearch_aws` | boolean | no | **[PREMIUM]** Enable the use of AWS hosted Elasticsearch | +| `elasticsearch_aws_access_key` | string | no | **[PREMIUM]** AWS IAM access key | +| `elasticsearch_aws_region` | string | no | **[PREMIUM]** The AWS region the elasticsearch domain is configured | +| `elasticsearch_aws_secret_access_key` | string | no | **[PREMIUM]** AWS IAM secret access key | +| `elasticsearch_experimental_indexer` | boolean | no | **[PREMIUM]** Use the experimental elasticsearch indexer. More info: | +| `elasticsearch_indexing` | boolean | no | **[PREMIUM]** Enable Elasticsearch indexing | +| `elasticsearch_search` | boolean | no | **[PREMIUM]** Enable Elasticsearch search | +| `elasticsearch_url` | string | no | **[PREMIUM]** The url to use for connecting to Elasticsearch. Use a comma-separated list to support cluster (e.g., `http://localhost:9200, http://localhost:9201"`). If your Elasticsearch instance is password protected, pass the `username:password` in the URL (e.g., `http://:@:9200/`). | +| `elasticsearch_limit_indexing` | boolean | no | **[PREMIUM]** Limit Elasticsearch to index certain namespaces and projects | +| `elasticsearch_project_ids` | array of integers | no | **[PREMIUM]** The projects to index via Elasticsearch if `elasticsearch_limit_indexing` is enabled. | +| `elasticsearch_namespace_ids` | array of integers | no | **[PREMIUM]** The namespaces to index via Elasticsearch if `elasticsearch_limit_indexing` is enabled. | +| `email_additional_text` | string | no | **[PREMIUM]** Additional text added to the bottom of every email for legal/auditing/compliance reasons | | `email_author_in_body` | boolean | no | Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. | | `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. | | `enforce_terms` | boolean | no | (**If enabled, requires:** `terms`) Enforce application ToS to all users. | -| `external_auth_client_cert` | string | no | **[Premium]** (**If enabled, requires:** `external_auth_client_key`) The certificate to use to authenticate with the external authorization service | -| `external_auth_client_key` | string | required by: `external_auth_client_cert` | **[Premium]** Private key for the certificate when authentication is required for the external authorization service, this is encrypted when stored | -| `external_auth_client_key_pass` | string | no | **[Premium]** Passphrase to use for the private key when authenticating with the external service this is encrypted when stored | -| `external_authorization_service_enabled` | boolean | no | **[Premium]** (**If enabled, requires:** `external_authorization_service_default_label`, `external_authorization_service_timeout` and `external_authorization_service_url` ) Enable using an external authorization service for accessing projects | -| `external_authorization_service_default_label` | string | required by: `external_authorization_service_enabled` | **[Premium]** The default classification label to use when requesting authorization and no classification label has been specified on the project | -| `external_authorization_service_timeout` | float | required by: `external_authorization_service_enabled` | **[Premium]** The timeout after which an authorization request is aborted, in seconds. When a request times out, access is denied to the user. (min: 0.001, max: 10, step: 0.001) | -| `external_authorization_service_url` | string | required by: `external_authorization_service_enabled` | **[Premium]** URL to which authorization requests will be directed | -| `file_template_project_id` | integer | no | **[Premium]** The ID of a project to load custom file templates from | +| `external_auth_client_cert` | string | no | **[PREMIUM]** (**If enabled, requires:** `external_auth_client_key`) The certificate to use to authenticate with the external authorization service | +| `external_auth_client_key` | string | required by: `external_auth_client_cert` | **[PREMIUM]** Private key for the certificate when authentication is required for the external authorization service, this is encrypted when stored | +| `external_auth_client_key_pass` | string | no | **[PREMIUM]** Passphrase to use for the private key when authenticating with the external service this is encrypted when stored | +| `external_authorization_service_enabled` | boolean | no | **[PREMIUM]** (**If enabled, requires:** `external_authorization_service_default_label`, `external_authorization_service_timeout` and `external_authorization_service_url` ) Enable using an external authorization service for accessing projects | +| `external_authorization_service_default_label` | string | required by: `external_authorization_service_enabled` | **[PREMIUM]** The default classification label to use when requesting authorization and no classification label has been specified on the project | +| `external_authorization_service_timeout` | float | required by: `external_authorization_service_enabled` | **[PREMIUM]** The timeout after which an authorization request is aborted, in seconds. When a request times out, access is denied to the user. (min: 0.001, max: 10, step: 0.001) | +| `external_authorization_service_url` | string | required by: `external_authorization_service_enabled` | **[PREMIUM]** URL to which authorization requests will be directed | +| `file_template_project_id` | integer | no | **[PREMIUM]** The ID of a project to load custom file templates from | | `first_day_of_week` | integer | no | Start day of the week for calendar views and date pickers. Valid values are `0` (default) for Sunday, `1` for Monday, and `6` for Saturday. | -| `geo_status_timeout` | integer | no | **[Premium]** The amount of seconds after which a request to get a secondary node status will time out. | +| `geo_status_timeout` | integer | no | **[PREMIUM]** The amount of seconds after which a request to get a secondary node status will time out. | | `gitaly_timeout_default` | integer | no | Default Gitaly timeout, in seconds. This timeout is not enforced for git fetch/push operations or Sidekiq jobs. Set to `0` to disable timeouts. | | `gitaly_timeout_fast` | integer | no | Gitaly fast operation timeout, in seconds. Some Gitaly operations are expected to be fast. If they exceed this threshold, there may be a problem with a storage shard and 'failing fast' can help maintain the stability of the GitLab instance. Set to `0` to disable timeouts. | | `gitaly_timeout_medium` | integer | no | Medium Gitaly timeout, in seconds. This should be a value between the Fast and the Default timeout. Set to `0` to disable timeouts. | @@ -224,7 +229,7 @@ are listed in the descriptions of the relevant settings. | `help_page_hide_commercial_content` | boolean | no | Hide marketing-related entries from help. | | `help_page_support_url` | string | no | Alternate support URL for help page. | | `help_page_text` | string | no | Custom text displayed on the help page. | -| `help_text` | string | no | **[Premium]** GitLab server administrator information | +| `help_text` | string | no | **[PREMIUM]** GitLab server administrator information | | `hide_third_party_offers` | boolean | no | Do not display offers from third parties within GitLab. | | `home_page_url` | string | no | Redirect to this URL when not logged in. | | `housekeeping_bitmaps_enabled` | boolean | required by: `housekeeping_enabled` | Enable Git pack file bitmap creation. | @@ -247,9 +252,9 @@ are listed in the descriptions of the relevant settings. | `metrics_sample_interval` | integer | required by: `metrics_enabled` | The sampling interval in seconds. | | `metrics_timeout` | integer | required by: `metrics_enabled` | The amount of seconds after which InfluxDB will time out. | | `mirror_available` | boolean | no | Allow mirrors to be set up for projects. If disabled, only admins will be able to set up mirrors in projects. | -| `mirror_capacity_threshold` | integer | no | **[Premium]** Minimum capacity to be available before scheduling more mirrors preemptively | -| `mirror_max_capacity` | integer | no | **[Premium]** Maximum number of mirrors that can be synchronizing at the same time. | -| `mirror_max_delay` | integer | no | **[Premium]** Maximum time (in minutes) between updates that a mirror can have when scheduled to synchronize. | +| `mirror_capacity_threshold` | integer | no | **[PREMIUM]** Minimum capacity to be available before scheduling more mirrors preemptively | +| `mirror_max_capacity` | integer | no | **[PREMIUM]** Maximum number of mirrors that can be synchronizing at the same time. | +| `mirror_max_delay` | integer | no | **[PREMIUM]** Maximum time (in minutes) between updates that a mirror can have when scheduled to synchronize. | | `pages_domain_verification_enabled` | boolean | no | Require users to prove ownership of custom domains. Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled. | | `password_authentication_enabled_for_git` | boolean | no | Enable authentication for Git over HTTP(S) via a GitLab account password. Default is `true`. | | `password_authentication_enabled_for_web` | boolean | no | Enable authentication for the web interface via a GitLab account password. Default is `true`. | @@ -261,12 +266,12 @@ are listed in the descriptions of the relevant settings. | `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to `0` to disable polling. | | `project_export_enabled` | boolean | no | Enable project export. | | `prometheus_metrics_enabled` | boolean | no | Enable prometheus metrics. | -| `pseudonymizer_enabled` | boolean | no | **[Premium]** When enabled, GitLab will run a background job that will produce pseudonymized CSVs of the GitLab database that will be uploaded to your configured object storage directory. +| `pseudonymizer_enabled` | boolean | no | **[PREMIUM]** When enabled, GitLab will run a background job that will produce pseudonymized CSVs of the GitLab database that will be uploaded to your configured object storage directory. | `recaptcha_enabled` | boolean | no | (**If enabled, requires:** `recaptcha_private_key` and `recaptcha_site_key`) Enable recaptcha. | | `recaptcha_private_key` | string | required by: `recaptcha_enabled` | Private key for recaptcha. | | `recaptcha_site_key` | string | required by: `recaptcha_enabled` | Site key for recaptcha. | | `repository_checks_enabled` | boolean | no | GitLab will periodically run `git fsck` in all project and wiki repositories to look for silent disk corruption issues. | -| `repository_size_limit` | integer | no | **[Premium]** Size limit per repository (MB) | +| `repository_size_limit` | integer | no | **[PREMIUM]** Size limit per repository (MB) | | `repository_storages` | array of strings | no | A list of names of enabled storage paths, taken from `gitlab.yml`. New projects will be created in one of these stores, chosen at random. | | `require_two_factor_authentication` | boolean | no | (**If enabled, requires:** `two_factor_grace_period`) Require all users to set up Two-factor authentication. | | `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-admin users for groups, projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is `null` which means there is no restriction. | @@ -274,15 +279,15 @@ are listed in the descriptions of the relevant settings. | `send_user_confirmation_email` | boolean | no | Send confirmation email on sign-up. | | `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes | | `shared_runners_enabled` | boolean | no | (**If enabled, requires:** `shared_runners_text` and `shared_runners_minutes`) Enable shared runners for new projects. | -| `shared_runners_minutes` | integer | required by: `shared_runners_enabled` | **[Premium]** Set the maximum number of pipeline minutes that a group can use on shared Runners per month. | +| `shared_runners_minutes` | integer | required by: `shared_runners_enabled` | **[PREMIUM]** Set the maximum number of pipeline minutes that a group can use on shared Runners per month. | | `shared_runners_text` | string | required by: `shared_runners_enabled` | Shared runners text. | | `sign_in_text` | string | no | Text on the login page. | | `signin_enabled` | string | no | (Deprecated: Use `password_authentication_enabled_for_web` instead) Flag indicating if password authentication is enabled for the web interface. | | `signup_enabled` | boolean | no | Enable registration. Default is `true`. | -| `slack_app_enabled` | boolean | no | **[Premium]** (**If enabled, requires:** `slack_app_id`, `slack_app_secret` and `slack_app_secret`) Enable Slack app. | -| `slack_app_id` | string | required by: `slack_app_enabled` | **[Premium]** The app id of the Slack-app. | -| `slack_app_secret` | string | required by: `slack_app_enabled` | **[Premium]** The app secret of the Slack-app. | -| `slack_app_verification_token` | string | required by: `slack_app_enabled` | **[Premium]** The verification token of the Slack-app. | +| `slack_app_enabled` | boolean | no | **[PREMIUM]** (**If enabled, requires:** `slack_app_id`, `slack_app_secret` and `slack_app_secret`) Enable Slack app. | +| `slack_app_id` | string | required by: `slack_app_enabled` | **[PREMIUM]** The app id of the Slack-app. | +| `slack_app_secret` | string | required by: `slack_app_enabled` | **[PREMIUM]** The app secret of the Slack-app. | +| `slack_app_verification_token` | string | required by: `slack_app_enabled` | **[PREMIUM]** The verification token of the Slack-app. | | `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to `0` for unlimited time. | | `terms` | text | required by: `enforce_terms` | (**Required by:** `enforce_terms`) Markdown content for the ToS. | | `throttle_authenticated_api_enabled` | boolean | no | (**If enabled, requires:** `throttle_authenticated_api_period_in_seconds` and `throttle_authenticated_api_requests_per_period`) Enable authenticated API request rate limit. Helps reduce request volume (e.g. from crawlers or abusive bots). | @@ -305,4 +310,4 @@ are listed in the descriptions of the relevant settings. | `user_show_add_ssh_key_message` | boolean | no | When set to `false` disable the "You won't be able to pull or push project code via SSH" warning shown to users with no uploaded SSH key. | | `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. | | `local_markdown_version` | integer | no | Increase this value when any cached markdown should be invalidated. | -| `geo_node_allowed_ips` | string | yes | **[Premium]** Comma-separated list of IPs and CIDRs of allowed secondary nodes. For example, `1.1.1.1, 2.2.2.0/24`. | +| `geo_node_allowed_ips` | string | yes | **[PREMIUM]** Comma-separated list of IPs and CIDRs of allowed secondary nodes. For example, `1.1.1.1, 2.2.2.0/24`. | diff --git a/doc/api/users.md b/doc/api/users.md index 6be097e6364..e1fccc14df3 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -70,11 +70,11 @@ Username search is case insensitive. GET /users ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `order_by` | string | no | Return users ordered by `id`, `name`, `username`, `created_at`, or `updated_at` fields. Default is `id` | -| `sort` | string | no | Return users sorted in `asc` or `desc` order. Default is `desc` | -| `two_factor` | string | no | Filter users by Two-factor authentication. Filter values are `enabled` or `disabled`. By default it returns all users | +| Attribute | Type | Required | Description | +| ------------ | ------ | -------- | ----------- | +| `order_by` | string | no | Return users ordered by `id`, `name`, `username`, `created_at`, or `updated_at` fields. Default is `id` | +| `sort` | string | no | Return users sorted in `asc` or `desc` order. Default is `desc` | +| `two_factor` | string | no | Filter users by Two-factor authentication. Filter values are `enabled` or `disabled`. By default it returns all users | ```json [ @@ -284,7 +284,19 @@ Example Responses: Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see the `shared_runners_minutes_limit` and `extra_shared_runners_minutes_limit` parameters. -Users on GitLab Silver will also see the `group_saml` option: + +```json +{ + "id": 1, + "username": "john_smith", + "shared_runners_minutes_limit": 133, + "extra_shared_runners_minutes_limit": 133 + ... +} +``` + +Users on GitLab.com [Silver, or higher](https://about.gitlab.com/pricing/) will also +see the `group_saml` option: ```json { @@ -408,9 +420,7 @@ Parameters: [moved to the ghost user](../user/profile/account/delete_account.md#associated-records) will be deleted instead, as well as groups owned solely by this user. -## User - -### For normal users +## List current user (for normal users) Gets currently authenticated user. @@ -456,7 +466,7 @@ GET /user } ``` -### For admins +## List current user (for admins) Parameters: @@ -535,9 +545,9 @@ Get the status of a user. GET /users/:id_or_username/status ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id_or_username` | string | yes | The id or username of the user to get a status of | +| Attribute | Type | Required | Description | +| ---------------- | ------ | -------- | ----------- | +| `id_or_username` | string | yes | The id or username of the user to get a status of | ```bash curl "https://gitlab.example.com/users/janedoe/status" @@ -561,8 +571,8 @@ Set the status of the current user. PUT /user/status ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | +| Attribute | Type | Required | Description | +| --------- | ------ | -------- | ----------- | | `emoji` | string | no | The name of the emoji to use as status, if omitted `speech_balloon` is used. Emoji name can be one of the specified names in the [Gemojione index][gemojione-index]. | | `message` | string | no | The message to set as a status. It can also contain emoji codes. | @@ -584,7 +594,7 @@ Example responses ## List user projects -Please refer to the [List of user projects ](projects.md#list-user-projects). +Please refer to the [List of user projects](projects.md#list-user-projects). ## List SSH keys @@ -760,9 +770,9 @@ GET /user/gpg_keys/:key_id Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `key_id` | integer | yes | The ID of the GPG key | +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | ----------- | +| `key_id` | integer | yes | The ID of the GPG key | ```bash curl --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/user/gpg_keys/1 @@ -788,9 +798,9 @@ POST /user/gpg_keys Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| key | string | yes | The new GPG key | +| Attribute | Type | Required | Description | +| --------- | ------ | -------- | ----------- | +| key | string | yes | The new GPG key | ```bash curl --data "key=-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFV..." --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/user/gpg_keys @@ -818,9 +828,9 @@ DELETE /user/gpg_keys/:key_id Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `key_id` | integer | yes | The ID of the GPG key | +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | ----------- | +| `key_id` | integer | yes | The ID of the GPG key | ```bash curl --request DELETE --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/user/gpg_keys/1 @@ -838,9 +848,9 @@ GET /users/:id/gpg_keys Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the user | +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | ----------- | +| `id` | integer | yes | The ID of the user | ```bash curl --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/users/2/gpg_keys @@ -868,10 +878,10 @@ GET /users/:id/gpg_keys/:key_id Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the user | -| `key_id` | integer | yes | The ID of the GPG key | +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | ----------- | +| `id` | integer | yes | The ID of the user | +| `key_id` | integer | yes | The ID of the GPG key | ```bash curl --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/users/2/gpg_keys/1 @@ -897,10 +907,10 @@ POST /users/:id/gpg_keys Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the user | -| `key_id` | integer | yes | The ID of the GPG key | +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | ----------- | +| `id` | integer | yes | The ID of the user | +| `key_id` | integer | yes | The ID of the GPG key | ```bash curl --data "key=-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFV..." --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/users/2/gpg_keys @@ -928,10 +938,10 @@ DELETE /users/:id/gpg_keys/:key_id Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the user | -| `key_id` | integer | yes | The ID of the GPG key | +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | ----------- | +| `id` | integer | yes | The ID of the user | +| `key_id` | integer | yes | The ID of the GPG key | ```bash curl --request DELETE --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/users/2/gpg_keys/1 @@ -1112,10 +1122,10 @@ GET /users/:user_id/impersonation_tokens Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `user_id` | integer | yes | The ID of the user | -| `state` | string | no | filter tokens based on state (`all`, `active`, `inactive`) | +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | ---------------------------------------------------------- | +| `user_id` | integer | yes | The ID of the user | +| `state` | string | no | filter tokens based on state (`all`, `active`, `inactive`) | ``` curl --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/users/42/impersonation_tokens @@ -1164,10 +1174,10 @@ GET /users/:user_id/impersonation_tokens/:impersonation_token_id Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `user_id` | integer | yes | The ID of the user | -| `impersonation_token_id` | integer | yes | The ID of the impersonation token | +| Attribute | Type | Required | Description | +| ------------------------ | ------- | -------- | --------------------------------- | +| `user_id` | integer | yes | The ID of the user | +| `impersonation_token_id` | integer | yes | The ID of the impersonation token | ``` curl --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/users/42/impersonation_tokens/2 @@ -1193,7 +1203,6 @@ Example response: ## Create an impersonation token > Requires admin permissions. - > Token values are returned once. Make sure you save it - you won't be able to access it again. It creates a new impersonation token. Note that only administrators can do this. @@ -1205,12 +1214,12 @@ settings page. POST /users/:user_id/impersonation_tokens ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `user_id` | integer | yes | The ID of the user | -| `name` | string | yes | The name of the impersonation token | -| `expires_at` | date | no | The expiration date of the impersonation token in ISO format (`YYYY-MM-DD`)| -| `scopes` | array | yes | The array of scopes of the impersonation token (`api`, `read_user`) | +| Attribute | Type | Required | Description | +| ------------ | ------- | -------- | ----------- | +| `user_id` | integer | yes | The ID of the user | +| `name` | string | yes | The name of the impersonation token | +| `expires_at` | date | no | The expiration date of the impersonation token in ISO format (`YYYY-MM-DD`)| +| `scopes` | array | yes | The array of scopes of the impersonation token (`api`, `read_user`) | ``` curl --request POST --header "PRIVATE-TOKEN: " --data "name=mytoken" --data "expires_at=2017-04-04" --data "scopes[]=api" https://gitlab.example.com/api/v4/users/42/impersonation_tokens @@ -1257,15 +1266,15 @@ Parameters: ### Get user activities (admin only) ->**Note:** This API endpoint is only available on 8.15 (EE) and 9.1 (CE) and above. +NOTE: **Note:** This API endpoint is only available on 8.15 (EE) and 9.1 (CE) and above. Get the last activity date for all users, sorted from oldest to newest. The activities that update the timestamp are: - - Git HTTP/SSH activities (such as clone, push) - - User logging in into GitLab - - User visiting pages related to Dashboards, Projects, Issues and Merge Requests ([introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/54947) in GitLab 11.8) +- Git HTTP/SSH activities (such as clone, push) +- User logging in into GitLab +- User visiting pages related to Dashboards, Projects, Issues and Merge Requests ([introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/54947) in GitLab 11.8) By default, it shows the activity for all users in the last 6 months, but this can be amended by using the `from` parameter. From 73d4425327d4860935472a130457c43ea4c7ea9f Mon Sep 17 00:00:00 2001 From: Marcia Ramos Date: Thu, 4 Jul 2019 08:53:04 +0000 Subject: [PATCH 046/195] Docs: Clearly state "access control" from Pages index --- doc/user/project/pages/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md index fa79c393b72..64b1e259292 100644 --- a/doc/user/project/pages/index.md +++ b/doc/user/project/pages/index.md @@ -140,7 +140,7 @@ To learn more about configuration options for GitLab Pages, read the following: | [Static websites and Pages domains](getting_started_part_one.md) | Understand what is a static website, and how GitLab Pages default domains work. | | [Projects and URL structure](getting_started_part_two.md) | Forking projects and creating new ones from scratch, understanding URLs structure and baseurls. | | [GitLab CI/CD for GitLab Pages](getting_started_part_four.md) | Understand how to create your own `.gitlab-ci.yml` for your site. | -| [Exploring GitLab Pages](introduction.md) | Requirements, technical aspects, specific GitLab CI's configuration options, custom 404 pages, limitations, FAQ. | +| [Exploring GitLab Pages](introduction.md) | Requirements, technical aspects, specific GitLab CI's configuration options, Access Control, custom 404 pages, limitations, FAQ. | |---+---| | [Custom domains and SSL/TLS Certificates](getting_started_part_three.md) | How to add custom domains and subdomains to your website, configure DNS records and SSL/TLS certificates. | | [CloudFlare certificates](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) | Secure your Pages site with CloudFlare certificates. | From efde6f7db97bd3962732b6885040dcc98ab46cd4 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Thu, 4 Jul 2019 08:53:31 +0000 Subject: [PATCH 047/195] OmniAuth full_host spec helper Allows us to correctly set omniauth's full_host so redirects take the port into account. Needed when running selenium tests on a different port --- spec/features/oauth_login_spec.rb | 12 ++---------- spec/support/helpers/devise_helpers.rb | 12 ++++++++++++ spec/support/helpers/fake_u2f_device.rb | 4 ++++ spec/support/helpers/login_helpers.rb | 12 +++++++++++- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb index 5ebfc32952d..86331728f88 100644 --- a/spec/features/oauth_login_spec.rb +++ b/spec/features/oauth_login_spec.rb @@ -16,16 +16,8 @@ describe 'OAuth Login', :js, :allow_forgery_protection do providers = [:github, :twitter, :bitbucket, :gitlab, :google_oauth2, :facebook, :cas3, :auth0, :authentiq, :salesforce] - before(:all) do - # The OmniAuth `full_host` parameter doesn't get set correctly (it gets set to something like `http://localhost` - # here), and causes integration tests to fail with 404s. We set the `full_host` by removing the request path (and - # anything after it) from the request URI. - @omniauth_config_full_host = OmniAuth.config.full_host - OmniAuth.config.full_host = ->(request) { request['REQUEST_URI'].sub(/#{request['REQUEST_PATH']}.*/, '') } - end - - after(:all) do - OmniAuth.config.full_host = @omniauth_config_full_host + around(:all) do |example| + with_omniauth_full_host { example.run } end def login_with_provider(provider, enter_two_factor: false) diff --git a/spec/support/helpers/devise_helpers.rb b/spec/support/helpers/devise_helpers.rb index d32bc2424c0..fb2a110422a 100644 --- a/spec/support/helpers/devise_helpers.rb +++ b/spec/support/helpers/devise_helpers.rb @@ -21,4 +21,16 @@ module DeviseHelpers context.env end end + + def with_omniauth_full_host(&block) + # The OmniAuth `full_host` parameter doesn't get set correctly (it gets set to something like `http://localhost` + # here), and causes integration tests to fail with 404s. We set the `full_host` by removing the request path (and + # anything after it) from the request URI. + omniauth_config_full_host = OmniAuth.config.full_host + OmniAuth.config.full_host = ->(request) { ActionDispatch::Request.new(request).base_url } + + yield + + OmniAuth.config.full_host = omniauth_config_full_host + end end diff --git a/spec/support/helpers/fake_u2f_device.rb b/spec/support/helpers/fake_u2f_device.rb index a7605cd483a..22cd8152d77 100644 --- a/spec/support/helpers/fake_u2f_device.rb +++ b/spec/support/helpers/fake_u2f_device.rb @@ -32,6 +32,10 @@ class FakeU2fDevice ") end + def fake_u2f_authentication + @page.execute_script("window.gl.u2fAuthenticate.renderAuthenticated('abc');") + end + private def u2f_device(app_id) diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb index 0bb2d2510c2..0cb99b4e087 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -87,12 +87,17 @@ module LoginHelpers click_link "oauth-login-#{provider}" end + def fake_successful_u2f_authentication + allow(U2fRegistration).to receive(:authenticate).and_return(true) + FakeU2fDevice.new(page, nil).fake_u2f_authentication + end + def mock_auth_hash_with_saml_xml(provider, uid, email, saml_response) response_object = { document: saml_xml(saml_response) } mock_auth_hash(provider, uid, email, response_object: response_object) end - def mock_auth_hash(provider, uid, email, response_object: nil) + def configure_mock_auth(provider, uid, email, response_object: nil) # The mock_auth configuration allows you to set per-provider (or default) # authentication hashes to return during integration testing. OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({ @@ -118,6 +123,11 @@ module LoginHelpers response_object: response_object } }) + end + + def mock_auth_hash(provider, uid, email, response_object: nil) + configure_mock_auth(provider, uid, email, response_object: response_object) + original_env_config_omniauth_auth = Rails.application.env_config['omniauth.auth'] Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym] From ae7041d4dae5650172858ec86bcb6ca92ec4512a Mon Sep 17 00:00:00 2001 From: Igor Date: Thu, 4 Jul 2019 09:13:50 +0000 Subject: [PATCH 048/195] Backports for EE's "Allow adding groups to CODEOWNERS file" Some general code has been added/removed in EE version which needs to be backported in CE --- app/models/group.rb | 2 + doc/user/project/code_owners.md | 15 +++-- lib/gitlab/user_extractor.rb | 56 ------------------ spec/lib/gitlab/user_extractor_spec.rb | 78 -------------------------- 4 files changed, 13 insertions(+), 138 deletions(-) delete mode 100644 lib/gitlab/user_extractor.rb delete mode 100644 spec/lib/gitlab/user_extractor_spec.rb diff --git a/app/models/group.rb b/app/models/group.rb index 8e89c7ecfb1..9520db1bc0a 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -63,6 +63,8 @@ class Group < Namespace after_save :update_two_factor_requirement after_update :path_changed_hook, if: :saved_change_to_path? + scope :with_users, -> { includes(:users) } + class << self def sort_by_attribute(method) if method == 'storage_size_desc' diff --git a/doc/user/project/code_owners.md b/doc/user/project/code_owners.md index ae04616943f..c76847616b3 100644 --- a/doc/user/project/code_owners.md +++ b/doc/user/project/code_owners.md @@ -1,10 +1,12 @@ # Code Owners **[STARTER]** -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6916) +> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6916) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.3. +> - [Support for group namespaces](https://gitlab.com/gitlab-org/gitlab-ce/issues/53182) added in GitLab Starter 12.1. -You can use a `CODEOWNERS` file to specify users that are responsible -for certain files in a repository. +You can use a `CODEOWNERS` file to specify users or +[shared groups](members/share_project_with_groups.md) +that are responsible for certain files in a repository. You can choose and add the `CODEOWNERS` file in three places: @@ -25,7 +27,8 @@ the given file. Files can be specified using the same kind of patterns you would use in the `.gitignore` file followed by the `@username` or email of one -or more users that should be owners of the file. +or more users or by the `@name` of one or more groups that should +be owners of the file. The order in which the paths are defined is significant: the last pattern that matches a given path will be used to find the code @@ -63,6 +66,10 @@ CODEOWNERS @multiple @owners @tab-separated # owner for the LICENSE file LICENSE @legal this_does_not_match janedoe@gitlab.com +# Group names can be used to match groups and nested groups to specify +# them as owners for a file +README @group @group/with-nested/subgroup + # Ending a path in a `/` will specify the code owners for every file # nested in that directory, on any level /docs/ @all-docs diff --git a/lib/gitlab/user_extractor.rb b/lib/gitlab/user_extractor.rb deleted file mode 100644 index ede60c9ab1d..00000000000 --- a/lib/gitlab/user_extractor.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -# This class extracts all users found in a piece of text by the username or the -# email address - -module Gitlab - class UserExtractor - # Not using `Devise.email_regexp` to filter out any chars that an email - # does not end with and not pinning the email to a start of end of a string. - EMAIL_REGEXP = /(?([^@\s]+@[^@\s]+(? Date: Thu, 4 Jul 2019 09:29:06 +0000 Subject: [PATCH 049/195] Set default project sort method prior to initial sort on page loading --- .../dashboard/projects_controller.rb | 2 +- changelogs/unreleased/issue-63222.yml | 5 +++++ .../dashboard/projects_controller_spec.rb | 18 +++++++++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/issue-63222.yml diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index d43f5393ecc..daeb8fda417 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -7,8 +7,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) } before_action :set_non_archived_param - before_action :projects, only: [:index] before_action :default_sorting + before_action :projects, only: [:index] skip_cross_project_access_check :index, :starred def index diff --git a/changelogs/unreleased/issue-63222.yml b/changelogs/unreleased/issue-63222.yml new file mode 100644 index 00000000000..bc6520b9fc5 --- /dev/null +++ b/changelogs/unreleased/issue-63222.yml @@ -0,0 +1,5 @@ +--- +title: Set default sort method for dashboard projects list +merge_request: 29830 +author: David Palubin +type: fixed diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb index ea68eae12ed..6591901a9dc 100644 --- a/spec/controllers/dashboard/projects_controller_spec.rb +++ b/spec/controllers/dashboard/projects_controller_spec.rb @@ -11,8 +11,10 @@ describe Dashboard::ProjectsController do end context 'user logged in' do + let(:user) { create(:user) } + before do - sign_in create(:user) + sign_in(user) end context 'external authorization' do @@ -24,6 +26,20 @@ describe Dashboard::ProjectsController do expect(response).to have_gitlab_http_status(200) end end + + it 'orders the projects by last activity by default' do + project = create(:project) + project.add_developer(user) + project.update!(last_repository_updated_at: 3.days.ago, last_activity_at: 3.days.ago) + + project2 = create(:project) + project2.add_developer(user) + project2.update!(last_repository_updated_at: 10.days.ago, last_activity_at: 10.days.ago) + + get :index + + expect(assigns(:projects)).to eq([project, project2]) + end end end From de69040f70718042d7041752ef64bc15d97b3191 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 4 Jul 2019 10:55:45 +0100 Subject: [PATCH 050/195] Removes EE differences --- app/views/projects/_flash_messages.html.haml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml index b2dab0b5348..d95045c9cce 100644 --- a/app/views/projects/_flash_messages.html.haml +++ b/app/views/projects/_flash_messages.html.haml @@ -5,6 +5,7 @@ - if current_user && can?(current_user, :download_code, project) = render 'shared/no_ssh' = render 'shared/no_password' + = render_if_exists 'shared/shared_runners_minutes_limit', project: project - unless project.empty_repo? = render 'shared/auto_devops_implicitly_enabled_banner', project: project = render_if_exists 'projects/above_size_limit_warning', project: project From ac8bb05f0994787d720fd365dd27cb67172e9b58 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 4 Jul 2019 12:02:54 +0200 Subject: [PATCH 051/195] Remove empty lines in config/README.md --- config/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/README.md b/config/README.md index 2778d0d4f02..9226f71a374 100644 --- a/config/README.md +++ b/config/README.md @@ -39,7 +39,7 @@ If desired, the routing URL provided by these settings can be used with: 1. `host name` or IP for each Redis instance desired 2. TCP port number for each Redis instance desired 3. `database number` for each Redis instance desired - + ## Example URL attribute formats for GitLab Redis `.yml` configuration files * Unix Socket, default Redis database (0) * `url: unix:/path/to/redis.sock` @@ -147,4 +147,3 @@ searched): 3. the configuration file pointed to by the `GITLAB_REDIS_CONFIG_FILE` environment variable 4. the configuration file `resque.yml` - From 0e4cef8589e36d22e5da125e1fe83475f1b27856 Mon Sep 17 00:00:00 2001 From: Lukas Eipert Date: Thu, 4 Jul 2019 12:18:51 +0200 Subject: [PATCH 052/195] Resolve CE/EE diff in main.js Moving ee/main.js to ee/main_ee.js allows to add a noop file in CE and utilize ee_else_ce. --- app/assets/javascripts/main.js | 2 ++ app/assets/javascripts/main_ee.js | 1 + 2 files changed, 3 insertions(+) create mode 100644 app/assets/javascripts/main_ee.js diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 9f30a989295..7b42f267890 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -33,6 +33,8 @@ import GlFieldErrors from './gl_field_errors'; import initUserPopovers from './user_popovers'; import { __ } from './locale'; +import 'ee_else_ce/main_ee'; + // expose jQuery as global (TODO: remove these) window.jQuery = jQuery; window.$ = jQuery; diff --git a/app/assets/javascripts/main_ee.js b/app/assets/javascripts/main_ee.js new file mode 100644 index 00000000000..84d74775163 --- /dev/null +++ b/app/assets/javascripts/main_ee.js @@ -0,0 +1 @@ +// This is an empty file to satisfy ee_else_ce import for the EE main entry point From 0c028a816b6d8941b435291f298e1fffaa7b5af0 Mon Sep 17 00:00:00 2001 From: Ezekiel Kigbo Date: Thu, 4 Jul 2019 10:20:38 +0000 Subject: [PATCH 053/195] Vue-i18n: app/assets/javascripts/notes directory i18n linting for .vue files under the app/assets/javascripts/notes directory --- .../notes/components/comment_form.vue | 43 +++++++++-------- .../notes/components/diff_with_note.vue | 2 +- .../notes/components/note_awards_list.vue | 16 ++++--- .../notes/components/note_form.vue | 32 +++++++++---- .../notes/components/note_header.vue | 2 +- .../components/note_signed_out_widget.vue | 19 ++++++-- .../notes/components/noteable_discussion.vue | 5 +- .../notes/components/noteable_note.vue | 18 ++++--- .../notes/components/notes_app.vue | 3 +- locale/gitlab.pot | 48 +++++++++++++++++++ 10 files changed, 138 insertions(+), 50 deletions(-) diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 5a4b5f9398b..6c1738f0f1b 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -65,14 +65,12 @@ export default { return this.getUserData.id; }, commentButtonTitle() { - return this.noteType === constants.COMMENT ? 'Comment' : 'Start thread'; + return this.noteType === constants.COMMENT ? __('Comment') : __('Start thread'); }, startDiscussionDescription() { - let text = 'Discuss a specific suggestion or question'; - if (this.getNoteableData.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE) { - text += ' that needs to be resolved'; - } - return `${text}.`; + return this.getNoteableData.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE + ? __('Discuss a specific suggestion or question that needs to be resolved.') + : __('Discuss a specific suggestion or question.'); }, isOpen() { return this.openState === constants.OPENED || this.openState === constants.REOPENED; @@ -127,8 +125,8 @@ export default { }, issuableTypeTitle() { return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE - ? 'merge request' - : 'issue'; + ? __('merge request') + : __('issue'); }, trackingLabel() { return slugifyWithUnderscore(`${this.commentButtonTitle} button`); @@ -203,7 +201,7 @@ export default { this.discard(); } else { Flash( - 'Something went wrong while adding your comment. Please try again.', + __('Something went wrong while adding your comment. Please try again.'), 'alert', this.$refs.commentForm, ); @@ -219,8 +217,9 @@ export default { .catch(() => { this.enableButton(); this.discard(false); - const msg = `Your comment could not be submitted! -Please check your network connection and try again.`; + const msg = __( + 'Your comment could not be submitted! Please check your network connection and try again.', + ); Flash(msg, 'alert', this.$el); this.note = noteData.data.note.note; // Restore textarea content. this.removePlaceholderNotes(); @@ -298,7 +297,7 @@ Please check your network connection and try again.`; const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType)); this.autosave = new Autosave($(this.$refs.textarea), [ - 'Note', + __('Note'), noteableType, this.getNoteableData.id, ]); @@ -359,8 +358,8 @@ Please check your network connection and try again.`; class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input" data-supports-quick-actions="true" - aria-label="Description" - placeholder="Write a comment or drag your files here…" + :aria-label="__('Description')" + :placeholder="__('Write a comment or drag your files here…')" @keydown.up="editCurrentUserLastNote()" @keydown.meta.enter="handleSave()" @keydown.ctrl.enter="handleSave()" @@ -381,7 +380,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" data-track-event="click_button" @click.prevent="handleSave()" > - {{ __(commentButtonTitle) }} + {{ commentButtonTitle }} @@ -404,8 +403,14 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" >
- Comment -

Add a general comment to this {{ noteableDisplayName }}.

+ {{ __('Comment') }} +

+ {{ + sprintf(__('Add a general comment to this %{noteableDisplayName}.'), { + noteableDisplayName, + }) + }} +

@@ -418,7 +423,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" >
- Start thread + {{ __('Start thread') }}

{{ startDiscussionDescription }}

diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index 54c242b2fda..164e79c6294 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -100,7 +100,7 @@ export default { class="btn-link btn-link-retry btn-no-padding js-toggle-lazy-diff-retry-button" @click="fetchDiff" > - Try again + {{ __('Try again') }}
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index 941b6d5cab3..d4a57d5d58d 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -4,6 +4,7 @@ import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; import Flash from '../../flash'; import { glEmojiTag } from '../../emoji'; +import { __, sprintf } from '~/locale'; export default { components: { @@ -108,23 +109,26 @@ export default { // Add myself to the beginning of the list so title will start with You. if (hasReactionByCurrentUser) { - namesToShow.unshift('You'); + namesToShow.unshift(__('You')); } let title = ''; // We have 10+ awarded user, join them with comma and add `and x more`. if (remainingAwardList.length) { - title = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`; + title = sprintf(__(`%{listToShow}, and %{awardsListLength} more.`), { + listToShow: namesToShow.join(', '), + awardsListLength: remainingAwardList.length, + }); } else if (namesToShow.length > 1) { // Join all names with comma but not the last one, it will be added with and text. title = namesToShow.slice(0, namesToShow.length - 1).join(', '); // If we have more than 2 users we need an extra comma before and text. title += namesToShow.length > 2 ? ',' : ''; - title += ` and ${namesToShow.slice(-1)}`; // Append and text + title += sprintf(__(` and %{sliced}`), { sliced: namesToShow.slice(-1) }); // Append and text } else { // We have only 2 users so join them with and. - title = namesToShow.join(' and '); + title = namesToShow.join(__(' and ')); } return title; @@ -155,7 +159,7 @@ export default { awardName: parsedName, }; - this.toggleAwardRequest(data).catch(() => Flash('Something went wrong on our end.')); + this.toggleAwardRequest(data).catch(() => Flash(__('Something went wrong on our end.'))); }, }, }; @@ -184,7 +188,7 @@ export default { :class="{ 'js-user-authored': isAuthoredByMe }" class="award-control btn js-add-award" title="Add reaction" - aria-label="Add reaction" + :aria-label="__('Add reaction')" data-boundary="viewport" type="button" > diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 01be4f2b094..3823861c0b9 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -1,14 +1,14 @@ diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index a71a89cfffc..3fbd0a9f715 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -283,8 +283,9 @@ export default { this.removePlaceholderNotes(); this.isReplying = true; this.$nextTick(() => { - const msg = `Your comment could not be submitted! -Please check your network connection and try again.`; + const msg = __( + 'Your comment could not be submitted! Please check your network connection and try again.', + ); Flash(msg, 'alert', this.$el); this.$refs.noteForm.note = noteText; callback(err); diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index aa80e25a3e0..2f201839d45 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -5,7 +5,7 @@ import { escape } from 'underscore'; import { truncateSha } from '~/lib/utils/text_utility'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import draftMixin from 'ee_else_ce/notes/mixins/draft'; -import { s__, sprintf } from '../../locale'; +import { __, s__, sprintf } from '../../locale'; import Flash from '../../flash'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import noteHeader from './note_header.vue'; @@ -128,9 +128,13 @@ export default { this.$emit('handleEdit'); }, deleteHandler() { - const typeOfComment = this.note.isDraft ? 'pending comment' : 'comment'; - // eslint-disable-next-line no-alert - if (window.confirm(`Are you sure you want to delete this ${typeOfComment}?`)) { + const typeOfComment = this.note.isDraft ? __('pending comment') : __('comment'); + if ( + // eslint-disable-next-line no-alert + window.confirm( + sprintf(__('Are you sure you want to delete this %{typeOfComment}?'), { typeOfComment }), + ) + ) { this.isDeleting = true; this.$emit('handleDeleteNote', this.note); @@ -141,7 +145,7 @@ export default { this.isDeleting = false; }) .catch(() => { - Flash('Something went wrong while deleting your note. Please try again.'); + Flash(__('Something went wrong while deleting your note. Please try again.')); this.isDeleting = false; }); } @@ -185,7 +189,7 @@ export default { this.isRequesting = false; this.isEditing = true; this.$nextTick(() => { - const msg = 'Something went wrong while editing your comment. Please try again.'; + const msg = __('Something went wrong while editing your comment. Please try again.'); Flash(msg, 'alert', this.$el); this.recoverNoteContent(noteText); callback(); @@ -195,7 +199,7 @@ export default { formCancelHandler(shouldConfirm, isDirty) { if (shouldConfirm && isDirty) { // eslint-disable-next-line no-alert - if (!window.confirm('Are you sure you want to cancel editing this comment?')) return; + if (!window.confirm(__('Are you sure you want to cancel editing this comment?'))) return; } this.$refs.noteBody.resetAutoSave(); if (this.oldContent) { diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 4d00e957973..a0695f9e191 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -1,4 +1,5 @@ diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b9bea9bd31a..ed6245bf242 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -118,6 +118,9 @@ msgstr[1] "" msgid "%{actionText} & %{openOrClose} %{noteable}" msgstr "" +msgid "%{canMergeCount}/%{assigneesCount} can merge" +msgstr "" + msgid "%{commit_author_link} authored %{commit_timeago}" msgstr "" @@ -260,6 +263,9 @@ msgstr "" msgid "%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc." msgstr "" +msgid "%{userName}'s avatar" +msgstr "" + msgid "%{user_name} profile page" msgstr "" @@ -284,6 +290,9 @@ msgstr "" msgid "+ %{moreCount} more" msgstr "" +msgid "+ %{numberOfHiddenAssignees} more" +msgstr "" + msgid ", or " msgstr "" @@ -1335,7 +1344,9 @@ msgid "Assigned to me" msgstr "" msgid "Assignee" -msgstr "" +msgid_plural "%d Assignees" +msgstr[0] "" +msgstr[1] "" msgid "Assignee(s)" msgstr "" @@ -1906,6 +1917,9 @@ msgstr "" msgid "Cannot create the abuse report. This user has been blocked." msgstr "" +msgid "Cannot merge" +msgstr "" + msgid "Cannot modify managed Kubernetes cluster" msgstr "" @@ -4244,6 +4258,9 @@ msgstr "" msgid "Error occurred when fetching sidebar data" msgstr "" +msgid "Error occurred when saving assignees" +msgstr "" + msgid "Error occurred when toggling the notification subscription" msgstr "" @@ -6867,6 +6884,9 @@ msgstr "" msgid "No milestones to show" msgstr "" +msgid "No one can merge" +msgstr "" + msgid "No other labels with such name or description" msgstr "" @@ -10906,12 +10926,21 @@ msgstr "" msgid "TimeTrackingEstimated|Est" msgstr "" +msgid "TimeTracking|%{startTag}Spent: %{endTag}%{timeSpentHumanReadable}" +msgstr "" + msgid "TimeTracking|Estimated:" msgstr "" +msgid "TimeTracking|Over by %{timeRemainingHumanReadable}" +msgstr "" + msgid "TimeTracking|Spent" msgstr "" +msgid "TimeTracking|Time remaining: %{timeRemainingHumanReadable}" +msgstr "" + msgid "Timeago|%s days ago" msgstr "" @@ -11177,6 +11206,9 @@ msgstr "" msgid "Toggle navigation" msgstr "" +msgid "Toggle sidebar" +msgstr "" + msgid "Toggle thread" msgstr "" @@ -11294,6 +11326,12 @@ msgstr "" msgid "Tuesday" msgstr "" +msgid "Turn Off" +msgstr "" + +msgid "Turn On" +msgstr "" + msgid "Twitter" msgstr "" @@ -12493,6 +12531,9 @@ msgstr "" msgid "among other things" msgstr "" +msgid "assign yourself" +msgstr "" + msgid "attach a new file" msgstr "" From 2e630a18a3a895ef8e64891b67868b84651914c7 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Thu, 4 Jul 2019 10:53:09 +0000 Subject: [PATCH 056/195] Extract mounting of multiple boards switcher --- app/assets/javascripts/boards/index.js | 3 +++ .../javascripts/boards/mount_multiple_boards_switcher.js | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 app/assets/javascripts/boards/mount_multiple_boards_switcher.js diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index a020765f335..ae9537fcb86 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import Vue from 'vue'; +import mountMultipleBoardsSwitcher from 'ee_else_ce/boards/mount_multiple_boards_switcher'; import Flash from '~/flash'; import { __ } from '~/locale'; import './models/label'; @@ -278,4 +279,6 @@ export default () => { `, }); } + + mountMultipleBoardsSwitcher(); }; diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js new file mode 100644 index 00000000000..bdb14a7f2f2 --- /dev/null +++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js @@ -0,0 +1,2 @@ +// this will be moved from EE to CE as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/53811 +export default () => {}; From d505f2a8f04b249541423fa301a8c8c6ee3b2bb2 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Wed, 3 Jul 2019 15:18:01 +0200 Subject: [PATCH 057/195] Remove unused Gitaly feature flags When GitLab was migrating to Gitaly, feature flags were used. These are now out of use, and essentially a no-op. But they do make the output of chatops ugly and the feature table is selected in full by the application. --- ...90703130053_remove_gitaly_feature_flags.rb | 48 +++++++++++++++++++ db/schema.rb | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20190703130053_remove_gitaly_feature_flags.rb diff --git a/db/migrate/20190703130053_remove_gitaly_feature_flags.rb b/db/migrate/20190703130053_remove_gitaly_feature_flags.rb new file mode 100644 index 00000000000..13ac10a5e21 --- /dev/null +++ b/db/migrate/20190703130053_remove_gitaly_feature_flags.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +class RemoveGitalyFeatureFlags < ActiveRecord::Migration[5.1] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + FEATURES = %w[ + gitaly_batch_lfs_pointers gitaly_blame gitaly_blob_get_all_lfs_pointers gitaly_blob_get_new_lfs_pointers + gitaly_branch_names gitaly_branch_names_contains_sha gitaly_branches gitaly_bundle_to_disk + gitaly_calculate_checksum gitaly_can_be_merged gitaly_cherry_pick gitaly_commit_count + gitaly_commit_deltas gitaly_commit_languages gitaly_commit_messages gitaly_commit_patch + gitaly_commit_raw_diffs gitaly_commit_stats gitaly_commit_tree_entry gitaly_commits_between + gitaly_commits_by_message gitaly_conflicts_list_conflict_files gitaly_conflicts_resolve_conflicts gitaly_count_commits + gitaly_count_diverging_commits_no_max gitaly_create_branch gitaly_create_repo_from_bundle gitaly_create_repository + gitaly_delete_branch gitaly_delete_refs gitaly_delta_islands gitaly_deny_disk_acces + gitaly_diff_between gitaly_extract_commit_signature gitaly_fetch_ref gitaly_fetch_remote + gitaly_fetch_source_branch gitaly_filter_shas_with_signature gitaly_filter_shas_with_signatures gitaly_find_all_commits + gitaly_find_branch gitaly_find_commit gitaly_find_commits gitaly_find_ref_name + gitaly_force_push gitaly_fork_repository gitaly_garbage_collect gitaly_get_info_attributes + gitaly_git_blob_load_all_data gitaly_git_blob_raw gitaly_git_fsck gitaly_go-find-all-tags + gitaly_has_local_branches gitaly_import_repository gitaly_is_ancestor gitaly_last_commit_for_path + gitaly_license_short_name gitaly_list_blobs_by_sha_path gitaly_list_commits_by_oid gitaly_local_branches + gitaly_ls_files gitaly_merge_base gitaly_merged_branch_names gitaly_new_commits + gitaly_operation_user_add_tag gitaly_operation_user_commit_file gitaly_operation_user_commit_files gitaly_operation_user_create_branch + gitaly_operation_user_delete_branch gitaly_operation_user_delete_tag gitaly_operation_user_ff_branch gitaly_operation_user_merge_branch + gitaly_post_receive_pack gitaly_post_upload_pack gitaly_project_raw_show gitaly_raw_changes_between + gitaly_rebase gitaly_rebase_in_progress gitaly_ref_delete_refs gitaly_ref_exists + gitaly_ref_exists_branch gitaly_ref_exists_branches gitaly_ref_find_all_remote_branches gitaly_remote_add_remote + gitaly_remote_fetch_internal_remote gitaly_remote_remove_remote gitaly_remote_update_remote_mirror gitaly_remove_namespace + gitaly_repack_full gitaly_repack_incremental gitaly_repository_cleanup gitaly_repository_exists + gitaly_repository_size gitaly_root_ref gitaly_search_files_by_content gitaly_search_files_by_name + gitaly_squash gitaly_squash_in_progress gitaly_ssh_receive_pack gitaly_ssh_upload_pack + gitaly_submodule_url_for gitaly_tag_messages gitaly_tag_names gitaly_tag_names_contains_sha + gitaly_tags gitaly_tree_entries gitaly_wiki_delete_page gitaly_wiki_find_file + gitaly_wiki_find_page gitaly_wiki_get_all_pages gitaly_wiki_page_formatted_data gitaly_wiki_page_versions + gitaly_wiki_update_page gitaly_wiki_write_page gitaly_workhorse_archive gitaly_workhorse_raw_show + gitaly_workhorse_send_git_diff gitaly_workhorse_send_git_patch gitaly_write_config gitaly_write_ref + ] + + class Feature < ActiveRecord::Base + self.table_name = 'features' + end + + def up + Feature.where(key: FEATURES).delete_all + end +end diff --git a/db/schema.rb b/db/schema.rb index 32a25f643ce..00ca19c1e6e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20190628185004) do +ActiveRecord::Schema.define(version: 20190703130053) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" From bca7fa81bf438861346328e8b970d714c78ece3d Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Thu, 4 Jul 2019 13:44:52 +0000 Subject: [PATCH 058/195] Omit Gitaly path where not needed, add where required --- doc/administration/gitaly/index.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index a3cbc4272f0..7c7bb9045c7 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -247,8 +247,10 @@ gitlab: repositories: storages: default: + path: /mnt/gitlab/default/repositories gitaly_address: tcp://gitaly.internal:8075 storage1: + path: /mnt/gitlab/storage1/repositories gitaly_address: tcp://gitaly.internal:8075 gitaly: @@ -293,8 +295,8 @@ sum(rate(gitaly_connections_total[5m])) by (type) ```ruby # /etc/gitlab/gitlab.rb git_data_dirs({ - 'default' => { 'path' => '/mnt/gitlab/default', 'gitaly_address' => 'tls://gitaly.internal:9999' }, - 'storage1' => { 'path' => '/mnt/gitlab/storage1', 'gitaly_address' => 'tls://gitaly.internal:9999' }, + 'default' => { 'gitaly_address' => 'tls://gitaly.internal:9999' }, + 'storage1' => { 'gitaly_address' => 'tls://gitaly.internal:9999' }, }) gitlab_rails['gitaly_token'] = 'abc123secret' From 0a169cb80586c76517230e177af58fcdc012bca2 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Thu, 4 Jul 2019 14:08:48 +0000 Subject: [PATCH 059/195] Create EE-specific initNewListDropdown() function (cherry picked from commit 844666193f0740ab9c5f91d60d61acb6ce14cfaf) --- app/assets/javascripts/boards/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index ae9537fcb86..ca359da60d4 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -21,7 +21,7 @@ import modalMixin from './mixins/modal_mixins'; import './filters/due_date_filters'; import Board from './components/board'; import BoardSidebar from './components/board_sidebar'; -import initNewListDropdown from './components/new_list_dropdown'; +import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown'; import BoardAddIssuesModal from './components/modal/index.vue'; import '~/vue_shared/vue_resource_interceptor'; import { From 3c89dc99d9ac063feab29a25157280df53cd8f53 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Thu, 4 Jul 2019 14:11:03 +0000 Subject: [PATCH 060/195] Copy content from BoardService to boardsStore (cherry picked from commit 813299edd83ace98256b7fc9302f586f0dc2cabc) --- app/assets/javascripts/boards/index.js | 3 +- .../boards/services/board_service.js | 76 +++--------- .../javascripts/boards/stores/boards_store.js | 111 ++++++++++++++++++ .../boards/services/board_service_spec.js | 4 +- spec/javascripts/boards/mock_data.js | 5 +- 5 files changed, 138 insertions(+), 61 deletions(-) diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index ca359da60d4..23b107abefa 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -79,13 +79,14 @@ export default () => { }, }, created() { - gl.boardService = new BoardService({ + boardsStore.setEndpoints({ boardsEndpoint: this.boardsEndpoint, recentBoardsEndpoint: this.recentBoardsEndpoint, listsEndpoint: this.listsEndpoint, bulkUpdatePath: this.bulkUpdatePath, boardId: this.boardId, }); + gl.boardService = new BoardService(); boardsStore.rootPath = this.boardsEndpoint; eventHub.$on('updateTokens', this.updateTokens); diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index 7d463f17ab1..580d04a3649 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -1,106 +1,66 @@ -import axios from '../../lib/utils/axios_utils'; -import { mergeUrlParams } from '../../lib/utils/url_utility'; +/* eslint-disable class-methods-use-this */ + +import boardsStore from '~/boards/stores/boards_store'; export default class BoardService { - constructor({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) { - this.boardsEndpoint = boardsEndpoint; - this.boardId = boardId; - this.listsEndpoint = listsEndpoint; - this.listsEndpointGenerate = `${listsEndpoint}/generate.json`; - this.bulkUpdatePath = bulkUpdatePath; - this.recentBoardsEndpoint = `${recentBoardsEndpoint}.json`; - } - generateBoardsPath(id) { - return `${this.boardsEndpoint}${id ? `/${id}` : ''}.json`; + return boardsStore.generateBoardsPath(id); } generateIssuesPath(id) { - return `${this.listsEndpoint}${id ? `/${id}` : ''}/issues`; + return boardsStore.generateIssuesPath(id); } static generateIssuePath(boardId, id) { - return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${ - id ? `/${id}` : '' - }`; + return boardsStore.generateIssuePath(boardId, id); } all() { - return axios.get(this.listsEndpoint); + return boardsStore.all(); } generateDefaultLists() { - return axios.post(this.listsEndpointGenerate, {}); + return boardsStore.generateDefaultLists(); } createList(entityId, entityType) { - const list = { - [entityType]: entityId, - }; - - return axios.post(this.listsEndpoint, { - list, - }); + return boardsStore.createList(entityId, entityType); } updateList(id, position) { - return axios.put(`${this.listsEndpoint}/${id}`, { - list: { - position, - }, - }); + return boardsStore.updateList(id, position); } destroyList(id) { - return axios.delete(`${this.listsEndpoint}/${id}`); + return boardsStore.destroyList(id); } getIssuesForList(id, filter = {}) { - const data = { id }; - Object.keys(filter).forEach(key => { - data[key] = filter[key]; - }); - - return axios.get(mergeUrlParams(data, this.generateIssuesPath(id))); + return boardsStore.getIssuesForList(id, filter); } moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) { - return axios.put(BoardService.generateIssuePath(this.boardId, id), { - from_list_id: fromListId, - to_list_id: toListId, - move_before_id: moveBeforeId, - move_after_id: moveAfterId, - }); + return boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId); } newIssue(id, issue) { - return axios.post(this.generateIssuesPath(id), { - issue, - }); + return boardsStore.newIssue(id, issue); } getBacklog(data) { - return axios.get( - mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`), - ); + return boardsStore.getBacklog(data); } bulkUpdate(issueIds, extraData = {}) { - const data = { - update: Object.assign(extraData, { - issuable_ids: issueIds.join(','), - }), - }; - - return axios.post(this.bulkUpdatePath, data); + return boardsStore.bulkUpdate(issueIds, extraData); } static getIssueInfo(endpoint) { - return axios.get(endpoint); + return boardsStore.getIssueInfo(endpoint); } static toggleIssueSubscription(endpoint) { - return axios.post(endpoint); + return boardsStore.toggleIssueSubscription(endpoint); } } diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 4ba4cde6bae..b9cd4a143ef 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -8,6 +8,8 @@ import Cookies from 'js-cookie'; import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee'; import { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; +import axios from '~/lib/utils/axios_utils'; +import { mergeUrlParams } from '~/lib/utils/url_utility'; import eventHub from '../eventhub'; const boardsStore = { @@ -28,6 +30,7 @@ const boardsStore = { }, currentPage: '', reload: false, + endpoints: {}, }, detail: { issue: {}, @@ -36,6 +39,19 @@ const boardsStore = { issue: {}, list: {}, }, + + setEndpoints({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) { + const listsEndpointGenerate = `${listsEndpoint}/generate.json`; + this.state.endpoints = { + boardsEndpoint, + boardId, + listsEndpoint, + listsEndpointGenerate, + bulkUpdatePath, + recentBoardsEndpoint: `${recentBoardsEndpoint}.json`, + }; + }, + create() { this.state.lists = []; this.filter.path = getUrlParamsArray().join('&'); @@ -229,6 +245,101 @@ const boardsStore = { setTimeTrackingLimitToHours(limitToHours) { this.timeTracking.limitToHours = parseBoolean(limitToHours); }, + + generateBoardsPath(id) { + return `${this.state.endpoints.boardsEndpoint}${id ? `/${id}` : ''}.json`; + }, + + generateIssuesPath(id) { + return `${this.state.endpoints.listsEndpoint}${id ? `/${id}` : ''}/issues`; + }, + + generateIssuePath(boardId, id) { + return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${ + id ? `/${id}` : '' + }`; + }, + + all() { + return axios.get(this.state.endpoints.listsEndpoint); + }, + + generateDefaultLists() { + return axios.post(this.state.endpoints.listsEndpointGenerate, {}); + }, + + createList(entityId, entityType) { + const list = { + [entityType]: entityId, + }; + + return axios.post(this.state.endpoints.listsEndpoint, { + list, + }); + }, + + updateList(id, position) { + return axios.put(`${this.state.endpoints.listsEndpoint}/${id}`, { + list: { + position, + }, + }); + }, + + destroyList(id) { + return axios.delete(`${this.state.endpoints.listsEndpoint}/${id}`); + }, + + getIssuesForList(id, filter = {}) { + const data = { id }; + Object.keys(filter).forEach(key => { + data[key] = filter[key]; + }); + + return axios.get(mergeUrlParams(data, this.generateIssuesPath(id))); + }, + + moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) { + return axios.put(this.generateIssuePath(this.state.endpoints.boardId, id), { + from_list_id: fromListId, + to_list_id: toListId, + move_before_id: moveBeforeId, + move_after_id: moveAfterId, + }); + }, + + newIssue(id, issue) { + return axios.post(this.generateIssuesPath(id), { + issue, + }); + }, + + getBacklog(data) { + return axios.get( + mergeUrlParams( + data, + `${gon.relative_url_root}/-/boards/${this.state.endpoints.boardId}/issues.json`, + ), + ); + }, + + bulkUpdate(issueIds, extraData = {}) { + const data = { + update: Object.assign(extraData, { + issuable_ids: issueIds.join(','), + }), + }; + + return axios.post(this.state.endpoints.bulkUpdatePath, data); + }, + + getIssueInfo(endpoint) { + return axios.get(endpoint); + }, + + toggleIssueSubscription(endpoint) { + return axios.post(endpoint); + }, }; BoardsStoreEE.initEESpecific(boardsStore); diff --git a/spec/frontend/boards/services/board_service_spec.js b/spec/frontend/boards/services/board_service_spec.js index de9fc998360..a8a322e7237 100644 --- a/spec/frontend/boards/services/board_service_spec.js +++ b/spec/frontend/boards/services/board_service_spec.js @@ -2,6 +2,7 @@ import BoardService from '~/boards/services/board_service'; import { TEST_HOST } from 'helpers/test_constants'; import AxiosMockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; +import boardsStore from '~/boards/stores/boards_store'; describe('BoardService', () => { const dummyResponse = "without type checking this doesn't matter"; @@ -18,10 +19,11 @@ describe('BoardService', () => { beforeEach(() => { axiosMock = new AxiosMockAdapter(axios); - service = new BoardService({ + boardsStore.setEndpoints({ ...endpoints, boardId, }); + service = new BoardService(); }); describe('all', () => { diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js index 9854cf49e97..ea22ae5c4e7 100644 --- a/spec/javascripts/boards/mock_data.js +++ b/spec/javascripts/boards/mock_data.js @@ -1,4 +1,5 @@ import BoardService from '~/boards/services/board_service'; +import boardsStore from '~/boards/stores/boards_store'; export const boardObj = { id: 1, @@ -76,12 +77,14 @@ export const mockBoardService = (opts = {}) => { const bulkUpdatePath = opts.bulkUpdatePath || ''; const boardId = opts.boardId || '1'; - return new BoardService({ + boardsStore.setEndpoints({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, }); + + return new BoardService(); }; export const mockAssigneesList = [ From 6e560ea39bcd3f7ae43b5f76e9e4fb68e6708f74 Mon Sep 17 00:00:00 2001 From: GitalyBot Date: Thu, 4 Jul 2019 14:18:08 +0000 Subject: [PATCH 061/195] Upgrade Gitaly to v1.51.0 --- GITALY_SERVER_VERSION | 2 +- changelogs/unreleased/gitaly-version-v1.51.0.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/gitaly-version-v1.51.0.yml diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 7f3a46a841e..ba0a719118c 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.49.0 +1.51.0 diff --git a/changelogs/unreleased/gitaly-version-v1.51.0.yml b/changelogs/unreleased/gitaly-version-v1.51.0.yml new file mode 100644 index 00000000000..00d52a190f3 --- /dev/null +++ b/changelogs/unreleased/gitaly-version-v1.51.0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade to Gitaly v1.51.0 +merge_request: 30353 +author: +type: changed From 4c32824640693cb1c8f4f18f1d4264aee128198b Mon Sep 17 00:00:00 2001 From: Eugenia Grieff Date: Thu, 4 Jul 2019 14:24:56 +0000 Subject: [PATCH 062/195] Add documentation on quick actions for add/remove epic parent relations. --- doc/user/project/quick_actions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index 1281ba561b8..01371b43819 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -88,3 +88,5 @@ The following quick actions are applicable for epics threads and description: | `/relabel ~label1 ~label2` | Replace label | | /child_epic <&epic | group&epic | Epic URL> | Adds child epic to epic ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-ee/issues/7330)) | | /remove_child_epic <&epic | group&epic | Epic URL> | Removes child epic from epic ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-ee/issues/7330)) | +| /parent_epic <&epic | group&epic | Epic URL> | Sets parent epic to epic ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-ee/issues/10556)) | +| /remove_parent_epic | Removes parent epic from epic ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-ee/issues/10556)) | From 4e68cf9457f7eefe3fb2cdbbc0f25901edd87bb6 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 4 Jul 2019 16:55:06 +0200 Subject: [PATCH 063/195] Definition of done includes deployed change --- doc/development/contributing/merge_request_workflow.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md index ce5d9786e6e..3f61ad7cb13 100644 --- a/doc/development/contributing/merge_request_workflow.md +++ b/doc/development/contributing/merge_request_workflow.md @@ -193,6 +193,7 @@ requirements. 1. [Changelog entry added](../changelog.md), if necessary. 1. Reviewed by relevant (UX/FE/BE/tech writing) reviewers and all concerns are addressed. 1. Merged by a project maintainer. +1. Confirmed to be working in the [Canary stage](https://about.gitlab.com/handbook/engineering/#canary-testing) or on GitLab.com once the contribution is deployed. 1. Added to the [release post](https://about.gitlab.com/handbook/marketing/blog/release-posts/), if relevant. 1. Added to [the website](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/features.yml), if relevant. From 133b9f41088288a53d0b7cca0e9d5bab4a633567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Thu, 4 Jul 2019 15:46:46 +0200 Subject: [PATCH 064/195] Fix race in forbid_sidekiq_in_transactions.rb Current code uses module attribute which stores value global instead of locally (thread locally). This results in concurrent accesses to overwrite the each other values --- .../fix-sidekiq-transaction-check-race.yml | 5 +++++ .../initializers/forbid_sidekiq_in_transactions.rb | 13 +++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 changelogs/unreleased/fix-sidekiq-transaction-check-race.yml diff --git a/changelogs/unreleased/fix-sidekiq-transaction-check-race.yml b/changelogs/unreleased/fix-sidekiq-transaction-check-race.yml new file mode 100644 index 00000000000..89ae4abfe11 --- /dev/null +++ b/changelogs/unreleased/fix-sidekiq-transaction-check-race.yml @@ -0,0 +1,5 @@ +--- +title: Fix race in forbid_sidekiq_in_transactions.rb +merge_request: 30359 +author: +type: fixed diff --git a/config/initializers/forbid_sidekiq_in_transactions.rb b/config/initializers/forbid_sidekiq_in_transactions.rb index a69f1ba090e..bb190af60b5 100644 --- a/config/initializers/forbid_sidekiq_in_transactions.rb +++ b/config/initializers/forbid_sidekiq_in_transactions.rb @@ -2,15 +2,16 @@ module Sidekiq module Worker EnqueueFromTransactionError = Class.new(StandardError) - mattr_accessor :skip_transaction_check - self.skip_transaction_check = false - def self.skipping_transaction_check(&block) - skip_transaction_check = self.skip_transaction_check - self.skip_transaction_check = true + previous_skip_transaction_check = self.skip_transaction_check + Thread.current[:sidekiq_worker_skip_transaction_check] = true yield ensure - self.skip_transaction_check = skip_transaction_check + Thread.current[:sidekiq_worker_skip_transaction_check] = previous_skip_transaction_check + end + + def self.skip_transaction_check + Thread.current[:sidekiq_worker_skip_transaction_check] end module ClassMethods From c433082f89f65f944262040454e53ca460ec08be Mon Sep 17 00:00:00 2001 From: Christie Lenneville Date: Thu, 4 Jul 2019 15:45:54 +0000 Subject: [PATCH 065/195] Change 'Todo' to 'To Do' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently, we label items to be done as "Todo." This is grammatically incorrect and (therefore) confusing—especially to our Spanish-speaking users for whom "todo" has a specific and unrelated meaning. We should use "To Do" and always use it as singular (not "To Dos"). Updates to wording in a few places per MR (ee) discussion Updating locale/gitlab.pot Updates to wording in a few places per MR (ee) discussion Updating locale/gitlab.pot --- .../pages/dashboard/todos/index/todos.js | 4 +- .../sidebar/components/todo_toggle/todo.vue | 4 +- app/controllers/dashboard/todos_controller.rb | 4 +- app/helpers/issuables_helper.rb | 4 +- app/helpers/preferences_helper.rb | 2 +- app/views/dashboard/todos/_todo.html.haml | 2 +- app/views/dashboard/todos/index.html.haml | 24 +-- app/views/layouts/header/_default.html.haml | 2 +- app/views/shared/issuable/_sidebar.html.haml | 2 +- changelogs/unreleased/update-todo-in-ui.yml | 5 + config/no_todos_messages.yml | 8 +- .../project/issues/issue_data_and_actions.md | 19 ++- doc/user/project/quick_actions.md | 8 +- doc/user/search/index.md | 6 +- doc/workflow/todos.md | 138 ++++++++++-------- lib/gitlab/quick_actions/issuable_actions.rb | 4 +- locale/gitlab.pot | 40 ++--- spec/features/dashboard/shortcuts_spec.rb | 2 +- spec/features/dashboard/todos/todos_spec.rb | 14 +- spec/features/issues/todo_spec.rb | 8 +- spec/helpers/preferences_helper_spec.rb | 2 +- .../collapsed_sidebar_todo_spec.js | 16 +- spec/javascripts/sidebar/todo_spec.js | 8 +- 23 files changed, 181 insertions(+), 145 deletions(-) create mode 100644 changelogs/unreleased/update-todo-in-ui.yml diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js index 1b56b97f751..d51d411f3c6 100644 --- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js @@ -82,7 +82,7 @@ export default class Todos { }) .catch(() => { this.updateRowState(target, true); - return flash(__('Error updating todo status.')); + return flash(__('Error updating status of to-do item.')); }); } @@ -124,7 +124,7 @@ export default class Todos { this.updateAllState(target, data); this.updateBadges(data); }) - .catch(() => flash(__('Error updating status for all todos.'))); + .catch(() => flash(__('Error updating status for all to-do items.'))); } updateAllState(target, data) { diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue index 57125c78cf6..e6f2fe2b5fc 100644 --- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue +++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue @@ -5,8 +5,8 @@ import { GlLoadingIcon } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; -const MARK_TEXT = __('Mark todo as done'); -const TODO_TEXT = __('Add todo'); +const MARK_TEXT = __('Mark as done'); +const TODO_TEXT = __('Add a To Do'); export default { directives: { diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index 27980466a42..8f6fcb362d2 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -22,7 +22,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController format.html do redirect_to dashboard_todos_path, status: 302, - notice: _('Todo was successfully marked as done.') + notice: _('To-do item successfully marked as done.') end format.js { head :ok } format.json { render json: todos_counts } @@ -33,7 +33,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController updated_ids = TodoService.new.mark_todos_as_done(@todos, current_user) respond_to do |format| - format.html { redirect_to dashboard_todos_path, status: 302, notice: _('All todos were marked as done.') } + format.html { redirect_to dashboard_todos_path, status: 302, notice: _('Everything on your to-do list is marked as done.') } format.js { head :ok } format.json { render json: todos_counts.merge(updated_ids: updated_ids) } end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index cd2669ef6ad..67685ba4e1d 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -390,8 +390,8 @@ module IssuablesHelper def issuable_todo_button_data(issuable, is_collapsed) { - todo_text: _('Add todo'), - mark_text: _('Mark todo as done'), + todo_text: _('Add a To Do'), + mark_text: _('Mark as done'), todo_icon: sprite_icon('todo-add'), mark_icon: sprite_icon('todo-done', css_class: 'todo-undone'), issuable_id: issuable[:id], diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 766508b6609..3672d8b1b03 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -16,7 +16,7 @@ module PreferencesHelper project_activity: _("Your Projects' Activity"), starred_project_activity: _("Starred Projects' Activity"), groups: _("Your Groups"), - todos: _("Your Todos"), + todos: _("Your To-Do List"), issues: _("Assigned Issues"), merge_requests: _("Assigned Merge Requests"), operations: _("Operations Dashboard") diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index db6e40a6fd0..8cdfc7369a0 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -49,5 +49,5 @@ - else .todo-actions = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-add-todo', data: { href: restore_dashboard_todo_path(todo) } do - Add todo + Add a To Do = icon('spinner spin') diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 8212fb8bb33..731e763f2be 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -1,11 +1,11 @@ - @hide_top_links = true -- page_title "Todos" -- header_title "Todos", dashboard_todos_path +- page_title "To-Do List" +- header_title "To-Do List", dashboard_todos_path = render_dashboard_gold_trial(current_user) .page-title-holder.d-flex.align-items-center - %h1.page-title= _('Todos') + %h1.page-title= _('To-Do List') - if current_user.todos.any? .top-area @@ -13,7 +13,7 @@ %li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }> = link_to todos_filter_path(state: 'pending') do %span - Todos + To Do %span.badge.badge-pill = number_with_delimiter(todos_pending_count) %li.todos-done{ class: active_when(params[:state] == 'done') }> @@ -102,24 +102,24 @@ %p Are you looking for things to do? Take a look at = succeed "," do - = link_to "the opened issues", issues_dashboard_path + = link_to "open issues", issues_dashboard_path contribute to - = link_to "merge requests", merge_requests_dashboard_path - or mention someone in a comment to assign a new todo automatically. + = link_to "a merge request\,", merge_requests_dashboard_path + or mention someone in a comment to automatically assign them a new to-do item. - else %h4.text-center - There are no todos to show. + Nothing is on your to-do list. Nice work! - else .todos-empty .todos-empty-hero.svg-content = image_tag 'illustrations/todos_empty.svg' .todos-empty-content %h4 - Todos let you see what you should do next + Your To-Do List shows what to work on next %p - When an issue or merge request is assigned to you, or when you + When an issue or merge request is assigned to you, or when you receive a %strong @mention - in a comment, this will trigger a new item in your todo list, automatically. + in a comment, this automatically triggers a new item in your To-Do List. %p - You will always know what to work on next. + It's how you always know what to work on next. diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index f8b7d0c530a..f9ee6f42e23 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -53,7 +53,7 @@ = number_with_delimiter(merge_requests_count) - if header_link?(:todos) = nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do - = link_to dashboard_todos_path, title: _('Todos'), aria: { label: _('Todos') }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = link_to dashboard_todos_path, title: _('To-Do List'), aria: { label: _('To-Do List') }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('todo-done', size: 16) %span.badge.badge-pill.todos-count{ class: ('hidden' if todos_pending_count.zero?) } = todos_count_format(todos_pending_count) diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index e87e560266f..b4f8377c008 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -10,7 +10,7 @@ .block.issuable-sidebar-header - if signed_in %span.issuable-header-text.hide-collapsed.float-left - = _('Todo') + = _('To Do') %a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } } = sidebar_gutter_toggle_icon - if signed_in diff --git a/changelogs/unreleased/update-todo-in-ui.yml b/changelogs/unreleased/update-todo-in-ui.yml new file mode 100644 index 00000000000..dddcf0f3983 --- /dev/null +++ b/changelogs/unreleased/update-todo-in-ui.yml @@ -0,0 +1,5 @@ +--- +title: Changes "Todo" to "To Do" in the UI for clarity +merge_request: 28844 +author: +type: other diff --git a/config/no_todos_messages.yml b/config/no_todos_messages.yml index da721a9b6e6..d2076f235fd 100644 --- a/config/no_todos_messages.yml +++ b/config/no_todos_messages.yml @@ -4,8 +4,8 @@ # If you come up with a fun one, please feel free to contribute it to GitLab! # https://about.gitlab.com/contributing/ --- -- Good job! Looks like you don't have any todos left -- Isn't an empty todo list beautiful? +- Good job! Looks like you don't have anything left on your To-Do List +- Isn't an empty To-Do List beautiful? - Give yourself a pat on the back! -- Nothing left to do, high five! -- Henceforth you shall be known as "Todo Destroyer" +- Nothing left to do. High five! +- Henceforth, you shall be known as "To-Do Destroyer" diff --git a/doc/user/project/issues/issue_data_and_actions.md b/doc/user/project/issues/issue_data_and_actions.md index 9898cd6cf15..b34263f0eec 100644 --- a/doc/user/project/issues/issue_data_and_actions.md +++ b/doc/user/project/issues/issue_data_and_actions.md @@ -39,11 +39,14 @@ after it is closed. ![Report Abuse](img/report-abuse.png) -#### 2. Todos +#### 2. To Do -You can click **add todo** to add the issue to your [GitLab Todo](../../../workflow/todos.md) -list. If it is already on your todo list, the buttom will show **mark todo as done**, -which you can click to mark that issue as done (which will be reflected in the Todo list). +You can add issues to and remove issues from your [GitLab To-Do List](../../../workflow/todos.md). + +The button to do this has a different label depending on whether the issue is already on your To-Do List or not. If the issue is: + +- Already on your To-Do List: The button is labeled **Mark as done**. Click the button to remove the issue from your To-Do List. +- Not on your To-Do List: The button is labelled **Add a To Do**. Click the button to add the issue to your To-Do List. #### 3. Assignee @@ -206,6 +209,14 @@ You can filter what is displayed in the issue history by clicking on **Show all and selecting either **Show comments only**, which only shows discussions and hides updates to the issue, or **Show history only**, which hides discussions and only shows updates. +- You can mention a user or a group present in your GitLab instance with + `@username` or `@groupname` and they will be notified via To-Do items + and email, unless they have [disabled all notifications](#13-notifications) + in their profile settings. +- Mentions for yourself (the current logged in user), will be highlighted + in a different color, allowing you to easily see which comments involve you, + helping you focus on them quickly. + ![Show all activity](img/show-all-activity.png) #### 22. Create Merge Request diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index 01371b43819..d20b44d4b92 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -16,8 +16,8 @@ discussions, and descriptions: |:---------------------------|:------------------------------ |:------|:--------------| | `/tableflip ` | Append the comment with `(╯°□°)╯︵ ┻━┻` | ✓ | ✓ | | `/shrug ` | Append the comment with `¯\_(ツ)_/¯` | ✓ | ✓ | -| `/todo` | Add a todo | ✓ | ✓ | -| `/done` | Mark todo as done | ✓ | ✓ | +| `/todo` | Add a To Do | ✓ | ✓ | +| `/done` | Mark To Do as done | ✓ | ✓ | | `/subscribe` | Subscribe | ✓ | ✓ | | `/unsubscribe` | Unsubscribe | ✓ | ✓ | | `/close` | Close | ✓ | ✓ | @@ -75,8 +75,8 @@ The following quick actions are applicable for epics threads and description: |:---------------------------|:----------------------------------------| | `/tableflip ` | Append the comment with `(╯°□°)╯︵ ┻━┻` | | `/shrug ` | Append the comment with `¯\_(ツ)_/¯` | -| `/todo` | Add a todo | -| `/done` | Mark todo as done | +| `/todo` | Add a To Do | +| `/done` | Mark To Do as done | | `/subscribe` | Subscribe | | `/unsubscribe` | Unsubscribe | | `/close` | Close | diff --git a/doc/user/search/index.md b/doc/user/search/index.md index bb6c48471c7..d6e2f036cf2 100644 --- a/doc/user/search/index.md +++ b/doc/user/search/index.md @@ -97,10 +97,10 @@ quickly access issues and merge requests created or assigned to you within that ![search per project - shortcut](img/project_search.png) -## Todos +## To-Do List -Your [todos](../../workflow/todos.md#gitlab-todos) can be searched by "to do" and "done". -You can [filter](../../workflow/todos.md#filtering-your-todos) them per project, +Your [To-Do List](../../workflow/todos.md#gitlab-to-do-list) can be searched by "to do" and "done". +You can [filter](../../workflow/todos.md#filtering-your-to-do-list) them per project, author, type, and action. Also, you can sort them by [**Label priority**](../../user/project/labels.md#label-priority), **Last created** and **Oldest created**. diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md index 3eac79427cf..f501a222cd5 100644 --- a/doc/workflow/todos.md +++ b/doc/workflow/todos.md @@ -1,50 +1,57 @@ -# GitLab Todos +# GitLab To-Do List > [Introduced][ce-2817] in GitLab 8.5. When you log into GitLab, you normally want to see where you should spend your -time and take some action, or what you need to keep an eye on. All without the -mess of a huge pile of e-mail notifications. GitLab is where you do your work, -so being able to get started quickly is very important. +time, take some action, or know what you need to keep an eye on without +a huge pile of e-mail notifications. GitLab is where you do your work, +so being able to get started quickly is important. -Todos is a chronological list of to-dos that are waiting for your input, all +Your To-Do List offers a chronological list of items that are waiting for your input, all in a simple dashboard. -![Todos screenshot showing a list of items to check on](img/todos_index.png) +![To Do screenshot showing a list of items to check on](img/todos_index.png) --- -You can quickly access the Todos dashboard using the checkmark icon next to the -search bar in the upper right corner. The number in blue is the number of Todos -you still have open if the count is < 100, else it's 99+. The exact number -will still be shown in the body of the _To do_ tab. +You can quickly access your To-Do List by clicking the checkmark icon next to the +search bar in the top navigation. If the count is: -![Todos icon](img/todos_icon.png) +- Less than 100, the number in blue is the number of To-Do items. +- 100 or more, the number displays as 99+. The exact number displays + on the To-Do List. +you still have open. Otherwise, the number displays as 99+. The exact number +displays on the To-Do List. -## What triggers a Todo +![To Do icon](img/todos_icon.png) -A Todo appears in your Todos dashboard when: +## What triggers a To Do -- an issue or merge request is assigned to you -- you are `@mentioned` in the description or in a comment of an issue, merge request, or epic **[ULTIMATE]** -- you are `@mentioned` in a comment on a commit, -- a job in the CI pipeline running for your merge request failed, but this - job is not allowed to fail. -- an open merge request becomes unmergeable due to conflict, and you are either: - - the author, or - - have set it to automatically merge once pipeline succeeds. +A To Do displays on your To-Do List when: -Todo triggers are not affected by [GitLab Notification Email settings](notifications.md). +- An issue or merge request is assigned to you +- You are `@mentioned` in the description or comment of an: + - Issue + - Merge Request + - Epic **[ULTIMATE]** +- You are `@mentioned` in a comment on a commit +- A job in the CI pipeline running for your merge request failed, but this + job is not allowed to fail +- An open merge request becomes unmergeable due to conflict, and you are either: + - The author + - Have set it to automatically merge once the pipeline succeeds + +To-do triggers are not affected by [GitLab Notification Email settings](notifications.md). NOTE: **Note:** -When an user no longer has access to a resource related to a Todo like an issue, merge request, project or group the related Todos, for security reasons, gets deleted within the next hour. The delete is delayed to prevent data loss in case user got their access revoked by mistake. +When a user no longer has access to a resource related to a To Do (like an issue, merge request, project, or group) the related To-Do items are deleted within the next hour for security reasons. The delete is delayed to prevent data loss, in case the user's access was revoked by mistake. -### Directly addressed Todos +### Directly addressing a To Do > [Introduced][ce-7926] in GitLab 9.0. -If you are mentioned at the start of a line, the todo you receive will be listed -as 'directly addressed'. For instance, in this comment: +If you are mentioned at the start of a line, the To Do you receive will be listed +as 'directly addressed'. For example, in this comment: ```markdown @alice What do you think? cc: @bob @@ -58,67 +65,80 @@ as 'directly addressed'. For instance, in this comment: @erin @frank thank you! ``` -The people receiving directly addressed todos are `@alice`, `@erin`, and -`@frank`. Directly addressed todos only differ from mention todos in their type, -for filtering; otherwise, they appear as normal. +The people receiving directly addressed To-Do items are `@alice`, `@erin`, and +`@frank`. Directly addressed To-Do items only differ from mentions in their type +for filtering purposes; otherwise, they appear as normal. -### Manually creating a Todo +### Manually creating a To Do -You can also add an issue, merge request or epic to your Todos dashboard by clicking -the "Add todo" button in the sidebar of the issue, merge request, or epic **[ULTIMATE]**. +You can also add the following to your To-Do List by clicking the **Add a To Do** button on an: -![Adding a Todo from the issuable sidebar](img/todos_add_todo_sidebar.png) +- Issue +- Merge Request +- Epic **[ULTIMATE]** -## Marking a Todo as done +![Adding a To Do from the issuable sidebar](img/todos_add_todo_sidebar.png) -Any action to the corresponding issue, merge request or epic **[ULTIMATE]** will mark your Todo as -**Done**. Actions that dismiss Todos include: +## Marking a To Do as done -- changing the assignee -- changing the milestone -- adding/removing a label -- commenting on the issue +Any action to the following will mark the corresponding To Do as done: + +- Issue +- Merge Request +- Epic **[ULTIMATE]** + +Actions that dismiss To-Do items include: + +- Changing the assignee +- Changing the milestone +- Adding/removing a label +- Commenting on the issue --- -Todos are personal, and they're only marked as done if the action is coming from -you. If you close the issue or merge request, your Todo will automatically -be marked as done. +Your To-Do List is personal, and items are only marked as done if the action comes from +you. If you close the issue or merge request, your To Do is automatically +marked as done. -If someone else closes, merges, or takes action on the issue, epic or merge -request, your Todo will remain pending. This prevents other users from closing issues without you being notified. +To prevent other users from closing issues without you being notified, if someone else closes, merges, or takes action on the any of the following, your To Do will remain pending: -There is just one Todo per issue, epic or merge request, so mentioning a user a -hundred times in an issue will only trigger one Todo. +- Issue +- Merge request +- Epic **[ULTIMATE]** + +There is just one To Do for each of these, so mentioning a user a hundred times in an issue will only trigger one To Do. --- -If no action is needed, you can manually mark the Todo as done by clicking the -corresponding **Done** button, and it will disappear from your Todo list. +If no action is needed, you can manually mark the To Do as done by clicking the +corresponding **Done** button, and it will disappear from your To-Do List. -![A Todo in the Todos dashboard](img/todo_list_item.png) +![A To Do in the To-Do List](img/todo_list_item.png) -A Todo can also be marked as done from the issue, merge request or epic sidebar using -the "Mark todo as done" button. +You can also mark a To Do as done by clicking the **Mark as done** button in the sidebar of the following: -![Mark todo as done from the issuable sidebar](img/todos_mark_done_sidebar.png) +- Issue +- Merge Request +- Epic **[ULTIMATE]** -You can mark all your Todos as done at once by clicking on the **Mark all as +![Mark as done from the issuable sidebar](img/todos_mark_done_sidebar.png) + +You can mark all your To-Do items as done at once by clicking the **Mark all as done** button. -## Filtering your Todos +## Filtering your To-Do List -There are four kinds of filters you can use on your Todos dashboard. +There are four kinds of filters you can use on your To-Do List. | Filter | Description | | ------- | ----------- | | Project | Filter by project | | Group | Filter by group | -| Author | Filter by the author that triggered the Todo | +| Author | Filter by the author that triggered the To Do | | Type | Filter by issue, merge request, or epic **[ULTIMATE]** | -| Action | Filter by the action that triggered the Todo | +| Action | Filter by the action that triggered the To Do | -You can also filter by more than one of these at the same time. The possible Actions are `Any Action`, `Assigned`, `Mentioned`, `Added`, `Pipelines`, and `Directly Addressed`, [as described above](#what-triggers-a-todo). +You can also filter by more than one of these at the same time. The possible Actions are `Any Action`, `Assigned`, `Mentioned`, `Added`, `Pipelines`, and `Directly Addressed`, [as described above](#what-triggers-a-to-do). [ce-2817]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2817 [ce-7926]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7926 diff --git a/lib/gitlab/quick_actions/issuable_actions.rb b/lib/gitlab/quick_actions/issuable_actions.rb index 572c55efcc2..f7f89d4e897 100644 --- a/lib/gitlab/quick_actions/issuable_actions.rb +++ b/lib/gitlab/quick_actions/issuable_actions.rb @@ -146,8 +146,8 @@ module Gitlab @updates[:todo_event] = 'add' end - desc _('Mark todo as done') - explanation _('Marks todo as done.') + desc _('Mark to do as done') + explanation _('Marks to do as done.') types Issuable condition do quick_action_target.persisted? && diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ed6245bf242..01bf4949213 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -626,6 +626,9 @@ msgstr "" msgid "Add a Grafana button in the admin sidebar, monitoring section, to access a variety of statistics on the health and performance of GitLab." msgstr "" +msgid "Add a To Do" +msgstr "" + msgid "Add a bullet list" msgstr "" @@ -704,9 +707,6 @@ msgstr "" msgid "Add to review" msgstr "" -msgid "Add todo" -msgstr "" - msgid "Add user(s) to the group:" msgstr "" @@ -893,9 +893,6 @@ msgstr "" msgid "All projects" msgstr "" -msgid "All todos were marked as done." -msgstr "" - msgid "All users" msgstr "" @@ -4288,10 +4285,10 @@ msgstr "" msgid "Error updating %{issuableType}" msgstr "" -msgid "Error updating status for all todos." +msgid "Error updating status for all to-do items." msgstr "" -msgid "Error updating todo status." +msgid "Error updating status of to-do item." msgstr "" msgid "Error uploading file" @@ -4390,6 +4387,9 @@ msgstr "" msgid "Everyone can contribute" msgstr "" +msgid "Everything on your to-do list is marked as done." +msgstr "" + msgid "Everything you need to create a GitLab Pages site using GitBook." msgstr "" @@ -6249,6 +6249,9 @@ msgstr "" msgid "March" msgstr "" +msgid "Mark as done" +msgstr "" + msgid "Mark as resolved" msgstr "" @@ -6258,7 +6261,7 @@ msgstr "" msgid "Mark this issue as a duplicate of another issue" msgstr "" -msgid "Mark todo as done" +msgid "Mark to do as done" msgstr "" msgid "Markdown" @@ -6273,7 +6276,7 @@ msgstr "" msgid "Marks this issue as a duplicate of %{duplicate_reference}." msgstr "" -msgid "Marks todo as done." +msgid "Marks to do as done." msgstr "" msgid "Max access level" @@ -11173,18 +11176,15 @@ msgstr "" msgid "To widen your search, change or remove filters above" msgstr "" +msgid "To-Do List" +msgstr "" + +msgid "To-do item successfully marked as done." +msgstr "" + msgid "Today" msgstr "" -msgid "Todo" -msgstr "" - -msgid "Todo was successfully marked as done." -msgstr "" - -msgid "Todos" -msgstr "" - msgid "Toggle Sidebar" msgstr "" @@ -12429,7 +12429,7 @@ msgstr "" msgid "Your SSH keys (%{count})" msgstr "" -msgid "Your Todos" +msgid "Your To-Do List" msgstr "" msgid "Your U2F device did not send a valid JSON response." diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb index 55f5ff04d01..254bb12573c 100644 --- a/spec/features/dashboard/shortcuts_spec.rb +++ b/spec/features/dashboard/shortcuts_spec.rb @@ -22,7 +22,7 @@ describe 'Dashboard shortcuts', :js do find('body').send_keys([:shift, 'T']) - check_page_title('Todos') + check_page_title('To-Do List') find('body').send_keys([:shift, 'P']) diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb index d58e3b2841e..c48229fc0a0 100644 --- a/spec/features/dashboard/todos/todos_spec.rb +++ b/spec/features/dashboard/todos/todos_spec.rb @@ -13,7 +13,7 @@ describe 'Dashboard Todos' do end it 'shows "All done" message' do - expect(page).to have_content 'Todos let you see what you should do next' + expect(page).to have_content 'Your To-Do List shows what to work on next' end end @@ -72,7 +72,7 @@ describe 'Dashboard Todos' do end it 'updates todo count' do - expect(page).to have_content 'Todos 0' + expect(page).to have_content 'To Do 0' expect(page).to have_content 'Done 1' end @@ -101,7 +101,7 @@ describe 'Dashboard Todos' do end it 'updates todo count' do - expect(page).to have_content 'Todos 1' + expect(page).to have_content 'To Do 1' expect(page).to have_content 'Done 0' end end @@ -211,7 +211,7 @@ describe 'Dashboard Todos' do describe 'restoring the todo' do before do within first('.todo') do - click_link 'Add todo' + click_link 'Add a To Do' end end @@ -220,7 +220,7 @@ describe 'Dashboard Todos' do end it 'updates todo count' do - expect(page).to have_content 'Todos 1' + expect(page).to have_content 'To Do 1' expect(page).to have_content 'Done 0' end end @@ -276,7 +276,7 @@ describe 'Dashboard Todos' do end it 'shows "All done" message!' do - expect(page).to have_content 'Todos 0' + expect(page).to have_content 'To Do 0' expect(page).to have_content "You're all done!" expect(page).not_to have_selector('.gl-pagination') end @@ -303,7 +303,7 @@ describe 'Dashboard Todos' do it 'updates todo count' do mark_all_and_undo - expect(page).to have_content 'Todos 2' + expect(page).to have_content 'To Do 2' expect(page).to have_content 'Done 0' end diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb index 0114178b9be..07ae159eef4 100644 --- a/spec/features/issues/todo_spec.rb +++ b/spec/features/issues/todo_spec.rb @@ -13,8 +13,8 @@ describe 'Manually create a todo item from issue', :js do it 'creates todo when clicking button' do page.within '.issuable-sidebar' do - click_button 'Add todo' - expect(page).to have_content 'Mark todo as done' + click_button 'Add a To Do' + expect(page).to have_content 'Mark as done' end page.within '.header-content .todos-count' do @@ -30,8 +30,8 @@ describe 'Manually create a todo item from issue', :js do it 'marks a todo as done' do page.within '.issuable-sidebar' do - click_button 'Add todo' - click_button 'Mark todo as done' + click_button 'Add a To Do' + click_button 'Mark as done' end expect(page).to have_selector('.todos-count', visible: false) diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index db0d45c3692..554c08add2d 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -28,7 +28,7 @@ describe PreferencesHelper do ["Your Projects' Activity", 'project_activity'], ["Starred Projects' Activity", 'starred_project_activity'], ["Your Groups", 'groups'], - ["Your Todos", 'todos'], + ["Your To-Do List", 'todos'], ["Assigned Issues", 'issues'], ["Assigned Merge Requests", 'merge_requests'] ] diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js index bb90e53e525..f75d63c8f57 100644 --- a/spec/javascripts/collapsed_sidebar_todo_spec.js +++ b/spec/javascripts/collapsed_sidebar_todo_spec.js @@ -58,7 +58,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { it('sets default tooltip title', () => { expect( document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('title'), - ).toBe('Add todo'); + ).toBe('Add a To Do'); }); it('toggle todo state', done => { @@ -85,7 +85,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { setTimeout(() => { expect( document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(), - ).toBe('Mark todo as done'); + ).toBe('Mark as done'); done(); }); @@ -99,7 +99,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { document .querySelector('.js-issuable-todo.sidebar-collapsed-icon') .getAttribute('data-original-title'), - ).toBe('Mark todo as done'); + ).toBe('Mark as done'); done(); }); @@ -124,13 +124,13 @@ describe('Issuable right sidebar collapsed todo toggle', () => { expect( document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(), - ).toBe('Add todo'); + ).toBe('Add a To Do'); }) .then(done) .catch(done.fail); }); - it('updates aria-label to mark todo as done', done => { + it('updates aria-label to Mark as done', done => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); setTimeout(() => { @@ -138,7 +138,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { document .querySelector('.js-issuable-todo.sidebar-collapsed-icon') .getAttribute('aria-label'), - ).toBe('Mark todo as done'); + ).toBe('Mark as done'); done(); }); @@ -153,7 +153,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { document .querySelector('.js-issuable-todo.sidebar-collapsed-icon') .getAttribute('aria-label'), - ).toBe('Mark todo as done'); + ).toBe('Mark as done'); document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); }) @@ -163,7 +163,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { document .querySelector('.js-issuable-todo.sidebar-collapsed-icon') .getAttribute('aria-label'), - ).toBe('Add todo'); + ).toBe('Add a To Do'); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/sidebar/todo_spec.js b/spec/javascripts/sidebar/todo_spec.js index f46ea5a0499..e7abd19c865 100644 --- a/spec/javascripts/sidebar/todo_spec.js +++ b/spec/javascripts/sidebar/todo_spec.js @@ -53,14 +53,14 @@ describe('SidebarTodo', () => { describe('buttonLabel', () => { it('returns todo button text for marking todo as done when `isTodo` prop is `true`', () => { - expect(vm.buttonLabel).toBe('Mark todo as done'); + expect(vm.buttonLabel).toBe('Mark as done'); }); it('returns todo button text for add todo when `isTodo` prop is `false`', done => { vm.isTodo = false; Vue.nextTick() .then(() => { - expect(vm.buttonLabel).toBe('Add todo'); + expect(vm.buttonLabel).toBe('Add a To Do'); }) .then(done) .catch(done.fail); @@ -131,14 +131,14 @@ describe('SidebarTodo', () => { }); it('check button label computed property', () => { - expect(vm.buttonLabel).toEqual('Mark todo as done'); + expect(vm.buttonLabel).toEqual('Mark as done'); }); it('renders button label element when `collapsed` prop is `false`', () => { const buttonLabelEl = vm.$el.querySelector('span.issuable-todo-inner'); expect(buttonLabelEl).not.toBeNull(); - expect(buttonLabelEl.innerText.trim()).toBe('Mark todo as done'); + expect(buttonLabelEl.innerText.trim()).toBe('Mark as done'); }); it('renders button icon when `collapsed` prop is `true`', done => { From d132f73d42aec530c78680f53bf8a612bac61a3b Mon Sep 17 00:00:00 2001 From: Mayra Cabrera Date: Thu, 4 Jul 2019 15:52:02 +0000 Subject: [PATCH 066/195] Implements lease_release on NamespaceAggregation Sets lease_release? to false to prevent the job to be re-executed more often than lease timeout Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/64079 --- app/models/namespace/aggregation_schedule.rb | 8 ++++ .../namespace/aggregation_schedule_spec.rb | 31 ++++++++++++- .../schedule_aggregation_worker_spec.rb | 43 ++++++++++++------- 3 files changed, 65 insertions(+), 17 deletions(-) diff --git a/app/models/namespace/aggregation_schedule.rb b/app/models/namespace/aggregation_schedule.rb index 355593597c6..0bef352cf24 100644 --- a/app/models/namespace/aggregation_schedule.rb +++ b/app/models/namespace/aggregation_schedule.rb @@ -44,4 +44,12 @@ class Namespace::AggregationSchedule < ApplicationRecord def lease_key "namespace:namespaces_root_statistics:#{namespace_id}" end + + # Used by ExclusiveLeaseGuard + # Overriding value as we never release the lease + # before the timeout in order to prevent multiple + # RootStatisticsWorker to start in a short span of time + def lease_release? + false + end end diff --git a/spec/models/namespace/aggregation_schedule_spec.rb b/spec/models/namespace/aggregation_schedule_spec.rb index 8ed0248e1b2..0f1283717e0 100644 --- a/spec/models/namespace/aggregation_schedule_spec.rb +++ b/spec/models/namespace/aggregation_schedule_spec.rb @@ -53,10 +53,39 @@ RSpec.describe Namespace::AggregationSchedule, :clean_gitlab_redis_shared_state, expect(Namespaces::RootStatisticsWorker) .to receive(:perform_in).once - .with(described_class::DEFAULT_LEASE_TIMEOUT, aggregation_schedule.namespace_id ) + .with(described_class::DEFAULT_LEASE_TIMEOUT, aggregation_schedule.namespace_id) aggregation_schedule.save! end + + it 'does not release the lease' do + stub_exclusive_lease(lease_key, timeout: described_class::DEFAULT_LEASE_TIMEOUT) + + aggregation_schedule.save! + + exclusive_lease = aggregation_schedule.exclusive_lease + expect(exclusive_lease.exists?).to be_truthy + end + + it 'only executes the workers once' do + # Avoid automatic deletion of Namespace::AggregationSchedule + # for testing purposes. + expect(Namespaces::RootStatisticsWorker) + .to receive(:perform_async).once + .and_return(nil) + + expect(Namespaces::RootStatisticsWorker) + .to receive(:perform_in).once + .with(described_class::DEFAULT_LEASE_TIMEOUT, aggregation_schedule.namespace_id) + .and_return(nil) + + # Scheduling workers for the first time + aggregation_schedule.schedule_root_storage_statistics + + # Executing again, this time workers should not be scheduled + # due to the lease not been released. + aggregation_schedule.schedule_root_storage_statistics + end end context 'with a personalized lease timeout' do diff --git a/spec/workers/namespaces/schedule_aggregation_worker_spec.rb b/spec/workers/namespaces/schedule_aggregation_worker_spec.rb index 7432ca12f2a..d4a49a3f53a 100644 --- a/spec/workers/namespaces/schedule_aggregation_worker_spec.rb +++ b/spec/workers/namespaces/schedule_aggregation_worker_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Namespaces::ScheduleAggregationWorker, '#perform' do +describe Namespaces::ScheduleAggregationWorker, '#perform', :clean_gitlab_redis_shared_state do let(:group) { create(:group) } subject(:worker) { described_class.new } @@ -10,6 +10,8 @@ describe Namespaces::ScheduleAggregationWorker, '#perform' do context 'when group is the root ancestor' do context 'when aggregation schedule exists' do it 'does not create a new one' do + stub_aggregation_schedule_statistics + Namespace::AggregationSchedule.safe_find_or_create_by!(namespace_id: group.id) expect do @@ -18,6 +20,18 @@ describe Namespaces::ScheduleAggregationWorker, '#perform' do end end + context 'when aggregation schedule does not exist' do + it 'creates one' do + stub_aggregation_schedule_statistics + + expect do + worker.perform(group.id) + end.to change(Namespace::AggregationSchedule, :count).by(1) + + expect(group.aggregation_schedule).to be_present + end + end + context 'when update_statistics_namespace is off' do it 'does not create a new one' do stub_feature_flags(update_statistics_namespace: false, namespace: group) @@ -27,19 +41,6 @@ describe Namespaces::ScheduleAggregationWorker, '#perform' do end.not_to change(Namespace::AggregationSchedule, :count) end end - - context 'when aggregation schedule does not exist' do - it 'creates one' do - allow_any_instance_of(Namespace::AggregationSchedule) - .to receive(:schedule_root_storage_statistics).and_return(nil) - - expect do - worker.perform(group.id) - end.to change(Namespace::AggregationSchedule, :count).by(1) - - expect(group.aggregation_schedule).to be_present - end - end end context 'when group is not the root ancestor' do @@ -47,8 +48,7 @@ describe Namespaces::ScheduleAggregationWorker, '#perform' do let(:group) { create(:group, parent: parent_group) } it 'creates an aggregation schedule for the root' do - allow_any_instance_of(Namespace::AggregationSchedule) - .to receive(:schedule_root_storage_statistics).and_return(nil) + stub_aggregation_schedule_statistics worker.perform(group.id) @@ -63,4 +63,15 @@ describe Namespaces::ScheduleAggregationWorker, '#perform' do worker.perform(12345) end end + + def stub_aggregation_schedule_statistics + # Namespace::Aggregations are deleted by + # Namespace::AggregationSchedule::schedule_root_storage_statistics, + # which is executed async. Stubing the service so instances are not deleted + # while still running the specs. + expect_next_instance_of(Namespace::AggregationSchedule) do |aggregation_schedule| + expect(aggregation_schedule) + .to receive(:schedule_root_storage_statistics) + end + end end From 69e1ed0dc85be33395d5c050923c71b53268b2a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 28 Jun 2019 09:18:28 +0200 Subject: [PATCH 067/195] Retry review-deploy only once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- .gitlab/ci/review.gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index 61fd48fd72e..ce019de213b 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -77,7 +77,7 @@ schedule:review-build-cng: .review-deploy-base: &review-deploy-base <<: *review-base allow_failure: true - retry: 2 + retry: 1 stage: review variables: HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}" From 29dbac2e124f67f27407d2f9324730b223f7846b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 28 Jun 2019 11:38:38 +0200 Subject: [PATCH 068/195] Add resources requests and limits for all Review Apps resources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- scripts/review_apps/review-apps.sh | 128 +++++++++++++++++++++++------ 1 file changed, 105 insertions(+), 23 deletions(-) diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh index 633ea28e96c..2bf654b1e24 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -131,6 +131,7 @@ function install_external_dns() { if ! deploy_exists "${KUBE_NAMESPACE}" "${release_name}" || previous_deploy_failed "${release_name}" ; then echoinfo "Installing external-dns Helm chart" helm repo update + # Default requested: CPU => 0, memory => 0 helm install stable/external-dns \ -n "${release_name}" \ --namespace "${KUBE_NAMESPACE}" \ @@ -141,7 +142,11 @@ function install_external_dns() { --set domainFilters[0]="${domain}" \ --set txtOwnerId="${KUBE_NAMESPACE}" \ --set rbac.create="true" \ - --set policy="sync" + --set policy="sync" \ + --set resources.requests.cpu=50m \ + --set resources.limits.cpu=100m \ + --set resources.requests.memory=100M \ + --set resources.limits.memory=200M else echoinfo "The external-dns Helm chart is already successfully deployed." fi @@ -196,45 +201,122 @@ HELM_CMD=$(cat << EOF helm upgrade --install \ --wait \ --timeout 600 \ - --set global.appConfig.enableUsagePing=false \ --set releaseOverride="$CI_ENVIRONMENT_SLUG" \ + --set global.appConfig.enableUsagePing=false \ --set global.imagePullPolicy=Always \ --set global.hosts.hostSuffix="$HOST_SUFFIX" \ --set global.hosts.domain="$REVIEW_APPS_DOMAIN" \ - --set certmanager.install=false \ - --set prometheus.install=false \ --set global.ingress.configureCertmanager=false \ --set global.ingress.tls.secretName=tls-cert \ --set global.ingress.annotations."external-dns\.alpha\.kubernetes\.io/ttl"="10" \ + --set certmanager.install=false \ + --set prometheus.install=false \ --set nginx-ingress.controller.service.enableHttp=false \ - --set nginx-ingress.defaultBackend.resources.requests.memory=7Mi \ - --set nginx-ingress.controller.resources.requests.memory=440M \ --set nginx-ingress.controller.replicaCount=2 \ - --set gitlab.unicorn.resources.requests.cpu=200m \ - --set gitlab.sidekiq.resources.requests.cpu=100m \ - --set gitlab.sidekiq.resources.requests.memory=800M \ - --set gitlab.gitlab-shell.resources.requests.cpu=100m \ - --set redis.resources.requests.cpu=100m \ - --set minio.resources.requests.cpu=100m \ + --set nginx-ingress.controller.config.ssl-ciphers="ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4" \ --set gitlab.migrations.image.repository="$gitlab_migrations_image_repository" \ --set gitlab.migrations.image.tag="$CI_COMMIT_REF_SLUG" \ - --set gitlab.sidekiq.image.repository="$gitlab_sidekiq_image_repository" \ - --set gitlab.sidekiq.image.tag="$CI_COMMIT_REF_SLUG" \ - --set gitlab.unicorn.image.repository="$gitlab_unicorn_image_repository" \ - --set gitlab.unicorn.image.tag="$CI_COMMIT_REF_SLUG" \ - --set gitlab.task-runner.image.repository="$gitlab_task_runner_image_repository" \ - --set gitlab.task-runner.image.tag="$CI_COMMIT_REF_SLUG" \ --set gitlab.gitaly.image.repository="$gitlab_gitaly_image_repository" \ --set gitlab.gitaly.image.tag="v$GITALY_VERSION" \ --set gitlab.gitlab-shell.image.repository="$gitlab_shell_image_repository" \ --set gitlab.gitlab-shell.image.tag="v$GITLAB_SHELL_VERSION" \ + --set gitlab.sidekiq.image.repository="$gitlab_sidekiq_image_repository" \ + --set gitlab.sidekiq.image.tag="$CI_COMMIT_REF_SLUG" \ + --set gitlab.unicorn.image.repository="$gitlab_unicorn_image_repository" \ + --set gitlab.unicorn.image.tag="$CI_COMMIT_REF_SLUG" \ --set gitlab.unicorn.workhorse.image="$gitlab_workhorse_image_repository" \ --set gitlab.unicorn.workhorse.tag="$CI_COMMIT_REF_SLUG" \ - --set nginx-ingress.controller.config.ssl-ciphers="ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4" \ - --namespace="$KUBE_NAMESPACE" \ - --version="$CI_PIPELINE_ID-$CI_JOB_ID" \ - "$name" \ - . + --set gitlab.task-runner.image.repository="$gitlab_task_runner_image_repository" \ + --set gitlab.task-runner.image.tag="$CI_COMMIT_REF_SLUG" +EOF +) + +# Default requested: CPU => 100m, memory => 100Mi +HELM_CMD=$(cat << EOF + $HELM_CMD \ + --set nginx-ingress.controller.resources.limits.cpu=200m \ + --set nginx-ingress.controller.resources.requests.memory=210M \ + --set nginx-ingress.controller.resources.limits.memory=420M +EOF +) + +# Default requested: CPU => 5m, memory => 5Mi +HELM_CMD=$(cat << EOF + $HELM_CMD \ + --set nginx-ingress.defaultBackend.resources.limits.cpu=10m \ + --set nginx-ingress.defaultBackend.resources.requests.memory=12M \ + --set nginx-ingress.defaultBackend.resources.limits.memory=24M +EOF +) + +# Default requested: CPU => 100m, memory => 200Mi +HELM_CMD=$(cat << EOF + $HELM_CMD \ + --set gitlab.gitaly.resources.requests.cpu=150m \ + --set gitlab.gitaly.resources.limits.cpu=300m \ + --set gitlab.gitaly.resources.limits.memory=420M +EOF +) + +# Default requested: CPU => 0, memory => 6M +HELM_CMD=$(cat << EOF + $HELM_CMD \ + --set gitlab.gitlab-shell.resources.requests.cpu=70m \ + --set gitlab.gitlab-shell.resources.limits.cpu=140m \ + --set gitlab.gitlab-shell.resources.requests.memory=20M \ + --set gitlab.gitlab-shell.resources.limits.memory=40M +EOF +) + +# Default requested: CPU => 50m, memory => 650M +HELM_CMD=$(cat << EOF + $HELM_CMD \ + --set gitlab.sidekiq.resources.requests.cpu=200m \ + --set gitlab.sidekiq.resources.limits.cpu=300m \ + --set gitlab.sidekiq.resources.requests.memory=800M \ + --set gitlab.sidekiq.resources.limits.memory=1.2G +EOF +) + +# Default requested: CPU => 300m + 100m (workhorse), memory => 1.2G + 100M (workhorse) +HELM_CMD=$(cat << EOF + $HELM_CMD \ + --set gitlab.unicorn.resources.limits.cpu=800m \ + --set gitlab.unicorn.resources.limits.memory=2.6G +EOF +) + +# Default requested: CPU => 100m, memory => 64Mi +HELM_CMD=$(cat << EOF + $HELM_CMD \ + --set redis.resources.limits.cpu=200m \ + --set redis.resources.limits.memory=130M +EOF +) + +# Default requested: CPU => 100m, memory => 128Mi +HELM_CMD=$(cat << EOF + $HELM_CMD \ + --set minio.resources.limits.cpu=200m \ + --set minio.resources.limits.memory=280M +EOF +) + +# Default requested: CPU => 0, memory => 0 +HELM_CMD=$(cat << EOF + $HELM_CMD \ + --set gitlab-runner.resources.requests.cpu=300m \ + --set gitlab-runner.resources.limits.cpu=600m \ + --set gitlab-runner.resources.requests.memory=300M \ + --set gitlab-runner.resources.limits.memory=600M +EOF +) + +HELM_CMD=$(cat << EOF + $HELM_CMD \ + --namespace="$KUBE_NAMESPACE" \ + --version="$CI_PIPELINE_ID-$CI_JOB_ID" \ + "$name" . EOF ) From 4f04c4c90b2db8ddcf5f3e28a9bbefd20c8bbda0 Mon Sep 17 00:00:00 2001 From: Mario de la Ossa Date: Wed, 12 Jun 2019 18:08:44 -0600 Subject: [PATCH 069/195] Ignore min_chars_for_partial_matching unles trigrm If we're not using a trigram index, then ignore the min_chars_for_partial_matching setting --- app/finders/issuable_finder.rb | 2 +- app/models/concerns/issuable.rb | 4 ++-- .../unreleased/40379-CJK-search-min-chars.yml | 5 ++++ lib/gitlab/sql/pattern.rb | 24 +++++++++++-------- spec/lib/gitlab/sql/pattern_spec.rb | 12 ++++++++++ spec/models/concerns/issuable_spec.rb | 10 ++++++++ 6 files changed, 44 insertions(+), 13 deletions(-) create mode 100644 changelogs/unreleased/40379-CJK-search-min-chars.yml diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 3592505a977..f4fbeacfaba 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -429,7 +429,7 @@ class IssuableFinder items = klass.with(cte.to_arel).from(klass.table_name) end - items.full_search(search, matched_columns: params[:in]) + items.full_search(search, matched_columns: params[:in], use_minimum_char_limit: !use_cte_for_search?) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 299e413321d..952de92cae1 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -168,7 +168,7 @@ module Issuable # matched_columns - Modify the scope of the query. 'title', 'description' or joining them with a comma. # # Returns an ActiveRecord::Relation. - def full_search(query, matched_columns: 'title,description') + def full_search(query, matched_columns: 'title,description', use_minimum_char_limit: true) allowed_columns = [:title, :description] matched_columns = matched_columns.to_s.split(',').map(&:to_sym) matched_columns &= allowed_columns @@ -176,7 +176,7 @@ module Issuable # Matching title or description if the matched_columns did not contain any allowed columns. matched_columns = [:title, :description] if matched_columns.empty? - fuzzy_search(query, matched_columns) + fuzzy_search(query, matched_columns, use_minimum_char_limit: use_minimum_char_limit) end def simple_sorts diff --git a/changelogs/unreleased/40379-CJK-search-min-chars.yml b/changelogs/unreleased/40379-CJK-search-min-chars.yml new file mode 100644 index 00000000000..6f6c4df464f --- /dev/null +++ b/changelogs/unreleased/40379-CJK-search-min-chars.yml @@ -0,0 +1,5 @@ +--- +title: Remove minimum character limits for fuzzy searches when using a CTE +merge_request: 29810 +author: +type: fixed diff --git a/lib/gitlab/sql/pattern.rb b/lib/gitlab/sql/pattern.rb index fd108b4c124..f6edbfced7f 100644 --- a/lib/gitlab/sql/pattern.rb +++ b/lib/gitlab/sql/pattern.rb @@ -9,14 +9,16 @@ module Gitlab REGEX_QUOTED_WORD = /(?<=\A| )"[^"]+"(?= |\z)/.freeze class_methods do - def fuzzy_search(query, columns) - matches = columns.map { |col| fuzzy_arel_match(col, query) }.compact.reduce(:or) + def fuzzy_search(query, columns, use_minimum_char_limit: true) + matches = columns.map do |col| + fuzzy_arel_match(col, query, use_minimum_char_limit: use_minimum_char_limit) + end.compact.reduce(:or) where(matches) end - def to_pattern(query) - if partial_matching?(query) + def to_pattern(query, use_minimum_char_limit: true) + if partial_matching?(query, use_minimum_char_limit: use_minimum_char_limit) "%#{sanitize_sql_like(query)}%" else sanitize_sql_like(query) @@ -27,7 +29,9 @@ module Gitlab MIN_CHARS_FOR_PARTIAL_MATCHING end - def partial_matching?(query) + def partial_matching?(query, use_minimum_char_limit: true) + return true unless use_minimum_char_limit + query.length >= min_chars_for_partial_matching end @@ -35,14 +39,14 @@ module Gitlab # query - The text to search for. # lower_exact_match - When set to `true` we'll fall back to using # `LOWER(column) = query` instead of using `ILIKE`. - def fuzzy_arel_match(column, query, lower_exact_match: false) + def fuzzy_arel_match(column, query, lower_exact_match: false, use_minimum_char_limit: true) query = query.squish return unless query.present? - words = select_fuzzy_words(query) + words = select_fuzzy_words(query, use_minimum_char_limit: use_minimum_char_limit) if words.any? - words.map { |word| arel_table[column].matches(to_pattern(word)) }.reduce(:and) + words.map { |word| arel_table[column].matches(to_pattern(word, use_minimum_char_limit: use_minimum_char_limit)) }.reduce(:and) else # No words of at least 3 chars, but we can search for an exact # case insensitive match with the query as a whole @@ -56,7 +60,7 @@ module Gitlab end end - def select_fuzzy_words(query) + def select_fuzzy_words(query, use_minimum_char_limit: true) quoted_words = query.scan(REGEX_QUOTED_WORD) query = quoted_words.reduce(query) { |q, quoted_word| q.sub(quoted_word, '') } @@ -67,7 +71,7 @@ module Gitlab words.concat(quoted_words) - words.select { |word| partial_matching?(word) } + words.select { |word| partial_matching?(word, use_minimum_char_limit: use_minimum_char_limit) } end end end diff --git a/spec/lib/gitlab/sql/pattern_spec.rb b/spec/lib/gitlab/sql/pattern_spec.rb index 5b5052de372..98838712eae 100644 --- a/spec/lib/gitlab/sql/pattern_spec.rb +++ b/spec/lib/gitlab/sql/pattern_spec.rb @@ -10,6 +10,12 @@ describe Gitlab::SQL::Pattern do it 'returns exact matching pattern' do expect(to_pattern).to eq('12') end + + context 'and ignore_minimum_char_limit is true' do + it 'returns partial matching pattern' do + expect(User.to_pattern(query, use_minimum_char_limit: false)).to eq('%12%') + end + end end context 'when a query with a escape character is shorter than 3 chars' do @@ -18,6 +24,12 @@ describe Gitlab::SQL::Pattern do it 'returns sanitized exact matching pattern' do expect(to_pattern).to eq('\_2') end + + context 'and ignore_minimum_char_limit is true' do + it 'returns sanitized partial matching pattern' do + expect(User.to_pattern(query, use_minimum_char_limit: false)).to eq('%\_2%') + end + end end context 'when a query is equal to 3 chars' do diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 64f02978d79..68224a56515 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -223,6 +223,16 @@ describe Issuable do expect(issuable_class.full_search(searchable_issue2.description.downcase)).to eq([searchable_issue2]) end + it 'returns issues with a fuzzy matching description for a query shorter than 3 chars if told to do so' do + search = searchable_issue2.description.downcase.scan(/\w+/).sample[-1] + + expect(issuable_class.full_search(search, use_minimum_char_limit: false)).to include(searchable_issue2) + end + + it 'returns issues with a fuzzy matching title for a query shorter than 3 chars if told to do so' do + expect(issuable_class.full_search('i', use_minimum_char_limit: false)).to include(searchable_issue) + end + context 'when matching columns is "title"' do it 'returns issues with a matching title' do expect(issuable_class.full_search(searchable_issue.title, matched_columns: 'title')) From d34f43499e23469c82d293d4a43efa53f56d8fb0 Mon Sep 17 00:00:00 2001 From: Mayra Cabrera Date: Thu, 4 Jul 2019 16:58:42 +0000 Subject: [PATCH 070/195] Add new info for auth.log From 12.1, user information (id and username) are also included on auth.log. Documentation was updated to reflect this. Related to https://gitlab.com/gitlab-org/gitlab-ce/issues/62756 --- doc/administration/logs.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/administration/logs.md b/doc/administration/logs.md index 9921ffd8ea0..b49d8c8a28f 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -288,6 +288,9 @@ installations from source. It logs information whenever [Rack Attack] registers an abusive request. +NOTE: **Note:** +From [%12.1](https://gitlab.com/gitlab-org/gitlab-ce/issues/62756), user id and username are available on this log. + ## `graphql_json.log` > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/59587) in GitLab 12.0. From f33707d54444d28bb618e00084f2825f554b207e Mon Sep 17 00:00:00 2001 From: Markus Koller Date: Mon, 1 Jul 2019 11:19:04 +0200 Subject: [PATCH 071/195] Fix call to removed GitPushService --- db/fixtures/development/17_cycle_analytics.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb index 7a86fe2eb7c..78ceb74da65 100644 --- a/db/fixtures/development/17_cycle_analytics.rb +++ b/db/fixtures/development/17_cycle_analytics.rb @@ -146,11 +146,13 @@ class Gitlab::Seeder::CycleAnalytics commit_sha = issue.project.repository.create_file(@user, filename, "content", message: "Commit for #{issue.to_reference}", branch_name: branch_name) issue.project.repository.commit(commit_sha) - GitPushService.new(issue.project, - @user, - oldrev: issue.project.repository.commit("master").sha, - newrev: commit_sha, - ref: 'refs/heads/master').execute + Git::BranchPushService.new( + issue.project, + @user, + oldrev: issue.project.repository.commit("master").sha, + newrev: commit_sha, + ref: 'refs/heads/master' + ).execute branch_name end From e169b030b94bad0cd60995686feeac8d84033aaf Mon Sep 17 00:00:00 2001 From: Cynthia Ng Date: Thu, 4 Jul 2019 17:23:06 +0000 Subject: [PATCH 072/195] Docs: Clarify project and repo size --- .../settings/account_and_limit_settings.md | 4 +++- doc/user/project/container_registry.md | 2 +- doc/user/project/repository/index.md | 13 +++++-------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md index 001e4b6bf48..756a07e0b80 100644 --- a/doc/user/admin_area/settings/account_and_limit_settings.md +++ b/doc/user/admin_area/settings/account_and_limit_settings.md @@ -44,10 +44,12 @@ The first push of a new project, including LFS objects, will be checked for size and **will** be rejected if the sum of their sizes exceeds the maximum allowed repository size. +**Note:** The repository size limit includes repository files and LFS, and does not include artifacts. + For details on manually purging files, see [reducing the repository size using Git](../../project/repository/reducing_the_repo_size_using_git.md). NOTE: **Note:** -For GitLab.com, the repository size limit is 10 GB. +GitLab.com repository size [is set by GitLab](../../gitlab_com/index.md#repository-size-limit). |once done| B2 + A2[`Trigger-docker` stage
`Trigger:gitlab-docker` job] -->|once done| B2 end subgraph gitlab-qa pipeline diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md index 28ebb6f0f64..98df0b5ea7c 100644 --- a/doc/development/testing_guide/frontend_testing.md +++ b/doc/development/testing_guide/frontend_testing.md @@ -560,7 +560,6 @@ end [vue-test]: https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components [rspec]: https://github.com/rspec/rspec-rails#feature-specs [capybara]: https://github.com/teamcapybara/capybara -[karma]: http://karma-runner.github.io/ [jasmine]: https://jasmine.github.io/ --- diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md index 93ee2a6371a..c4b18391cb2 100644 --- a/doc/development/testing_guide/index.md +++ b/doc/development/testing_guide/index.md @@ -80,8 +80,6 @@ Everything you should know about how to run end-to-end tests using [Return to Development documentation](../README.md) -[^1]: /ci/yaml/README.html#dependencies - [rails]: http://rubyonrails.org/ [RSpec]: https://github.com/rspec/rspec-rails#feature-specs [Capybara]: https://github.com/teamcapybara/capybara From d23a654fc5fa1a3c36538b8f0206e34a549689aa Mon Sep 17 00:00:00 2001 From: Mark Lapierre Date: Fri, 5 Jul 2019 09:13:29 +1000 Subject: [PATCH 082/195] Initialize Issue labels This provides a valid default value when labels are not specified explicitly. --- qa/qa/resource/issue.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb index 9c57a0f5afb..51b2af8b4ef 100644 --- a/qa/qa/resource/issue.rb +++ b/qa/qa/resource/issue.rb @@ -16,6 +16,10 @@ module QA attribute :labels attribute :title + def initialize + @labels = [] + end + def fabricate! project.visit! @@ -38,7 +42,7 @@ module QA def api_post_body { - labels: [labels], + labels: labels, title: title } end From 02530168067d671eb4c1ee7212390e274bef02f9 Mon Sep 17 00:00:00 2001 From: Mayra Cabrera Date: Fri, 5 Jul 2019 01:54:18 +0000 Subject: [PATCH 083/195] Add pgFormatter as a database tooling Includes a PostgreSQL syntax beautifier as a database tooling. --- doc/development/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/development/README.md b/doc/development/README.md index 5df6ec5fd56..1566173992a 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -82,6 +82,7 @@ description: 'Learn how to contribute to GitLab.' - [Understanding EXPLAIN plans](understanding_explain_plans.md) - [explain.depesz.com](https://explain.depesz.com/) for visualising the output of `EXPLAIN` +- [pgFormatter](http://sqlformat.darold.net/) a PostgreSQL SQL syntax beautifier ### Migrations From da767e3c689cd45a12fa0b37e1acf8cc995cecf6 Mon Sep 17 00:00:00 2001 From: Mark Lapierre Date: Fri, 5 Jul 2019 04:00:20 +0000 Subject: [PATCH 084/195] Revert "Merge branch 'qa-quarantine-failing-push-mirror-repo-spec' into 'master'" This reverts merge request !25590 --- .../3_create/repository/push_mirroring_over_http_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb index 23008a58af8..448d4980727 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true module QA - # https://gitlab.com/gitlab-org/quality/staging/issues/40 - context 'Create', :quarantine do + context 'Create' do describe 'Push mirror a repository over HTTP' do it 'configures and syncs a (push) mirrored repository' do Runtime::Browser.visit(:gitlab, Page::Main::Login) From b9b3ec83540a82fc96ddd64715a51d7adeb7312c Mon Sep 17 00:00:00 2001 From: Imre Farkas Date: Fri, 5 Jul 2019 08:12:29 +0200 Subject: [PATCH 085/195] CE port of "Require session with smartcard login for Git access" --- lib/gitlab/namespaced_session_store.rb | 15 ++++++---- .../gitlab/namespaced_session_store_spec.rb | 30 ++++++++++++++----- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/lib/gitlab/namespaced_session_store.rb b/lib/gitlab/namespaced_session_store.rb index 34520078bfb..f0f24c081c3 100644 --- a/lib/gitlab/namespaced_session_store.rb +++ b/lib/gitlab/namespaced_session_store.rb @@ -4,19 +4,24 @@ module Gitlab class NamespacedSessionStore delegate :[], :[]=, to: :store - def initialize(key) + def initialize(key, session = Session.current) @key = key + @session = session end def initiated? - !Session.current.nil? + !session.nil? end def store - return unless Session.current + return unless session - Session.current[@key] ||= {} - Session.current[@key] + session[@key] ||= {} + session[@key] end + + private + + attr_reader :session end end diff --git a/spec/lib/gitlab/namespaced_session_store_spec.rb b/spec/lib/gitlab/namespaced_session_store_spec.rb index c0af2ede32a..e177c44ad67 100644 --- a/spec/lib/gitlab/namespaced_session_store_spec.rb +++ b/spec/lib/gitlab/namespaced_session_store_spec.rb @@ -4,19 +4,33 @@ require 'spec_helper' describe Gitlab::NamespacedSessionStore do let(:key) { :some_key } - subject { described_class.new(key) } - it 'stores data under the specified key' do - Gitlab::Session.with_session({}) do - subject[:new_data] = 123 + context 'current session' do + subject { described_class.new(key) } - expect(Thread.current[:session_storage][key]).to eq(new_data: 123) + it 'stores data under the specified key' do + Gitlab::Session.with_session({}) do + subject[:new_data] = 123 + + expect(Thread.current[:session_storage][key]).to eq(new_data: 123) + end + end + + it 'retrieves data from the given key' do + Thread.current[:session_storage] = { key => { existing_data: 123 } } + + expect(subject[:existing_data]).to eq 123 end end - it 'retrieves data from the given key' do - Thread.current[:session_storage] = { key => { existing_data: 123 } } + context 'passed in session' do + let(:data) { { 'data' => 42 } } + let(:session) { { 'some_key' => data } } - expect(subject[:existing_data]).to eq 123 + subject { described_class.new(key, session.with_indifferent_access) } + + it 'retrieves data from the given key' do + expect(subject['data']).to eq 42 + end end end From 587ffd11480db3ed492459ec2b6fac32a459ebaa Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Wed, 26 Jun 2019 19:24:09 +0700 Subject: [PATCH 086/195] Split AutoMergeService interfaces into two `cancel` and `abort` Create explicit endpoint - abort. --- app/services/auto_merge/base_service.rb | 14 ++++++- .../merge_when_pipeline_succeeds_service.rb | 8 +++- app/services/auto_merge_service.rb | 6 +++ app/services/merge_requests/base_service.rb | 4 +- app/services/merge_requests/close_service.rb | 2 +- .../merge_requests/refresh_service.rb | 6 +-- app/services/merge_requests/update_service.rb | 2 +- app/services/system_note_service.rb | 10 +++++ spec/services/auto_merge/base_service_spec.rb | 40 ++++++++++++++++--- ...rge_when_pipeline_succeeds_service_spec.rb | 11 +++++ spec/services/auto_merge_service_spec.rb | 25 ++++++++++++ spec/services/system_note_service_spec.rb | 16 ++++++++ 12 files changed, 129 insertions(+), 15 deletions(-) diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb index d726085b89a..e06659a39cd 100644 --- a/app/services/auto_merge/base_service.rb +++ b/app/services/auto_merge/base_service.rb @@ -29,7 +29,7 @@ module AutoMerge end def cancel(merge_request) - if cancel_auto_merge(merge_request) + if clear_auto_merge_parameters(merge_request) yield if block_given? success @@ -38,6 +38,16 @@ module AutoMerge end end + def abort(merge_request, reason) + if clear_auto_merge_parameters(merge_request) + yield if block_given? + + success + else + error("Can't abort the automatic merge", 406) + end + end + private def strategy @@ -46,7 +56,7 @@ module AutoMerge end end - def cancel_auto_merge(merge_request) + def clear_auto_merge_parameters(merge_request) merge_request.auto_merge_enabled = false merge_request.merge_user = nil diff --git a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb index cde8c19e8fc..6a33ec071db 100644 --- a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb +++ b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb @@ -19,7 +19,13 @@ module AutoMerge def cancel(merge_request) super do - SystemNoteService.cancel_merge_when_pipeline_succeeds(merge_request, @project, @current_user) + SystemNoteService.cancel_merge_when_pipeline_succeeds(merge_request, project, current_user) + end + end + + def abort(merge_request, reason) + super do + SystemNoteService.abort_merge_when_pipeline_succeeds(merge_request, project, current_user, reason) end end diff --git a/app/services/auto_merge_service.rb b/app/services/auto_merge_service.rb index 926d2f5fc66..95bf2db2018 100644 --- a/app/services/auto_merge_service.rb +++ b/app/services/auto_merge_service.rb @@ -42,6 +42,12 @@ class AutoMergeService < BaseService get_service_instance(merge_request.auto_merge_strategy).cancel(merge_request) end + def abort(merge_request, reason) + return error("Can't abort the automatic merge", 406) unless merge_request.auto_merge_enabled? + + get_service_instance(merge_request.auto_merge_strategy).abort(merge_request, reason) + end + def available_strategies(merge_request) self.class.all_strategies.select do |strategy| get_service_instance(strategy).available_for?(merge_request) diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index c34fbeb2adb..067510a8a0a 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -68,8 +68,8 @@ module MergeRequests !merge_request.for_fork? end - def cancel_auto_merge(merge_request) - AutoMergeService.new(project, current_user).cancel(merge_request) + def abort_auto_merge(merge_request, reason) + AutoMergeService.new(project, current_user).abort(merge_request, reason) end # Returns all origin and fork merge requests from `@project` satisfying passed arguments. diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb index b81a4dd81d2..c2174d2a130 100644 --- a/app/services/merge_requests/close_service.rb +++ b/app/services/merge_requests/close_service.rb @@ -18,7 +18,7 @@ module MergeRequests invalidate_cache_counts(merge_request, users: merge_request.assignees) merge_request.update_project_counter_caches cleanup_environments(merge_request) - cancel_auto_merge(merge_request) + abort_auto_merge(merge_request, 'merge request was closed') end merge_request diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 4b199bd8fa8..8961d2e1023 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -24,7 +24,7 @@ module MergeRequests reload_merge_requests outdate_suggestions refresh_pipelines_on_merge_requests - cancel_auto_merges + abort_auto_merges mark_pending_todos_done cache_merge_requests_closing_issues @@ -142,9 +142,9 @@ module MergeRequests end end - def cancel_auto_merges + def abort_auto_merges merge_requests_for_source_branch.each do |merge_request| - cancel_auto_merge(merge_request) + abort_auto_merge(merge_request, 'source branch was updated') end end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 0066cd0491f..d361e96babf 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -44,7 +44,7 @@ module MergeRequests merge_request.previous_changes['target_branch'].first, merge_request.target_branch) - cancel_auto_merge(merge_request) + abort_auto_merge(merge_request, 'target branch was changed') end if merge_request.assignees != old_assignees diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 4783417ad6d..005050ad08b 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -234,6 +234,16 @@ module SystemNoteService create_note(NoteSummary.new(noteable, project, author, body, action: 'merge')) end + # Called when 'merge when pipeline succeeds' is aborted + def abort_merge_when_pipeline_succeeds(noteable, project, author, reason) + body = "aborted the automatic merge because #{reason}" + + ## + # TODO: Abort message should be sent by the system, not a particular user. + # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63187. + create_note(NoteSummary.new(noteable, project, author, body, action: 'merge')) + end + def handle_merge_request_wip(noteable, project, author) prefix = noteable.work_in_progress? ? "marked" : "unmarked" diff --git a/spec/services/auto_merge/base_service_spec.rb b/spec/services/auto_merge/base_service_spec.rb index 24cb63a0d61..a409f21a7e4 100644 --- a/spec/services/auto_merge/base_service_spec.rb +++ b/spec/services/auto_merge/base_service_spec.rb @@ -121,11 +121,7 @@ describe AutoMerge::BaseService do end end - describe '#cancel' do - subject { service.cancel(merge_request) } - - let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) } - + shared_examples_for 'Canceled or Dropped' do it 'removes properies from the merge request' do subject @@ -173,6 +169,20 @@ describe AutoMerge::BaseService do it 'does not yield block' do expect { |b| service.execute(merge_request, &b) }.not_to yield_control end + end + end + + describe '#cancel' do + subject { service.cancel(merge_request) } + + let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) } + + it_behaves_like 'Canceled or Dropped' + + context 'when failed to save' do + before do + allow(merge_request).to receive(:save) { false } + end it 'returns error status' do expect(subject[:status]).to eq(:error) @@ -180,4 +190,24 @@ describe AutoMerge::BaseService do end end end + + describe '#abort' do + subject { service.abort(merge_request, reason) } + + let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) } + let(:reason) { 'an error'} + + it_behaves_like 'Canceled or Dropped' + + context 'when failed to save' do + before do + allow(merge_request).to receive(:save) { false } + end + + it 'returns error status' do + expect(subject[:status]).to eq(:error) + expect(subject[:message]).to eq("Can't abort the automatic merge") + end + end + end end diff --git a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb index 5e84ef052ce..931b52470c4 100644 --- a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb +++ b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb @@ -177,6 +177,17 @@ describe AutoMerge::MergeWhenPipelineSucceedsService do end end + describe "#abort" do + before do + service.abort(mr_merge_if_green_enabled, 'an error') + end + + it 'posts a system note' do + note = mr_merge_if_green_enabled.notes.last + expect(note.note).to include 'aborted the automatic merge' + end + end + describe 'pipeline integration' do context 'when there are multiple stages in the pipeline' do let(:ref) { mr_merge_if_green_enabled.source_branch } diff --git a/spec/services/auto_merge_service_spec.rb b/spec/services/auto_merge_service_spec.rb index 93a22e60498..50dfc49a59c 100644 --- a/spec/services/auto_merge_service_spec.rb +++ b/spec/services/auto_merge_service_spec.rb @@ -161,4 +161,29 @@ describe AutoMergeService do end end end + + describe '#abort' do + subject { service.abort(merge_request, error) } + + let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) } + let(:error) { 'an error' } + + it 'delegates to a relevant service instance' do + expect_next_instance_of(AutoMerge::MergeWhenPipelineSucceedsService) do |service| + expect(service).to receive(:abort).with(merge_request, error) + end + + subject + end + + context 'when auto merge is not enabled' do + let(:merge_request) { create(:merge_request) } + + it 'returns error' do + expect(subject[:message]).to eq("Can't abort the automatic merge") + expect(subject[:status]).to eq(:error) + expect(subject[:http_status]).to eq(406) + end + end + end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 9f60e49290e..2d33e1c37eb 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -359,6 +359,22 @@ describe SystemNoteService do end end + describe '.abort_merge_when_pipeline_succeeds' do + let(:noteable) do + create(:merge_request, source_project: project, target_project: project) + end + + subject { described_class.abort_merge_when_pipeline_succeeds(noteable, project, author, 'merge request was closed') } + + it_behaves_like 'a system note' do + let(:action) { 'merge' } + end + + it "posts the 'merge when pipeline succeeds' system note" do + expect(subject.note).to eq "aborted the automatic merge because merge request was closed" + end + end + describe '.change_title' do let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum') } From 6e552981248a551e8c70c8d2866a401a8d1090b5 Mon Sep 17 00:00:00 2001 From: Thong Kuah Date: Fri, 5 Jul 2019 07:07:46 +0000 Subject: [PATCH 087/195] Remove obsolete comment --- lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml index dcf8254ef94..108f0119ae1 100644 --- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml @@ -246,7 +246,6 @@ rollout 100%: auto_database_url=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${CI_ENVIRONMENT_SLUG}-postgres:5432/${POSTGRES_DB} export DATABASE_URL=${DATABASE_URL-$auto_database_url} export TILLER_NAMESPACE=$KUBE_NAMESPACE - # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products function get_replicas() { track="${1:-stable}" From ef66b8db797b63c8207cfa6a21f097ac00ce1731 Mon Sep 17 00:00:00 2001 From: Marcel Amirault Date: Fri, 5 Jul 2019 08:17:39 +0000 Subject: [PATCH 088/195] Move workflow images to img dir Clean out workflow directory by moving images from gitlab_flow doc to /img --- doc/workflow/gitlab_flow.md | 35 +++++++++--------- doc/workflow/{ => img}/ci_mr.png | Bin doc/workflow/{ => img}/close_issue_mr.png | Bin .../{ => img}/environment_branches.png | Bin doc/workflow/{ => img}/four_stages.png | Bin doc/workflow/{ => img}/git_pull.png | Bin doc/workflow/{ => img}/gitdashflow.png | Bin doc/workflow/{ => img}/github_flow.png | Bin doc/workflow/{ => img}/gitlab_flow.png | Bin doc/workflow/{ => img}/good_commit.png | Bin doc/workflow/{ => img}/merge_commits.png | Bin doc/workflow/{ => img}/merge_request.png | Bin doc/workflow/{ => img}/messy_flow.png | Bin doc/workflow/{ => img}/mr_inline_comments.png | Bin doc/workflow/{ => img}/production_branch.png | Bin doc/workflow/{ => img}/rebase.png | Bin doc/workflow/{ => img}/release_branches.png | Bin doc/workflow/{ => img}/remove_checkbox.png | Bin 18 files changed, 18 insertions(+), 17 deletions(-) rename doc/workflow/{ => img}/ci_mr.png (100%) rename doc/workflow/{ => img}/close_issue_mr.png (100%) rename doc/workflow/{ => img}/environment_branches.png (100%) rename doc/workflow/{ => img}/four_stages.png (100%) rename doc/workflow/{ => img}/git_pull.png (100%) rename doc/workflow/{ => img}/gitdashflow.png (100%) rename doc/workflow/{ => img}/github_flow.png (100%) rename doc/workflow/{ => img}/gitlab_flow.png (100%) rename doc/workflow/{ => img}/good_commit.png (100%) rename doc/workflow/{ => img}/merge_commits.png (100%) rename doc/workflow/{ => img}/merge_request.png (100%) rename doc/workflow/{ => img}/messy_flow.png (100%) rename doc/workflow/{ => img}/mr_inline_comments.png (100%) rename doc/workflow/{ => img}/production_branch.png (100%) rename doc/workflow/{ => img}/rebase.png (100%) rename doc/workflow/{ => img}/release_branches.png (100%) rename doc/workflow/{ => img}/remove_checkbox.png (100%) diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index 0cb390e1242..2f365e42cc9 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -1,7 +1,8 @@ -![GitLab Flow](gitlab_flow.png) # Introduction to GitLab Flow +![GitLab Flow](img/gitlab_flow.png) + Git allows a wide variety of branching strategies and workflows. Because of this, many organizations end up with workflows that are too complicated, not clearly defined, or not integrated with issue tracking systems. Therefore, we propose GitLab flow as a clearly defined set of best practices. @@ -11,7 +12,7 @@ Organizations coming to Git from other version control systems frequently find i This article describes GitLab flow, which integrates the Git workflow with an issue tracking system. It offers a simple, transparent, and effective way to work with Git. -![Four stages (working copy, index, local repo, remote repo) and three steps between them](four_stages.png) +![Four stages (working copy, index, local repo, remote repo) and three steps between them](img/four_stages.png) When converting to Git, you have to get used to the fact that it takes three steps to share a commit with colleagues. Most version control systems have only one step: committing from the working copy to a shared server. @@ -19,7 +20,7 @@ In Git, you add files from the working copy to the staging area. After that, you The third step is pushing to a shared remote repository. After getting used to these three steps, the next challenge is the branching model. -![Multiple long-running branches and merging in all directions](messy_flow.png) +![Multiple long-running branches and merging in all directions](img/messy_flow.png) Since many organizations new to Git have no conventions for how to work with it, their repositories can quickly become messy. The biggest problem is that many long-running branches emerge that all contain part of the changes. @@ -31,7 +32,7 @@ For a video introduction of how this works in GitLab, see [GitLab Flow](https:// ## Git flow and its problems -![Git Flow timeline by Vincent Driessen, used with permission](gitdashflow.png) +![Git Flow timeline by Vincent Driessen, used with permission](img/gitdashflow.png) Git flow was one of the first proposals to use Git branches, and it has received a lot of attention. It suggests a `master` branch and a separate `develop` branch, as well as supporting branches for features, releases, and hotfixes. @@ -54,7 +55,7 @@ For example, many projects do releases but don't need to do hotfixes. ## GitHub flow as a simpler alternative -![Master branch with feature branches merged in](github_flow.png) +![Master branch with feature branches merged in](img/github_flow.png) In reaction to Git flow, GitHub created a simpler alternative. [GitHub flow](https://guides.github.com/introduction/flow/index.html) has only feature branches and a `master` branch. @@ -66,7 +67,7 @@ With GitLab flow, we offer additional guidance for these questions. ## Production branch with GitLab flow -![Master branch and production branch with an arrow that indicates a deployment](production_branch.png) +![Master branch and production branch with an arrow that indicates a deployment](img/production_branch.png) GitHub flow assumes you can deploy to production every time you merge a feature branch. While this is possible in some cases, such as SaaS applications, there are many cases where this is not possible. @@ -82,7 +83,7 @@ This flow prevents the overhead of releasing, tagging, and merging that happens ## Environment branches with GitLab flow -![Multiple branches with the code cascading from one to another](environment_branches.png) +![Multiple branches with the code cascading from one to another](img/environment_branches.png) It might be a good idea to have an environment that is automatically updated to the `master` branch. Only, in this case, the name of this environment might differ from the branch name. @@ -98,7 +99,7 @@ If this is not possible because more manual testing is required, you can send me ## Release branches with GitLab flow -![Master and multiple release branches that vary in length with cherry-picks from master](release_branches.png) +![Master and multiple release branches that vary in length with cherry-picks from master](img/release_branches.png) You only need to work with release branches if you need to release software to the outside world. In this case, each branch contains a minor version, for example, 2-3-stable, 2-4-stable, etc. @@ -114,7 +115,7 @@ In this flow, it is not common to have a production branch (or Git flow `master` ## Merge/pull requests with GitLab flow -![Merge request with inline comments](mr_inline_comments.png) +![Merge request with inline comments](img/mr_inline_comments.png) Merge or pull requests are created in a Git management application. They ask an assigned person to merge two branches. Tools such as GitHub and Bitbucket choose the name "pull request" since the first manual action is to pull the feature branch. @@ -147,11 +148,11 @@ It also ensures that if someone reopens the issue, they can use the same branch NOTE: **Note:** When you reopen an issue you need to create a new merge request. -![Remove checkbox for branch in merge requests](remove_checkbox.png) +![Remove checkbox for branch in merge requests](img/remove_checkbox.png) ## Issue tracking with GitLab flow -![Merge request with the branch name "15-require-a-password-to-change-it" and assignee field shown](merge_request.png) +![Merge request with the branch name "15-require-a-password-to-change-it" and assignee field shown](img/merge_request.png) GitLab flow is a way to make the relation between the code and the issue tracker more transparent. @@ -192,7 +193,7 @@ It is possible that one feature branch solves more than one issue. ## Linking and closing issues from merge requests -![Merge request showing the linked issues that will be closed](close_issue_mr.png) +![Merge request showing the linked issues that will be closed](img/close_issue_mr.png) Link to issues by mentioning them in commit messages or the description of a merge request, for example, "Fixes #16" or "Duck typing is preferred. See #12." GitLab then creates links to the mentioned issues and creates comments in the issues linking back to the merge request. @@ -203,7 +204,7 @@ If you have an issue that spans across multiple repositories, create an issue fo ## Squashing commits with rebase -![Vim screen showing the rebase view](rebase.png) +![Vim screen showing the rebase view](img/rebase.png) With Git, you can use an interactive rebase (`rebase -i`) to squash multiple commits into one or reorder them. This functionality is useful if you want to replace a couple of small commits with a single commit, or if you want to make the order more logical. @@ -229,7 +230,7 @@ Git does not allow you to merge the code again otherwise. ## Reducing merge commits in feature branches -![List of sequential merge commits](merge_commits.png) +![List of sequential merge commits](img/merge_commits.png) Having lots of merge commits can make your repository history messy. Therefore, you should try to avoid merge commits in feature branches. @@ -289,7 +290,7 @@ Sharing your work before it's complete also allows for discussion and feedback a ## How to write a good commit message -![Good and bad commit message](good_commit.png) +![Good and bad commit message](img/good_commit.png) A commit message should reflect your intention, not just the contents of the commit. It is easy to see the changes in a commit, so the commit message should explain why you made those changes. @@ -300,7 +301,7 @@ For more information about formatting commit messages, please see this excellent ## Testing before merging -![Merge requests showing the test states: red, yellow, and green](ci_mr.png) +![Merge requests showing the test states: red, yellow, and green](img/ci_mr.png) In old workflows, the continuous integration (CI) server commonly ran tests on the `master` branch only. Developers had to ensure their code did not break the `master` branch. @@ -317,7 +318,7 @@ As said before, if you often have feature branches that last for more than a few ## Working with feature branches -![Shell output showing git pull output](git_pull.png) +![Shell output showing git pull output](img/git_pull.png) When creating a feature branch, always branch from an up-to-date `master`. If you know before you start that your work depends on another branch, you can also branch from there. diff --git a/doc/workflow/ci_mr.png b/doc/workflow/img/ci_mr.png similarity index 100% rename from doc/workflow/ci_mr.png rename to doc/workflow/img/ci_mr.png diff --git a/doc/workflow/close_issue_mr.png b/doc/workflow/img/close_issue_mr.png similarity index 100% rename from doc/workflow/close_issue_mr.png rename to doc/workflow/img/close_issue_mr.png diff --git a/doc/workflow/environment_branches.png b/doc/workflow/img/environment_branches.png similarity index 100% rename from doc/workflow/environment_branches.png rename to doc/workflow/img/environment_branches.png diff --git a/doc/workflow/four_stages.png b/doc/workflow/img/four_stages.png similarity index 100% rename from doc/workflow/four_stages.png rename to doc/workflow/img/four_stages.png diff --git a/doc/workflow/git_pull.png b/doc/workflow/img/git_pull.png similarity index 100% rename from doc/workflow/git_pull.png rename to doc/workflow/img/git_pull.png diff --git a/doc/workflow/gitdashflow.png b/doc/workflow/img/gitdashflow.png similarity index 100% rename from doc/workflow/gitdashflow.png rename to doc/workflow/img/gitdashflow.png diff --git a/doc/workflow/github_flow.png b/doc/workflow/img/github_flow.png similarity index 100% rename from doc/workflow/github_flow.png rename to doc/workflow/img/github_flow.png diff --git a/doc/workflow/gitlab_flow.png b/doc/workflow/img/gitlab_flow.png similarity index 100% rename from doc/workflow/gitlab_flow.png rename to doc/workflow/img/gitlab_flow.png diff --git a/doc/workflow/good_commit.png b/doc/workflow/img/good_commit.png similarity index 100% rename from doc/workflow/good_commit.png rename to doc/workflow/img/good_commit.png diff --git a/doc/workflow/merge_commits.png b/doc/workflow/img/merge_commits.png similarity index 100% rename from doc/workflow/merge_commits.png rename to doc/workflow/img/merge_commits.png diff --git a/doc/workflow/merge_request.png b/doc/workflow/img/merge_request.png similarity index 100% rename from doc/workflow/merge_request.png rename to doc/workflow/img/merge_request.png diff --git a/doc/workflow/messy_flow.png b/doc/workflow/img/messy_flow.png similarity index 100% rename from doc/workflow/messy_flow.png rename to doc/workflow/img/messy_flow.png diff --git a/doc/workflow/mr_inline_comments.png b/doc/workflow/img/mr_inline_comments.png similarity index 100% rename from doc/workflow/mr_inline_comments.png rename to doc/workflow/img/mr_inline_comments.png diff --git a/doc/workflow/production_branch.png b/doc/workflow/img/production_branch.png similarity index 100% rename from doc/workflow/production_branch.png rename to doc/workflow/img/production_branch.png diff --git a/doc/workflow/rebase.png b/doc/workflow/img/rebase.png similarity index 100% rename from doc/workflow/rebase.png rename to doc/workflow/img/rebase.png diff --git a/doc/workflow/release_branches.png b/doc/workflow/img/release_branches.png similarity index 100% rename from doc/workflow/release_branches.png rename to doc/workflow/img/release_branches.png diff --git a/doc/workflow/remove_checkbox.png b/doc/workflow/img/remove_checkbox.png similarity index 100% rename from doc/workflow/remove_checkbox.png rename to doc/workflow/img/remove_checkbox.png From 0fd2a53061baf4af112a38f4993f115d8a17bb59 Mon Sep 17 00:00:00 2001 From: Lukas Eipert Date: Fri, 5 Jul 2019 08:31:57 +0000 Subject: [PATCH 089/195] Update dependency @gitlab/ui to ^5.3.2 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index e645eb8ed1c..47e7dd9c9c9 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@babel/preset-env": "^7.4.4", "@gitlab/csslab": "^1.9.0", "@gitlab/svgs": "^1.66.0", - "@gitlab/ui": "^5.1.0", + "@gitlab/ui": "^5.3.2", "apollo-cache-inmemory": "^1.5.1", "apollo-client": "^2.5.1", "apollo-link": "^1.2.11", diff --git a/yarn.lock b/yarn.lock index 1e04c82df1c..b76eba830b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -705,10 +705,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.66.0.tgz#3c02da455421ea241f32e915671842435df027ff" integrity sha512-nxOoQPnofMs3BjRr3SVzQcclM0G6QFrLM8L4nnUCN+8Gxq2u8ukfSU5FCrkivXz+FP9Qo/FYilWV7CY8kDkt6A== -"@gitlab/ui@^5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.1.0.tgz#b8c8f266edc68f616e92c0ba3a18a692002393e4" - integrity sha512-IPgk5W7mSXcbni+zNuJeVU89Co72jSQAXTxU7AtmItt5XT6nI9US2ZAWNUl8XCiOOw81jzYv0PLp4bMiXdLkww== +"@gitlab/ui@^5.3.2": + version "5.3.2" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.3.2.tgz#8ee906cf0586834de0077e165f25764c0bf8a9e9" + integrity sha512-VgxlDXqG2q+u72Km+/Ljdvjh0DzvljvsztiXTxnOO+Eb+/I26JBWfdboqFr3E02JzT8W4s4rRinhRttLWfcM/A== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.2.1" From b71250ca0f1b9df4f728bdb322502e3544058ca5 Mon Sep 17 00:00:00 2001 From: sujay patel Date: Thu, 13 Jun 2019 01:33:21 +0530 Subject: [PATCH 090/195] Adding order by to list runner jobs api. --- app/finders/runner_jobs_finder.rb | 22 ++++++++++- .../51794-add-ordering-to-runner-jobs-api.yml | 5 +++ doc/api/runners.md | 2 + spec/finders/runner_jobs_finder_spec.rb | 37 +++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml diff --git a/app/finders/runner_jobs_finder.rb b/app/finders/runner_jobs_finder.rb index 4fca4ec94f3..f1ee1d38255 100644 --- a/app/finders/runner_jobs_finder.rb +++ b/app/finders/runner_jobs_finder.rb @@ -3,6 +3,8 @@ class RunnerJobsFinder attr_reader :runner, :params + ALLOWED_INDEXED_COLUMNS = %w[id created_at].freeze + def initialize(runner, params = {}) @runner = runner @params = params @@ -11,7 +13,7 @@ class RunnerJobsFinder def execute items = @runner.builds items = by_status(items) - items + sort_items(items) end private @@ -23,4 +25,22 @@ class RunnerJobsFinder items.where(status: params[:status]) end # rubocop: enable CodeReuse/ActiveRecord + + # rubocop: disable CodeReuse/ActiveRecord + def sort_items(items) + order_by = if ALLOWED_INDEXED_COLUMNS.include?(params[:order_by]) + params[:order_by] + else + :id + end + + sort = if params[:sort] =~ /\A(ASC|DESC)\z/i + params[:sort] + else + :desc + end + + items.order(order_by => sort) + end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml b/changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml new file mode 100644 index 00000000000..6af61d7b145 --- /dev/null +++ b/changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml @@ -0,0 +1,5 @@ +--- +title: 51794-add-order-by-to-list-runner-jobs-api +merge_request: +author: Sujay Patel +type: added diff --git a/doc/api/runners.md b/doc/api/runners.md index 90e9fbff247..425b9aeef1b 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -291,6 +291,8 @@ GET /runners/:id/jobs |-----------|---------|----------|---------------------| | `id` | integer | yes | The ID of a runner | | `status` | string | no | Status of the job; one of: `running`, `success`, `failed`, `canceled` | +| `order_by`| string | no | Order jobs by `id` or `created_at` (default: id) | +| `sort` | string | no | Sort jobs in `asc` or `desc` order (default: `desc`) | ``` curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/runners/1/jobs?status=running" diff --git a/spec/finders/runner_jobs_finder_spec.rb b/spec/finders/runner_jobs_finder_spec.rb index 97304170c4e..9ed6f50ddfb 100644 --- a/spec/finders/runner_jobs_finder_spec.rb +++ b/spec/finders/runner_jobs_finder_spec.rb @@ -35,5 +35,42 @@ describe RunnerJobsFinder do end end end + + context 'when order_by and sort are specified' do + context 'when order_by created_at' do + let(:params) { { order_by: 'created_at', sort: 'asc' } } + let!(:jobs) { Array.new(2) { create(:ci_build, runner: runner, project: project, user: create(:user)) } } + + it 'sorts as created_at: :asc' do + is_expected.to match_array(jobs) + end + + context 'when sort is invalid' do + let(:params) { { order_by: 'created_at', sort: 'invalid_sort' } } + + it 'sorts as created_at: :desc' do + is_expected.to eq(jobs.sort_by { |p| -p.user.id }) + end + end + end + + context 'when order_by is invalid' do + let(:params) { { order_by: 'invalid_column', sort: 'asc' } } + let!(:jobs) { Array.new(2) { create(:ci_build, runner: runner, project: project, user: create(:user)) } } + + it 'sorts as id: :asc' do + is_expected.to eq(jobs.sort_by { |p| p.id }) + end + end + + context 'when both are nil' do + let(:params) { { order_by: nil, sort: nil } } + let!(:jobs) { Array.new(2) { create(:ci_build, runner: runner, project: project, user: create(:user)) } } + + it 'sorts as id: :desc' do + is_expected.to eq(jobs.sort_by { |p| -p.id }) + end + end + end end end From 7fd7406d56f93db751d7ec3c69e3a30c1e6df14c Mon Sep 17 00:00:00 2001 From: Sarah Yasonik Date: Fri, 5 Jul 2019 08:49:33 +0000 Subject: [PATCH 091/195] Fix alert creation dropdown menu --- .../javascripts/monitoring/stores/utils.js | 17 +++++++-- .../monitoring/store/utils_spec.js | 37 +++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 spec/javascripts/monitoring/store/utils_spec.js diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js index 84e1f1c4c20..721942f9d3b 100644 --- a/app/assets/javascripts/monitoring/stores/utils.js +++ b/app/assets/javascripts/monitoring/stores/utils.js @@ -36,15 +36,26 @@ function removeTimeSeriesNoData(queries) { // { metricId: 2, ...query2Attrs }] }, // { title: 'new title', y_label: 'MB', queries: [{ metricId: 3, ...query3Attrs }]} // ] -function groupQueriesByChartInfo(metrics) { +export function groupQueriesByChartInfo(metrics) { const metricsByChart = metrics.reduce((accumulator, metric) => { const { queries, ...chart } = metric; - const metricId = chart.id ? chart.id.toString() : null; const chartKey = `${chart.title}|${chart.y_label}`; accumulator[chartKey] = accumulator[chartKey] || { ...chart, queries: [] }; - queries.forEach(queryAttrs => accumulator[chartKey].queries.push({ metricId, ...queryAttrs })); + queries.forEach(queryAttrs => { + let metricId; + + if (chart.id) { + metricId = chart.id.toString(); + } else if (queryAttrs.metric_id) { + metricId = queryAttrs.metric_id.toString(); + } else { + metricId = null; + } + + accumulator[chartKey].queries.push({ metricId, ...queryAttrs }); + }); return accumulator; }, {}); diff --git a/spec/javascripts/monitoring/store/utils_spec.js b/spec/javascripts/monitoring/store/utils_spec.js new file mode 100644 index 00000000000..73dd370ffb3 --- /dev/null +++ b/spec/javascripts/monitoring/store/utils_spec.js @@ -0,0 +1,37 @@ +import { groupQueriesByChartInfo } from '~/monitoring/stores/utils'; + +describe('groupQueriesByChartInfo', () => { + let input; + let output; + + it('groups metrics with the same chart title and y_axis label', () => { + input = [ + { title: 'title', y_label: 'MB', queries: [{}] }, + { title: 'title', y_label: 'MB', queries: [{}] }, + { title: 'new title', y_label: 'MB', queries: [{}] }, + ]; + + output = [ + { title: 'title', y_label: 'MB', queries: [{ metricId: null }, { metricId: null }] }, + { title: 'new title', y_label: 'MB', queries: [{ metricId: null }] }, + ]; + + expect(groupQueriesByChartInfo(input)).toEqual(output); + }); + + // Functionality associated with the /additional_metrics endpoint + it("associates a chart's stringified metric_id with the metric", () => { + input = [{ id: 3, title: 'new title', y_label: 'MB', queries: [{}] }]; + output = [{ id: 3, title: 'new title', y_label: 'MB', queries: [{ metricId: '3' }] }]; + + expect(groupQueriesByChartInfo(input)).toEqual(output); + }); + + // Functionality associated with the /metrics_dashboard endpoint + it('aliases a stringified metrics_id on the metric to the metricId key', () => { + input = [{ title: 'new title', y_label: 'MB', queries: [{ metric_id: 3 }] }]; + output = [{ title: 'new title', y_label: 'MB', queries: [{ metricId: '3', metric_id: 3 }] }]; + + expect(groupQueriesByChartInfo(input)).toEqual(output); + }); +}); From 1391ba13b788f7c7c1042c8499019a32299947e5 Mon Sep 17 00:00:00 2001 From: Lucas Charles Date: Fri, 5 Jul 2019 08:50:39 +0000 Subject: [PATCH 092/195] Update SAST.gitlab-ci.yml w/ FAIL_NEVER ENV https://gitlab.com/gitlab-org/gitlab-ee/issues/10564 --- lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml index 8713b833011..0a97a16b83c 100644 --- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml @@ -54,6 +54,7 @@ sast: MAVEN_PATH \ MAVEN_REPO_PATH \ SBT_PATH \ + FAIL_NEVER \ ) \ --volume "$PWD:/code" \ --volume /var/run/docker.sock:/var/run/docker.sock \ From e241c89977c32fabbbe5a49c1ba69564d5e09e31 Mon Sep 17 00:00:00 2001 From: sujay patel Date: Thu, 13 Jun 2019 01:33:21 +0530 Subject: [PATCH 093/195] Adding order by to list runner jobs api. --- app/finders/runner_jobs_finder.rb | 11 ++--- .../51794-add-ordering-to-runner-jobs-api.yml | 4 +- doc/api/runners.md | 2 +- lib/api/runners.rb | 2 + spec/finders/runner_jobs_finder_spec.rb | 35 +++++---------- spec/requests/api/runners_spec.rb | 44 +++++++++++++++++++ 6 files changed, 63 insertions(+), 35 deletions(-) diff --git a/app/finders/runner_jobs_finder.rb b/app/finders/runner_jobs_finder.rb index f1ee1d38255..ef90817416a 100644 --- a/app/finders/runner_jobs_finder.rb +++ b/app/finders/runner_jobs_finder.rb @@ -3,7 +3,7 @@ class RunnerJobsFinder attr_reader :runner, :params - ALLOWED_INDEXED_COLUMNS = %w[id created_at].freeze + ALLOWED_INDEXED_COLUMNS = %w[id].freeze def initialize(runner, params = {}) @runner = runner @@ -28,13 +28,10 @@ class RunnerJobsFinder # rubocop: disable CodeReuse/ActiveRecord def sort_items(items) - order_by = if ALLOWED_INDEXED_COLUMNS.include?(params[:order_by]) - params[:order_by] - else - :id - end + return items unless ALLOWED_INDEXED_COLUMNS.include?(params[:order_by]) - sort = if params[:sort] =~ /\A(ASC|DESC)\z/i + order_by = params[:order_by] + sort = if /\A(ASC|DESC)\z/i.match?(params[:sort]) params[:sort] else :desc diff --git a/changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml b/changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml index 6af61d7b145..908a132688c 100644 --- a/changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml +++ b/changelogs/unreleased/51794-add-ordering-to-runner-jobs-api.yml @@ -1,5 +1,5 @@ --- -title: 51794-add-order-by-to-list-runner-jobs-api -merge_request: +title: Add order_by and sort params to list runner jobs api +merge_request: 29629 author: Sujay Patel type: added diff --git a/doc/api/runners.md b/doc/api/runners.md index 425b9aeef1b..1318b9ca828 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -291,7 +291,7 @@ GET /runners/:id/jobs |-----------|---------|----------|---------------------| | `id` | integer | yes | The ID of a runner | | `status` | string | no | Status of the job; one of: `running`, `success`, `failed`, `canceled` | -| `order_by`| string | no | Order jobs by `id` or `created_at` (default: id) | +| `order_by`| string | no | Order jobs by `id`. | | `sort` | string | no | Sort jobs in `asc` or `desc` order (default: `desc`) | ``` diff --git a/lib/api/runners.rb b/lib/api/runners.rb index f3fea463e7f..c2d371b6867 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -115,6 +115,8 @@ module API params do requires :id, type: Integer, desc: 'The ID of the runner' optional :status, type: String, desc: 'Status of the job', values: Ci::Build::AVAILABLE_STATUSES + optional :order_by, type: String, desc: 'Order by `id` or not', values: RunnerJobsFinder::ALLOWED_INDEXED_COLUMNS + optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Sort by asc (ascending) or desc (descending)' use :pagination end get ':id/jobs' do diff --git a/spec/finders/runner_jobs_finder_spec.rb b/spec/finders/runner_jobs_finder_spec.rb index 9ed6f50ddfb..01f45a37ba8 100644 --- a/spec/finders/runner_jobs_finder_spec.rb +++ b/spec/finders/runner_jobs_finder_spec.rb @@ -37,38 +37,23 @@ describe RunnerJobsFinder do end context 'when order_by and sort are specified' do - context 'when order_by created_at' do - let(:params) { { order_by: 'created_at', sort: 'asc' } } - let!(:jobs) { Array.new(2) { create(:ci_build, runner: runner, project: project, user: create(:user)) } } - - it 'sorts as created_at: :asc' do - is_expected.to match_array(jobs) - end - - context 'when sort is invalid' do - let(:params) { { order_by: 'created_at', sort: 'invalid_sort' } } - - it 'sorts as created_at: :desc' do - is_expected.to eq(jobs.sort_by { |p| -p.user.id }) - end - end - end - - context 'when order_by is invalid' do - let(:params) { { order_by: 'invalid_column', sort: 'asc' } } - let!(:jobs) { Array.new(2) { create(:ci_build, runner: runner, project: project, user: create(:user)) } } + context 'when order_by id and sort is asc' do + let(:params) { { order_by: 'id', sort: 'asc' } } + let!(:jobs) { create_list(:ci_build, 2, runner: runner, project: project, user: create(:user)) } it 'sorts as id: :asc' do - is_expected.to eq(jobs.sort_by { |p| p.id }) + is_expected.to eq(jobs.sort_by(&:id)) end end + end - context 'when both are nil' do - let(:params) { { order_by: nil, sort: nil } } - let!(:jobs) { Array.new(2) { create(:ci_build, runner: runner, project: project, user: create(:user)) } } + context 'when order_by is specified and sort is not specified' do + context 'when order_by id and sort is not specified' do + let(:params) { { order_by: 'id' } } + let!(:jobs) { create_list(:ci_build, 2, runner: runner, project: project, user: create(:user)) } it 'sorts as id: :desc' do - is_expected.to eq(jobs.sort_by { |p| -p.id }) + is_expected.to eq(jobs.sort_by(&:id).reverse) end end end diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index 5548e3fd01a..f5ce3a3570e 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -584,6 +584,34 @@ describe API::Runners do end end + context 'when valid order_by is provided' do + context 'when sort order is not specified' do + it 'return jobs in descending order' do + get api("/runners/#{project_runner.id}/jobs?order_by=id", admin) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(2) + expect(json_response.first).to include('id' => job_5.id) + end + end + + context 'when sort order is specified as asc' do + it 'return jobs sorted in ascending order' do + get api("/runners/#{project_runner.id}/jobs?order_by=id&sort=asc", admin) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(2) + expect(json_response.first).to include('id' => job_4.id) + end + end + end + context 'when invalid status is provided' do it 'return 400' do get api("/runners/#{project_runner.id}/jobs?status=non-existing", admin) @@ -591,6 +619,22 @@ describe API::Runners do expect(response).to have_gitlab_http_status(400) end end + + context 'when invalid order_by is provided' do + it 'return 400' do + get api("/runners/#{project_runner.id}/jobs?order_by=non-existing", admin) + + expect(response).to have_gitlab_http_status(400) + end + end + + context 'when invalid sort is provided' do + it 'return 400' do + get api("/runners/#{project_runner.id}/jobs?sort=non-existing", admin) + + expect(response).to have_gitlab_http_status(400) + end + end end context "when runner doesn't exist" do From 8e6dea3a3d11a8bf5c3fb15ce9dade4cd71936e8 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 5 Jul 2019 11:14:46 +0100 Subject: [PATCH 094/195] Removes EE differences --- app/views/profiles/preferences/show.html.haml | 12 ++++++------ changelogs/unreleased/12553-preferences.yml | 5 +++++ 2 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 changelogs/unreleased/12553-preferences.yml diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 4ebfaff0860..d16e2dddbe0 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -92,21 +92,21 @@ .col-lg-8 .form-group %h5= s_('Preferences|Time format') - .checkbox-icon-inline-wrapper.form-check + .checkbox-icon-inline-wrapper - time_format_label = capture do = s_('Preferences|Display time in 24-hour format') - = f.check_box :time_format_in_24h, class: 'form-check-input' + = f.check_box :time_format_in_24h = f.label :time_format_in_24h do = time_format_label %h5= s_('Preferences|Time display') - .checkbox-icon-inline-wrapper.form-check + .checkbox-icon-inline-wrapper - time_display_label = capture do = s_('Preferences|Use relative times') - = f.check_box :time_display_relative, class: 'form-check-input' + = f.check_box :time_display_relative = f.label :time_display_relative do = time_display_label - .text-muted - = s_('Preferences|For example: 30 mins ago.') + .form-text.text-muted + = s_('Preferences|For example: 30 mins ago.') .col-lg-4.profile-settings-sidebar .col-lg-8 .form-group diff --git a/changelogs/unreleased/12553-preferences.yml b/changelogs/unreleased/12553-preferences.yml new file mode 100644 index 00000000000..c41a8c98e4e --- /dev/null +++ b/changelogs/unreleased/12553-preferences.yml @@ -0,0 +1,5 @@ +--- +title: Removes EE diff for app/views/profiles/preferences/show.html.haml +merge_request: +author: +type: other From 48307fac1ec7cd207fbd53762fd1226a9d6fb1a2 Mon Sep 17 00:00:00 2001 From: Shinya Maeda Date: Thu, 20 Jun 2019 19:45:46 +0700 Subject: [PATCH 095/195] Extend MergeToRefService for creating merge ref from the other ref Currently, MergeToRefService is specifically designed for createing merge commits from source branch and target branch of merge reqeusts. We extend this behavior to source branch and any target ref paths. --- Gemfile | 2 +- Gemfile.lock | 4 +- app/models/repository.rb | 4 +- .../merge_requests/merge_to_ref_service.rb | 16 +++++- .../unreleased/create-merge-train-ref-ce.yml | 5 ++ lib/gitlab/git/repository.rb | 4 +- lib/gitlab/gitaly_client/operation_service.rb | 5 +- spec/lib/gitlab/git/repository_spec.rb | 7 ++- .../gitaly_client/operation_service_spec.rb | 4 +- spec/models/repository_spec.rb | 5 +- .../merge_to_ref_service_spec.rb | 55 ++++++++++++++++--- 11 files changed, 85 insertions(+), 26 deletions(-) create mode 100644 changelogs/unreleased/create-merge-train-ref-ce.yml diff --git a/Gemfile b/Gemfile index 1264d75eac6..b1750afec91 100644 --- a/Gemfile +++ b/Gemfile @@ -429,7 +429,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 1.32.0', require: 'gitaly' +gem 'gitaly-proto', '~> 1.36.0', require: 'gitaly' gem 'grpc', '~> 1.19.0' diff --git a/Gemfile.lock b/Gemfile.lock index 5b648d43137..5099167e03f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -303,7 +303,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (1.32.0) + gitaly-proto (1.36.0) grpc (~> 1.0) github-markup (1.7.0) gitlab-labkit (0.3.0) @@ -1092,7 +1092,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 1.32.0) + gitaly-proto (~> 1.36.0) github-markup (~> 1.7.0) gitlab-labkit (~> 0.3.0) gitlab-markup (~> 1.7.0) diff --git a/app/models/repository.rb b/app/models/repository.rb index d087a5a7bbd..a408db7ebbe 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -839,10 +839,10 @@ class Repository end end - def merge_to_ref(user, source_sha, merge_request, target_ref, message) + def merge_to_ref(user, source_sha, merge_request, target_ref, message, first_parent_ref) branch = merge_request.target_branch - raw.merge_to_ref(user, source_sha, branch, target_ref, message) + raw.merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref) end def ff_merge(user, source, target_branch, merge_request: nil) diff --git a/app/services/merge_requests/merge_to_ref_service.rb b/app/services/merge_requests/merge_to_ref_service.rb index efe4dcd6255..0ea50a5dbf5 100644 --- a/app/services/merge_requests/merge_to_ref_service.rb +++ b/app/services/merge_requests/merge_to_ref_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module MergeRequests - # Performs the merge between source SHA and the target branch. Instead + # Performs the merge between source SHA and the target branch or the specified first parent ref. Instead # of writing the result to the MR target branch, it targets the `target_ref`. # # Ideally this should leave the `target_ref` state with the same state the @@ -56,12 +56,22 @@ module MergeRequests raise_error(error) if error end + ## + # The parameter `target_ref` is where the merge result will be written. + # Default is the merge ref i.e. `refs/merge-requests/:iid/merge`. def target_ref - merge_request.merge_ref_path + params[:target_ref] || merge_request.merge_ref_path + end + + ## + # The parameter `first_parent_ref` is the main line of the merge commit. + # Default is the target branch ref of the merge request. + def first_parent_ref + params[:first_parent_ref] || merge_request.target_branch_ref end def commit - repository.merge_to_ref(current_user, source, merge_request, target_ref, commit_message) + repository.merge_to_ref(current_user, source, merge_request, target_ref, commit_message, first_parent_ref) rescue Gitlab::Git::PreReceiveError => error raise MergeError, error.message end diff --git a/changelogs/unreleased/create-merge-train-ref-ce.yml b/changelogs/unreleased/create-merge-train-ref-ce.yml new file mode 100644 index 00000000000..b0b95275f58 --- /dev/null +++ b/changelogs/unreleased/create-merge-train-ref-ce.yml @@ -0,0 +1,5 @@ +--- +title: Extend `MergeToRefService` to create merge ref from an arbitrary ref +merge_request: 30361 +author: +type: added diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 19b6aab1c4f..060a29be782 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -536,9 +536,9 @@ module Gitlab tags.find { |tag| tag.name == name } end - def merge_to_ref(user, source_sha, branch, target_ref, message) + def merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref) wrapped_gitaly_errors do - gitaly_operation_client.user_merge_to_ref(user, source_sha, branch, target_ref, message) + gitaly_operation_client.user_merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref) end end diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index b42e6cbad8d..783c2ff0915 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -100,14 +100,15 @@ module Gitlab end end - def user_merge_to_ref(user, source_sha, branch, target_ref, message) + def user_merge_to_ref(user, source_sha, branch, target_ref, message, first_parent_ref) request = Gitaly::UserMergeToRefRequest.new( repository: @gitaly_repo, source_sha: source_sha, branch: encode_binary(branch), target_ref: encode_binary(target_ref), user: Gitlab::Git::User.from_gitlab(user).to_gitaly, - message: encode_binary(message) + message: encode_binary(message), + first_parent_ref: encode_binary(first_parent_ref) ) response = GitalyClient.call(@repository.storage, :operation_service, :user_merge_to_ref, request) diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index cceeae8afe6..a28b95e5bff 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1694,14 +1694,15 @@ describe Gitlab::Git::Repository, :seed_helper do let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } let(:left_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } let(:right_branch) { 'test-master' } + let(:first_parent_ref) { 'refs/heads/test-master' } let(:target_ref) { 'refs/merge-requests/999/merge' } before do - repository.create_branch(right_branch, branch_head) unless repository.branch_exists?(right_branch) + repository.create_branch(right_branch, branch_head) unless repository.ref_exists?(first_parent_ref) end def merge_to_ref - repository.merge_to_ref(user, left_sha, right_branch, target_ref, 'Merge message') + repository.merge_to_ref(user, left_sha, right_branch, target_ref, 'Merge message', first_parent_ref) end it 'generates a commit in the target_ref' do @@ -1716,7 +1717,7 @@ describe Gitlab::Git::Repository, :seed_helper do end it 'does not change the right branch HEAD' do - expect { merge_to_ref }.not_to change { repository.find_branch(right_branch).target } + expect { merge_to_ref }.not_to change { repository.commit(first_parent_ref).sha } end end diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index 18663a72fcd..f38b8d31237 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -79,13 +79,13 @@ describe Gitlab::GitalyClient::OperationService do end describe '#user_merge_to_ref' do - let(:branch) { 'my-branch' } + let(:first_parent_ref) { 'refs/heads/my-branch' } let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } let(:ref) { 'refs/merge-requests/x/merge' } let(:message) { 'validación' } let(:response) { Gitaly::UserMergeToRefResponse.new(commit_id: 'new-commit-id') } - subject { client.user_merge_to_ref(user, source_sha, branch, ref, message) } + subject { client.user_merge_to_ref(user, source_sha, nil, ref, message, first_parent_ref) } it 'sends a user_merge_to_ref message' do expect_any_instance_of(Gitaly::OperationService::Stub) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 13da7bd7407..3d967aa4ab8 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1420,12 +1420,13 @@ describe Repository do source_project: project) end - it 'writes merge of source and target to MR merge_ref_path' do + it 'writes merge of source SHA and first parent ref to MR merge_ref_path' do merge_commit_id = repository.merge_to_ref(user, merge_request.diff_head_sha, merge_request, merge_request.merge_ref_path, - 'Custom message') + 'Custom message', + merge_request.target_branch_ref) merge_commit = repository.commit(merge_commit_id) diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb index 61f99f82a76..e2f201677fa 100644 --- a/spec/services/merge_requests/merge_to_ref_service_spec.rb +++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb @@ -22,7 +22,6 @@ describe MergeRequests::MergeToRefService do shared_examples_for 'successfully merges to ref with merge method' do it 'writes commit to merge ref' do repository = project.repository - target_ref = merge_request.merge_ref_path expect(repository.ref_exists?(target_ref)).to be(false) @@ -33,7 +32,7 @@ describe MergeRequests::MergeToRefService do expect(result[:status]).to eq(:success) expect(result[:commit_id]).to be_present expect(result[:source_id]).to eq(merge_request.source_branch_sha) - expect(result[:target_id]).to eq(merge_request.target_branch_sha) + expect(result[:target_id]).to eq(repository.commit(first_parent_ref).sha) expect(repository.ref_exists?(target_ref)).to be(true) expect(ref_head.id).to eq(result[:commit_id]) end @@ -74,17 +73,22 @@ describe MergeRequests::MergeToRefService do describe '#execute' do let(:service) do - described_class.new(project, user, commit_message: 'Awesome message', - should_remove_source_branch: true) + described_class.new(project, user, **params) end + let(:params) { { commit_message: 'Awesome message', should_remove_source_branch: true } } + def process_merge_to_ref perform_enqueued_jobs do service.execute(merge_request) end end - it_behaves_like 'successfully merges to ref with merge method' + it_behaves_like 'successfully merges to ref with merge method' do + let(:first_parent_ref) { 'refs/heads/master' } + let(:target_ref) { merge_request.merge_ref_path } + end + it_behaves_like 'successfully evaluates pre-condition checks' context 'commit history comparison with regular MergeService' do @@ -129,14 +133,22 @@ describe MergeRequests::MergeToRefService do context 'when semi-linear merge method' do let(:merge_method) { :rebase_merge } - it_behaves_like 'successfully merges to ref with merge method' + it_behaves_like 'successfully merges to ref with merge method' do + let(:first_parent_ref) { 'refs/heads/master' } + let(:target_ref) { merge_request.merge_ref_path } + end + it_behaves_like 'successfully evaluates pre-condition checks' end context 'when fast-forward merge method' do let(:merge_method) { :ff } - it_behaves_like 'successfully merges to ref with merge method' + it_behaves_like 'successfully merges to ref with merge method' do + let(:first_parent_ref) { 'refs/heads/master' } + let(:target_ref) { merge_request.merge_ref_path } + end + it_behaves_like 'successfully evaluates pre-condition checks' end @@ -178,5 +190,34 @@ describe MergeRequests::MergeToRefService do it { expect(todo).not_to be_done } end + + describe 'cascading merge refs' do + set(:project) { create(:project, :repository) } + let(:params) { { commit_message: 'Cascading merge', first_parent_ref: first_parent_ref, target_ref: target_ref } } + + context 'when first merge happens' do + let(:merge_request) do + create(:merge_request, source_project: project, source_branch: 'feature', + target_project: project, target_branch: 'master') + end + + it_behaves_like 'successfully merges to ref with merge method' do + let(:first_parent_ref) { 'refs/heads/master' } + let(:target_ref) { 'refs/merge-requests/1/train' } + end + + context 'when second merge happens' do + let(:merge_request) do + create(:merge_request, source_project: project, source_branch: 'improve/awesome', + target_project: project, target_branch: 'master') + end + + it_behaves_like 'successfully merges to ref with merge method' do + let(:first_parent_ref) { 'refs/merge-requests/1/train' } + let(:target_ref) { 'refs/merge-requests/2/train' } + end + end + end + end end end From 4c954a5c9e54e768aaca0eb787d8d9d2f63ebcae Mon Sep 17 00:00:00 2001 From: Denys Mishunov Date: Wed, 3 Jul 2019 10:19:32 +0200 Subject: [PATCH 096/195] Mark images as binary on init --- app/assets/javascripts/ide/lib/files.js | 5 +++-- .../vue_shared/components/content_viewer/lib/viewer_utils.js | 1 + spec/frontend/ide/lib/files_spec.js | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/ide/lib/files.js b/app/assets/javascripts/ide/lib/files.js index b8abaa41f23..51278640b5b 100644 --- a/app/assets/javascripts/ide/lib/files.js +++ b/app/assets/javascripts/ide/lib/files.js @@ -77,6 +77,7 @@ export const decorateFiles = ({ const fileFolder = parent && insertParent(parent); if (name) { + const previewMode = viewerInformationForPath(name); parentPath = fileFolder && fileFolder.path; file = decorateData({ @@ -92,9 +93,9 @@ export const decorateFiles = ({ changed: tempFile, content, base64, - binary, + binary: (previewMode && previewMode.binary) || binary, rawPath, - previewMode: viewerInformationForPath(name), + previewMode, parentPath, }); diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js index ba63683f5c0..da0b45110e2 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js +++ b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js @@ -3,6 +3,7 @@ import { __ } from '~/locale'; const viewers = { image: { id: 'image', + binary: true, }, markdown: { id: 'markdown', diff --git a/spec/frontend/ide/lib/files_spec.js b/spec/frontend/ide/lib/files_spec.js index aa1fa0373db..08a31318544 100644 --- a/spec/frontend/ide/lib/files_spec.js +++ b/spec/frontend/ide/lib/files_spec.js @@ -12,6 +12,7 @@ const createEntries = paths => { const { name, parent } = splitParent(path); const parentEntry = acc[parent]; + const previewMode = viewerInformationForPath(name); acc[path] = { ...decorateData({ @@ -22,7 +23,8 @@ const createEntries = paths => { path, url: createUrl(`/${TEST_PROJECT_ID}/${type}/${TEST_BRANCH_ID}/-/${escapeFileUrl(path)}`), type, - previewMode: viewerInformationForPath(path), + previewMode, + binary: (previewMode && previewMode.binary) || false, parentPath: parent, parentTreeUrl: parentEntry ? parentEntry.url From fad92a2ecd943e52323e9e978df84ba963057a6e Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 5 Jul 2019 11:50:07 +0100 Subject: [PATCH 097/195] Fix divergence graph loading error Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/64143 --- app/assets/javascripts/branches/divergence_graph.js | 4 +++- spec/frontend/branches/divergence_graph_spec.js | 12 ++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/branches/divergence_graph.js b/app/assets/javascripts/branches/divergence_graph.js index 96bc6a5f8e8..7dbaf984acf 100644 --- a/app/assets/javascripts/branches/divergence_graph.js +++ b/app/assets/javascripts/branches/divergence_graph.js @@ -36,7 +36,9 @@ export default endpoint => { }, 100); Object.entries(data).forEach(([branchName, val]) => { - const el = document.querySelector(`.js-branch-${branchName} .js-branch-divergence-graph`); + const el = document.querySelector( + `[data-name="${branchName}"] .js-branch-divergence-graph`, + ); if (!el) return; diff --git a/spec/frontend/branches/divergence_graph_spec.js b/spec/frontend/branches/divergence_graph_spec.js index 4ed77c3a036..8283bc966e4 100644 --- a/spec/frontend/branches/divergence_graph_spec.js +++ b/spec/frontend/branches/divergence_graph_spec.js @@ -10,12 +10,14 @@ describe('Divergence graph', () => { mock.onGet('/-/diverging_counts').reply(200, { master: { ahead: 1, behind: 1 }, + 'test/hello-world': { ahead: 1, behind: 1 }, }); jest.spyOn(axios, 'get'); document.body.innerHTML = ` -
+
+
`; }); @@ -26,7 +28,13 @@ describe('Divergence graph', () => { it('calls axos get with list of branch names', () => init('/-/diverging_counts').then(() => { expect(axios.get).toHaveBeenCalledWith('/-/diverging_counts', { - params: { names: ['master'] }, + params: { names: ['master', 'test/hello-world'] }, }); })); + + it('creates Vue components', () => + init('/-/diverging_counts').then(() => { + expect(document.querySelector('[data-name="master"]').innerHTML).not.toEqual(''); + expect(document.querySelector('[data-name="test/hello-world"]').innerHTML).not.toEqual(''); + })); }); From 6ef6693e3767d480362ce528dd0beff159c895ff Mon Sep 17 00:00:00 2001 From: Patrick Bajao Date: Fri, 5 Jul 2019 11:03:47 +0000 Subject: [PATCH 098/195] Refactor PositionTracer to support different types This is to prepare for supporing image type position tracing --- .../update_diff_position_service.rb | 3 +- app/services/system_note_service.rb | 8 +- changelogs/unreleased/57793-fix-line-age.yml | 5 + lib/gitlab/diff/position.rb | 4 + lib/gitlab/diff/position_tracer.rb | 192 +- .../diff/position_tracer/base_strategy.rb | 26 + .../diff/position_tracer/image_strategy.rb | 50 + .../diff/position_tracer/line_strategy.rb | 201 ++ spec/lib/gitlab/diff/position_spec.rb | 13 + .../position_tracer/image_strategy_spec.rb | 238 ++ .../position_tracer/line_strategy_spec.rb | 1805 +++++++++++++++ spec/lib/gitlab/diff/position_tracer_spec.rb | 1930 +---------------- spec/services/system_note_service_spec.rb | 24 +- .../helpers/position_tracer_helpers.rb | 93 + 14 files changed, 2533 insertions(+), 2059 deletions(-) create mode 100644 changelogs/unreleased/57793-fix-line-age.yml create mode 100644 lib/gitlab/diff/position_tracer/base_strategy.rb create mode 100644 lib/gitlab/diff/position_tracer/image_strategy.rb create mode 100644 lib/gitlab/diff/position_tracer/line_strategy.rb create mode 100644 spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb create mode 100644 spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb create mode 100644 spec/support/helpers/position_tracer_helpers.rb diff --git a/app/services/discussions/update_diff_position_service.rb b/app/services/discussions/update_diff_position_service.rb index c61437fb2e3..7bdf7711155 100644 --- a/app/services/discussions/update_diff_position_service.rb +++ b/app/services/discussions/update_diff_position_service.rb @@ -3,7 +3,8 @@ module Discussions class UpdateDiffPositionService < BaseService def execute(discussion) - result = tracer.trace(discussion.position) + old_position = discussion.position + result = tracer.trace(old_position) return unless result position = result[:position] diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 4783417ad6d..1d02d7ed787 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -268,11 +268,13 @@ module SystemNoteService merge_request = discussion.noteable diff_refs = change_position.diff_refs version_index = merge_request.merge_request_diffs.viewable.count + position_on_text = change_position.on_text? + text_parts = ["changed this #{position_on_text ? 'line' : 'file'} in"] - text_parts = ["changed this line in"] if version_params = merge_request.version_params_for(diff_refs) - line_code = change_position.line_code(project.repository) - url = url_helpers.diffs_project_merge_request_path(project, merge_request, version_params.merge(anchor: line_code)) + repository = project.repository + anchor = position_on_text ? change_position.line_code(repository) : change_position.file_hash + url = url_helpers.diffs_project_merge_request_path(project, merge_request, version_params.merge(anchor: anchor)) text_parts << "[version #{version_index} of the diff](#{url})" else diff --git a/changelogs/unreleased/57793-fix-line-age.yml b/changelogs/unreleased/57793-fix-line-age.yml new file mode 100644 index 00000000000..cf4e328e54a --- /dev/null +++ b/changelogs/unreleased/57793-fix-line-age.yml @@ -0,0 +1,5 @@ +--- +title: Support note position tracing on an image +merge_request: 30158 +author: +type: fixed diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb index d349c378e53..dfa80eb4a64 100644 --- a/lib/gitlab/diff/position.rb +++ b/lib/gitlab/diff/position.rb @@ -134,6 +134,10 @@ module Gitlab @line_code ||= diff_file(repository)&.line_code_for_position(self) end + def file_hash + @file_hash ||= Digest::SHA1.hexdigest(file_path) + end + def on_image? position_type == 'image' end diff --git a/lib/gitlab/diff/position_tracer.rb b/lib/gitlab/diff/position_tracer.rb index af3df820422..a1c82ce9afc 100644 --- a/lib/gitlab/diff/position_tracer.rb +++ b/lib/gitlab/diff/position_tracer.rb @@ -17,187 +17,13 @@ module Gitlab @paths = paths end - def trace(ab_position) + def trace(old_position) return unless old_diff_refs&.complete? && new_diff_refs&.complete? - return unless ab_position.diff_refs == old_diff_refs + return unless old_position.diff_refs == old_diff_refs - # Suppose we have an MR with source branch `feature` and target branch `master`. - # When the MR was created, the head of `master` was commit A, and the - # head of `feature` was commit B, resulting in the original diff A->B. - # Since creation, `master` was updated to C. - # Now `feature` is being updated to D, and the newly generated MR diff is C->D. - # It is possible that C and D are direct descendants of A and B respectively, - # but this isn't necessarily the case as rebases and merges come into play. - # - # Suppose we have a diff note on the original diff A->B. Now that the MR - # is updated, we need to find out what line in C->D corresponds to the - # line the note was originally created on, so that we can update the diff note's - # records and continue to display it in the right place in the diffs. - # If we cannot find this line in the new diff, this means the diff note is now - # outdated, and we will display that fact to the user. - # - # In the new diff, the file the diff note was originally created on may - # have been renamed, deleted or even created, if the file existed in A and B, - # but was removed in C, and restored in D. - # - # Every diff note stores a Position object that defines a specific location, - # identified by paths and line numbers, within a specific diff, identified - # by start, head and base commit ids. - # - # For diff notes for diff A->B, the position looks like this: - # Position - # start_sha - ID of commit A - # head_sha - ID of commit B - # base_sha - ID of base commit of A and B - # old_path - path as of A (nil if file was newly created) - # new_path - path as of B (nil if file was deleted) - # old_line - line number as of A (nil if file was newly created) - # new_line - line number as of B (nil if file was deleted) - # - # We can easily update `start_sha` and `head_sha` to hold the IDs of - # commits C and D, and can trivially determine `base_sha` based on those, - # but need to find the paths and line numbers as of C and D. - # - # If the file was unchanged or newly created in A->B, the path as of D can be found - # by generating diff B->D ("head to head"), finding the diff file with - # `diff_file.old_path == position.new_path`, and taking `diff_file.new_path`. - # The path as of C can be found by taking diff C->D, finding the diff file - # with that same `new_path` and taking `diff_file.old_path`. - # The line number as of D can be found by using the LineMapper on diff B->D - # and providing the line number as of B. - # The line number as of C can be found by using the LineMapper on diff C->D - # and providing the line number as of D. - # - # If the file was deleted in A->B, the path as of C can be found - # by generating diff A->C ("base to base"), finding the diff file with - # `diff_file.old_path == position.old_path`, and taking `diff_file.new_path`. - # The path as of D can be found by taking diff C->D, finding the diff file - # with `old_path` set to that `diff_file.new_path` and taking `diff_file.new_path`. - # The line number as of C can be found by using the LineMapper on diff A->C - # and providing the line number as of A. - # The line number as of D can be found by using the LineMapper on diff C->D - # and providing the line number as of C. + strategy = old_position.on_text? ? LineStrategy : ImageStrategy - if ab_position.added? - trace_added_line(ab_position) - elsif ab_position.removed? - trace_removed_line(ab_position) - else # unchanged - trace_unchanged_line(ab_position) - end - end - - private - - def trace_added_line(ab_position) - b_path = ab_position.new_path - b_line = ab_position.new_line - - bd_diff = bd_diffs.diff_file_with_old_path(b_path) - - d_path = bd_diff&.new_path || b_path - d_line = LineMapper.new(bd_diff).old_to_new(b_line) - - if d_line - cd_diff = cd_diffs.diff_file_with_new_path(d_path) - - c_path = cd_diff&.old_path || d_path - c_line = LineMapper.new(cd_diff).new_to_old(d_line) - - if c_line - # If the line is still in D but also in C, it has turned from an - # added line into an unchanged one. - new_position = position(cd_diff, c_line, d_line) - if valid_position?(new_position) - # If the line is still in the MR, we don't treat this as outdated. - { position: new_position, outdated: false } - else - # If the line is no longer in the MR, we unfortunately cannot show - # the current state on the CD diff, so we treat it as outdated. - ac_diff = ac_diffs.diff_file_with_new_path(c_path) - - { position: position(ac_diff, nil, c_line), outdated: true } - end - else - # If the line is still in D and not in C, it is still added. - { position: position(cd_diff, nil, d_line), outdated: false } - end - else - # If the line is no longer in D, it has been removed from the MR. - { position: position(bd_diff, b_line, nil), outdated: true } - end - end - - def trace_removed_line(ab_position) - a_path = ab_position.old_path - a_line = ab_position.old_line - - ac_diff = ac_diffs.diff_file_with_old_path(a_path) - - c_path = ac_diff&.new_path || a_path - c_line = LineMapper.new(ac_diff).old_to_new(a_line) - - if c_line - cd_diff = cd_diffs.diff_file_with_old_path(c_path) - - d_path = cd_diff&.new_path || c_path - d_line = LineMapper.new(cd_diff).old_to_new(c_line) - - if d_line - # If the line is still in C but also in D, it has turned from a - # removed line into an unchanged one. - bd_diff = bd_diffs.diff_file_with_new_path(d_path) - - { position: position(bd_diff, nil, d_line), outdated: true } - else - # If the line is still in C and not in D, it is still removed. - { position: position(cd_diff, c_line, nil), outdated: false } - end - else - # If the line is no longer in C, it has been removed outside of the MR. - { position: position(ac_diff, a_line, nil), outdated: true } - end - end - - def trace_unchanged_line(ab_position) - a_path = ab_position.old_path - a_line = ab_position.old_line - b_path = ab_position.new_path - b_line = ab_position.new_line - - ac_diff = ac_diffs.diff_file_with_old_path(a_path) - - c_path = ac_diff&.new_path || a_path - c_line = LineMapper.new(ac_diff).old_to_new(a_line) - - bd_diff = bd_diffs.diff_file_with_old_path(b_path) - - d_line = LineMapper.new(bd_diff).old_to_new(b_line) - - cd_diff = cd_diffs.diff_file_with_old_path(c_path) - - if c_line && d_line - # If the line is still in C and D, it is still unchanged. - new_position = position(cd_diff, c_line, d_line) - if valid_position?(new_position) - # If the line is still in the MR, we don't treat this as outdated. - { position: new_position, outdated: false } - else - # If the line is no longer in the MR, we unfortunately cannot show - # the current state on the CD diff or any change on the BD diff, - # so we treat it as outdated. - { position: nil, outdated: true } - end - elsif d_line # && !c_line - # If the line is still in D but no longer in C, it has turned from - # an unchanged line into an added one. - # We don't treat this as outdated since the line is still in the MR. - { position: position(cd_diff, nil, d_line), outdated: false } - else # !d_line && (c_line || !c_line) - # If the line is no longer in D, it has turned from an unchanged line - # into a removed one. - { position: position(bd_diff, b_line, nil), outdated: true } - end + strategy.new(self).trace(old_position) end def ac_diffs @@ -216,18 +42,12 @@ module Gitlab @cd_diffs ||= compare(new_diff_refs.start_sha, new_diff_refs.head_sha) end + private + def compare(start_sha, head_sha, straight: false) compare = CompareService.new(project, head_sha).execute(project, start_sha, straight: straight) compare.diffs(paths: paths, expanded: true) end - - def position(diff_file, old_line, new_line) - Position.new(diff_file: diff_file, old_line: old_line, new_line: new_line) - end - - def valid_position?(position) - !!position.diff_line(project.repository) - end end end end diff --git a/lib/gitlab/diff/position_tracer/base_strategy.rb b/lib/gitlab/diff/position_tracer/base_strategy.rb new file mode 100644 index 00000000000..65049daabf4 --- /dev/null +++ b/lib/gitlab/diff/position_tracer/base_strategy.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Gitlab + module Diff + class PositionTracer + class BaseStrategy + attr_reader :tracer + + delegate \ + :project, + :ac_diffs, + :bd_diffs, + :cd_diffs, + to: :tracer + + def initialize(tracer) + @tracer = tracer + end + + def trace(position) + raise NotImplementedError + end + end + end + end +end diff --git a/lib/gitlab/diff/position_tracer/image_strategy.rb b/lib/gitlab/diff/position_tracer/image_strategy.rb new file mode 100644 index 00000000000..79244a17951 --- /dev/null +++ b/lib/gitlab/diff/position_tracer/image_strategy.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Gitlab + module Diff + class PositionTracer + class ImageStrategy < BaseStrategy + def trace(position) + b_path = position.new_path + + # If file exists in B->D (e.g. updated, renamed, removed), let the + # note become outdated. + bd_diff = bd_diffs.diff_file_with_old_path(b_path) + + return { position: new_position(position, bd_diff), outdated: true } if bd_diff + + # If file still exists in the new diff, update the position. + cd_diff = cd_diffs.diff_file_with_new_path(bd_diff&.new_path || b_path) + + return { position: new_position(position, cd_diff), outdated: false } if cd_diff + + # If file exists in A->C (e.g. rebased and same changes were present + # in target branch), let the note become outdated. + ac_diff = ac_diffs.diff_file_with_old_path(position.old_path) + + return { position: new_position(position, ac_diff), outdated: true } if ac_diff + + # If ever there's a case that the file no longer exists in any diff, + # don't set a change position and let the note become outdated. + # + # This should never happen given the file should exist in one of the + # diffs above. + { outdated: true } + end + + private + + def new_position(position, diff_file) + Position.new( + diff_file: diff_file, + x: position.x, + y: position.y, + width: position.width, + height: position.height, + position_type: position.position_type + ) + end + end + end + end +end diff --git a/lib/gitlab/diff/position_tracer/line_strategy.rb b/lib/gitlab/diff/position_tracer/line_strategy.rb new file mode 100644 index 00000000000..8db0fc6f963 --- /dev/null +++ b/lib/gitlab/diff/position_tracer/line_strategy.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true + +module Gitlab + module Diff + class PositionTracer + class LineStrategy < BaseStrategy + def trace(position) + # Suppose we have an MR with source branch `feature` and target branch `master`. + # When the MR was created, the head of `master` was commit A, and the + # head of `feature` was commit B, resulting in the original diff A->B. + # Since creation, `master` was updated to C. + # Now `feature` is being updated to D, and the newly generated MR diff is C->D. + # It is possible that C and D are direct descendants of A and B respectively, + # but this isn't necessarily the case as rebases and merges come into play. + # + # Suppose we have a diff note on the original diff A->B. Now that the MR + # is updated, we need to find out what line in C->D corresponds to the + # line the note was originally created on, so that we can update the diff note's + # records and continue to display it in the right place in the diffs. + # If we cannot find this line in the new diff, this means the diff note is now + # outdated, and we will display that fact to the user. + # + # In the new diff, the file the diff note was originally created on may + # have been renamed, deleted or even created, if the file existed in A and B, + # but was removed in C, and restored in D. + # + # Every diff note stores a Position object that defines a specific location, + # identified by paths and line numbers, within a specific diff, identified + # by start, head and base commit ids. + # + # For diff notes for diff A->B, the position looks like this: + # Position + # start_sha - ID of commit A + # head_sha - ID of commit B + # base_sha - ID of base commit of A and B + # old_path - path as of A (nil if file was newly created) + # new_path - path as of B (nil if file was deleted) + # old_line - line number as of A (nil if file was newly created) + # new_line - line number as of B (nil if file was deleted) + # + # We can easily update `start_sha` and `head_sha` to hold the IDs of + # commits C and D, and can trivially determine `base_sha` based on those, + # but need to find the paths and line numbers as of C and D. + # + # If the file was unchanged or newly created in A->B, the path as of D can be found + # by generating diff B->D ("head to head"), finding the diff file with + # `diff_file.old_path == position.new_path`, and taking `diff_file.new_path`. + # The path as of C can be found by taking diff C->D, finding the diff file + # with that same `new_path` and taking `diff_file.old_path`. + # The line number as of D can be found by using the LineMapper on diff B->D + # and providing the line number as of B. + # The line number as of C can be found by using the LineMapper on diff C->D + # and providing the line number as of D. + # + # If the file was deleted in A->B, the path as of C can be found + # by generating diff A->C ("base to base"), finding the diff file with + # `diff_file.old_path == position.old_path`, and taking `diff_file.new_path`. + # The path as of D can be found by taking diff C->D, finding the diff file + # with `old_path` set to that `diff_file.new_path` and taking `diff_file.new_path`. + # The line number as of C can be found by using the LineMapper on diff A->C + # and providing the line number as of A. + # The line number as of D can be found by using the LineMapper on diff C->D + # and providing the line number as of C. + + if position.added? + trace_added_line(position) + elsif position.removed? + trace_removed_line(position) + else # unchanged + trace_unchanged_line(position) + end + end + + private + + def trace_added_line(position) + b_path = position.new_path + b_line = position.new_line + + bd_diff = bd_diffs.diff_file_with_old_path(b_path) + + d_path = bd_diff&.new_path || b_path + d_line = LineMapper.new(bd_diff).old_to_new(b_line) + + if d_line + cd_diff = cd_diffs.diff_file_with_new_path(d_path) + + c_path = cd_diff&.old_path || d_path + c_line = LineMapper.new(cd_diff).new_to_old(d_line) + + if c_line + # If the line is still in D but also in C, it has turned from an + # added line into an unchanged one. + new_position = new_position(cd_diff, c_line, d_line) + if valid_position?(new_position) + # If the line is still in the MR, we don't treat this as outdated. + { position: new_position, outdated: false } + else + # If the line is no longer in the MR, we unfortunately cannot show + # the current state on the CD diff, so we treat it as outdated. + ac_diff = ac_diffs.diff_file_with_new_path(c_path) + + { position: new_position(ac_diff, nil, c_line), outdated: true } + end + else + # If the line is still in D and not in C, it is still added. + { position: new_position(cd_diff, nil, d_line), outdated: false } + end + else + # If the line is no longer in D, it has been removed from the MR. + { position: new_position(bd_diff, b_line, nil), outdated: true } + end + end + + def trace_removed_line(position) + a_path = position.old_path + a_line = position.old_line + + ac_diff = ac_diffs.diff_file_with_old_path(a_path) + + c_path = ac_diff&.new_path || a_path + c_line = LineMapper.new(ac_diff).old_to_new(a_line) + + if c_line + cd_diff = cd_diffs.diff_file_with_old_path(c_path) + + d_path = cd_diff&.new_path || c_path + d_line = LineMapper.new(cd_diff).old_to_new(c_line) + + if d_line + # If the line is still in C but also in D, it has turned from a + # removed line into an unchanged one. + bd_diff = bd_diffs.diff_file_with_new_path(d_path) + + { position: new_position(bd_diff, nil, d_line), outdated: true } + else + # If the line is still in C and not in D, it is still removed. + { position: new_position(cd_diff, c_line, nil), outdated: false } + end + else + # If the line is no longer in C, it has been removed outside of the MR. + { position: new_position(ac_diff, a_line, nil), outdated: true } + end + end + + def trace_unchanged_line(position) + a_path = position.old_path + a_line = position.old_line + b_path = position.new_path + b_line = position.new_line + + ac_diff = ac_diffs.diff_file_with_old_path(a_path) + + c_path = ac_diff&.new_path || a_path + c_line = LineMapper.new(ac_diff).old_to_new(a_line) + + bd_diff = bd_diffs.diff_file_with_old_path(b_path) + + d_line = LineMapper.new(bd_diff).old_to_new(b_line) + + cd_diff = cd_diffs.diff_file_with_old_path(c_path) + + if c_line && d_line + # If the line is still in C and D, it is still unchanged. + new_position = new_position(cd_diff, c_line, d_line) + if valid_position?(new_position) + # If the line is still in the MR, we don't treat this as outdated. + { position: new_position, outdated: false } + else + # If the line is no longer in the MR, we unfortunately cannot show + # the current state on the CD diff or any change on the BD diff, + # so we treat it as outdated. + { position: nil, outdated: true } + end + elsif d_line # && !c_line + # If the line is still in D but no longer in C, it has turned from + # an unchanged line into an added one. + # We don't treat this as outdated since the line is still in the MR. + { position: new_position(cd_diff, nil, d_line), outdated: false } + else # !d_line && (c_line || !c_line) + # If the line is no longer in D, it has turned from an unchanged line + # into a removed one. + { position: new_position(bd_diff, b_line, nil), outdated: true } + end + end + + def new_position(diff_file, old_line, new_line) + Position.new( + diff_file: diff_file, + old_line: old_line, + new_line: new_line + ) + end + + def valid_position?(position) + !!position.diff_line(project.repository) + end + end + end + end +end diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index aea02d21048..b755cd1aff0 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -610,4 +610,17 @@ describe Gitlab::Diff::Position do it_behaves_like "diff position json" end end + + describe "#file_hash" do + subject do + described_class.new( + old_path: "image.jpg", + new_path: "image.jpg" + ) + end + + it "returns SHA1 representation of the file_path" do + expect(subject.file_hash).to eq(Digest::SHA1.hexdigest(subject.file_path)) + end + end end diff --git a/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb b/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb new file mode 100644 index 00000000000..900816af53a --- /dev/null +++ b/spec/lib/gitlab/diff/position_tracer/image_strategy_spec.rb @@ -0,0 +1,238 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Diff::PositionTracer::ImageStrategy do + include PositionTracerHelpers + + let(:project) { create(:project, :repository) } + let(:current_user) { project.owner } + let(:file_name) { 'test-file' } + let(:new_file_name) { "#{file_name}-new" } + let(:second_file_name) { "#{file_name}-2" } + let(:branch_name) { 'position-tracer-test' } + let(:old_position) { position(old_path: file_name, new_path: file_name, position_type: 'image') } + + let(:tracer) do + Gitlab::Diff::PositionTracer.new( + project: project, + old_diff_refs: old_diff_refs, + new_diff_refs: new_diff_refs + ) + end + + let(:strategy) { described_class.new(tracer) } + + subject { strategy.trace(old_position) } + + let(:initial_commit) do + project.commit(create_branch(branch_name, 'master')[:branch].name) + end + + describe '#trace' do + describe 'diff scenarios' do + let(:create_file_commit) do + initial_commit + + create_file( + branch_name, + file_name, + Base64.encode64('content') + ) + end + + let(:update_file_commit) do + create_file_commit + + update_file( + branch_name, + file_name, + Base64.encode64('updatedcontent') + ) + end + + let(:update_file_again_commit) do + update_file_commit + + update_file( + branch_name, + file_name, + Base64.encode64('updatedcontentagain') + ) + end + + let(:delete_file_commit) do + create_file_commit + delete_file(branch_name, file_name) + end + + let(:rename_file_commit) do + delete_file_commit + + create_file( + branch_name, + new_file_name, + Base64.encode64('renamedcontent') + ) + end + + let(:create_second_file_commit) do + create_file_commit + + create_file( + branch_name, + second_file_name, + Base64.encode64('morecontent') + ) + end + + let(:create_another_file_commit) do + create_file( + branch_name, + second_file_name, + Base64.encode64('morecontent') + ) + end + + let(:update_another_file_commit) do + update_file( + branch_name, + second_file_name, + Base64.encode64('updatedmorecontent') + ) + end + + context 'when the file was created in the old diff' do + context 'when the file is unchanged between the old and the new diff' do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, create_second_file_commit) } + + it 'returns the new position' do + expect_new_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file was updated between the old and the new diff' do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_file_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file was renamed in between the old and the new diff' do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, rename_file_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, rename_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file was removed in between the old and the new diff' do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, delete_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file is unchanged in the new diff' do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(create_another_file_commit, update_another_file_commit) } + let(:change_diff_refs) { diff_refs(initial_commit, create_another_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + end + + context 'when the file was changed in the old diff' do + context 'when the file is unchanged in between the old and the new diff' do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, create_second_file_commit) } + + it 'returns the new position' do + expect_new_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file was updated in between the old and the new diff' do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:change_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file was renamed in between the old and the new diff' do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, rename_file_commit) } + let(:change_diff_refs) { diff_refs(update_file_commit, rename_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file was removed in between the old and the new diff' do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(update_file_commit, delete_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + + context 'when the file is unchanged in the new diff' do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_another_file_commit, update_another_file_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, create_another_file_commit) } + + it 'returns the position of the change' do + expect_change_position( + old_path: file_name, + new_path: file_name + ) + end + end + end + end + end +end diff --git a/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb b/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb new file mode 100644 index 00000000000..7f4902c5b86 --- /dev/null +++ b/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb @@ -0,0 +1,1805 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Diff::PositionTracer::LineStrategy do + # Douwe's diary New York City, 2016-06-28 + # -------------------------------------------------------------------------- + # + # Dear diary, + # + # Ideally, we would have a test for every single diff scenario that can + # occur and that the PositionTracer should correctly trace a position + # through, across the following variables: + # + # - Old diff file type: created, changed, renamed, deleted, unchanged (5) + # - Old diff line type: added, removed, unchanged (3) + # - New diff file type: created, changed, renamed, deleted, unchanged (5) + # - New diff line type: added, removed, unchanged (3) + # - Old-to-new diff line change: kept, moved, undone (3) + # + # This adds up to 5 * 3 * 5 * 3 * 3 = 675 different potential scenarios, + # and 675 different tests to cover them all. In reality, it would be fewer, + # since one cannot have a removed line in a created file diff, for example, + # but for the sake of this diary entry, let's be pessimistic. + # + # Writing these tests is a manual and time consuming process, as every test + # requires the manual construction or finding of a combination of diffs that + # create the exact diff scenario we are looking for, and can take between + # 1 and 10 minutes, depending on the farfetchedness of the scenario and + # complexity of creating it. + # + # This means that writing tests to cover all of these scenarios would end up + # taking between 11 and 112 hours in total, which I do not believe is the + # best use of my time. + # + # A better course of action would be to think of scenarios that are likely + # to occur, but also potentially tricky to trace correctly, and only cover + # those, with a few more obvious scenarios thrown in to cover our bases. + # + # Unfortunately, I only came to the above realization once I was about + # 1/5th of the way through the process of writing ALL THE SPECS, having + # already wasted about 3 hours trying to be thorough. + # + # I did find 2 bugs while writing those though, so that's good. + # + # In any case, all of this means that the tests below will be extremely + # (excessively, unjustifiably) thorough for scenarios where "the file was + # created in the old diff" and then drop off to comparatively lackluster + # testing of other scenarios. + # + # I did still try to cover most of the obvious and potentially tricky + # scenarios, though. + + include RepoHelpers + include PositionTracerHelpers + + let(:project) { create(:project, :repository) } + let(:current_user) { project.owner } + let(:repository) { project.repository } + let(:file_name) { "test-file" } + let(:new_file_name) { "#{file_name}-new" } + let(:second_file_name) { "#{file_name}-2" } + let(:branch_name) { "position-tracer-test" } + + let(:old_diff_refs) { raise NotImplementedError } + let(:new_diff_refs) { raise NotImplementedError } + let(:change_diff_refs) { raise NotImplementedError } + let(:old_position) { raise NotImplementedError } + + let(:tracer) do + Gitlab::Diff::PositionTracer.new( + project: project, + old_diff_refs: old_diff_refs, + new_diff_refs: new_diff_refs + ) + end + + let(:strategy) { described_class.new(tracer) } + + subject { strategy.trace(old_position) } + + let(:initial_commit) do + project.commit(create_branch(branch_name, 'master')[:branch].name) + end + + describe "#trace" do + describe "diff scenarios" do + let(:create_file_commit) do + initial_commit + + create_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + B + C + CONTENT + ) + end + + let(:create_second_file_commit) do + create_file_commit + + create_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + E + CONTENT + ) + end + + let(:update_line_commit) do + create_second_file_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + BB + C + CONTENT + ) + end + + let(:update_second_file_line_commit) do + update_line_commit + + update_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + EE + CONTENT + ) + end + + let(:move_line_commit) do + update_second_file_line_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + BB + A + C + CONTENT + ) + end + + let(:add_second_file_line_commit) do + move_line_commit + + update_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + EE + F + CONTENT + ) + end + + let(:move_second_file_line_commit) do + add_second_file_line_commit + + update_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + F + EE + CONTENT + ) + end + + let(:delete_line_commit) do + move_second_file_line_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + BB + A + CONTENT + ) + end + + let(:delete_second_file_line_commit) do + delete_line_commit + + update_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + F + CONTENT + ) + end + + let(:delete_file_commit) do + delete_second_file_line_commit + + delete_file(branch_name, file_name) + end + + let(:rename_file_commit) do + delete_file_commit + + create_file( + branch_name, + new_file_name, + <<-CONTENT.strip_heredoc + BB + A + CONTENT + ) + end + + let(:update_line_again_commit) do + rename_file_commit + + update_file( + branch_name, + new_file_name, + <<-CONTENT.strip_heredoc + BB + AA + CONTENT + ) + end + + let(:move_line_again_commit) do + update_line_again_commit + + update_file( + branch_name, + new_file_name, + <<-CONTENT.strip_heredoc + AA + BB + CONTENT + ) + end + + let(:delete_line_again_commit) do + move_line_again_commit + + update_file( + branch_name, + new_file_name, + <<-CONTENT.strip_heredoc + AA + CONTENT + ) + end + + context "when the file was created in the old diff" do + context "when the file is created in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, create_second_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 + A + # 2 + B + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 + BB + # 2 + A + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: 1 + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:change_diff_refs) { diff_refs(update_line_commit, delete_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 + A + # 2 + BB + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 3, + new_line: nil + ) + end + end + end + end + end + + context "when the file is changed in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 - A + # 2 1 BB + # 2 + A + # 3 3 C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 - A + # 2 1 BB + # 2 + A + # 3 3 C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: 1 + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + BB + # 2 + A + # 3 + C + # + # new diff: + # 1 1 BB + # 2 2 A + # 3 - C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 3, + new_line: nil + ) + end + end + end + end + end + + context "when the file is renamed in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, rename_file_commit) } + let(:change_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 1 BB + # 2 2 A + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: nil, + new_line: 2 + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 1 BB + # 2 - A + # 2 + AA + + it "returns the new position" do + expect_new_position( + old_path: file_name, + new_path: new_file_name, + old_line: old_position.new_line, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, move_line_again_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 + AA + # 1 2 BB + # 2 - A + + it "returns the new position" do + expect_new_position( + old_path: file_name, + new_path: new_file_name, + old_line: 1, + new_line: 2 + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } + let(:change_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 1 BB + # 2 - A + # 2 + AA + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: new_file_name, + old_line: 2, + new_line: nil + ) + end + end + end + end + end + + context "when the file is deleted in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # 1 - BB + # 2 - A + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + BB + # 2 + A + # 3 + C + # + # new diff: + # 1 - BB + # 2 - A + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(update_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 - BB + # 2 - A + # 3 - C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 - A + # 2 - BB + # 3 - C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + BB + # 2 + A + # 3 + C + # + # new diff: + # 1 - BB + # 2 - A + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 3, + new_line: nil + ) + end + end + end + end + end + + context "when the file is unchanged in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, create_second_file_commit) } + let(:change_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 1 A + # 2 2 B + # 3 3 C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: nil, + new_line: 2 + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) } + let(:change_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 1 A + # 2 2 BB + # 3 3 C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: nil, + new_line: 1 + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(move_line_commit, move_second_file_line_commit) } + let(:change_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 1 BB + # 2 2 A + # 3 3 C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: nil, + new_line: 1 + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 1 A + # 2 2 BB + # 3 3 C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_second_file_line_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, delete_second_file_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + BB + # 2 + A + # 3 + C + # + # new diff: + # 1 1 BB + # 2 2 A + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 3, + new_line: nil + ) + end + end + end + end + end + end + + context "when the file was changed in the old diff" do + context "when the file is created in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 + BB + # 2 + A + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 + BB + # 2 + A + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: 1 + ) + end + end + + context "when that line was changed or deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, create_file_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 + A + # 2 + B + # 3 + C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 1, + new_line: nil + ) + end + end + end + end + + context "when the position pointed at a deleted line in the old diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, initial_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) + end + end + + context "when the position pointed at an unchanged line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 1) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 2) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 + BB + # 2 + A + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2, new_line: 2) } + + # old diff: + # 1 1 BB + # 2 2 A + # 3 - C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: 1 + ) + end + end + + context "when that line was changed or deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 3, new_line: 3) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 + A + # 2 + B + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 3, + new_line: nil + ) + end + end + end + end + end + + context "when the file is changed in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the new position" do + expect_new_position( + old_path: old_position.old_path, + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 1 BB + # 2 2 A + # 3 - C + + it "returns the new position" do + expect_new_position( + old_path: old_position.old_path, + new_path: old_position.new_path, + old_line: 1, + new_line: 1 + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 - A + # 2 1 BB + # 2 + A + # 3 3 C + + it "returns the new position" do + expect_new_position( + old_path: old_position.old_path, + new_path: old_position.new_path, + old_line: 2, + new_line: 1 + ) + end + end + + context "when that line was changed or deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 1, + new_line: nil + ) + end + end + end + end + + context "when the position pointed at a deleted line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the new position" do + expect_new_position( + old_path: old_position.old_path, + new_path: old_position.new_path, + old_line: old_position.old_line, + new_line: nil + ) + end + end + end + end + end + end + + describe "typical use scenarios" do + let(:second_branch_name) { "#{branch_name}-2" } + + def expect_new_positions(old_attrs, new_attrs) + old_positions = old_attrs.map do |old_attrs| + position(old_attrs) + end + + new_positions = old_positions.map do |old_position| + strategy.trace(old_position) + end + + aggregate_failures do + new_positions.zip(new_attrs).each do |new_position, new_attrs| + if new_attrs&.delete(:change) + expect_change_position(new_attrs, new_position) + else + expect_new_position(new_attrs, new_position) + end + end + end + end + + let(:create_file_commit) do + initial_commit + + create_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + B + C + D + E + F + CONTENT + ) + end + + let(:second_create_file_commit) do + create_file_commit + + create_branch(second_branch_name, branch_name) + + update_file( + second_branch_name, + file_name, + <<-CONTENT.strip_heredoc + Z + Z + Z + A + B + C + D + E + F + CONTENT + ) + end + + let(:update_file_commit) do + second_create_file_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + C + DD + E + F + G + CONTENT + ) + end + + let(:update_file_again_commit) do + update_file_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + BB + C + D + E + FF + G + CONTENT + ) + end + + describe "simple push of new commit" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:change_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 3 2 C + # 4 - D + # 3 + DD + # 5 4 E + # 6 5 F + # 6 + G + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C + { old_path: file_name, old_line: 4 }, # - D + { new_path: file_name, new_line: 3 }, # + DD + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E + { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F + { new_path: file_name, new_line: 6 } # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, + { old_path: file_name, old_line: 2 }, + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, + { new_path: file_name, new_line: 4, change: true }, + { new_path: file_name, old_line: 3, change: true }, + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, + { new_path: file_name, old_line: 5, change: true }, + { new_path: file_name, new_line: 7 } + ] + + expect_new_positions(old_position_attrs, new_position_attrs) + end + end + + describe "force push to overwrite last commit" do + let(:second_create_file_commit) do + create_file_commit + + create_branch(second_branch_name, branch_name) + + update_file( + second_branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + BB + C + D + E + FF + G + CONTENT + ) + end + + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, second_create_file_commit) } + let(:change_diff_refs) { diff_refs(update_file_commit, second_create_file_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 3 2 C + # 4 - D + # 3 + DD + # 5 4 E + # 6 5 F + # 6 + G + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C + { old_path: file_name, old_line: 4 }, # - D + { new_path: file_name, new_line: 3 }, # + DD + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E + { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F + { new_path: file_name, new_line: 6 } # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, + { old_path: file_name, old_line: 2 }, + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, + { new_path: file_name, new_line: 4, change: true }, + { old_path: file_name, old_line: 3, change: true }, + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, + { old_path: file_name, old_line: 5, change: true }, + { new_path: file_name, new_line: 7 } + ] + + expect_new_positions(old_position_attrs, new_position_attrs) + end + end + + describe "force push to delete last commit" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:change_diff_refs) { diff_refs(update_file_again_commit, update_file_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + # + # new diff: + # 1 1 A + # 2 - B + # 3 2 C + # 4 - D + # 3 + DD + # 5 4 E + # 6 5 F + # 6 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 2 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 6 }, # + FF + { new_path: file_name, new_line: 7 } # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, + { old_path: file_name, old_line: 2 }, + { old_path: file_name, old_line: 2, change: true }, + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, + { old_path: file_name, old_line: 4, change: true }, + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, + { new_path: file_name, new_line: 5, change: true }, + { old_path: file_name, old_line: 6, change: true }, + { new_path: file_name, new_line: 6 } + ] + + expect_new_positions(old_position_attrs, new_position_attrs) + end + end + + describe "rebase on top of target branch" do + let(:second_update_file_commit) do + update_file_commit + + update_file( + second_branch_name, + file_name, + <<-CONTENT.strip_heredoc + Z + Z + Z + A + C + DD + E + F + G + CONTENT + ) + end + + let(:update_file_again_commit) do + second_update_file_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + BB + C + D + E + FF + G + CONTENT + ) + end + + let(:overwrite_update_file_again_commit) do + update_file_again_commit + + update_file( + second_branch_name, + file_name, + <<-CONTENT.strip_heredoc + Z + Z + Z + A + BB + C + D + E + FF + G + CONTENT + ) + end + + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, overwrite_update_file_again_commit) } + let(:change_diff_refs) { diff_refs(update_file_again_commit, overwrite_update_file_again_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + # + # new diff: + # 1 + Z + # 2 + Z + # 3 + Z + # 1 4 A + # 2 - B + # 5 + BB + # 3 6 C + # 4 7 D + # 5 8 E + # 6 - F + # 9 + FF + # 0 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 2 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 6 }, # + FF + { new_path: file_name, new_line: 7 } # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 5 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 9 }, # + FF + { new_path: file_name, new_line: 10 } # + G + ] + + expect_new_positions(old_position_attrs, new_position_attrs) + end + end + + describe "merge of target branch" do + let(:merge_commit) do + second_create_file_commit + + merge_request = create(:merge_request, source_branch: second_branch_name, target_branch: branch_name, source_project: project) + + repository.merge(current_user, merge_request.diff_head_sha, merge_request, "Merge branches") + + project.commit(branch_name) + end + + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, merge_commit) } + let(:change_diff_refs) { diff_refs(update_file_again_commit, merge_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + # + # new diff: + # 1 + Z + # 2 + Z + # 3 + Z + # 1 4 A + # 2 - B + # 5 + BB + # 3 6 C + # 4 7 D + # 5 8 E + # 6 - F + # 9 + FF + # 0 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 2 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 6 }, # + FF + { new_path: file_name, new_line: 7 } # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 5 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 9 }, # + FF + { new_path: file_name, new_line: 10 } # + G + ] + + expect_new_positions(old_position_attrs, new_position_attrs) + end + end + + describe "changing target branch" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:new_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + # + # new diff: + # 1 1 A + # 2 + BB + # 2 3 C + # 3 - DD + # 4 + D + # 4 5 E + # 5 - F + # 6 + FF + # 7 G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 2 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 6 }, # + FF + { new_path: file_name, new_line: 7 } # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, + { old_path: file_name, old_line: 2, change: true }, + { new_path: file_name, new_line: 2 }, + { old_path: file_name, new_path: file_name, old_line: 2, new_line: 3 }, + { new_path: file_name, new_line: 4 }, + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 5 }, + { old_path: file_name, old_line: 5 }, + { new_path: file_name, new_line: 6 }, + { new_path: file_name, new_line: 7 } + ] + + expect_new_positions(old_position_attrs, new_position_attrs) + end + end + end + end +end diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index 866550753a8..79b33d4d276 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -1,1896 +1,98 @@ require 'spec_helper' describe Gitlab::Diff::PositionTracer do - # Douwe's diary New York City, 2016-06-28 - # -------------------------------------------------------------------------- - # - # Dear diary, - # - # Ideally, we would have a test for every single diff scenario that can - # occur and that the PositionTracer should correctly trace a position - # through, across the following variables: - # - # - Old diff file type: created, changed, renamed, deleted, unchanged (5) - # - Old diff line type: added, removed, unchanged (3) - # - New diff file type: created, changed, renamed, deleted, unchanged (5) - # - New diff line type: added, removed, unchanged (3) - # - Old-to-new diff line change: kept, moved, undone (3) - # - # This adds up to 5 * 3 * 5 * 3 * 3 = 675 different potential scenarios, - # and 675 different tests to cover them all. In reality, it would be fewer, - # since one cannot have a removed line in a created file diff, for example, - # but for the sake of this diary entry, let's be pessimistic. - # - # Writing these tests is a manual and time consuming process, as every test - # requires the manual construction or finding of a combination of diffs that - # create the exact diff scenario we are looking for, and can take between - # 1 and 10 minutes, depending on the farfetchedness of the scenario and - # complexity of creating it. - # - # This means that writing tests to cover all of these scenarios would end up - # taking between 11 and 112 hours in total, which I do not believe is the - # best use of my time. - # - # A better course of action would be to think of scenarios that are likely - # to occur, but also potentially tricky to trace correctly, and only cover - # those, with a few more obvious scenarios thrown in to cover our bases. - # - # Unfortunately, I only came to the above realization once I was about - # 1/5th of the way through the process of writing ALL THE SPECS, having - # already wasted about 3 hours trying to be thorough. - # - # I did find 2 bugs while writing those though, so that's good. - # - # In any case, all of this means that the tests below will be extremely - # (excessively, unjustifiably) thorough for scenarios where "the file was - # created in the old diff" and then drop off to comparatively lackluster - # testing of other scenarios. - # - # I did still try to cover most of the obvious and potentially tricky - # scenarios, though. + include PositionTracerHelpers - include RepoHelpers - - let(:project) { create(:project, :repository) } - let(:current_user) { project.owner } - let(:repository) { project.repository } - let(:file_name) { "test-file" } - let(:new_file_name) { "#{file_name}-new" } - let(:second_file_name) { "#{file_name}-2" } - let(:branch_name) { "position-tracer-test" } - - let(:old_diff_refs) { raise NotImplementedError } - let(:new_diff_refs) { raise NotImplementedError } - let(:change_diff_refs) { raise NotImplementedError } - let(:old_position) { raise NotImplementedError } - - let(:position_tracer) { described_class.new(project: project, old_diff_refs: old_diff_refs, new_diff_refs: new_diff_refs) } - subject { position_tracer.trace(old_position) } - - def diff_refs(base_commit, head_commit) - Gitlab::Diff::DiffRefs.new(base_sha: base_commit.id, head_sha: head_commit.id) - end - - def text_position_attrs - [:old_line, :new_line] - end - - def position(attrs = {}) - attrs.reverse_merge!( - diff_refs: old_diff_refs + subject do + described_class.new( + project: project, + old_diff_refs: old_diff_refs, + new_diff_refs: new_diff_refs ) - Gitlab::Diff::Position.new(attrs) end - def expect_new_position(attrs, result = subject) - aggregate_failures("expect new position #{attrs.inspect}") do - if attrs.nil? - expect(result[:outdated]).to be_truthy - else - expect(result[:outdated]).to be_falsey + describe '#trace' do + let(:diff_refs) { double(complete?: true) } + let(:project) { double } + let(:old_diff_refs) { diff_refs } + let(:new_diff_refs) { diff_refs } + let(:position) { double(on_text?: on_text?, diff_refs: diff_refs) } + let(:tracer) { double } - new_position = result[:position] - expect(new_position).not_to be_nil + context 'position is on text' do + let(:on_text?) { true } - expect(new_position.diff_refs).to eq(new_diff_refs) + it 'calls LineStrategy#trace' do + expect(Gitlab::Diff::PositionTracer::LineStrategy) + .to receive(:new) + .with(subject) + .and_return(tracer) + expect(tracer).to receive(:trace).with(position) - attrs.each do |attr, value| - if text_position_attrs.include?(attr) - expect(new_position.formatter.send(attr)).to eq(value) - else - expect(new_position.send(attr)).to eq(value) - end - end + subject.trace(position) + end + end + + context 'position is not on text' do + let(:on_text?) { false } + + it 'calls ImageStrategy#trace' do + expect(Gitlab::Diff::PositionTracer::ImageStrategy) + .to receive(:new) + .with(subject) + .and_return(tracer) + expect(tracer).to receive(:trace).with(position) + + subject.trace(position) end end end - def expect_change_position(attrs, result = subject) - aggregate_failures("expect change position #{attrs.inspect}") do - expect(result[:outdated]).to be_truthy - - change_position = result[:position] - if attrs.nil? || attrs.empty? - expect(change_position).to be_nil - else - expect(change_position).not_to be_nil - - expect(change_position.diff_refs).to eq(change_diff_refs) - - attrs.each do |attr, value| - if text_position_attrs.include?(attr) - expect(change_position.formatter.send(attr)).to eq(value) - else - expect(change_position.send(attr)).to eq(value) - end - end - end - end - end - - def create_branch(new_name, branch_name) - CreateBranchService.new(project, current_user).execute(new_name, branch_name) - end - - def create_file(branch_name, file_name, content) - Files::CreateService.new( - project, - current_user, - start_branch: branch_name, - branch_name: branch_name, - commit_message: "Create file", - file_path: file_name, - file_content: content - ).execute - project.commit(branch_name) - end - - def update_file(branch_name, file_name, content) - Files::UpdateService.new( - project, - current_user, - start_branch: branch_name, - branch_name: branch_name, - commit_message: "Update file", - file_path: file_name, - file_content: content - ).execute - project.commit(branch_name) - end - - def delete_file(branch_name, file_name) - Files::DeleteService.new( - project, - current_user, - start_branch: branch_name, - branch_name: branch_name, - commit_message: "Delete file", - file_path: file_name - ).execute - project.commit(branch_name) - end - - let(:initial_commit) do - create_branch(branch_name, "master")[:branch].name - project.commit(branch_name) - end - - describe "#trace" do - describe "diff scenarios" do - let(:create_file_commit) do - initial_commit - - create_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - B - C - CONTENT - ) - end - - let(:create_second_file_commit) do - create_file_commit - - create_file( - branch_name, - second_file_name, - <<-CONTENT.strip_heredoc - D - E - CONTENT - ) - end - - let(:update_line_commit) do - create_second_file_commit - - update_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - BB - C - CONTENT - ) - end - - let(:update_second_file_line_commit) do - update_line_commit - - update_file( - branch_name, - second_file_name, - <<-CONTENT.strip_heredoc - D - EE - CONTENT - ) - end - - let(:move_line_commit) do - update_second_file_line_commit - - update_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - BB - A - C - CONTENT - ) - end - - let(:add_second_file_line_commit) do - move_line_commit - - update_file( - branch_name, - second_file_name, - <<-CONTENT.strip_heredoc - D - EE - F - CONTENT - ) - end - - let(:move_second_file_line_commit) do - add_second_file_line_commit - - update_file( - branch_name, - second_file_name, - <<-CONTENT.strip_heredoc - D - F - EE - CONTENT - ) - end - - let(:delete_line_commit) do - move_second_file_line_commit - - update_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - BB - A - CONTENT - ) - end - - let(:delete_second_file_line_commit) do - delete_line_commit - - update_file( - branch_name, - second_file_name, - <<-CONTENT.strip_heredoc - D - F - CONTENT - ) - end - - let(:delete_file_commit) do - delete_second_file_line_commit - - delete_file(branch_name, file_name) - end - - let(:rename_file_commit) do - delete_file_commit - - create_file( - branch_name, - new_file_name, - <<-CONTENT.strip_heredoc - BB - A - CONTENT - ) - end - - let(:update_line_again_commit) do - rename_file_commit - - update_file( - branch_name, - new_file_name, - <<-CONTENT.strip_heredoc - BB - AA - CONTENT - ) - end - - let(:move_line_again_commit) do - update_line_again_commit - - update_file( - branch_name, - new_file_name, - <<-CONTENT.strip_heredoc - AA - BB - CONTENT - ) - end - - let(:delete_line_again_commit) do - move_line_again_commit - - update_file( - branch_name, - new_file_name, - <<-CONTENT.strip_heredoc - AA - CONTENT - ) - end - - context "when the file was created in the old diff" do - context "when the file is created in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, create_second_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 + A - # 2 + B - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - new_line: old_position.new_line - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 1) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 + A - # 2 + BB - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - new_line: old_position.new_line - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 + BB - # 2 + A - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - new_line: 1 - ) - end - end - - context "when that line was changed between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:change_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 + A - # 2 + BB - # 3 + C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when that line was deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:change_diff_refs) { diff_refs(update_line_commit, delete_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 3) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 + A - # 2 + BB - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 3, - new_line: nil - ) - end - end - end - end - end - - context "when the file is changed in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 1) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - new_line: old_position.new_line - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 3) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 - A - # 2 1 BB - # 2 + A - # 3 3 C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - new_line: old_position.new_line - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 - A - # 2 1 BB - # 2 + A - # 3 3 C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - new_line: 1 - ) - end - end - - context "when that line was changed between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:change_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when that line was deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 3) } - - # old diff: - # 1 + BB - # 2 + A - # 3 + C - # - # new diff: - # 1 1 BB - # 2 2 A - # 3 - C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 3, - new_line: nil - ) - end - end - end - end - end - - context "when the file is renamed in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, rename_file_commit) } - let(:change_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + BB - # 2 + A - # - # new diff: - # file_name -> new_file_name - # 1 1 BB - # 2 2 A - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: nil, - new_line: 2 - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } - let(:old_position) { position(new_path: file_name, new_line: 1) } - - # old diff: - # 1 + BB - # 2 + A - # - # new diff: - # file_name -> new_file_name - # 1 1 BB - # 2 - A - # 2 + AA - - it "returns the new position" do - expect_new_position( - old_path: file_name, - new_path: new_file_name, - old_line: old_position.new_line, - new_line: old_position.new_line - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, move_line_again_commit) } - let(:old_position) { position(new_path: file_name, new_line: 1) } - - # old diff: - # 1 + BB - # 2 + A - # - # new diff: - # file_name -> new_file_name - # 1 + AA - # 1 2 BB - # 2 - A - - it "returns the new position" do - expect_new_position( - old_path: file_name, - new_path: new_file_name, - old_line: 1, - new_line: 2 - ) - end - end - - context "when that line was changed between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } - let(:change_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + BB - # 2 + A - # - # new diff: - # file_name -> new_file_name - # 1 1 BB - # 2 - A - # 2 + AA - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: new_file_name, - old_line: 2, - new_line: nil - ) - end - end - end - end - end - - context "when the file is deleted in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } - let(:change_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + BB - # 2 + A - # - # new diff: - # 1 - BB - # 2 - A - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + BB - # 2 + A - # 3 + C - # - # new diff: - # 1 - BB - # 2 - A - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } - let(:change_diff_refs) { diff_refs(update_line_commit, delete_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 - BB - # 2 - A - # 3 - C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when that line was changed between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(update_line_commit, delete_file_commit) } - let(:change_diff_refs) { diff_refs(create_file_commit, delete_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 - A - # 2 - BB - # 3 - C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when that line was deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 3) } - - # old diff: - # 1 + BB - # 2 + A - # 3 + C - # - # new diff: - # 1 - BB - # 2 - A - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 3, - new_line: nil - ) - end - end - end - end - end - - context "when the file is unchanged in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, create_second_file_commit) } - let(:change_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 1 A - # 2 2 B - # 3 3 C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: nil, - new_line: 2 - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) } - let(:change_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 1) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 1 A - # 2 2 BB - # 3 3 C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: nil, - new_line: 1 - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(move_line_commit, move_second_file_line_commit) } - let(:change_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + BB - # 3 + C - # - # new diff: - # 1 1 BB - # 2 2 A - # 3 3 C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: nil, - new_line: 1 - ) - end - end - - context "when that line was changed between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) } - let(:change_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 2) } - - # old diff: - # 1 + A - # 2 + B - # 3 + C - # - # new diff: - # 1 1 A - # 2 2 BB - # 3 3 C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when that line was deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, delete_second_file_line_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, delete_second_file_line_commit) } - let(:old_position) { position(new_path: file_name, new_line: 3) } - - # old diff: - # 1 + BB - # 2 + A - # 3 + C - # - # new diff: - # 1 1 BB - # 2 2 A - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 3, - new_line: nil - ) - end - end - end - end - end - end - - context "when the file was changed in the old diff" do - context "when the file is created in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 + A - # 2 + BB - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - old_line: nil, - new_line: old_position.new_line - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } - - # old diff: - # 1 + BB - # 1 2 A - # 2 - B - # 3 3 C - # - # new diff: - # 1 + BB - # 2 + A - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - old_line: nil, - new_line: old_position.new_line - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 + BB - # 2 + A - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - old_line: nil, - new_line: 1 - ) - end - end - - context "when that line was changed or deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, create_file_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, create_file_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } - - # old diff: - # 1 + BB - # 1 2 A - # 2 - B - # 3 3 C - # - # new diff: - # 1 + A - # 2 + B - # 3 + C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 1, - new_line: nil - ) - end - end - end - end - - context "when the position pointed at a deleted line in the old diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:change_diff_refs) { diff_refs(create_file_commit, initial_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 + A - # 2 + BB - # 3 + C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 2, - new_line: nil - ) - end - end - - context "when the position pointed at an unchanged line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 1) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 + A - # 2 + BB - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - old_line: nil, - new_line: old_position.new_line - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 2) } - - # old diff: - # 1 + BB - # 1 2 A - # 2 - B - # 3 3 C - # - # new diff: - # 1 + BB - # 2 + A - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - old_line: nil, - new_line: old_position.new_line - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2, new_line: 2) } - - # old diff: - # 1 1 BB - # 2 2 A - # 3 - C - # - # new diff: - # 1 + A - # 2 + BB - # 3 + C - - it "returns the new position" do - expect_new_position( - new_path: old_position.new_path, - old_line: nil, - new_line: 1 - ) - end - end - - context "when that line was changed or deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 3, new_line: 3) } - - # old diff: - # 1 + BB - # 1 2 A - # 2 - B - # 3 3 C - # - # new diff: - # 1 + A - # 2 + B - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 3, - new_line: nil - ) - end - end - end - end - end - - context "when the file is changed in the new diff" do - context "when the position pointed at an added line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - - it "returns the new position" do - expect_new_position( - old_path: old_position.old_path, - new_path: old_position.new_path, - old_line: nil, - new_line: old_position.new_line - ) - end - end - - context "when the file's content was changed between the old and the new diff" do - context "when that line was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } - - # old diff: - # 1 + BB - # 1 2 A - # 2 - B - # 3 3 C - # - # new diff: - # 1 1 BB - # 2 2 A - # 3 - C - - it "returns the new position" do - expect_new_position( - old_path: old_position.old_path, - new_path: old_position.new_path, - old_line: 1, - new_line: 1 - ) - end - end - - context "when that line was moved between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 - A - # 2 1 BB - # 2 + A - # 3 3 C - - it "returns the new position" do - expect_new_position( - old_path: old_position.old_path, - new_path: old_position.new_path, - old_line: 2, - new_line: 1 - ) - end - end - - context "when that line was changed or deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:change_diff_refs) { diff_refs(move_line_commit, update_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } - - # old diff: - # 1 + BB - # 1 2 A - # 2 - B - # 3 3 C - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - - it "returns the position of the change" do - expect_change_position( - old_path: file_name, - new_path: file_name, - old_line: 1, - new_line: nil - ) - end - end - end - end - - context "when the position pointed at a deleted line in the old diff" do - context "when the file's content was unchanged between the old and the new diff" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } - let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - - it "returns the new position" do - expect_new_position( - old_path: old_position.old_path, - new_path: old_position.new_path, - old_line: old_position.old_line, - new_line: nil - ) - end - end - end - end - end - end - end - - describe "typical use scenarios" do - let(:second_branch_name) { "#{branch_name}-2" } - - def expect_new_positions(old_attrs, new_attrs) - old_positions = old_attrs.map do |old_attrs| - position(old_attrs) - end - - new_positions = old_positions.map do |old_position| - position_tracer.trace(old_position) - end - - aggregate_failures do - new_positions.zip(new_attrs).each do |new_position, new_attrs| - if new_attrs&.delete(:change) - expect_change_position(new_attrs, new_position) - else - expect_new_position(new_attrs, new_position) - end - end - end - end - - let(:create_file_commit) do - initial_commit - - create_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - B - C - D - E - F - CONTENT + describe 'diffs methods' do + let(:project) { create(:project, :repository) } + let(:current_user) { project.owner } + + let(:old_diff_refs) do + diff_refs( + project.commit(create_branch('new-branch', 'master')[:branch].name), + create_file('new-branch', 'file.md', 'content') ) end - let(:second_create_file_commit) do - create_file_commit - - create_branch(second_branch_name, branch_name) - - update_file( - second_branch_name, - file_name, - <<-CONTENT.strip_heredoc - Z - Z - Z - A - B - C - D - E - F - CONTENT + let(:new_diff_refs) do + diff_refs( + create_file('new-branch', 'file.md', 'content'), + update_file('new-branch', 'file.md', 'updatedcontent') ) end - let(:update_file_commit) do - second_create_file_commit + describe '#ac_diffs' do + it 'returns the diffs between the base of old and new diff' do + diff_refs = subject.ac_diffs.diff_refs - update_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - C - DD - E - F - G - CONTENT - ) - end - - let(:update_file_again_commit) do - update_file_commit - - update_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - BB - C - D - E - FF - G - CONTENT - ) - end - - describe "simple push of new commit" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } - let(:change_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) } - - # old diff: - # 1 1 A - # 2 - B - # 3 2 C - # 4 - D - # 3 + DD - # 5 4 E - # 6 5 F - # 6 + G - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # 4 4 D - # 5 5 E - # 6 - F - # 6 + FF - # 7 + G - - it "returns the new positions" do - old_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A - { old_path: file_name, old_line: 2 }, # - B - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C - { old_path: file_name, old_line: 4 }, # - D - { new_path: file_name, new_line: 3 }, # + DD - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E - { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F - { new_path: file_name, new_line: 6 }, # + G - ] - - new_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, - { old_path: file_name, old_line: 2 }, - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, - { new_path: file_name, new_line: 4, change: true }, - { new_path: file_name, old_line: 3, change: true }, - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, - { new_path: file_name, old_line: 5, change: true }, - { new_path: file_name, new_line: 7 } - ] - - expect_new_positions(old_position_attrs, new_position_attrs) + expect(diff_refs.base_sha).to eq(old_diff_refs.base_sha) + expect(diff_refs.start_sha).to eq(old_diff_refs.base_sha) + expect(diff_refs.head_sha).to eq(new_diff_refs.base_sha) end end - describe "force push to overwrite last commit" do - let(:second_create_file_commit) do - create_file_commit + describe '#bd_diffs' do + it 'returns the diffs between the HEAD of old and new diff' do + diff_refs = subject.bd_diffs.diff_refs - create_branch(second_branch_name, branch_name) - - update_file( - second_branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - BB - C - D - E - FF - G - CONTENT - ) - end - - let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, second_create_file_commit) } - let(:change_diff_refs) { diff_refs(update_file_commit, second_create_file_commit) } - - # old diff: - # 1 1 A - # 2 - B - # 3 2 C - # 4 - D - # 3 + DD - # 5 4 E - # 6 5 F - # 6 + G - # - # new diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # 4 4 D - # 5 5 E - # 6 - F - # 6 + FF - # 7 + G - - it "returns the new positions" do - old_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A - { old_path: file_name, old_line: 2 }, # - B - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C - { old_path: file_name, old_line: 4 }, # - D - { new_path: file_name, new_line: 3 }, # + DD - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E - { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F - { new_path: file_name, new_line: 6 }, # + G - ] - - new_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, - { old_path: file_name, old_line: 2 }, - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, - { new_path: file_name, new_line: 4, change: true }, - { old_path: file_name, old_line: 3, change: true }, - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, - { old_path: file_name, old_line: 5, change: true }, - { new_path: file_name, new_line: 7 } - ] - - expect_new_positions(old_position_attrs, new_position_attrs) + expect(diff_refs.base_sha).to eq(old_diff_refs.head_sha) + expect(diff_refs.start_sha).to eq(old_diff_refs.head_sha) + expect(diff_refs.head_sha).to eq(new_diff_refs.head_sha) end end - describe "force push to delete last commit" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, update_file_commit) } - let(:change_diff_refs) { diff_refs(update_file_again_commit, update_file_commit) } + describe '#cd_diffs' do + it 'returns the diffs in the new diff' do + diff_refs = subject.cd_diffs.diff_refs - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # 4 4 D - # 5 5 E - # 6 - F - # 6 + FF - # 7 + G - # - # new diff: - # 1 1 A - # 2 - B - # 3 2 C - # 4 - D - # 3 + DD - # 5 4 E - # 6 5 F - # 6 + G - - it "returns the new positions" do - old_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A - { old_path: file_name, old_line: 2 }, # - B - { new_path: file_name, new_line: 2 }, # + BB - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E - { old_path: file_name, old_line: 6 }, # - F - { new_path: file_name, new_line: 6 }, # + FF - { new_path: file_name, new_line: 7 }, # + G - ] - - new_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, - { old_path: file_name, old_line: 2 }, - { old_path: file_name, old_line: 2, change: true }, - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, - { old_path: file_name, old_line: 4, change: true }, - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, - { new_path: file_name, new_line: 5, change: true }, - { old_path: file_name, old_line: 6, change: true }, - { new_path: file_name, new_line: 6 } - ] - - expect_new_positions(old_position_attrs, new_position_attrs) - end - end - - describe "rebase on top of target branch" do - let(:second_update_file_commit) do - update_file_commit - - update_file( - second_branch_name, - file_name, - <<-CONTENT.strip_heredoc - Z - Z - Z - A - C - DD - E - F - G - CONTENT - ) - end - - let(:update_file_again_commit) do - second_update_file_commit - - update_file( - branch_name, - file_name, - <<-CONTENT.strip_heredoc - A - BB - C - D - E - FF - G - CONTENT - ) - end - - let(:overwrite_update_file_again_commit) do - update_file_again_commit - - update_file( - second_branch_name, - file_name, - <<-CONTENT.strip_heredoc - Z - Z - Z - A - BB - C - D - E - FF - G - CONTENT - ) - end - - let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, overwrite_update_file_again_commit) } - let(:change_diff_refs) { diff_refs(update_file_again_commit, overwrite_update_file_again_commit) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # 4 4 D - # 5 5 E - # 6 - F - # 6 + FF - # 7 + G - # - # new diff: - # 1 + Z - # 2 + Z - # 3 + Z - # 1 4 A - # 2 - B - # 5 + BB - # 3 6 C - # 4 7 D - # 5 8 E - # 6 - F - # 9 + FF - # 0 + G - - it "returns the new positions" do - old_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A - { old_path: file_name, old_line: 2 }, # - B - { new_path: file_name, new_line: 2 }, # + BB - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E - { old_path: file_name, old_line: 6 }, # - F - { new_path: file_name, new_line: 6 }, # + FF - { new_path: file_name, new_line: 7 }, # + G - ] - - new_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A - { old_path: file_name, old_line: 2 }, # - B - { new_path: file_name, new_line: 5 }, # + BB - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E - { old_path: file_name, old_line: 6 }, # - F - { new_path: file_name, new_line: 9 }, # + FF - { new_path: file_name, new_line: 10 }, # + G - ] - - expect_new_positions(old_position_attrs, new_position_attrs) - end - end - - describe "merge of target branch" do - let(:merge_commit) do - second_create_file_commit - - merge_request = create(:merge_request, source_branch: second_branch_name, target_branch: branch_name, source_project: project) - - repository.merge(current_user, merge_request.diff_head_sha, merge_request, "Merge branches") - - project.commit(branch_name) - end - - let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } - let(:new_diff_refs) { diff_refs(create_file_commit, merge_commit) } - let(:change_diff_refs) { diff_refs(update_file_again_commit, merge_commit) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # 4 4 D - # 5 5 E - # 6 - F - # 6 + FF - # 7 + G - # - # new diff: - # 1 + Z - # 2 + Z - # 3 + Z - # 1 4 A - # 2 - B - # 5 + BB - # 3 6 C - # 4 7 D - # 5 8 E - # 6 - F - # 9 + FF - # 0 + G - - it "returns the new positions" do - old_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A - { old_path: file_name, old_line: 2 }, # - B - { new_path: file_name, new_line: 2 }, # + BB - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E - { old_path: file_name, old_line: 6 }, # - F - { new_path: file_name, new_line: 6 }, # + FF - { new_path: file_name, new_line: 7 }, # + G - ] - - new_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A - { old_path: file_name, old_line: 2 }, # - B - { new_path: file_name, new_line: 5 }, # + BB - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E - { old_path: file_name, old_line: 6 }, # - F - { new_path: file_name, new_line: 9 }, # + FF - { new_path: file_name, new_line: 10 }, # + G - ] - - expect_new_positions(old_position_attrs, new_position_attrs) - end - end - - describe "changing target branch" do - let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } - let(:new_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) } - let(:change_diff_refs) { diff_refs(create_file_commit, update_file_commit) } - - # old diff: - # 1 1 A - # 2 - B - # 2 + BB - # 3 3 C - # 4 4 D - # 5 5 E - # 6 - F - # 6 + FF - # 7 + G - # - # new diff: - # 1 1 A - # 2 + BB - # 2 3 C - # 3 - DD - # 4 + D - # 4 5 E - # 5 - F - # 6 + FF - # 7 G - - it "returns the new positions" do - old_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A - { old_path: file_name, old_line: 2 }, # - B - { new_path: file_name, new_line: 2 }, # + BB - { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D - { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E - { old_path: file_name, old_line: 6 }, # - F - { new_path: file_name, new_line: 6 }, # + FF - { new_path: file_name, new_line: 7 }, # + G - ] - - new_position_attrs = [ - { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, - { old_path: file_name, old_line: 2, change: true }, - { new_path: file_name, new_line: 2 }, - { old_path: file_name, new_path: file_name, old_line: 2, new_line: 3 }, - { new_path: file_name, new_line: 4 }, - { old_path: file_name, new_path: file_name, old_line: 4, new_line: 5 }, - { old_path: file_name, old_line: 5 }, - { new_path: file_name, new_line: 6 }, - { new_path: file_name, new_line: 7 } - ] - - expect_new_positions(old_position_attrs, new_position_attrs) + expect(diff_refs.base_sha).to eq(new_diff_refs.base_sha) + expect(diff_refs.start_sha).to eq(new_diff_refs.base_sha) + expect(diff_refs.head_sha).to eq(new_diff_refs.head_sha) end end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 9f60e49290e..b934533b1ab 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -1175,16 +1175,30 @@ describe SystemNoteService do end it 'links to the diff in the system note' do - expect(subject.note).to include('version 1') - diff_id = merge_request.merge_request_diff.id line_code = change_position.line_code(project.repository) - expect(subject.note).to include(diffs_project_merge_request_path(project, merge_request, diff_id: diff_id, anchor: line_code)) + link = diffs_project_merge_request_path(project, merge_request, diff_id: diff_id, anchor: line_code) + + expect(subject.note).to eq("changed this line in [version 1 of the diff](#{link})") + end + + context 'discussion is on an image' do + let(:discussion) { create(:image_diff_note_on_merge_request, project: project).to_discussion } + + it 'links to the diff in the system note' do + diff_id = merge_request.merge_request_diff.id + file_hash = change_position.file_hash + link = diffs_project_merge_request_path(project, merge_request, diff_id: diff_id, anchor: file_hash) + + expect(subject.note).to eq("changed this file in [version 1 of the diff](#{link})") + end end end - context 'when the change_position is invalid for the discussion' do - let(:change_position) { project.commit(sample_commit.id) } + context 'when the change_position does not point to a valid version' do + before do + allow(merge_request).to receive(:version_params_for).and_return(nil) + end it 'creates a new note in the discussion' do # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded. diff --git a/spec/support/helpers/position_tracer_helpers.rb b/spec/support/helpers/position_tracer_helpers.rb new file mode 100644 index 00000000000..bbf6e06dd40 --- /dev/null +++ b/spec/support/helpers/position_tracer_helpers.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +module PositionTracerHelpers + def diff_refs(base_commit, head_commit) + Gitlab::Diff::DiffRefs.new(base_sha: base_commit.id, head_sha: head_commit.id) + end + + def position(attrs = {}) + attrs.reverse_merge!( + diff_refs: old_diff_refs + ) + Gitlab::Diff::Position.new(attrs) + end + + def expect_new_position(attrs, result = subject) + aggregate_failures("expect new position #{attrs.inspect}") do + if attrs.nil? + expect(result[:outdated]).to be_truthy + else + new_position = result[:position] + + expect(result[:outdated]).to be_falsey + expect(new_position).not_to be_nil + expect(new_position.diff_refs).to eq(new_diff_refs) + + attrs.each do |attr, value| + expect(new_position.send(attr)).to eq(value) + end + end + end + end + + def expect_change_position(attrs, result = subject) + aggregate_failures("expect change position #{attrs.inspect}") do + change_position = result[:position] + + expect(result[:outdated]).to be_truthy + + if attrs.nil? || attrs.empty? + expect(change_position).to be_nil + else + expect(change_position).not_to be_nil + expect(change_position.diff_refs).to eq(change_diff_refs) + + attrs.each do |attr, value| + expect(change_position.send(attr)).to eq(value) + end + end + end + end + + def create_branch(new_name, branch_name) + CreateBranchService.new(project, current_user).execute(new_name, branch_name) + end + + def create_file(branch_name, file_name, content) + Files::CreateService.new( + project, + current_user, + start_branch: branch_name, + branch_name: branch_name, + commit_message: "Create file", + file_path: file_name, + file_content: content + ).execute + project.commit(branch_name) + end + + def update_file(branch_name, file_name, content) + Files::UpdateService.new( + project, + current_user, + start_branch: branch_name, + branch_name: branch_name, + commit_message: "Update file", + file_path: file_name, + file_content: content + ).execute + project.commit(branch_name) + end + + def delete_file(branch_name, file_name) + Files::DeleteService.new( + project, + current_user, + start_branch: branch_name, + branch_name: branch_name, + commit_message: "Delete file", + file_path: file_name + ).execute + project.commit(branch_name) + end +end From 77c35d5d001a0ce0626bc8aeec574eca36c2233b Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 5 Jul 2019 11:14:56 +0100 Subject: [PATCH 099/195] Create private merge requests in forks https://gitlab.com/gitlab-org/gitlab-ce/issues/58583 --- .../components/dropdown.vue | 58 ++++++++ .../components/project_form_group.vue | 136 ++++++++++++++++++ .../confidential_merge_request/index.js | 30 ++++ .../confidential_merge_request/state.js | 5 + .../create_merge_request_dropdown.js | 47 +++++- .../stylesheets/framework/dropdowns.scss | 4 +- .../projects/branches_controller.rb | 2 +- app/controllers/projects/issues_controller.rb | 2 +- app/helpers/issues_helper.rb | 2 +- .../projects/issues/_new_branch.html.haml | 13 +- locale/gitlab.pot | 18 +++ ...creates_confidential_merge_request_spec.rb | 54 +++++++ .../project_form_group_spec.js.snap | 101 +++++++++++++ .../components/dropdown_spec.js | 56 ++++++++ .../components/project_form_group_spec.js | 77 ++++++++++ .../create_merge_request_dropdown_spec.js | 9 +- 16 files changed, 599 insertions(+), 15 deletions(-) create mode 100644 app/assets/javascripts/confidential_merge_request/components/dropdown.vue create mode 100644 app/assets/javascripts/confidential_merge_request/components/project_form_group.vue create mode 100644 app/assets/javascripts/confidential_merge_request/index.js create mode 100644 app/assets/javascripts/confidential_merge_request/state.js create mode 100644 spec/features/issues/user_creates_confidential_merge_request_spec.rb create mode 100644 spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap create mode 100644 spec/frontend/confidential_merge_request/components/dropdown_spec.js create mode 100644 spec/frontend/confidential_merge_request/components/project_form_group_spec.js rename spec/{javascripts => frontend}/create_merge_request_dropdown_spec.js (92%) diff --git a/app/assets/javascripts/confidential_merge_request/components/dropdown.vue b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue new file mode 100644 index 00000000000..444640980af --- /dev/null +++ b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue @@ -0,0 +1,58 @@ + + + diff --git a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue new file mode 100644 index 00000000000..b89729375be --- /dev/null +++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue @@ -0,0 +1,136 @@ + + + diff --git a/app/assets/javascripts/confidential_merge_request/index.js b/app/assets/javascripts/confidential_merge_request/index.js new file mode 100644 index 00000000000..9672821d30e --- /dev/null +++ b/app/assets/javascripts/confidential_merge_request/index.js @@ -0,0 +1,30 @@ +import Vue from 'vue'; +import { parseBoolean } from '../lib/utils/common_utils'; +import ProjectFormGroup from './components/project_form_group.vue'; +import state from './state'; + +export function isConfidentialIssue() { + return parseBoolean(document.querySelector('.js-create-mr').dataset.isConfidential); +} + +export function canCreateConfidentialMergeRequest() { + return isConfidentialIssue() && Object.keys(state.selectedProject).length > 0; +} + +export function init() { + const el = document.getElementById('js-forked-project'); + + return new Vue({ + el, + render(h) { + return h(ProjectFormGroup, { + props: { + namespacePath: el.dataset.namespacePath, + projectPath: el.dataset.projectPath, + newForkPath: el.dataset.newForkPath, + helpPagePath: el.dataset.helpPagePath, + }, + }); + }, + }); +} diff --git a/app/assets/javascripts/confidential_merge_request/state.js b/app/assets/javascripts/confidential_merge_request/state.js new file mode 100644 index 00000000000..95b0580f4b9 --- /dev/null +++ b/app/assets/javascripts/confidential_merge_request/state.js @@ -0,0 +1,5 @@ +import Vue from 'vue'; + +export default Vue.observable({ + selectedProject: {}, +}); diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index 8f5cece0788..052168bb21c 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -5,6 +5,12 @@ import Flash from './flash'; import DropLab from './droplab/drop_lab'; import ISetter from './droplab/plugins/input_setter'; import { __, sprintf } from './locale'; +import { + init as initConfidentialMergeRequest, + isConfidentialIssue, + canCreateConfidentialMergeRequest, +} from './confidential_merge_request'; +import confidentialMergeRequestState from './confidential_merge_request/state'; // Todo: Remove this when fixing issue in input_setter plugin const InputSetter = Object.assign({}, ISetter); @@ -12,6 +18,17 @@ const InputSetter = Object.assign({}, ISetter); const CREATE_MERGE_REQUEST = 'create-mr'; const CREATE_BRANCH = 'create-branch'; +function createEndpoint(projectPath, endpoint) { + if (canCreateConfidentialMergeRequest()) { + return endpoint.replace( + projectPath, + confidentialMergeRequestState.selectedProject.pathWithNamespace, + ); + } + + return endpoint; +} + export default class CreateMergeRequestDropdown { constructor(wrapperEl) { this.wrapperEl = wrapperEl; @@ -42,6 +59,8 @@ export default class CreateMergeRequestDropdown { this.refIsValid = true; this.refsPath = this.wrapperEl.dataset.refsPath; this.suggestedRef = this.refInput.value; + this.projectPath = this.wrapperEl.dataset.projectPath; + this.projectId = this.wrapperEl.dataset.projectId; // These regexps are used to replace // a backend generated new branch name and its source (ref) @@ -58,6 +77,14 @@ export default class CreateMergeRequestDropdown { }; this.init(); + + if (isConfidentialIssue()) { + this.createMergeRequestButton.setAttribute( + 'data-dropdown-trigger', + '#create-merge-request-dropdown', + ); + initConfidentialMergeRequest(); + } } available() { @@ -113,7 +140,9 @@ export default class CreateMergeRequestDropdown { this.isCreatingBranch = true; return axios - .post(this.createBranchPath) + .post(createEndpoint(this.projectPath, this.createBranchPath), { + confidential_issue_project_id: canCreateConfidentialMergeRequest() ? this.projectId : null, + }) .then(({ data }) => { this.branchCreated = true; window.location.href = data.url; @@ -125,7 +154,11 @@ export default class CreateMergeRequestDropdown { this.isCreatingMergeRequest = true; return axios - .post(this.createMrPath) + .post(this.createMrPath, { + target_project_id: canCreateConfidentialMergeRequest() + ? confidentialMergeRequestState.selectedProject.id + : null, + }) .then(({ data }) => { this.mergeRequestCreated = true; window.location.href = data.url; @@ -149,6 +182,8 @@ export default class CreateMergeRequestDropdown { } enable() { + if (!canCreateConfidentialMergeRequest()) return; + this.createMergeRequestButton.classList.remove('disabled'); this.createMergeRequestButton.removeAttribute('disabled'); @@ -205,7 +240,7 @@ export default class CreateMergeRequestDropdown { if (!ref) return false; return axios - .get(`${this.refsPath}${encodeURIComponent(ref)}`) + .get(`${createEndpoint(this.projectPath, this.refsPath)}${encodeURIComponent(ref)}`) .then(({ data }) => { const branches = data[Object.keys(data)[0]]; const tags = data[Object.keys(data)[1]]; @@ -325,6 +360,12 @@ export default class CreateMergeRequestDropdown { let xhr = null; event.preventDefault(); + if (isConfidentialIssue() && !event.target.classList.contains('js-create-target')) { + this.droplab.hooks.forEach(hook => hook.list.toggle()); + + return; + } + if (this.isBusy()) { return; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index cd951f67293..a12029d2419 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -287,8 +287,8 @@ list-style: none; padding: 0 1px; - a, - button, + a:not(.help-link), + button:not(.btn), .menu-item { @include dropdown-link; } diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index d77f64a84f5..141a7dfb923 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -169,7 +169,7 @@ class Projects::BranchesController < Projects::ApplicationController end def confidential_issue_project - return unless Feature.enabled?(:create_confidential_merge_request, @project) + return unless helpers.create_confidential_merge_request_enabled? return if params[:confidential_issue_project_id].blank? confidential_issue_project = Project.find(params[:confidential_issue_project_id]) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index e275b417784..b866f574f67 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -172,7 +172,7 @@ class Projects::IssuesController < Projects::ApplicationController def create_merge_request create_params = params.slice(:branch_name, :ref).merge(issue_iid: issue.iid) - create_params[:target_project_id] = params[:target_project_id] if Feature.enabled?(:create_confidential_merge_request, @project) + create_params[:target_project_id] = params[:target_project_id] if helpers.create_confidential_merge_request_enabled? result = ::MergeRequests::CreateFromIssueService.new(project, current_user, create_params).execute if result[:status] == :success diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index dfadcfc33b2..5476a7cdff6 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -137,7 +137,7 @@ module IssuesHelper end def create_confidential_merge_request_enabled? - Feature.enabled?(:create_confidential_merge_request, @project) + Feature.enabled?(:create_confidential_merge_request, @project, default_enabled: true) end def show_new_branch_button? diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index 52bb797b5b3..8d3e54dc455 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -3,13 +3,14 @@ - data_action = can_create_merge_request ? 'create-mr' : 'create-branch' - value = can_create_merge_request ? 'Create merge request' : 'Create branch' - value = can_create_confidential_merge_request? ? _('Create confidential merge request') : value + - create_mr_text = can_create_confidential_merge_request? ? _('Create confidential merge request') : _('Create merge request') - can_create_path = can_create_branch_project_issue_path(@project, @issue) - create_mr_path = create_merge_request_project_issue_path(@project, @issue, branch_name: @issue.to_branch_name, ref: @project.default_branch) - create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid) - refs_path = refs_namespace_project_path(@project.namespace, @project, search: '') - .create-mr-dropdown-wrap.d-inline-block.full-width-mobile{ data: { can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path } } + .create-mr-dropdown-wrap.d-inline-block.full-width-mobile.js-create-mr{ data: { project_path: @project.full_path, project_id: @project.id, can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path, is_confidential: can_create_confidential_merge_request?.to_s } } .btn-group.btn-group-sm.unavailable %button.btn.btn-grouped{ type: 'button', disabled: 'disabled' } = icon('spinner', class: 'fa-spin') @@ -26,7 +27,7 @@ .droplab-dropdown %ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-right.gl-show-field-errors{ class: ("create-confidential-merge-request-dropdown-menu" if can_create_confidential_merge_request?), data: { dropdown: true } } - if can_create_merge_request - %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: _('Create merge request') } } + %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: create_mr_text } } .menu-item = icon('check', class: 'icon') - if can_create_confidential_merge_request? @@ -41,6 +42,8 @@ %li.divider.droplab-item-ignore %li.droplab-item-ignore.prepend-left-8.append-right-8.prepend-top-16 + - if can_create_confidential_merge_request? + #js-forked-project{ data: { namespace_path: @project.namespace.full_path, project_path: @project.full_path, new_fork_path: new_project_fork_path(@project), help_page_path: help_page_path('user/project/merge_requests') } } .form-group %label{ for: 'new-branch-name' } = _('Branch name') @@ -55,4 +58,8 @@ .form-group %button.btn.btn-success.js-create-target{ type: 'button', data: { action: 'create-mr' } } - = _('Create merge request') + = create_mr_text + + - if can_create_confidential_merge_request? + %p.text-warning.js-exposed-info-warning.hidden + = _('This may expose confidential information as the selected fork is in another namespace that can have other members.') diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8a4f57c5b13..05d291022dc 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4169,6 +4169,9 @@ msgstr "" msgid "Error fetching diverging counts for branches. Please try again." msgstr "" +msgid "Error fetching forked projects. Please try again." +msgstr "" + msgid "Error fetching labels." msgstr "" @@ -6825,6 +6828,9 @@ msgstr "" msgid "No files found." msgstr "" +msgid "No forks available to you." +msgstr "" + msgid "No job trace" msgstr "" @@ -9192,6 +9198,9 @@ msgstr "" msgid "Select members to invite" msgstr "" +msgid "Select private project" +msgstr "" + msgid "Select project" msgstr "" @@ -10753,6 +10762,9 @@ msgstr "" msgid "This job will automatically run after its timer finishes. Often they are used for incremental roll-out deploys to production environments. When unscheduled it converts into a manual action." msgstr "" +msgid "This may expose confidential information as the selected fork is in another namespace that can have other members." +msgstr "" + msgid "This means you can not push code until you create an empty repository or import existing one." msgstr "" @@ -11084,6 +11096,12 @@ msgstr "" msgid "To preserve performance only %{display_size} of %{real_size} files are displayed." msgstr "" +msgid "To protect this issues confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private." +msgstr "" + +msgid "To protect this issues confidentiality, a private fork of this project was selected." +msgstr "" + msgid "To see all the user's personal access tokens you must impersonate them first." msgstr "" diff --git a/spec/features/issues/user_creates_confidential_merge_request_spec.rb b/spec/features/issues/user_creates_confidential_merge_request_spec.rb new file mode 100644 index 00000000000..7ae4af4667b --- /dev/null +++ b/spec/features/issues/user_creates_confidential_merge_request_spec.rb @@ -0,0 +1,54 @@ +require 'rails_helper' + +describe 'User creates confidential merge request on issue page', :js do + include ProjectForksHelper + + let(:user) { create(:user) } + let(:project) { create(:project, :repository, :public) } + let(:issue) { create(:issue, project: project, confidential: true) } + + def visit_confidential_issue + sign_in(user) + visit project_issue_path(project, issue) + wait_for_requests + end + + before do + project.add_developer(user) + end + + context 'user has no private fork' do + before do + fork_project(project, user, repository: true) + visit_confidential_issue + end + + it 'shows that user has no fork available' do + click_button 'Create confidential merge request' + + page.within '.create-confidential-merge-request-dropdown-menu' do + expect(page).to have_content('No forks available to you') + end + end + end + + describe 'user has private fork' do + let(:forked_project) { fork_project(project, user, repository: true) } + + before do + forked_project.update(visibility: Gitlab::VisibilityLevel::PRIVATE) + visit_confidential_issue + end + + it 'create merge request in fork' do + click_button 'Create confidential merge request' + + page.within '.create-confidential-merge-request-dropdown-menu' do + expect(page).to have_button(forked_project.name_with_namespace) + click_button 'Create confidential merge request' + end + + expect(page).to have_content(forked_project.namespace.name) + end + end +end diff --git a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap new file mode 100644 index 00000000000..a241c764df7 --- /dev/null +++ b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Confidential merge request project form group component renders empty state when response is empty 1`] = ` +
+ + +
+ + +

+ + No forks available to you. +
+ + + To protect this issues confidentiality, + + fork the project + + and set the forks visiblity to private. + + + + + Read more + + +