From 47e0a6cf429fed8d7bd527f8ab9fa53a86caedfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Thu, 21 Dec 2017 17:47:39 +0100 Subject: [PATCH 01/61] Remove environment_scope in user/gcp show partial --- app/views/projects/clusters/gcp/_show.html.haml | 4 ---- app/views/projects/clusters/user/_show.html.haml | 4 ---- 2 files changed, 8 deletions(-) diff --git a/app/views/projects/clusters/gcp/_show.html.haml b/app/views/projects/clusters/gcp/_show.html.haml index bde85aed341..f3122a1bf47 100644 --- a/app/views/projects/clusters/gcp/_show.html.haml +++ b/app/views/projects/clusters/gcp/_show.html.haml @@ -9,10 +9,6 @@ = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| = form_errors(@cluster) - .form-group - = field.label :environment_scope, s_('ClusterIntegration|Environment scope') - = field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope') - = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| .form-group = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL') diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml index 89595bca007..5931e0b7f17 100644 --- a/app/views/projects/clusters/user/_show.html.haml +++ b/app/views/projects/clusters/user/_show.html.haml @@ -4,10 +4,6 @@ = field.label :name, s_('ClusterIntegration|Cluster name') = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name') - .form-group - = field.label :environment_scope, s_('ClusterIntegration|Environment scope') - = field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope') - = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| .form-group = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL') From 9812d8c283abcd2b2ab3c7fea061c77f6f4eeb99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Thu, 21 Dec 2017 17:58:46 +0100 Subject: [PATCH 02/61] Add environment_scope to enabled partial --- app/views/projects/clusters/_banner.html.haml | 14 +++------- .../projects/clusters/_enabled.html.haml | 26 +++++++++++++++---- app/views/projects/clusters/show.html.haml | 2 +- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/app/views/projects/clusters/_banner.html.haml b/app/views/projects/clusters/_banner.html.haml index 76a66fb92a2..6b9507c854f 100644 --- a/app/views/projects/clusters/_banner.html.haml +++ b/app/views/projects/clusters/_banner.html.haml @@ -1,6 +1,7 @@ -%h4= s_('ClusterIntegration|Enable cluster integration') -.settings-content +%h4= s_('ClusterIntegration|Cluster integration') +%p= s_('ClusterIntegration|Control how your cluster integrates with GitLab') +.settings-content .hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' } = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine') %p.js-error-reason @@ -10,12 +11,3 @@ .hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' } = s_('ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster\'s details') - - %p - - if @cluster.enabled? - - if can?(current_user, :update_cluster, @cluster) - = s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.') - - else - = s_('ClusterIntegration|Cluster integration is enabled for this project.') - - else - = s_('ClusterIntegration|Cluster integration is disabled for this project.') diff --git a/app/views/projects/clusters/_enabled.html.haml b/app/views/projects/clusters/_enabled.html.haml index 547b3c8446f..1eac2c9dc1f 100644 --- a/app/views/projects/clusters/_enabled.html.haml +++ b/app/views/projects/clusters/_enabled.html.haml @@ -1,7 +1,16 @@ = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| = form_errors(@cluster) - .form-group.append-bottom-20 - %label.append-bottom-10 + .form-group + %h5= s_('ClusterIntegration|Integration status') + %p + - if @cluster.enabled? + - if can?(current_user, :update_cluster, @cluster) + = s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.') + - else + = s_('ClusterIntegration|Cluster integration is enabled for this project.') + - else + = s_('ClusterIntegration|Cluster integration is disabled for this project.') + %label = field.hidden_field :enabled, { class: 'js-toggle-input'} %button{ type: 'button', @@ -12,6 +21,13 @@ = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') - - if can?(current_user, :update_cluster, @cluster) - .form-group - = field.submit _('Save'), class: 'btn btn-success' + .form-group + %h5= s_('ClusterIntegration|Environment scope') + %p + = s_("ClusterIntegration|Choose which of your project's environments will use this cluster.") + = link_to s_("ClusterIntegration|Learn more about environments"), help_page_path('ci/environments') + = field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope') + + - if can?(current_user, :update_cluster, @cluster) + .form-group + = field.submit _('Save changes'), class: 'btn btn-success' diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index fe6dacf1f0d..dfaef8716de 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -17,7 +17,7 @@ .js-cluster-application-notice .flash-container - %section.settings.no-animate.expanded + %section.settings.no-animate.expanded#cluster-integration = render 'banner' = render 'enabled' From d2d494196da1f9665034845c19bc5b87c3c67ebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Mon, 18 Dec 2017 19:22:53 +0100 Subject: [PATCH 03/61] Match updated clusters/show in feature specs --- spec/features/projects/clusters/gcp_spec.rb | 6 +++--- spec/features/projects/clusters/user_spec.rb | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 67b8901f8fb..882a2756b72 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -81,14 +81,14 @@ feature 'Gcp Cluster', :js do end it 'user sees a cluster details page' do - expect(page).to have_button('Save') + expect(page).to have_button('Save changes') expect(page.find(:css, '.cluster-name').value).to eq(cluster.name) end context 'when user disables the cluster' do before do page.find(:css, '.js-toggle-cluster').click - click_button 'Save' + page.within('#cluster-integration') { click_button 'Save changes' } end it 'user sees the successful message' do @@ -99,7 +99,7 @@ feature 'Gcp Cluster', :js do context 'when user changes cluster parameters' do before do fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace' - click_button 'Save changes' + page.within('#js-cluster-details') { click_button 'Save changes' } end it 'user sees the successful message' do diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb index 414f4acba86..a519b9f9c7e 100644 --- a/spec/features/projects/clusters/user_spec.rb +++ b/spec/features/projects/clusters/user_spec.rb @@ -29,7 +29,7 @@ feature 'User Cluster', :js do end it 'user sees a cluster details page' do - expect(page).to have_content('Enable cluster integration') + expect(page).to have_content('Cluster integration') expect(page.find_field('cluster[name]').value).to eq('dev-cluster') expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value) .to have_content('http://example.com') @@ -57,14 +57,14 @@ feature 'User Cluster', :js do end it 'user sees a cluster details page' do - expect(page).to have_button('Save') + expect(page).to have_button('Save changes') end context 'when user disables the cluster' do before do page.find(:css, '.js-toggle-cluster').click fill_in 'cluster_name', with: 'dev-cluster' - click_button 'Save' + page.within('#cluster-integration') { click_button 'Save changes' } end it 'user sees the successful message' do @@ -76,7 +76,7 @@ feature 'User Cluster', :js do before do fill_in 'cluster_name', with: 'my-dev-cluster' fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace' - click_button 'Save changes' + page.within('#js-cluster-details') { click_button 'Save changes' } end it 'user sees the successful message' do From 9e2febf82fa63ff07e513cd400c7a41e4a4fe4bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Wed, 20 Dec 2017 18:19:34 +0100 Subject: [PATCH 04/61] Environment pattern -> Environment scope --- app/views/projects/clusters/_cluster.html.haml | 2 +- app/views/projects/clusters/index.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/clusters/_cluster.html.haml b/app/views/projects/clusters/_cluster.html.haml index ad696daa259..3943dfc0856 100644 --- a/app/views/projects/clusters/_cluster.html.haml +++ b/app/views/projects/clusters/_cluster.html.haml @@ -4,7 +4,7 @@ .table-mobile-content = link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster) .table-section.section-30 - .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment pattern") + .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope") .table-mobile-content= cluster.environment_scope .table-section.section-30 .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Project namespace") diff --git a/app/views/projects/clusters/index.html.haml b/app/views/projects/clusters/index.html.haml index bec512be91c..74dbe859eea 100644 --- a/app/views/projects/clusters/index.html.haml +++ b/app/views/projects/clusters/index.html.haml @@ -13,7 +13,7 @@ .table-section.section-30{ role: "rowheader" } = s_("ClusterIntegration|Cluster") .table-section.section-30{ role: "rowheader" } - = s_("ClusterIntegration|Environment pattern") + = s_("ClusterIntegration|Environment scope") .table-section.section-30{ role: "rowheader" } = s_("ClusterIntegration|Project namespace") .table-section.section-10{ role: "rowheader" } From fea009bfdacad62f03d0cf3ea02d54ec9989390c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Thu, 21 Dec 2017 18:24:38 +0100 Subject: [PATCH 05/61] Rename enabled partial to integration_form --- app/views/projects/clusters/_banner.html.haml | 3 ++- .../{_enabled.html.haml => _integration_form.html.haml} | 0 app/views/projects/clusters/show.html.haml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) rename app/views/projects/clusters/{_enabled.html.haml => _integration_form.html.haml} (100%) diff --git a/app/views/projects/clusters/_banner.html.haml b/app/views/projects/clusters/_banner.html.haml index 6b9507c854f..26ca3307a4a 100644 --- a/app/views/projects/clusters/_banner.html.haml +++ b/app/views/projects/clusters/_banner.html.haml @@ -1,5 +1,4 @@ %h4= s_('ClusterIntegration|Cluster integration') -%p= s_('ClusterIntegration|Control how your cluster integrates with GitLab') .settings-content .hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' } @@ -11,3 +10,5 @@ .hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' } = s_('ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster\'s details') + + %p= s_('ClusterIntegration|Control how your cluster integrates with GitLab') diff --git a/app/views/projects/clusters/_enabled.html.haml b/app/views/projects/clusters/_integration_form.html.haml similarity index 100% rename from app/views/projects/clusters/_enabled.html.haml rename to app/views/projects/clusters/_integration_form.html.haml diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index dfaef8716de..c15785806b9 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -19,7 +19,7 @@ %section.settings.no-animate.expanded#cluster-integration = render 'banner' - = render 'enabled' + = render 'integration_form' .cluster-applications-table#js-cluster-applications From 59e50e33b3203bfe450f82d2ee2cc83b87f9e5fb Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Thu, 21 Dec 2017 15:05:35 +0100 Subject: [PATCH 06/61] Reroute batch blobs to single blob RPC Given the priorities shifted for the Gitaly team, this endpoint does not get a dedicated endpoint yet. To make it work in a cloud native environment the request needs to go to Gitaly, not rugged. This is achieved by rerouting to the generic TreeEntry endpoint. --- lib/gitlab/git/blob.rb | 31 +++++++++++++++++++++++++------ spec/lib/gitlab/git/blob_spec.rb | 15 ++++----------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index 228d97a87ab..bd91125d3b6 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -50,10 +50,19 @@ module Gitlab # to the caller to limit the number of blobs and blob_size_limit. # # Gitaly migration issue: https://gitlab.com/gitlab-org/gitaly/issues/798 - def batch(repository, blob_references, blob_size_limit: nil) - blob_size_limit ||= MAX_DATA_DISPLAY_SIZE - blob_references.map do |sha, path| - find_by_rugged(repository, sha, path, limit: blob_size_limit) + def batch(repository, blob_references, blob_size_limit: MAX_DATA_DISPLAY_SIZE) + Gitlab::GitalyClient.migrate(:list_blobs_by_sha_path) do |is_enabled| + if is_enabled + Gitlab::GitalyClient.allow_n_plus_1_calls do + blob_references.map do |sha, path| + find_by_gitaly(repository, sha, path, limit: blob_size_limit) + end + end + else + blob_references.map do |sha, path| + find_by_rugged(repository, sha, path, limit: blob_size_limit) + end + end end end @@ -122,13 +131,23 @@ module Gitlab ) end - def find_by_gitaly(repository, sha, path) + def find_by_gitaly(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE) path = path.sub(/\A\/*/, '') path = '/' if path.empty? name = File.basename(path) - entry = Gitlab::GitalyClient::CommitService.new(repository).tree_entry(sha, path, MAX_DATA_DISPLAY_SIZE) + + # Gitaly will think that setting the limit to 0 means unlimited, while + # the client might only need the metadata and thus set the limit to 0. + # In this method we'll than set the limit to 1, but clear the byte of data + # that we got back so fot the outside world it looks like the limit was + # actually 0. + req_limit = limit == 0 ? 1 : limit + + entry = Gitlab::GitalyClient::CommitService.new(repository).tree_entry(sha, path, req_limit) return unless entry + entry.data = "" if limit == 0 + case entry.type when :COMMIT new( diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index c04a9688503..7f5946b1658 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -202,16 +202,6 @@ describe Gitlab::Git::Blob, seed_helper: true do context 'limiting' do subject { described_class.batch(repository, blob_references, blob_size_limit: blob_size_limit) } - context 'default' do - let(:blob_size_limit) { nil } - - it 'limits to MAX_DATA_DISPLAY_SIZE' do - stub_const('Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE', 100) - - expect(subject.first.data.size).to eq(100) - end - end - context 'positive' do let(:blob_size_limit) { 10 } @@ -221,7 +211,10 @@ describe Gitlab::Git::Blob, seed_helper: true do context 'zero' do let(:blob_size_limit) { 0 } - it { expect(subject.first.data).to eq('') } + it 'only loads the metadata' do + expect(subject.first.size).not_to be(0) + expect(subject.first.data).to eq('') + end end context 'negative' do From 1f50eb4f57ae14336a677f53d9a2c9501033f966 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 20 Dec 2017 14:05:02 +0100 Subject: [PATCH 07/61] Add docs about end-to-end testing / GitLab QA tests --- .../testing_guide/end_to_end_tests.md | 42 +++++++++++++++++++ doc/development/testing_guide/index.md | 8 ++++ 2 files changed, 50 insertions(+) create mode 100644 doc/development/testing_guide/end_to_end_tests.md diff --git a/doc/development/testing_guide/end_to_end_tests.md b/doc/development/testing_guide/end_to_end_tests.md new file mode 100644 index 00000000000..87a21b9f7fc --- /dev/null +++ b/doc/development/testing_guide/end_to_end_tests.md @@ -0,0 +1,42 @@ +# End-to-End Testing + +## What is End-to-End testing? + +End-to-End testing is a strategy used to check whether your application works +as expected across entire software stack and architecture, including +integration of all microservices and components that are supposed to work +together. + +## How do we test GitLab? + +We use [Omnibus GitLab][omnibus-gitlab] to build GitLab packages and then we +test these packages using [GitLab QA][gitlab-qa] project, which is entirely +black-box, click-driven testing framework. + +### Testing nightly builds + +We run scheduled pipeline each night to test nightly builds created by Omnibus. +You can find these nightly pipelines at [GitLab QA pipelines page][gitlab-qa-pipelines]. + +### Testing code in merge requests + +It is also possible to trigger packages build and [GitLab QA pipeline][gitlab-qa-pipelines] +using a manual action that should be present in the merge request widget on +your merge request. Look for `package-qa` manual action. + +Below you can read more about how to use it and how does it work. + +## How does it work? + +We are using _multi-project pipelines_ to run end-to-end tests. + +## How do I test my code? + +## How do I contribute? + +## Where can I ask for help? + + +[omnibus-gitlab]: https://gitlab.com/gitlab-org/omnibus-gitlab +[gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa +[gitlab-qa-pipelines]: https://gitlab.com/gitlab-org/gitlab-qa/pipelines diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md index 65386f231a0..4ca192aee7e 100644 --- a/doc/development/testing_guide/index.md +++ b/doc/development/testing_guide/index.md @@ -65,6 +65,13 @@ Everything you should know about how to test Rake tasks. --- +## [End-to-end tests](end_to_end_tests.md) + +Everything you should know about how to run end-to-end tests, also known as +[GitLab QA][gitlab-qa] tests. + +--- + ## Spinach (feature) tests GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426) @@ -89,3 +96,4 @@ test should be re-implemented using RSpec instead. [Capybara]: https://github.com/teamcapybara/capybara [Karma]: http://karma-runner.github.io/ [Jasmine]: https://jasmine.github.io/ +[gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa From 629d966c4807174ddb91e753b9aa15ec2697cd08 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 2 Jan 2018 14:22:48 +0100 Subject: [PATCH 08/61] Extend documentation on end-to-end integration tests --- .../testing_guide/end_to_end_tests.md | 47 ++++++++++++++++--- doc/development/testing_guide/index.md | 4 +- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/doc/development/testing_guide/end_to_end_tests.md b/doc/development/testing_guide/end_to_end_tests.md index 87a21b9f7fc..6be0decd75d 100644 --- a/doc/development/testing_guide/end_to_end_tests.md +++ b/doc/development/testing_guide/end_to_end_tests.md @@ -20,23 +20,56 @@ You can find these nightly pipelines at [GitLab QA pipelines page][gitlab-qa-pip ### Testing code in merge requests -It is also possible to trigger packages build and [GitLab QA pipeline][gitlab-qa-pipelines] -using a manual action that should be present in the merge request widget on -your merge request. Look for `package-qa` manual action. +It is also possible to trigger build of GitLab packages and then pass these +package to GitLab QA to run tests in a [pipeline][gitlab-qa-pipelines]. + +Developers can trigger a `package-qa` manual action, that should be present in +the merge request widget in your merge request. + +It is possible to trigger Gitlab QA pipeline from merge requests in GitLab CE +and GitLab EE, but QA triggering manual action is also available in the Omnibus +GitLab project as well. Below you can read more about how to use it and how does it work. -## How does it work? +#### How does it work? -We are using _multi-project pipelines_ to run end-to-end tests. +Currently, we are _multi-project pipeline_-like approach to run QA pipelines. -## How do I test my code? +1. Developer triggers manual action in the CE or EE merge request, that starts +a chain of pipelines. +1. Triggering this action enqueues a new CI job that is going to be picked by a +runner. +1. The script, that is being executed, triggers a pipeline in GitLab Omnibus +projects, and waits for the resulting status. We call it _status attribution_. +1. GitLab packages are being built in the pipeline started in Omnibus. Packages +are going to be sent to Container Registry. +1. When packages are ready, and available in the registry, a final step in the +pipeline that is now running in Omnibus triggers a new pipeline in the GitLab +QA project. It also waits for the resulting status. +1. GitLab QA pulls images from the registry, spins-up containers and runs tests +against test environment that has been just orchestrated. +1. The result of GitLab QA pipeline is being propagated upstream, through +Omnibus, to CE / EE merge request. -## How do I contribute? +#### How do I write tests? + +In order to write new tests, you first need to learn more about GitLab QA +architecture. There is some documentation about it in GitLab QA project +[here][gitlab-qa-architecture]. + +Once you decided we to put test environment orchestration scenarios and +instance specs, take a looks at [relevant documentation][instance-qa-readme] ## Where can I ask for help? +You can ask question in `#qa` channel on Slack (GitLab internal) or you can +find an issue you would like to work on in [the issue tracker][gitlab-qa-issues] +and start a new discussion there. [omnibus-gitlab]: https://gitlab.com/gitlab-org/omnibus-gitlab [gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa [gitlab-qa-pipelines]: https://gitlab.com/gitlab-org/gitlab-qa/pipelines +[instance-qa-readme]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/README.md +[gitlab-qa-architecture]: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/architecture.md +[gitlab-qa-issues]: https://gitlab.com/gitlab-org/gitlab-qa/issues diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md index 4ca192aee7e..74d09eb91ff 100644 --- a/doc/development/testing_guide/index.md +++ b/doc/development/testing_guide/index.md @@ -67,8 +67,8 @@ Everything you should know about how to test Rake tasks. ## [End-to-end tests](end_to_end_tests.md) -Everything you should know about how to run end-to-end tests, also known as -[GitLab QA][gitlab-qa] tests. +Everything you should know about how to run end-to-end tests using +[GitLab QA][gitlab-qa] testing framework. --- From 51bb5abe269c8639b4f9ebbb6cb5e917d2710210 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 2 Jan 2018 14:30:29 +0100 Subject: [PATCH 09/61] Link to end to end test guideline from test pyramid --- doc/development/testing_guide/testing_levels.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md index 1cbd4350284..dd0e9a77164 100644 --- a/doc/development/testing_guide/testing_levels.md +++ b/doc/development/testing_guide/testing_levels.md @@ -121,6 +121,9 @@ running feature tests (i.e. using Capybara) against it. The actual test scenarios and steps are [part of GitLab Rails] so that they're always in-sync with the codebase. +Read a separate document about [end-to-end tests](../end_to_end_tests.md) to +learn more. + [multiple pieces]: ../architecture.md#components [GitLab Shell]: https://gitlab.com/gitlab-org/gitlab-shell [GitLab Workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse From 1aa25a3fe3b7167c2d9c73930cf9a05794be0dd2 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 2 Jan 2018 14:37:01 +0100 Subject: [PATCH 10/61] Copy-edit end-to-end testing guidelines --- .../testing_guide/end_to_end_tests.md | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/doc/development/testing_guide/end_to_end_tests.md b/doc/development/testing_guide/end_to_end_tests.md index 6be0decd75d..561547e1581 100644 --- a/doc/development/testing_guide/end_to_end_tests.md +++ b/doc/development/testing_guide/end_to_end_tests.md @@ -36,21 +36,24 @@ Below you can read more about how to use it and how does it work. Currently, we are _multi-project pipeline_-like approach to run QA pipelines. -1. Developer triggers manual action in the CE or EE merge request, that starts -a chain of pipelines. -1. Triggering this action enqueues a new CI job that is going to be picked by a -runner. +1. Developer triggers a manual action, that can be found in CE and EE merge +requests, what starts a chain of pipelines. + 1. The script, that is being executed, triggers a pipeline in GitLab Omnibus -projects, and waits for the resulting status. We call it _status attribution_. -1. GitLab packages are being built in the pipeline started in Omnibus. Packages -are going to be sent to Container Registry. +projects, and waits for the resulting status. We call this a _status attribution_. + +1. GitLab packages are being built in Omnibus pipeline. Packages are going to be +pushed to Container Registry. + 1. When packages are ready, and available in the registry, a final step in the -pipeline that is now running in Omnibus triggers a new pipeline in the GitLab -QA project. It also waits for the resulting status. +pipeline, that is now running in Omnibus, triggers a new pipeline in the GitLab +QA project. It also waits for a resulting status. + 1. GitLab QA pulls images from the registry, spins-up containers and runs tests -against test environment that has been just orchestrated. +against a test environment that has been just orchestrated by `gitlab-qa` tool. + 1. The result of GitLab QA pipeline is being propagated upstream, through -Omnibus, to CE / EE merge request. +Omnibus, back to CE / EE merge request. #### How do I write tests? @@ -58,8 +61,9 @@ In order to write new tests, you first need to learn more about GitLab QA architecture. There is some documentation about it in GitLab QA project [here][gitlab-qa-architecture]. -Once you decided we to put test environment orchestration scenarios and +Once you decided were to put test environment orchestration scenarios and instance specs, take a looks at [relevant documentation][instance-qa-readme] +and examples in [the `qa/` directory][instance-qa-examples]. ## Where can I ask for help? @@ -70,6 +74,7 @@ and start a new discussion there. [omnibus-gitlab]: https://gitlab.com/gitlab-org/omnibus-gitlab [gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa [gitlab-qa-pipelines]: https://gitlab.com/gitlab-org/gitlab-qa/pipelines -[instance-qa-readme]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/README.md [gitlab-qa-architecture]: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/architecture.md [gitlab-qa-issues]: https://gitlab.com/gitlab-org/gitlab-qa/issues +[instance-qa-readme]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/README.md +[instance-qa-examples]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa From 124ffb2134be85575ddc75bfa34903fb738c6930 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 2 Jan 2018 14:42:28 +0100 Subject: [PATCH 11/61] Fix link to end-to-end testing docs from test pyramid --- doc/development/testing_guide/testing_levels.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md index dd0e9a77164..4adf0dc7c7a 100644 --- a/doc/development/testing_guide/testing_levels.md +++ b/doc/development/testing_guide/testing_levels.md @@ -121,7 +121,7 @@ running feature tests (i.e. using Capybara) against it. The actual test scenarios and steps are [part of GitLab Rails] so that they're always in-sync with the codebase. -Read a separate document about [end-to-end tests](../end_to_end_tests.md) to +Read a separate document about [end-to-end tests](end_to_end_tests.md) to learn more. [multiple pieces]: ../architecture.md#components From 5e0143a84bca7fd8b2dccd175e0f50c87dea4b98 Mon Sep 17 00:00:00 2001 From: Alessio Caiazza Date: Sat, 27 May 2017 15:23:27 +0200 Subject: [PATCH 12/61] Add online attribute to runner api entity --- .../unreleased/feature-api_runners_online.yml | 4 +++ doc/api/runners.md | 29 +++++++++++++------ lib/api/entities.rb | 1 + 3 files changed, 25 insertions(+), 9 deletions(-) create mode 100644 changelogs/unreleased/feature-api_runners_online.yml diff --git a/changelogs/unreleased/feature-api_runners_online.yml b/changelogs/unreleased/feature-api_runners_online.yml new file mode 100644 index 00000000000..f5077507e5b --- /dev/null +++ b/changelogs/unreleased/feature-api_runners_online.yml @@ -0,0 +1,4 @@ +--- +title: Add online attribute to runner api entity +merge_request: 11750 +author: Alessio Caiazza diff --git a/doc/api/runners.md b/doc/api/runners.md index 015b09a745e..50981ed96bc 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -30,14 +30,16 @@ Example response: "description": "test-1-20150125", "id": 6, "is_shared": false, - "name": null + "name": null, + "online": true }, { "active": true, "description": "test-2-20150125", "id": 8, "is_shared": false, - "name": null + "name": null, + "online": false } ] ``` @@ -69,28 +71,32 @@ Example response: "description": "shared-runner-1", "id": 1, "is_shared": true, - "name": null + "name": null, + "online": true }, { "active": true, "description": "shared-runner-2", "id": 3, "is_shared": true, - "name": null + "name": null, + "online": false }, { "active": true, "description": "test-1-20150125", "id": 6, "is_shared": false, - "name": null + "name": null, + "online": true }, { "active": true, "description": "test-2-20150125", "id": 8, "is_shared": false, - "name": null + "name": null, + "online": false } ] ``` @@ -122,6 +128,7 @@ Example response: "is_shared": false, "contacted_at": "2016-01-25T16:39:48.066Z", "name": null, + "online": true, "platform": null, "projects": [ { @@ -176,6 +183,7 @@ Example response: "is_shared": false, "contacted_at": "2016-01-25T16:39:48.066Z", "name": null, + "online": true, "platform": null, "projects": [ { @@ -327,14 +335,16 @@ Example response: "description": "test-2-20150125", "id": 8, "is_shared": false, - "name": null + "name": null, + "online": false }, { "active": true, "description": "development_runner", "id": 5, "is_shared": true, - "name": null + "name": null, + "online": true } ] ``` @@ -364,7 +374,8 @@ Example response: "description": "test-2016-02-01", "id": 9, "is_shared": false, - "name": null + "name": null, + "online": true } ``` diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 4ad4a1f7867..c612dde7f73 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -862,6 +862,7 @@ module API expose :active expose :is_shared expose :name + expose :online?, as: :online end class RunnerDetails < Runner From 0d6b9e30cb5c3b76ee97cd14dea1dae12a74e8d6 Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas Date: Fri, 22 Dec 2017 12:25:24 -0600 Subject: [PATCH 13/61] Fix import project url not updating project name --- .../javascripts/projects/project_new.js | 7 ++-- .../jivl-fix-import-project-url-bug.yml | 5 +++ spec/javascripts/projects/project_new_spec.js | 32 +++++++++++-------- 3 files changed, 27 insertions(+), 17 deletions(-) create mode 100644 changelogs/unreleased/jivl-fix-import-project-url-bug.yml diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 3ecc0c2a6e5..4710e70d619 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -1,6 +1,7 @@ let hasUserDefinedProjectPath = false; -const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => { +const deriveProjectPathFromUrl = ($projectImportUrl) => { + const $currentProjectPath = $projectImportUrl.parents('.toggle-import-form').find('#project_path'); if (hasUserDefinedProjectPath) { return; } @@ -21,7 +22,7 @@ const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => { // extract everything after the last slash const pathMatch = /\/([^/]+)$/.exec(importUrl); if (pathMatch) { - $projectPath.val(pathMatch[1]); + $currentProjectPath.val(pathMatch[1]); } }; @@ -96,7 +97,7 @@ const bindEvents = () => { hasUserDefinedProjectPath = $projectPath.val().trim().length > 0; }); - $projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl, $projectPath)); + $projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl)); }; document.addEventListener('DOMContentLoaded', bindEvents); diff --git a/changelogs/unreleased/jivl-fix-import-project-url-bug.yml b/changelogs/unreleased/jivl-fix-import-project-url-bug.yml new file mode 100644 index 00000000000..0d97b9c9a53 --- /dev/null +++ b/changelogs/unreleased/jivl-fix-import-project-url-bug.yml @@ -0,0 +1,5 @@ +--- +title: Fix import project url not updating project name +merge_request: 16120 +author: +type: fixed diff --git a/spec/javascripts/projects/project_new_spec.js b/spec/javascripts/projects/project_new_spec.js index 850768f0e4f..c314ca8ab72 100644 --- a/spec/javascripts/projects/project_new_spec.js +++ b/spec/javascripts/projects/project_new_spec.js @@ -6,8 +6,12 @@ describe('New Project', () => { beforeEach(() => { setFixtures(` - - +
+
+ + +
+
`); $projectImportUrl = $('#project_import_url'); @@ -25,7 +29,7 @@ describe('New Project', () => { it('does not change project path for disabled $projectImportUrl', () => { $projectImportUrl.attr('disabled', true); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual(dummyImportUrl); }); @@ -38,7 +42,7 @@ describe('New Project', () => { it('does not change project path if it is set by user', () => { $projectPath.keyup(); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual(dummyImportUrl); }); @@ -46,7 +50,7 @@ describe('New Project', () => { it('does not change project path for empty $projectImportUrl', () => { $projectImportUrl.val(''); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual(dummyImportUrl); }); @@ -54,7 +58,7 @@ describe('New Project', () => { it('does not change project path for whitespace $projectImportUrl', () => { $projectImportUrl.val(' '); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual(dummyImportUrl); }); @@ -62,7 +66,7 @@ describe('New Project', () => { it('does not change project path for $projectImportUrl without slashes', () => { $projectImportUrl.val('has-no-slash'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual(dummyImportUrl); }); @@ -70,7 +74,7 @@ describe('New Project', () => { it('changes project path to last $projectImportUrl component', () => { $projectImportUrl.val('/this/is/last'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('last'); }); @@ -78,7 +82,7 @@ describe('New Project', () => { it('ignores trailing slashes in $projectImportUrl', () => { $projectImportUrl.val('/has/trailing/slash/'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('slash'); }); @@ -86,7 +90,7 @@ describe('New Project', () => { it('ignores fragment identifier in $projectImportUrl', () => { $projectImportUrl.val('/this/has/a#fragment-identifier/'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('a'); }); @@ -94,7 +98,7 @@ describe('New Project', () => { it('ignores query string in $projectImportUrl', () => { $projectImportUrl.val('/url/with?query=string'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('with'); }); @@ -102,7 +106,7 @@ describe('New Project', () => { it('ignores trailing .git in $projectImportUrl', () => { $projectImportUrl.val('/repository.git'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('repository'); }); @@ -110,7 +114,7 @@ describe('New Project', () => { it('changes project path for HTTPS URL in $projectImportUrl', () => { $projectImportUrl.val('https://username:password@gitlab.company.com/group/project.git'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('project'); }); @@ -118,7 +122,7 @@ describe('New Project', () => { it('changes project path for SSH URL in $projectImportUrl', () => { $projectImportUrl.val('git@gitlab.com:gitlab-org/gitlab-ce.git'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('gitlab-ce'); }); From 78cdac8401375cc85be54ae68e5d94d02a90233c Mon Sep 17 00:00:00 2001 From: Luc Didry Date: Wed, 3 Jan 2018 17:32:34 +0100 Subject: [PATCH 14/61] Expose project_id on /api/v4/pages/domains --- changelogs/unreleased/api-domains-expose-project_id.yml | 5 +++++ doc/api/pages_domains.md | 1 + lib/api/entities.rb | 1 + .../api/schemas/public_api/v4/pages_domain/basic.json | 3 ++- spec/requests/api/pages_domains_spec.rb | 1 + 5 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/api-domains-expose-project_id.yml diff --git a/changelogs/unreleased/api-domains-expose-project_id.yml b/changelogs/unreleased/api-domains-expose-project_id.yml new file mode 100644 index 00000000000..22617ffe9b5 --- /dev/null +++ b/changelogs/unreleased/api-domains-expose-project_id.yml @@ -0,0 +1,5 @@ +--- +title: Expose project_id on /api/v4/pages/domains +merge_request: 16200 +author: Luc Didry +type: changed diff --git a/doc/api/pages_domains.md b/doc/api/pages_domains.md index 50685f335f7..20275b902c6 100644 --- a/doc/api/pages_domains.md +++ b/doc/api/pages_domains.md @@ -21,6 +21,7 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/a { "domain": "ssl.domain.example", "url": "https://ssl.domain.example", + "project_id": 1337, "certificate": { "expired": false, "expiration": "2020-04-12T14:32:00.000Z" diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 4ad4a1f7867..270b456597d 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1133,6 +1133,7 @@ module API class PagesDomainBasic < Grape::Entity expose :domain expose :url + expose :project_id expose :certificate, as: :certificate_expiration, if: ->(pages_domain, _) { pages_domain.certificate? }, diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json index 4ba6422406c..e8c17298b43 100644 --- a/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json +++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json @@ -3,6 +3,7 @@ "properties": { "domain": { "type": "string" }, "url": { "type": "uri" }, + "project_id": { "type": "integer" }, "certificate_expiration": { "type": "object", "properties": { @@ -13,6 +14,6 @@ "additionalProperties": false } }, - "required": ["domain", "url"], + "required": ["domain", "url", "project_id"], "additionalProperties": false } diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb index d412b045e9f..5d01dc37f0e 100644 --- a/spec/requests/api/pages_domains_spec.rb +++ b/spec/requests/api/pages_domains_spec.rb @@ -46,6 +46,7 @@ describe API::PagesDomains do expect(json_response).to be_an Array expect(json_response.size).to eq(3) expect(json_response.last).to have_key('domain') + expect(json_response.last).to have_key('project_id') expect(json_response.last).to have_key('certificate_expiration') expect(json_response.last['certificate_expiration']['expired']).to be true expect(json_response.first).not_to have_key('certificate_expiration') From f6e339141d527fe50f61d9204ccf16b8ccc6d861 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 3 Jan 2018 21:03:39 +0000 Subject: [PATCH 15/61] Backport of methods and components added in EBackport of methods and components added in EEE --- .../javascripts/lib/utils/text_utility.js | 9 ++++ .../vue_shared/components/expand_button.vue | 46 +++++++++++++++++++ .../lib/utils/text_utility_spec.js | 10 ++++ .../components/expand_button_spec.js | 32 +++++++++++++ 4 files changed, 97 insertions(+) create mode 100644 app/assets/javascripts/vue_shared/components/expand_button.vue create mode 100644 spec/javascripts/vue_shared/components/expand_button_spec.js diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 9280b7f150c..cb6e06ea584 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -64,3 +64,12 @@ export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - export function capitalizeFirstCharacter(text) { return `${text[0].toUpperCase()}${text.slice(1)}`; } + +/** + * Replaces all html tags from a string with the given replacement. + * + * @param {String} string + * @param {*} replace + * @returns {String} + */ +export const stripeHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace); diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue new file mode 100644 index 00000000000..96991c4e268 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/expand_button.vue @@ -0,0 +1,46 @@ + + diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index 1f46c225071..6f8dad6b835 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -62,4 +62,14 @@ describe('text_utility', () => { expect(textUtils.slugify('João')).toEqual('joão'); }); }); + + describe('stripeHtml', () => { + it('replaces html tag with the default replacement', () => { + expect(textUtils.stripeHtml('This is a text with

html

.')).toEqual('This is a text with html.'); + }); + + it('replaces html tags with the provided replacement', () => { + expect(textUtils.stripeHtml('This is a text with

html

.', ' ')).toEqual('This is a text with html .'); + }); + }); }); diff --git a/spec/javascripts/vue_shared/components/expand_button_spec.js b/spec/javascripts/vue_shared/components/expand_button_spec.js new file mode 100644 index 00000000000..a33ab689dd1 --- /dev/null +++ b/spec/javascripts/vue_shared/components/expand_button_spec.js @@ -0,0 +1,32 @@ +import Vue from 'vue'; +import expandButton from '~/vue_shared/components/expand_button.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('expand button', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(expandButton); + vm = mountComponent(Component, { + slots: { + expanded: '

Expanded!

', + }, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a collpased button', () => { + expect(vm.$el.textContent.trim()).toEqual('...'); + }); + + it('hides expander on click', (done) => { + vm.$el.querySelector('button').click(); + vm.$nextTick(() => { + expect(vm.$el.querySelector('button').getAttribute('style')).toEqual('display: none;'); + done(); + }); + }); +}); From 5d3ade5cebfaefa38f888d2b2c3ae85c131ada7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Thu, 4 Jan 2018 01:55:18 +0100 Subject: [PATCH 16/61] Update Advanced cluster settings subtitle --- app/views/projects/clusters/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index c15785806b9..1f5426dcfa5 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -40,6 +40,6 @@ %h4= _('Advanced settings') %button.btn.js-settings-toggle = expanded ? 'Collapse' : 'Expand' - %p= s_('ClusterIntegration|Manage cluster integration on your GitLab project') + %p= s_("ClusterIntegration|Advanced options on this cluster's integration") .settings-content = render 'advanced_settings' From ab7382f90a8a59d1dcd4445eadb0a3adb0eda7c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Thu, 4 Jan 2018 01:58:28 +0100 Subject: [PATCH 17/61] Update Remove cluster subtitle and alert --- app/views/projects/clusters/_advanced_settings.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/projects/clusters/_advanced_settings.html.haml index 7032b892029..8a13713ae02 100644 --- a/app/views/projects/clusters/_advanced_settings.html.haml +++ b/app/views/projects/clusters/_advanced_settings.html.haml @@ -11,5 +11,5 @@ %label.text-danger = s_('ClusterIntegration|Remove cluster integration') %p - = s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine.') - = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Kubernetes Engine"}) + = s_("ClusterIntegration|Remove this cluster's configuration from this project. This will not delete your actual cluster.") + = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this cluster's integration? This will not delete your actual cluster.")}) From 20f79920e584f70218c78ce7a2c9c42328020031 Mon Sep 17 00:00:00 2001 From: Alessio Caiazza Date: Thu, 4 Jan 2018 10:29:16 +0100 Subject: [PATCH 18/61] Backport gitlab-org/gitlab-ci-yml!128 - Fix kubectl version to 1.8.6 This commit extracts `kubectl`, `helm` and `codeclimate` versions as CI variables. `kubectl` changes from latest stable version to `1.8.6`, the other two are just extracted in order to be easily updated; now we can also test tool upgrades overriding CI secret variables. --- .../unreleased/ac-autodevopfix-kubectl-version.yml | 5 +++++ vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml | 12 ++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/ac-autodevopfix-kubectl-version.yml diff --git a/changelogs/unreleased/ac-autodevopfix-kubectl-version.yml b/changelogs/unreleased/ac-autodevopfix-kubectl-version.yml new file mode 100644 index 00000000000..0ceeb7ccee1 --- /dev/null +++ b/changelogs/unreleased/ac-autodevopfix-kubectl-version.yml @@ -0,0 +1,5 @@ +--- +title: Force Auto DevOps kubectl version to 1.8.6 +merge_request: 16218 +author: +type: fixed diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml index 18910a46d11..06473fba8e1 100644 --- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml @@ -34,6 +34,10 @@ variables: POSTGRES_ENABLED: "true" POSTGRES_DB: $CI_ENVIRONMENT_SLUG + KUBERNETES_VERSION: 1.8.6 + HELM_VERSION: 2.6.1 + CODECLIMATE_VERSION: 0.69.0 + stages: - build - test @@ -250,8 +254,8 @@ production: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume /tmp/cc:/tmp/cc" - docker run ${cc_opts} codeclimate/codeclimate:0.69.0 init - docker run ${cc_opts} codeclimate/codeclimate:0.69.0 analyze -f json > codeclimate.json + docker run ${cc_opts} "codeclimate/codeclimate:${CODECLIMATE_VERSION}" init + docker run ${cc_opts} "codeclimate/codeclimate:${CODECLIMATE_VERSION}" analyze -f json > codeclimate.json } function sast() { @@ -323,11 +327,11 @@ production: apk add glibc-2.23-r3.apk rm glibc-2.23-r3.apk - curl https://kubernetes-helm.storage.googleapis.com/helm-v2.6.1-linux-amd64.tar.gz | tar zx + curl "https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz" | tar zx mv linux-amd64/helm /usr/bin/ helm version --client - curl -L -o /usr/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl + curl -L -o /usr/bin/kubectl "https://storage.googleapis.com/kubernetes-release/release/v${KUBERNETES_VERSION}/bin/linux/amd64/kubectl" chmod +x /usr/bin/kubectl kubectl version --client } From 260935868acfb7c0cb720088d4f8c4c1c1088ddb Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 14 Dec 2017 11:49:35 +0100 Subject: [PATCH 19/61] add new git fsck rake task and spec --- lib/tasks/gitlab/git.rake | 10 ++++++++++ spec/tasks/gitlab/git_rake_spec.rb | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 spec/tasks/gitlab/git_rake_spec.rb diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake index cf82134d97e..f3ffff43726 100644 --- a/lib/tasks/gitlab/git.rake +++ b/lib/tasks/gitlab/git.rake @@ -30,6 +30,16 @@ namespace :gitlab do end end + desc 'GitLab | Git | Check all repos integrity' + task fsck: :environment do + failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} fsck --name-objects --no-progress), "Checking integrity") + if failures.empty? + puts "Done".color(:green) + else + output_failures(failures) + end + end + def perform_git_cmd(cmd, message) puts "Starting #{message} on all repositories" diff --git a/spec/tasks/gitlab/git_rake_spec.rb b/spec/tasks/gitlab/git_rake_spec.rb new file mode 100644 index 00000000000..63a7f7efe73 --- /dev/null +++ b/spec/tasks/gitlab/git_rake_spec.rb @@ -0,0 +1,27 @@ +require 'rake_helper' + +describe 'gitlab:git rake tasks' do + before do + Rake.application.rake_require 'tasks/gitlab/git' + + stub_warn_user_is_not_gitlab + + FileUtils.mkdir(Settings.absolute('tmp/tests/default_storage')) + end + + after do + FileUtils.rm_rf(Settings.absolute('tmp/tests/default_storage')) + end + + describe 'fsck' do + let(:storages) do + { 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage') } } + end + + it 'outputs the right git command' do + expect(Kernel).to receive(:system).with('').and_return(true) + + run_rake_task('gitlab:git:fsck') + end + end +end From 7721e8dfca9d272376f58dcb03ff277aef0a9c31 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 14 Dec 2017 14:53:34 +0100 Subject: [PATCH 20/61] fix spec --- spec/tasks/gitlab/git_rake_spec.rb | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/spec/tasks/gitlab/git_rake_spec.rb b/spec/tasks/gitlab/git_rake_spec.rb index 63a7f7efe73..60b51186ceb 100644 --- a/spec/tasks/gitlab/git_rake_spec.rb +++ b/spec/tasks/gitlab/git_rake_spec.rb @@ -4,9 +4,11 @@ describe 'gitlab:git rake tasks' do before do Rake.application.rake_require 'tasks/gitlab/git' - stub_warn_user_is_not_gitlab + storages = { 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage') } } - FileUtils.mkdir(Settings.absolute('tmp/tests/default_storage')) + FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/repo/test.git')) + allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + stub_warn_user_is_not_gitlab end after do @@ -14,14 +16,8 @@ describe 'gitlab:git rake tasks' do end describe 'fsck' do - let(:storages) do - { 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage') } } - end - it 'outputs the right git command' do - expect(Kernel).to receive(:system).with('').and_return(true) - - run_rake_task('gitlab:git:fsck') + expect { run_rake_task('gitlab:git:fsck') }.to output(/Performed Checking integrity/).to_stdout end end end From bc46c822fc94cfa54a190cfb0e89afeae799f57a Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 21 Dec 2017 15:42:25 +0100 Subject: [PATCH 21/61] remove max-depth flag so it works with subgroups --- lib/tasks/gitlab/task_helpers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/gitlab/task_helpers.rb b/lib/tasks/gitlab/task_helpers.rb index 6723662703c..c1182af1014 100644 --- a/lib/tasks/gitlab/task_helpers.rb +++ b/lib/tasks/gitlab/task_helpers.rb @@ -130,7 +130,7 @@ module Gitlab def all_repos Gitlab.config.repositories.storages.each_value do |repository_storage| - IO.popen(%W(find #{repository_storage['path']} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find| + IO.popen(%W(find #{repository_storage['path']} -mindepth 2 -type d -name *.git)) do |find| find.each_line do |path| yield path.chomp end From f8e1b44dc5d2a78676672dfc7d44c17e6defeda6 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 3 Jan 2018 14:51:04 +0100 Subject: [PATCH 22/61] add locks chek --- lib/tasks/gitlab/git.rake | 26 +++++++++++++++++++++++++- spec/tasks/gitlab/git_rake_spec.rb | 4 +++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake index f3ffff43726..5c1b19860f0 100644 --- a/lib/tasks/gitlab/git.rake +++ b/lib/tasks/gitlab/git.rake @@ -32,7 +32,10 @@ namespace :gitlab do desc 'GitLab | Git | Check all repos integrity' task fsck: :environment do - failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} fsck --name-objects --no-progress), "Checking integrity") + failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} fsck --name-objects --no-progress), "Checking integrity") do |repo| + check_config_lock(repo) + check_ref_locks(repo) + end if failures.empty? puts "Done".color(:green) else @@ -50,6 +53,8 @@ namespace :gitlab do else failures << repo end + + yield(repo) if block_given? end failures @@ -59,5 +64,24 @@ namespace :gitlab do puts "The following repositories reported errors:".color(:red) failures.each { |f| puts "- #{f}" } end + + def check_config_lock(repo_dir) + config_exists = File.exist?(File.join(repo_dir, 'config.lock')) + config_output = config_exists ? 'yes'.color(:red) : 'no'.color(:green) + + puts "'config.lock' file exists?".color(:yellow) + " ... #{config_output}" + end + + def check_ref_locks(repo_dir) + lock_files = Dir.glob(File.join(repo_dir, 'refs/heads/*.lock')) + + if lock_files.present? + puts "Ref lock files exist:".color(:red) + + lock_files.each { |lock_file| puts " #{lock_file}" } + else + puts "No ref lock files exist".color(:green) + end + end end end diff --git a/spec/tasks/gitlab/git_rake_spec.rb b/spec/tasks/gitlab/git_rake_spec.rb index 60b51186ceb..19d298fb36d 100644 --- a/spec/tasks/gitlab/git_rake_spec.rb +++ b/spec/tasks/gitlab/git_rake_spec.rb @@ -1,3 +1,5 @@ + + require 'rake_helper' describe 'gitlab:git rake tasks' do @@ -6,7 +8,7 @@ describe 'gitlab:git rake tasks' do storages = { 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage') } } - FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/repo/test.git')) + FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@repo/1/2/test.git')) allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) stub_warn_user_is_not_gitlab end From 5b9e7773766eebbe73bb400025de002962532a7c Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 3 Jan 2018 15:32:16 +0100 Subject: [PATCH 23/61] add lock specs --- spec/tasks/gitlab/git_rake_spec.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/spec/tasks/gitlab/git_rake_spec.rb b/spec/tasks/gitlab/git_rake_spec.rb index 19d298fb36d..44a2607bea2 100644 --- a/spec/tasks/gitlab/git_rake_spec.rb +++ b/spec/tasks/gitlab/git_rake_spec.rb @@ -21,5 +21,18 @@ describe 'gitlab:git rake tasks' do it 'outputs the right git command' do expect { run_rake_task('gitlab:git:fsck') }.to output(/Performed Checking integrity/).to_stdout end + + it 'errors out about config.lock issues' do + FileUtils.touch(Settings.absolute('tmp/tests/default_storage/@repo/1/2/test.git/config.lock')) + + expect { run_rake_task('gitlab:git:fsck') }.to output(/file exists\? ... yes/).to_stdout + end + + it 'errors out about ref lock issues' do + FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@repo/1/2/test.git/refs/heads')) + FileUtils.touch(Settings.absolute('tmp/tests/default_storage/@repo/1/2/test.git/refs/heads/blah.lock')) + + expect { run_rake_task('gitlab:git:fsck') }.to output(/Ref lock files exist:/).to_stdout + end end end From 6ee122c04ee8263dc1cb9dfddd010c5c0b587e8e Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 3 Jan 2018 16:11:17 +0100 Subject: [PATCH 24/61] deprecate check integrity task --- lib/tasks/gitlab/check.rake | 41 ++----------------------------------- 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index dfade1f3885..903e84359cd 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -387,14 +387,8 @@ namespace :gitlab do namespace :repo do desc "GitLab | Check the integrity of the repositories managed by GitLab" task check: :environment do - Gitlab.config.repositories.storages.each do |name, repository_storage| - namespace_dirs = Dir.glob(File.join(repository_storage['path'], '*')) - - namespace_dirs.each do |namespace_dir| - repo_dirs = Dir.glob(File.join(namespace_dir, '*')) - repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) } - end - end + puts "This task is deprecated. Please use gitlab:git:fsck instead".color(:red) + Rake::Task["gitlab:git:fsck"].execute end end @@ -461,35 +455,4 @@ namespace :gitlab do puts "FAIL. Please update gitlab-shell to #{required_version} from #{current_version}".color(:red) end end - - def check_repo_integrity(repo_dir) - puts "\nChecking repo at #{repo_dir.color(:yellow)}" - - git_fsck(repo_dir) - check_config_lock(repo_dir) - check_ref_locks(repo_dir) - end - - def git_fsck(repo_dir) - puts "Running `git fsck`".color(:yellow) - system(*%W(#{Gitlab.config.git.bin_path} fsck), chdir: repo_dir) - end - - def check_config_lock(repo_dir) - config_exists = File.exist?(File.join(repo_dir, 'config.lock')) - config_output = config_exists ? 'yes'.color(:red) : 'no'.color(:green) - puts "'config.lock' file exists?".color(:yellow) + " ... #{config_output}" - end - - def check_ref_locks(repo_dir) - lock_files = Dir.glob(File.join(repo_dir, 'refs/heads/*.lock')) - if lock_files.present? - puts "Ref lock files exist:".color(:red) - lock_files.each do |lock_file| - puts " #{lock_file}" - end - else - puts "No ref lock files exist".color(:green) - end - end end From de36a8e27961d4c2af43d0ac2d700a391c245353 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 4 Jan 2018 11:02:43 +0100 Subject: [PATCH 25/61] refactor spec, add docs --- doc/administration/raketasks/check.md | 4 ++-- lib/tasks/gitlab/git.rake | 1 + spec/tasks/gitlab/git_rake_spec.rb | 16 ++++++++-------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/doc/administration/raketasks/check.md b/doc/administration/raketasks/check.md index c8b5434c068..7dabc014bad 100644 --- a/doc/administration/raketasks/check.md +++ b/doc/administration/raketasks/check.md @@ -34,13 +34,13 @@ This task loops through all repositories on the GitLab server and runs the **Omnibus Installation** ``` -sudo gitlab-rake gitlab:repo:check +sudo gitlab-rake gitlab:git:fsck ``` **Source Installation** ```bash -sudo -u git -H bundle exec rake gitlab:repo:check RAILS_ENV=production +sudo -u git -H bundle exec rake gitlab:git:fsck RAILS_ENV=production ``` ### Check repositories for a specific user diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake index 5c1b19860f0..3f5dd2ae3b3 100644 --- a/lib/tasks/gitlab/git.rake +++ b/lib/tasks/gitlab/git.rake @@ -36,6 +36,7 @@ namespace :gitlab do check_config_lock(repo) check_ref_locks(repo) end + if failures.empty? puts "Done".color(:green) else diff --git a/spec/tasks/gitlab/git_rake_spec.rb b/spec/tasks/gitlab/git_rake_spec.rb index 44a2607bea2..dacc5dc5ae7 100644 --- a/spec/tasks/gitlab/git_rake_spec.rb +++ b/spec/tasks/gitlab/git_rake_spec.rb @@ -1,5 +1,3 @@ - - require 'rake_helper' describe 'gitlab:git rake tasks' do @@ -8,8 +6,10 @@ describe 'gitlab:git rake tasks' do storages = { 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage') } } - FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@repo/1/2/test.git')) + FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/1/2/test.git')) allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + allow_any_instance_of(String).to receive(:color) { |string, _color| string } + stub_warn_user_is_not_gitlab end @@ -18,19 +18,19 @@ describe 'gitlab:git rake tasks' do end describe 'fsck' do - it 'outputs the right git command' do - expect { run_rake_task('gitlab:git:fsck') }.to output(/Performed Checking integrity/).to_stdout + it 'outputs the integrity check for a repo' do + expect { run_rake_task('gitlab:git:fsck') }.to output(/Performed Checking integrity at .*@hashed\/1\/2\/test.git/).to_stdout end it 'errors out about config.lock issues' do - FileUtils.touch(Settings.absolute('tmp/tests/default_storage/@repo/1/2/test.git/config.lock')) + FileUtils.touch(Settings.absolute('tmp/tests/default_storage/@hashed/1/2/test.git/config.lock')) expect { run_rake_task('gitlab:git:fsck') }.to output(/file exists\? ... yes/).to_stdout end it 'errors out about ref lock issues' do - FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@repo/1/2/test.git/refs/heads')) - FileUtils.touch(Settings.absolute('tmp/tests/default_storage/@repo/1/2/test.git/refs/heads/blah.lock')) + FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/1/2/test.git/refs/heads')) + FileUtils.touch(Settings.absolute('tmp/tests/default_storage/@hashed/1/2/test.git/refs/heads/blah.lock')) expect { run_rake_task('gitlab:git:fsck') }.to output(/Ref lock files exist:/).to_stdout end From 21d0a3a6c4ee78724e084f355da9e40c4243b036 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 4 Jan 2018 11:19:11 +0100 Subject: [PATCH 26/61] add missing changelog --- .../unreleased/40228-verify-integrity-of-repositories.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/40228-verify-integrity-of-repositories.yml diff --git a/changelogs/unreleased/40228-verify-integrity-of-repositories.yml b/changelogs/unreleased/40228-verify-integrity-of-repositories.yml new file mode 100644 index 00000000000..261d48652db --- /dev/null +++ b/changelogs/unreleased/40228-verify-integrity-of-repositories.yml @@ -0,0 +1,5 @@ +--- +title: Fix gitlab-rake gitlab:import:repos import schedule +merge_request: 15931 +author: +type: fixed From e8acb3f11755811fca28d38bb0cbba44add7b0af Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 4 Jan 2018 12:09:14 +0100 Subject: [PATCH 27/61] Copy-edit end-to-end testing guide documentation --- .../testing_guide/end_to_end_tests.md | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/doc/development/testing_guide/end_to_end_tests.md b/doc/development/testing_guide/end_to_end_tests.md index 561547e1581..30efe3e3b76 100644 --- a/doc/development/testing_guide/end_to_end_tests.md +++ b/doc/development/testing_guide/end_to_end_tests.md @@ -23,24 +23,25 @@ You can find these nightly pipelines at [GitLab QA pipelines page][gitlab-qa-pip It is also possible to trigger build of GitLab packages and then pass these package to GitLab QA to run tests in a [pipeline][gitlab-qa-pipelines]. -Developers can trigger a `package-qa` manual action, that should be present in -the merge request widget in your merge request. +Developers can trigger the `package-qa` manual action, that should be present in +the merge request widget. -It is possible to trigger Gitlab QA pipeline from merge requests in GitLab CE -and GitLab EE, but QA triggering manual action is also available in the Omnibus -GitLab project as well. +It is also possible to trigger Gitlab QA pipeline from merge requests in +Omnibus GitLab project. You can find a manual action that is similar to +`package-qa`, mentioned above, in your Omnibus-related merge requests as well. Below you can read more about how to use it and how does it work. #### How does it work? -Currently, we are _multi-project pipeline_-like approach to run QA pipelines. +Currently, we are using _multi-project pipeline_-like approach to run QA +pipelines. 1. Developer triggers a manual action, that can be found in CE and EE merge -requests, what starts a chain of pipelines. +requests. This starts a chain of pipelines in multiple projects. -1. The script, that is being executed, triggers a pipeline in GitLab Omnibus -projects, and waits for the resulting status. We call this a _status attribution_. +1. The script being executed triggers a pipeline in GitLab Omnibus and waits +for the resulting status. We call this a _status attribution_. 1. GitLab packages are being built in Omnibus pipeline. Packages are going to be pushed to Container Registry. @@ -50,24 +51,25 @@ pipeline, that is now running in Omnibus, triggers a new pipeline in the GitLab QA project. It also waits for a resulting status. 1. GitLab QA pulls images from the registry, spins-up containers and runs tests -against a test environment that has been just orchestrated by `gitlab-qa` tool. +against a test environment that has been just orchestrated by the `gitlab-qa` +tool. -1. The result of GitLab QA pipeline is being propagated upstream, through +1. The result of the GitLab QA pipeline is being propagated upstream, through Omnibus, back to CE / EE merge request. #### How do I write tests? In order to write new tests, you first need to learn more about GitLab QA -architecture. There is some documentation about it in GitLab QA project -[here][gitlab-qa-architecture]. +architecture. See the [documentation about it][gitlab-qa-architecture] in +GitLab QA project. -Once you decided were to put test environment orchestration scenarios and -instance specs, take a looks at [relevant documentation][instance-qa-readme] +Once you decided where to put test environment orchestration scenarios and +instance specs, take a look at the [relevant documentation][instance-qa-readme] and examples in [the `qa/` directory][instance-qa-examples]. ## Where can I ask for help? -You can ask question in `#qa` channel on Slack (GitLab internal) or you can +You can ask question in the `#qa` channel on Slack (GitLab internal) or you can find an issue you would like to work on in [the issue tracker][gitlab-qa-issues] and start a new discussion there. From 0ba0f9de08eb3d5113f4557b925506167484950a Mon Sep 17 00:00:00 2001 From: Ahmad Sherif Date: Wed, 3 Jan 2018 13:31:06 +0100 Subject: [PATCH 28/61] Prepare Gitlab::Git::Repository#rebase for Gitaly migration --- lib/gitlab/git/operation_service.rb | 5 +++++ lib/gitlab/git/repository.rb | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb index ef5bdbaf819..3fb0e2eed93 100644 --- a/lib/gitlab/git/operation_service.rb +++ b/lib/gitlab/git/operation_service.rb @@ -97,6 +97,11 @@ module Gitlab end end + def update_branch(branch_name, newrev, oldrev) + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name + update_ref_in_hooks(ref, newrev, oldrev) + end + private # Returns [newrev, should_run_after_create, should_run_after_create_branch] diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 176bd953ca1..7c6349f4e84 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -1212,9 +1212,16 @@ module Gitlab rebase_path = worktree_path(REBASE_WORKTREE_PREFIX, rebase_id) env = git_env_for_user(user) + if remote_repository.is_a?(RemoteRepository) + env.merge!(remote_repository.fetch_env) + remote_repo_path = GITALY_INTERNAL_URL + else + remote_repo_path = remote_repository.path + end + with_worktree(rebase_path, branch, env: env) do run_git!( - %W(pull --rebase #{remote_repository.path} #{remote_branch}), + %W(pull --rebase #{remote_repo_path} #{remote_branch}), chdir: rebase_path, env: env ) From b5fe3916752aafd5c79b2aa7a770fbd51f1b4bef Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 4 Jan 2018 15:44:00 +0100 Subject: [PATCH 29/61] Update some Gitaly annotations in Gitlab::Shell --- lib/gitlab/shell.rb | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 9cdd3d22f18..18da242e1cb 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -71,7 +71,6 @@ module Gitlab # Ex. # add_repository("/path/to/storage", "gitlab/gitlab-ci") # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 def add_repository(storage, name) relative_path = name.dup relative_path << '.git' unless relative_path.end_with?('.git') @@ -100,7 +99,7 @@ module Gitlab # Ex. # import_repository("/path/to/storage", "gitlab/gitlab-ci", "https://gitlab.com/gitlab-org/gitlab-test.git") # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 + # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/874 def import_repository(storage, name, url) # The timeout ensures the subprocess won't hang forever cmd = gitlab_projects(storage, "#{name}.git") @@ -122,7 +121,6 @@ module Gitlab # Ex. # fetch_remote(my_repo, "upstream") # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 def fetch_remote(repository, remote, ssh_auth: nil, forced: false, no_tags: false) gitaly_migrate(:fetch_remote) do |is_enabled| if is_enabled @@ -142,7 +140,7 @@ module Gitlab # Ex. # mv_repository("/path/to/storage", "gitlab/gitlab-ci", "randx/gitlab-ci-new") # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 + # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/873 def mv_repository(storage, path, new_path) gitlab_projects(storage, "#{path}.git").mv_project("#{new_path}.git") end @@ -156,7 +154,7 @@ module Gitlab # Ex. # fork_repository("/path/to/forked_from/storage", "gitlab/gitlab-ci", "/path/to/forked_to/storage", "new-namespace/gitlab-ci") # - # Gitaly note: JV: not easy to migrate because this involves two Gitaly servers, not one. + # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/817 def fork_repository(forked_from_storage, forked_from_disk_path, forked_to_storage, forked_to_disk_path) gitlab_projects(forked_from_storage, "#{forked_from_disk_path}.git") .fork_repository(forked_to_storage, "#{forked_to_disk_path}.git") @@ -170,7 +168,7 @@ module Gitlab # Ex. # remove_repository("/path/to/storage", "gitlab/gitlab-ci") # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 + # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/873 def remove_repository(storage, name) gitlab_projects(storage, "#{name}.git").rm_project end @@ -221,7 +219,6 @@ module Gitlab # Ex. # add_namespace("/path/to/storage", "gitlab") # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385 def add_namespace(storage, name) Gitlab::GitalyClient.migrate(:add_namespace) do |enabled| if enabled @@ -243,7 +240,6 @@ module Gitlab # Ex. # rm_namespace("/path/to/storage", "gitlab") # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385 def rm_namespace(storage, name) Gitlab::GitalyClient.migrate(:remove_namespace) do |enabled| if enabled @@ -261,7 +257,6 @@ module Gitlab # Ex. # mv_namespace("/path/to/storage", "gitlab", "gitlabhq") # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385 def mv_namespace(storage, old_name, new_name) Gitlab::GitalyClient.migrate(:rename_namespace) do |enabled| if enabled From 44d15e414348ab7befaa22636b85d1c0d9064d08 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 4 Jan 2018 16:34:37 +0100 Subject: [PATCH 30/61] get it working --- lib/gitlab/import_export/command_line_util.rb | 4 ++++ lib/gitlab/import_export/repo_restorer.rb | 4 +++- lib/gitlab/shell.rb | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index 0135b3c6f22..349f17cf0f8 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -15,6 +15,10 @@ module Gitlab execute(%W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all)) end + def git_clone_bundle(repo_path:, bundle_path:) + execute(%W(#{git_bin_path} clone --bare -- #{bundle_path} #{repo_path})) + end + def mkdir_p(path) FileUtils.mkdir_p(path, mode: DEFAULT_MODE) FileUtils.chmod(DEFAULT_MODE, path) diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index 32ca2809b2f..a7e00c71990 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -13,7 +13,9 @@ module Gitlab def restore return true unless File.exist?(@path_to_bundle) - gitlab_shell.import_repository(@project.repository_storage_path, @project.disk_path, @path_to_bundle) + repo_path = @project.repository.path_to_repo + git_clone_bundle(repo_path: repo_path, bundle_path: @path_to_bundle) + Gitlab::Git::Repository.create_hooks(repo_path, File.expand_path(Gitlab.config.gitlab_shell.hooks_path)) rescue => e @shared.error(e) false diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 9cdd3d22f18..1d0eae28f82 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -102,6 +102,10 @@ module Gitlab # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 def import_repository(storage, name, url) + if url.start_with?('.') || url.start_with?('/') + raise Error.new("don't use disk paths with import_repository: #{url.inspect}") + end + # The timeout ensures the subprocess won't hang forever cmd = gitlab_projects(storage, "#{name}.git") success = cmd.import_project(url, git_timeout) From 80242f246b69a803120bc06527b80601fafc526c Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 4 Jan 2018 16:37:18 +0100 Subject: [PATCH 31/61] Hide hooks stuff --- lib/gitlab/import_export/command_line_util.rb | 1 + lib/gitlab/import_export/repo_restorer.rb | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index 349f17cf0f8..dd5d35feab9 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -17,6 +17,7 @@ module Gitlab def git_clone_bundle(repo_path:, bundle_path:) execute(%W(#{git_bin_path} clone --bare -- #{bundle_path} #{repo_path})) + Gitlab::Git::Repository.create_hooks(repo_path, File.expand_path(Gitlab.config.gitlab_shell.hooks_path)) end def mkdir_p(path) diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index a7e00c71990..d0e5cfcfd3e 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -13,9 +13,7 @@ module Gitlab def restore return true unless File.exist?(@path_to_bundle) - repo_path = @project.repository.path_to_repo - git_clone_bundle(repo_path: repo_path, bundle_path: @path_to_bundle) - Gitlab::Git::Repository.create_hooks(repo_path, File.expand_path(Gitlab.config.gitlab_shell.hooks_path)) + git_clone_bundle(repo_path: @project.repository.path_to_repo, bundle_path: @path_to_bundle) rescue => e @shared.error(e) false From 3df6fa6c05ad86f1bc861a8f38d8096098110e37 Mon Sep 17 00:00:00 2001 From: Kushal Pandya Date: Thu, 4 Jan 2018 21:33:25 +0530 Subject: [PATCH 32/61] Enclose props in quotes --- app/assets/javascripts/groups/components/item_stats.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue index 803dc63d39c..3a94e57a028 100644 --- a/app/assets/javascripts/groups/components/item_stats.vue +++ b/app/assets/javascripts/groups/components/item_stats.vue @@ -43,27 +43,27 @@ export default { css-class="number-subgroups" icon-name="folder" :title="s__('Subgroups')" - :value=item.subgroupCount + :value="item.subgroupCount" /> Date: Thu, 4 Jan 2018 21:47:40 +0530 Subject: [PATCH 33/61] Use `__` instead of `s__` when context is not required --- app/assets/javascripts/groups/components/item_stats.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue index 3a94e57a028..2e42fb6c9a6 100644 --- a/app/assets/javascripts/groups/components/item_stats.vue +++ b/app/assets/javascripts/groups/components/item_stats.vue @@ -42,21 +42,21 @@ export default { v-if="isGroup" css-class="number-subgroups" icon-name="folder" - :title="s__('Subgroups')" + :title="__('Subgroups')" :value="item.subgroupCount" /> Date: Thu, 4 Jan 2018 16:26:14 +0000 Subject: [PATCH 34/61] Update settings.md --- doc/api/settings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/settings.md b/doc/api/settings.md index 0e4758cda2d..0b5b1f0c134 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -69,7 +69,7 @@ PUT /application/settings | `after_sign_up_text` | string | no | Text shown to the user after signing up | | `akismet_api_key` | string | no | API key for akismet spam protection | | `akismet_enabled` | boolean | no | Enable or disable akismet spam protection | -| `circuitbreaker_access_retries | integer | no | The number of attempts GitLab will make to access a storage. | +| `circuitbreaker_access_retries` | integer | no | The number of attempts GitLab will make to access a storage. | | `circuitbreaker_check_interval` | integer | no | Number of seconds in between storage checks. | | `circuitbreaker_failure_count_threshold` | integer | no | The number of failures of after which GitLab will completely prevent access to the storage. | | `circuitbreaker_failure_reset_time` | integer | no | Time in seconds GitLab will keep storage failure information. When no failures occur during this time, the failure information is reset. | From dcebe1494e35fcd8870b38f311c5176eab6b2a2f Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 4 Jan 2018 18:05:49 +0100 Subject: [PATCH 35/61] rubocop --- lib/gitlab/shell.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 1d0eae28f82..bef944ef1f9 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -102,7 +102,7 @@ module Gitlab # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 def import_repository(storage, name, url) - if url.start_with?('.') || url.start_with?('/') + if url.start_with?('.', '/') raise Error.new("don't use disk paths with import_repository: #{url.inspect}") end From 176b60d11055999d56e30b6fe0581fbede2740c4 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 4 Jan 2018 18:38:39 +0100 Subject: [PATCH 36/61] Remove the Project#repo method --- app/controllers/projects_controller.rb | 2 +- app/models/project.rb | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 6f609348402..6f229b08c0c 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -353,7 +353,7 @@ class ProjectsController < Projects::ApplicationController end def repo_exists? - project.repository_exists? && !project.empty_repo? && project.repo + project.repository_exists? && !project.empty_repo? rescue Gitlab::Git::Repository::NoRepository project.repository.expire_exists_cache diff --git a/app/models/project.rb b/app/models/project.rb index 9c0bbf697e2..4784bbc8a44 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -992,10 +992,6 @@ class Project < ActiveRecord::Base false end - def repo - repository.rugged - end - def url_to_repo gitlab_shell.url_to_repo(full_path) end @@ -1438,7 +1434,7 @@ class Project < ActiveRecord::Base # We'd need to keep track of project full path otherwise directory tree # created with hashed storage enabled cannot be usefully imported using # the import rake task. - repo.config['gitlab.fullpath'] = gl_full_path + repository.rugged.config['gitlab.fullpath'] = gl_full_path rescue Gitlab::Git::Repository::NoRepository => e Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.") nil From 5e148d4e931792733400f59864e1aa886ef55953 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Wed, 6 Dec 2017 17:07:47 -0200 Subject: [PATCH 37/61] EE-BACKPORT group boards --- app/finders/labels_finder.rb | 19 +- app/policies/group_policy.rb | 8 +- doc/api/boards.md | 200 ++++++++++++++++-- lib/api/api.rb | 4 +- lib/api/boards.rb | 84 +++----- lib/api/boards_responses.rb | 50 +++++ lib/api/entities.rb | 2 + lib/api/helpers.rb | 15 +- lib/api/labels.rb | 4 +- lib/api/v3/labels.rb | 2 +- spec/finders/labels_finder_spec.rb | 10 + .../api/schemas/public_api/v4/board.json | 86 ++++++++ .../api/schemas/public_api/v4/boards.json | 4 + .../api/schemas/public_api/v4/user/basic.json | 2 +- spec/requests/api/boards_spec.rb | 179 ++-------------- spec/support/api/boards_shared_examples.rb | 180 ++++++++++++++++ 16 files changed, 601 insertions(+), 248 deletions(-) create mode 100644 lib/api/boards_responses.rb create mode 100644 spec/fixtures/api/schemas/public_api/v4/board.json create mode 100644 spec/fixtures/api/schemas/public_api/v4/boards.json create mode 100644 spec/support/api/boards_shared_examples.rb diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index ce432ddbfe6..6de9eb89468 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -1,4 +1,6 @@ class LabelsFinder < UnionFinder + include Gitlab::Utils::StrongMemoize + def initialize(current_user, params = {}) @current_user = current_user @params = params @@ -32,6 +34,8 @@ class LabelsFinder < UnionFinder label_ids << project.labels end end + elsif only_group_labels? + label_ids << Label.where(group_id: group.id) else label_ids << Label.where(group_id: projects.group_ids) label_ids << Label.where(project_id: projects.select(:id)) @@ -51,6 +55,13 @@ class LabelsFinder < UnionFinder items.where(title: title) end + def group + strong_memoize(:group) do + group = Group.find(params[:group_id]) + authorized_to_read_labels?(group) && group + end + end + def group? params[:group_id].present? end @@ -63,6 +74,10 @@ class LabelsFinder < UnionFinder params[:project_ids].present? end + def only_group_labels? + params[:only_group_labels] + end + def title params[:title] || params[:name] end @@ -96,9 +111,9 @@ class LabelsFinder < UnionFinder @projects end - def authorized_to_read_labels?(project) + def authorized_to_read_labels?(label_parent) return true if skip_authorization - Ability.allowed?(current_user, :read_label, project) + Ability.allowed?(current_user, :read_label, label_parent) end end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index d2d45e402b0..f0bcba588a2 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -28,12 +28,18 @@ class GroupPolicy < BasePolicy with_options scope: :subject, score: 0 condition(:request_access_enabled) { @subject.request_access_enabled } - rule { public_group } .enable :read_group + rule { public_group }.policy do + enable :read_group + enable :read_list + enable :read_label + end + rule { logged_in_viewable }.enable :read_group rule { guest }.policy do enable :read_group enable :upload_file + enable :read_label end rule { admin } .enable :read_group diff --git a/doc/api/boards.md b/doc/api/boards.md index 69c47abc806..a5f455e1c43 100644 --- a/doc/api/boards.md +++ b/doc/api/boards.md @@ -15,10 +15,10 @@ GET /projects/:id/boards | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/boards +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards ``` Example response: @@ -27,6 +27,19 @@ Example response: [ { "id" : 1, + "project": { + "id": 5, + "name": "Diaspora Project Site", + "name_with_namespace": "Diaspora / Diaspora Project Site", + "path": "diaspora-project-site", + "path_with_namespace": "diaspora/diaspora-project-site", + "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", + "web_url": "http://example.com/diaspora/diaspora-project-site" + }, + "milestone": { + "id": 12 + "title": "10.0" + }, "lists" : [ { "id" : 1, @@ -60,6 +73,159 @@ Example response: ] ``` +## Single board + +Get a single board. + +``` +GET /projects/:id/boards/:board_id +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `board_id` | integer | yes | The ID of a board | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1 +``` + +Example response: + +```json + { + "id": 1, + "name:": "project issue board", + "project": { + "id": 5, + "name": "Diaspora Project Site", + "name_with_namespace": "Diaspora / Diaspora Project Site", + "path": "diaspora-project-site", + "path_with_namespace": "diaspora/diaspora-project-site", + "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", + "web_url": "http://example.com/diaspora/diaspora-project-site" + }, + "milestone": { + "id": 12 + "title": "10.0" + }, + "lists" : [ + { + "id" : 1, + "label" : { + "name" : "Testing", + "color" : "#F0AD4E", + "description" : null + }, + "position" : 1 + }, + { + "id" : 2, + "label" : { + "name" : "Ready", + "color" : "#FF0000", + "description" : null + }, + "position" : 2 + }, + { + "id" : 3, + "label" : { + "name" : "Production", + "color" : "#FF5F00", + "description" : null + }, + "position" : 3 + } + ] + } +``` + +## Create a board (EES-Only) + +Creates a board. + +``` +POST /projects/:id/boards +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `name` | string | yes | The name of the new board | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards?name=newboard +``` + +Example response: + +```json + { + "id": 1, + "project": { + "id": 5, + "name": "Diaspora Project Site", + "name_with_namespace": "Diaspora / Diaspora Project Site", + "path": "diaspora-project-site", + "path_with_namespace": "diaspora/diaspora-project-site", + "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", + "web_url": "http://example.com/diaspora/diaspora-project-site" + }, + "name": "newboard", + "milestone": { + "id": 12 + "title": "10.0" + }, + "lists" : [ + { + "id" : 1, + "label" : { + "name" : "Testing", + "color" : "#F0AD4E", + "description" : null + }, + "position" : 1 + }, + { + "id" : 2, + "label" : { + "name" : "Ready", + "color" : "#FF0000", + "description" : null + }, + "position" : 2 + }, + { + "id" : 3, + "label" : { + "name" : "Production", + "color" : "#FF5F00", + "description" : null + }, + "position" : 3 + } + ] + } +``` + +## Delete a board (EES-Only) + +Deletes a board. + +``` +DELETE /projects/:id/boards/:board_id +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `board_id` | integer | yes | The ID of a board | + +```bash +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1 +``` + ## List board lists Get a list of the board's lists. @@ -71,8 +237,8 @@ GET /projects/:id/boards/:board_id/lists | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `board_id` | integer | yes | The ID of a board | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `board_id` | integer | yes | The ID of a board | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists @@ -122,9 +288,9 @@ GET /projects/:id/boards/:board_id/lists/:list_id | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `board_id` | integer | yes | The ID of a board | -| `list_id`| integer | yes | The ID of a board's list | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `board_id` | integer | yes | The ID of a board | +| `list_id`| integer | yes | The ID of a board's list | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1 @@ -154,9 +320,9 @@ POST /projects/:id/boards/:board_id/lists | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `board_id` | integer | yes | The ID of a board | -| `label_id` | integer | yes | The ID of a label | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `board_id` | integer | yes | The ID of a board | +| `label_id` | integer | yes | The ID of a label | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists?label_id=5 @@ -186,10 +352,10 @@ PUT /projects/:id/boards/:board_id/lists/:list_id | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `board_id` | integer | yes | The ID of a board | -| `list_id` | integer | yes | The ID of a board's list | -| `position` | integer | yes | The position of the list | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `board_id` | integer | yes | The ID of a board | +| `list_id` | integer | yes | The ID of a board's list | +| `position` | integer | yes | The position of the list | ```bash curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1?position=2 @@ -219,9 +385,9 @@ DELETE /projects/:id/boards/:board_id/lists/:list_id | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `board_id` | integer | yes | The ID of a board | -| `list_id` | integer | yes | The ID of a board's list | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `board_id` | integer | yes | The ID of a board | +| `list_id` | integer | yes | The ID of a board's list | ```bash curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1 diff --git a/lib/api/api.rb b/lib/api/api.rb index 8094597d238..e0d14281c96 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -119,6 +119,7 @@ module API mount ::API::Features mount ::API::Files mount ::API::Groups + mount ::API::GroupMilestones mount ::API::Internal mount ::API::Issues mount ::API::Jobs @@ -129,8 +130,6 @@ module API mount ::API::Members mount ::API::MergeRequestDiffs mount ::API::MergeRequests - mount ::API::ProjectMilestones - mount ::API::GroupMilestones mount ::API::Namespaces mount ::API::Notes mount ::API::NotificationSettings @@ -139,6 +138,7 @@ module API mount ::API::PipelineSchedules mount ::API::ProjectHooks mount ::API::Projects + mount ::API::ProjectMilestones mount ::API::ProjectSnippets mount ::API::ProtectedBranches mount ::API::Repositories diff --git a/lib/api/boards.rb b/lib/api/boards.rb index 366b0dc9a6f..6c706b2b4e1 100644 --- a/lib/api/boards.rb +++ b/lib/api/boards.rb @@ -1,45 +1,46 @@ module API class Boards < Grape::API + include BoardsResponses include PaginationParams before { authenticate! } + helpers do + def board_parent + user_project + end + end + params do requires :id, type: String, desc: 'The ID of a project' end resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do - desc 'Get all project boards' do - detail 'This feature was introduced in 8.13' - success Entities::Board - end - params do - use :pagination - end - get ':id/boards' do - authorize!(:read_board, user_project) - present paginate(user_project.boards), with: Entities::Board + segment ':id/boards' do + desc 'Get all project boards' do + detail 'This feature was introduced in 8.13' + success Entities::Board + end + params do + use :pagination + end + get '/' do + authorize!(:read_board, user_project) + present paginate(board_parent.boards), with: Entities::Board + end + + desc 'Find a project board' do + detail 'This feature was introduced in 10.4' + success Entities::Board + end + get '/:board_id' do + present board, with: Entities::Board + end end params do requires :board_id, type: Integer, desc: 'The ID of a board' end segment ':id/boards/:board_id' do - helpers do - def project_board - board = user_project.boards.first - - if params[:board_id] == board.id - board - else - not_found!('Board') - end - end - - def board_lists - project_board.lists.destroyable - end - end - desc 'Get the lists of a project board' do detail 'Does not include `done` list. This feature was introduced in 8.13' success Entities::List @@ -72,22 +73,13 @@ module API requires :label_id, type: Integer, desc: 'The ID of an existing label' end post '/lists' do - unless available_labels.exists?(params[:label_id]) + unless available_labels_for(user_project).exists?(params[:label_id]) render_api_error!({ error: 'Label not found!' }, 400) end authorize!(:admin_list, user_project) - service = ::Boards::Lists::CreateService.new(user_project, current_user, - { label_id: params[:label_id] }) - - list = service.execute(project_board) - - if list.valid? - present list, with: Entities::List - else - render_validation_error!(list) - end + create_list end desc 'Moves a board list to a new position' do @@ -99,18 +91,11 @@ module API requires :position, type: Integer, desc: 'The position of the list' end put '/lists/:list_id' do - list = project_board.lists.movable.find(params[:list_id]) + list = board_lists.find(params[:list_id]) authorize!(:admin_list, user_project) - service = ::Boards::Lists::MoveService.new(user_project, current_user, - { position: params[:position] }) - - if service.execute(list) - present list, with: Entities::List - else - render_api_error!({ error: "List could not be moved!" }, 400) - end + move_list(list) end desc 'Delete a board list' do @@ -124,12 +109,7 @@ module API authorize!(:admin_list, user_project) list = board_lists.find(params[:list_id]) - destroy_conditionally!(list) do |list| - service = ::Boards::Lists::DestroyService.new(user_project, current_user) - unless service.execute(list) - render_api_error!({ error: 'List could not be deleted!' }, 400) - end - end + destroy_list(list) end end end diff --git a/lib/api/boards_responses.rb b/lib/api/boards_responses.rb new file mode 100644 index 00000000000..ead0943a74d --- /dev/null +++ b/lib/api/boards_responses.rb @@ -0,0 +1,50 @@ +module API + module BoardsResponses + extend ActiveSupport::Concern + + included do + helpers do + def board + board_parent.boards.find(params[:board_id]) + end + + def board_lists + board.lists.destroyable + end + + def create_list + create_list_service = + ::Boards::Lists::CreateService.new(board_parent, current_user, { label_id: params[:label_id] }) + + list = create_list_service.execute(board) + + if list.valid? + present list, with: Entities::List + else + render_validation_error!(list) + end + end + + def move_list(list) + move_list_service = + ::Boards::Lists::MoveService.new(board_parent, current_user, { position: params[:position].to_i }) + + if move_list_service.execute(list) + present list, with: Entities::List + else + render_api_error!({ error: "List could not be moved!" }, 400) + end + end + + def destroy_list(list) + destroy_conditionally!(list) do |list| + service = ::Boards::Lists::DestroyService.new(board_parent, current_user) + unless service.execute(list) + render_api_error!({ error: 'List could not be deleted!' }, 400) + end + end + end + end + end + end +end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 4ad4a1f7867..86ac10c39d0 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -791,6 +791,8 @@ module API class Board < Grape::Entity expose :id + expose :project, using: Entities::BasicProjectDetails + expose :lists, using: Entities::List do |board| board.lists.destroyable end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 9ba15893f55..c1f5ec2ab14 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -74,8 +74,15 @@ module API page || not_found!('Wiki Page') end - def available_labels - @available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute + def available_labels_for(label_parent) + search_params = + if label_parent.is_a?(Project) + { project_id: label_parent.id } + else + { group_id: label_parent.id, only_group_labels: true } + end + + LabelsFinder.new(current_user, search_params).execute end def find_user(id) @@ -141,7 +148,9 @@ module API end def find_project_label(id) - label = available_labels.find_by_id(id) || available_labels.find_by_title(id) + labels = available_labels_for(user_project) + label = labels.find_by_id(id) || labels.find_by_title(id) + label || not_found!('Label') end diff --git a/lib/api/labels.rb b/lib/api/labels.rb index e41a1720ac1..81eaf56e48e 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -15,7 +15,7 @@ module API use :pagination end get ':id/labels' do - present paginate(available_labels), with: Entities::Label, current_user: current_user, project: user_project + present paginate(available_labels_for(user_project)), with: Entities::Label, current_user: current_user, project: user_project end desc 'Create a new label' do @@ -30,7 +30,7 @@ module API post ':id/labels' do authorize! :admin_label, user_project - label = available_labels.find_by(title: params[:name]) + label = available_labels_for(user_project).find_by(title: params[:name]) conflict!('Label already exists') if label priority = params.delete(:priority) diff --git a/lib/api/v3/labels.rb b/lib/api/v3/labels.rb index bd5eb2175e8..4157462ec2a 100644 --- a/lib/api/v3/labels.rb +++ b/lib/api/v3/labels.rb @@ -11,7 +11,7 @@ module API success ::API::Entities::Label end get ':id/labels' do - present available_labels, with: ::API::Entities::Label, current_user: current_user, project: user_project + present available_labels_for(user_project), with: ::API::Entities::Label, current_user: current_user, project: user_project end desc 'Delete an existing label' do diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb index d507af3fd3d..06031aee217 100644 --- a/spec/finders/labels_finder_spec.rb +++ b/spec/finders/labels_finder_spec.rb @@ -56,6 +56,16 @@ describe LabelsFinder do expect(finder.execute).to eq [group_label_2, group_label_1, project_label_5] end + + context 'when only_group_labels is true' do + it 'returns only group labels' do + group_1.add_developer(user) + + finder = described_class.new(user, group_id: group_1.id, only_group_labels: true) + + expect(finder.execute).to eq [group_label_2, group_label_1] + end + end end context 'filtering by project_id' do diff --git a/spec/fixtures/api/schemas/public_api/v4/board.json b/spec/fixtures/api/schemas/public_api/v4/board.json new file mode 100644 index 00000000000..d667f1d631c --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/board.json @@ -0,0 +1,86 @@ +{ + "type": "object", + "required" : [ + "id", + "project", + "lists" + ], + "properties" : { + "id": { "type": "integer" }, + "project": { + "type": ["object", "null"], + "required": [ + "id", + "avatar_url", + "description", + "default_branch", + "tag_list", + "ssh_url_to_repo", + "http_url_to_repo", + "web_url", + "name", + "name_with_namespace", + "path", + "path_with_namespace", + "star_count", + "forks_count", + "created_at", + "last_activity_at" + ], + "properties": { + "id": { "type": "integer" }, + "avatar_url": { "type": ["string", "null"] }, + "description": { "type": ["string", "null"] }, + "default_branch": { "type": ["string", "null"] }, + "tag_list": { "type": "array" }, + "ssh_url_to_repo": { "type": "string" }, + "http_url_to_repo": { "type": "string" }, + "web_url": { "type": "string" }, + "name": { "type": "string" }, + "name_with_namespace": { "type": "string" }, + "path": { "type": "string" }, + "path_with_namespace": { "type": "string" }, + "star_count": { "type": "integer" }, + "forks_count": { "type": "integer" }, + "created_at": { "type": "date" }, + "last_activity_at": { "type": "date" } + }, + "additionalProperties": false + }, + "lists": { + "type": "array", + "items": { + "type": "object", + "required" : [ + "id", + "label", + "position" + ], + "properties" : { + "id": { "type": "integer" }, + "label": { + "type": ["object", "null"], + "required": [ + "id", + "color", + "description", + "name" + ], + "properties": { + "id": { "type": "integer" }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$" + }, + "description": { "type": ["string", "null"] }, + "name": { "type": "string" } + } + }, + "position": { "type": ["integer", "null"] } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": true +} diff --git a/spec/fixtures/api/schemas/public_api/v4/boards.json b/spec/fixtures/api/schemas/public_api/v4/boards.json new file mode 100644 index 00000000000..117564ef77a --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/boards.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "board.json" } +} diff --git a/spec/fixtures/api/schemas/public_api/v4/user/basic.json b/spec/fixtures/api/schemas/public_api/v4/user/basic.json index 9f69d31971c..bf330d8278c 100644 --- a/spec/fixtures/api/schemas/public_api/v4/user/basic.json +++ b/spec/fixtures/api/schemas/public_api/v4/user/basic.json @@ -1,5 +1,5 @@ { - "type": "object", + "type": ["object", "null"], "required": [ "id", "state", diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb index f65af69dc7f..c6c10025f7f 100644 --- a/spec/requests/api/boards_spec.rb +++ b/spec/requests/api/boards_spec.rb @@ -6,18 +6,18 @@ describe API::Boards do set(:non_member) { create(:user) } set(:guest) { create(:user) } set(:admin) { create(:user, :admin) } - set(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } + set(:board_parent) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } set(:dev_label) do - create(:label, title: 'Development', color: '#FFAABB', project: project) + create(:label, title: 'Development', color: '#FFAABB', project: board_parent) end set(:test_label) do - create(:label, title: 'Testing', color: '#FFAACC', project: project) + create(:label, title: 'Testing', color: '#FFAACC', project: board_parent) end set(:ux_label) do - create(:label, title: 'UX', color: '#FF0000', project: project) + create(:label, title: 'UX', color: '#FF0000', project: board_parent) end set(:dev_list) do @@ -28,180 +28,25 @@ describe API::Boards do create(:list, label: test_label, position: 2) end - set(:board) do - create(:board, project: project, lists: [dev_list, test_list]) - end + set(:milestone) { create(:milestone, project: board_parent) } + set(:board_label) { create(:label, project: board_parent) } + set(:board) { create(:board, project: board_parent, lists: [dev_list, test_list]) } - before do - project.add_reporter(user) - project.add_guest(guest) - end + it_behaves_like 'group and project boards', "/projects/:id/boards" - describe "GET /projects/:id/boards" do - let(:base_url) { "/projects/#{project.id}/boards" } - - context "when unauthenticated" do - it "returns authentication error" do - get api(base_url) - - expect(response).to have_gitlab_http_status(401) - end - end - - context "when authenticated" do - it "returns the project issue board" do - get api(base_url, user) - - 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(1) - expect(json_response.first['id']).to eq(board.id) - expect(json_response.first['lists']).to be_an Array - expect(json_response.first['lists'].length).to eq(2) - expect(json_response.first['lists'].last).to have_key('position') - end - end - end - - describe "GET /projects/:id/boards/:board_id/lists" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - - it 'returns issue board lists' do - get api(base_url, user) - - 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['label']['name']).to eq(dev_label.title) - end - - it 'returns 404 if board not found' do - get api("/projects/#{project.id}/boards/22343/lists", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe "GET /projects/:id/boards/:board_id/lists/:list_id" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - - it 'returns a list' do - get api("#{base_url}/#{dev_list.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(dev_list.id) - expect(json_response['label']['name']).to eq(dev_label.title) - expect(json_response['position']).to eq(1) - end - - it 'returns 404 if list not found' do - get api("#{base_url}/5324", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe "POST /projects/:id/board/lists" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } + describe "POST /projects/:id/boards/lists" do + let(:url) { "/projects/#{board_parent.id}/boards/#{board.id}/lists" } it 'creates a new issue board list for group labels' do group = create(:group) group_label = create(:group_label, group: group) - project.update(group: group) + board_parent.update(group: group) - post api(base_url, user), label_id: group_label.id + post api(url, user), label_id: group_label.id expect(response).to have_gitlab_http_status(201) expect(json_response['label']['name']).to eq(group_label.title) expect(json_response['position']).to eq(3) end - - it 'creates a new issue board list for project labels' do - post api(base_url, user), label_id: ux_label.id - - expect(response).to have_gitlab_http_status(201) - expect(json_response['label']['name']).to eq(ux_label.title) - expect(json_response['position']).to eq(3) - end - - it 'returns 400 when creating a new list if label_id is invalid' do - post api(base_url, user), label_id: 23423 - - expect(response).to have_gitlab_http_status(400) - end - - it 'returns 403 for project members with guest role' do - put api("#{base_url}/#{test_list.id}", guest), position: 1 - - expect(response).to have_gitlab_http_status(403) - end - end - - describe "PUT /projects/:id/boards/:board_id/lists/:list_id to update only position" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - - it "updates a list" do - put api("#{base_url}/#{test_list.id}", user), - position: 1 - - expect(response).to have_gitlab_http_status(200) - expect(json_response['position']).to eq(1) - end - - it "returns 404 error if list id not found" do - put api("#{base_url}/44444", user), - position: 1 - - expect(response).to have_gitlab_http_status(404) - end - - it "returns 403 for project members with guest role" do - put api("#{base_url}/#{test_list.id}", guest), - position: 1 - - expect(response).to have_gitlab_http_status(403) - end - end - - describe "DELETE /projects/:id/board/lists/:list_id" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - - it "rejects a non member from deleting a list" do - delete api("#{base_url}/#{dev_list.id}", non_member) - - expect(response).to have_gitlab_http_status(403) - end - - it "rejects a user with guest role from deleting a list" do - delete api("#{base_url}/#{dev_list.id}", guest) - - expect(response).to have_gitlab_http_status(403) - end - - it "returns 404 error if list id not found" do - delete api("#{base_url}/44444", user) - - expect(response).to have_gitlab_http_status(404) - end - - context "when the user is project owner" do - set(:owner) { create(:user) } - - before do - project.update(namespace: owner.namespace) - end - - it "deletes the list if an admin requests it" do - delete api("#{base_url}/#{dev_list.id}", owner) - - expect(response).to have_gitlab_http_status(204) - end - - it_behaves_like '412 response' do - let(:request) { api("#{base_url}/#{dev_list.id}", owner) } - end - end end end diff --git a/spec/support/api/boards_shared_examples.rb b/spec/support/api/boards_shared_examples.rb new file mode 100644 index 00000000000..943c1f6ffd7 --- /dev/null +++ b/spec/support/api/boards_shared_examples.rb @@ -0,0 +1,180 @@ +shared_examples_for 'group and project boards' do |route_definition, ee = false| + let(:root_url) { route_definition.gsub(":id", board_parent.id.to_s) } + + before do + board_parent.add_reporter(user) + board_parent.add_guest(guest) + end + + def expect_schema_match_for(response, schema_file, ee) + if ee + expect(response).to match_response_schema(schema_file, dir: "ee") + else + expect(response).to match_response_schema(schema_file) + end + end + + describe "GET #{route_definition}" do + context "when unauthenticated" do + it "returns authentication error" do + get api(root_url) + + expect(response).to have_gitlab_http_status(401) + end + end + + context "when authenticated" do + it "returns the issue boards" do + get api(root_url, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + + expect_schema_match_for(response, 'public_api/v4/boards', ee) + end + + describe "GET #{route_definition}/:board_id" do + let(:url) { "#{root_url}/#{board.id}" } + + it 'get a single board by id' do + get api(url, user) + + expect_schema_match_for(response, 'public_api/v4/board', ee) + end + end + end + end + + describe "GET #{route_definition}/:board_id/lists" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it 'returns issue board lists' do + get api(url, user) + + 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['label']['name']).to eq(dev_label.title) + end + + it 'returns 404 if board not found' do + get api("#{root_url}/22343/lists", user) + + expect(response).to have_gitlab_http_status(404) + end + end + + describe "GET #{route_definition}/:board_id/lists/:list_id" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it 'returns a list' do + get api("#{url}/#{dev_list.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['id']).to eq(dev_list.id) + expect(json_response['label']['name']).to eq(dev_label.title) + expect(json_response['position']).to eq(1) + end + + it 'returns 404 if list not found' do + get api("#{url}/5324", user) + + expect(response).to have_gitlab_http_status(404) + end + end + + describe "POST #{route_definition}/lists" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it 'creates a new issue board list for labels' do + post api(url, user), label_id: ux_label.id + + expect(response).to have_gitlab_http_status(201) + expect(json_response['label']['name']).to eq(ux_label.title) + expect(json_response['position']).to eq(3) + end + + it 'returns 400 when creating a new list if label_id is invalid' do + post api(url, user), label_id: 23423 + + expect(response).to have_gitlab_http_status(400) + end + + it 'returns 403 for members with guest role' do + put api("#{url}/#{test_list.id}", guest), position: 1 + + expect(response).to have_gitlab_http_status(403) + end + end + + describe "PUT #{route_definition}/:board_id/lists/:list_id to update only position" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it "updates a list" do + put api("#{url}/#{test_list.id}", user), + position: 1 + + expect(response).to have_gitlab_http_status(200) + expect(json_response['position']).to eq(1) + end + + it "returns 404 error if list id not found" do + put api("#{url}/44444", user), + position: 1 + + expect(response).to have_gitlab_http_status(404) + end + + it "returns 403 for members with guest role" do + put api("#{url}/#{test_list.id}", guest), + position: 1 + + expect(response).to have_gitlab_http_status(403) + end + end + + describe "DELETE #{route_definition}/lists/:list_id" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it "rejects a non member from deleting a list" do + delete api("#{url}/#{dev_list.id}", non_member) + + expect(response).to have_gitlab_http_status(403) + end + + it "rejects a user with guest role from deleting a list" do + delete api("#{url}/#{dev_list.id}", guest) + + expect(response).to have_gitlab_http_status(403) + end + + it "returns 404 error if list id not found" do + delete api("#{url}/44444", user) + + expect(response).to have_gitlab_http_status(404) + end + + context "when the user is parent owner" do + set(:owner) { create(:user) } + + before do + if board_parent.try(:namespace) + board_parent.update(namespace: owner.namespace) + else + board.parent.add_owner(owner) + end + end + + it "deletes the list if an admin requests it" do + delete api("#{url}/#{dev_list.id}", owner) + + expect(response).to have_gitlab_http_status(204) + end + + it_behaves_like '412 response' do + let(:request) { api("#{url}/#{dev_list.id}", owner) } + end + end + end +end From f01295a651ae8172823ce58031492d0b6d5220e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 4 Jan 2018 22:36:11 +0100 Subject: [PATCH 38/61] Ignore the Migration/Datetime cop in a migration that fix a column type to datetime_with_timezone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- .../20171221140220_schedule_issues_closed_at_type_change.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/post_migrate/20171221140220_schedule_issues_closed_at_type_change.rb b/db/post_migrate/20171221140220_schedule_issues_closed_at_type_change.rb index be18c5866ae..eeecc7b1de0 100644 --- a/db/post_migrate/20171221140220_schedule_issues_closed_at_type_change.rb +++ b/db/post_migrate/20171221140220_schedule_issues_closed_at_type_change.rb @@ -1,6 +1,6 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. - +# rubocop:disable Migration/Datetime class ScheduleIssuesClosedAtTypeChange < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers From 5152cc3bfb8d60814063e86c3776030aa8891e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Thu, 4 Jan 2018 19:27:37 -0300 Subject: [PATCH 39/61] Fix a bug where charlock_holmes was used needlessly to encode strings --- lib/gitlab/encoding_helper.rb | 26 +++++++++++++++---------- lib/gitlab/git.rb | 2 +- spec/lib/gitlab/encoding_helper_spec.rb | 18 +++++++++++++++++ 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb index 6b53eb4533d..c0edcabc6fd 100644 --- a/lib/gitlab/encoding_helper.rb +++ b/lib/gitlab/encoding_helper.rb @@ -14,14 +14,7 @@ module Gitlab ENCODING_CONFIDENCE_THRESHOLD = 50 def encode!(message) - return nil unless message.respond_to?(:force_encoding) - return message if message.encoding == Encoding::UTF_8 && message.valid_encoding? - - if message.respond_to?(:frozen?) && message.frozen? - message = message.dup - end - - message.force_encoding("UTF-8") + message = force_encode_utf8(message) return message if message.valid_encoding? # return message if message type is binary @@ -35,6 +28,8 @@ module Gitlab # encode and clean the bad chars message.replace clean(message) + rescue ArgumentError + return nil rescue encoding = detect ? detect[:encoding] : "unknown" "--broken encoding: #{encoding}" @@ -54,8 +49,8 @@ module Gitlab end def encode_utf8(message) - return nil unless message.is_a?(String) - return message if message.encoding == Encoding::UTF_8 && message.valid_encoding? + message = force_encode_utf8(message) + return message if message.valid_encoding? detect = CharlockHolmes::EncodingDetector.detect(message) if detect && detect[:encoding] @@ -69,6 +64,8 @@ module Gitlab else clean(message) end + rescue ArgumentError + return nil end def encode_binary(s) @@ -83,6 +80,15 @@ module Gitlab private + def force_encode_utf8(message) + raise ArgumentError unless message.respond_to?(:force_encoding) + return message if message.encoding == Encoding::UTF_8 && message.valid_encoding? + + message = message.dup if message.respond_to?(:frozen?) && message.frozen? + + message.force_encoding("UTF-8") + end + def clean(message) message.encode("UTF-16BE", undef: :replace, invalid: :replace, replace: "") .encode("UTF-8") diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index 1f7c35cafaa..71647099f83 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -11,7 +11,7 @@ module Gitlab include Gitlab::EncodingHelper def ref_name(ref) - encode_utf8(ref).sub(/\Arefs\/(tags|heads|remotes)\//, '') + encode!(ref).sub(/\Arefs\/(tags|heads|remotes)\//, '') end def branch_name(ref) diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb index 87ec2698fc1..4e9367323cb 100644 --- a/spec/lib/gitlab/encoding_helper_spec.rb +++ b/spec/lib/gitlab/encoding_helper_spec.rb @@ -120,6 +120,24 @@ describe Gitlab::EncodingHelper do it 'returns empty string on conversion errors' do expect { ext_class.encode_utf8('') }.not_to raise_error(ArgumentError) end + + context 'with strings that can be forcefully encoded into utf8' do + let(:test_string) do + "refs/heads/FixSymbolsTitleDropdown".encode("ASCII-8BIT") + end + let(:expected_string) do + "refs/heads/FixSymbolsTitleDropdown".encode("UTF-8") + end + + subject { ext_class.encode_utf8(test_string) } + + it "doesn't use CharlockHolmes if the encoding can be forced into utf_8" do + expect(CharlockHolmes::EncodingDetector).not_to receive(:detect) + + expect(subject).to eq(expected_string) + expect(subject.encoding.name).to eq('UTF-8') + end + end end describe '#clean' do From 93e9793ce38bb9b5d519f5ca86cb56201549ef19 Mon Sep 17 00:00:00 2001 From: Mayra Cabrera Date: Thu, 4 Jan 2018 22:35:41 +0000 Subject: [PATCH 40/61] Create Kubernetes based on Application Templates --- app/models/concerns/deployment_platform.rb | 47 ++++++++++++ app/models/project.rb | 7 +- app/models/service.rb | 5 ++ ...netes-integration-application-template.yml | 5 ++ .../import_export/export_file_spec.rb | 2 +- .../concerns/deployment_platform_spec.rb | 73 +++++++++++++++++++ spec/models/project_spec.rb | 19 ----- spec/models/service_spec.rb | 8 ++ 8 files changed, 140 insertions(+), 26 deletions(-) create mode 100644 app/models/concerns/deployment_platform.rb create mode 100644 changelogs/unreleased/41056-create-cluster-from-kubernetes-integration-application-template.yml create mode 100644 spec/models/concerns/deployment_platform_spec.rb diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb new file mode 100644 index 00000000000..e1373455e98 --- /dev/null +++ b/app/models/concerns/deployment_platform.rb @@ -0,0 +1,47 @@ +module DeploymentPlatform + def deployment_platform + @deployment_platform ||= find_cluster_platform_kubernetes + @deployment_platform ||= find_kubernetes_service_integration + @deployment_platform ||= build_cluster_and_deployment_platform + end + + private + + def find_cluster_platform_kubernetes + clusters.find_by(enabled: true)&.platform_kubernetes + end + + def find_kubernetes_service_integration + services.deployment.reorder(nil).find_by(active: true) + end + + def build_cluster_and_deployment_platform + return unless kubernetes_service_template + + cluster = ::Clusters::Cluster.create(cluster_attributes_from_service_template) + cluster.platform_kubernetes if cluster.persisted? + end + + def kubernetes_service_template + @kubernetes_service_template ||= KubernetesService.active.find_by_template + end + + def cluster_attributes_from_service_template + { + name: 'kubernetes-template', + projects: [self], + provider_type: :user, + platform_type: :kubernetes, + platform_kubernetes_attributes: platform_kubernetes_attributes_from_service_template + } + end + + def platform_kubernetes_attributes_from_service_template + { + api_url: kubernetes_service_template.api_url, + ca_pem: kubernetes_service_template.ca_pem, + token: kubernetes_service_template.token, + namespace: kubernetes_service_template.namespace + } + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 9c0bbf697e2..5d6c1b30587 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -19,6 +19,7 @@ class Project < ActiveRecord::Base include Routable include GroupDescendant include Gitlab::SQL::Pattern + include DeploymentPlatform extend Gitlab::ConfigHelper extend Gitlab::CurrentSettings @@ -904,12 +905,6 @@ class Project < ActiveRecord::Base @ci_service ||= ci_services.reorder(nil).find_by(active: true) end - # TODO: This will be extended for multiple enviroment clusters - def deployment_platform - @deployment_platform ||= clusters.find_by(enabled: true)&.platform_kubernetes - @deployment_platform ||= services.where(category: :deployment).reorder(nil).find_by(active: true) - end - def monitoring_services services.where(category: :monitoring) end diff --git a/app/models/service.rb b/app/models/service.rb index 176b472e724..24ba3039707 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -44,6 +44,7 @@ class Service < ActiveRecord::Base scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) } scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) } scope :external_issue_trackers, -> { issue_trackers.active.without_defaults } + scope :deployment, -> { where(category: 'deployment') } default_value_for :category, 'common' @@ -271,6 +272,10 @@ class Service < ActiveRecord::Base nil end + def self.find_by_template + find_by(template: true) + end + private def cache_project_has_external_issue_tracker diff --git a/changelogs/unreleased/41056-create-cluster-from-kubernetes-integration-application-template.yml b/changelogs/unreleased/41056-create-cluster-from-kubernetes-integration-application-template.yml new file mode 100644 index 00000000000..2dd6fc5f1b5 --- /dev/null +++ b/changelogs/unreleased/41056-create-cluster-from-kubernetes-integration-application-template.yml @@ -0,0 +1,5 @@ +--- +title: Allow automatic creation of Kubernetes Integration from template +merge_request: 16104 +author: +type: added diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb index 461aa39d0ad..6732cf61767 100644 --- a/spec/features/projects/import_export/export_file_spec.rb +++ b/spec/features/projects/import_export/export_file_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' # Integration test that exports a file using the Import/Export feature # It looks up for any sensitive word inside the JSON, so if a sensitive word is found -# we''l have to either include it adding the model that includes it to the +safe_list+ +# we'll have to either include it adding the model that includes it to the +safe_list+ # or make sure the attribute is blacklisted in the +import_export.yml+ configuration feature 'Import/Export - project export integration test', :js do include Select2Helper diff --git a/spec/models/concerns/deployment_platform_spec.rb b/spec/models/concerns/deployment_platform_spec.rb new file mode 100644 index 00000000000..7bb89fe41dc --- /dev/null +++ b/spec/models/concerns/deployment_platform_spec.rb @@ -0,0 +1,73 @@ +require 'rails_helper' + +describe DeploymentPlatform do + let(:project) { create(:project) } + + describe '#deployment_platform' do + subject { project.deployment_platform } + + context 'with no Kubernetes configuration on CI/CD, no Kubernetes Service and a Kubernetes template configured' do + let!(:kubernetes_service) { create(:kubernetes_service, template: true) } + + it 'returns a platform kubernetes' do + expect(subject).to be_a_kind_of(Clusters::Platforms::Kubernetes) + end + + it 'creates a cluster and a platform kubernetes' do + expect { subject } + .to change { Clusters::Cluster.count }.by(1) + .and change { Clusters::Platforms::Kubernetes.count }.by(1) + end + + it 'includes appropriate attributes for Cluster' do + cluster = subject.cluster + expect(cluster.name).to eq('kubernetes-template') + expect(cluster.project).to eq(project) + expect(cluster.provider_type).to eq('user') + expect(cluster.platform_type).to eq('kubernetes') + end + + it 'creates a platform kubernetes' do + expect { subject }.to change { Clusters::Platforms::Kubernetes.count }.by(1) + end + + it 'copies attributes from Clusters::Platform::Kubernetes template into the new Cluster::Platforms::Kubernetes' do + expect(subject.api_url).to eq(kubernetes_service.api_url) + expect(subject.ca_pem).to eq(kubernetes_service.ca_pem) + expect(subject.token).to eq(kubernetes_service.token) + expect(subject.namespace).to eq(kubernetes_service.namespace) + end + end + + context 'with no Kubernetes configuration on CI/CD, no Kubernetes Service and no Kubernetes template configured' do + it { is_expected.to be_nil } + end + + context 'when user configured kubernetes from CI/CD > Clusters' do + let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } + let(:platform_kubernetes) { cluster.platform_kubernetes } + + it 'returns the Kubernetes platform' do + expect(subject).to eq(platform_kubernetes) + end + end + + context 'when user configured kubernetes integration from project services' do + let!(:kubernetes_service) { create(:kubernetes_service, project: project) } + + it 'returns the Kubernetes service' do + expect(subject).to eq(kubernetes_service) + end + end + + context 'when the cluster creation fails' do + let!(:kubernetes_service) { create(:kubernetes_service, template: true) } + + before do + allow_any_instance_of(Clusters::Cluster).to receive(:persisted?).and_return(false) + end + + it { is_expected.to be_nil } + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index cea22bbd184..3c2ed043b82 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -3137,25 +3137,6 @@ describe Project do end end - describe '#deployment_platform' do - subject { project.deployment_platform } - - let(:project) { create(:project) } - - context 'when user configured kubernetes from Integration > Kubernetes' do - let!(:kubernetes_service) { create(:kubernetes_service, project: project) } - - it { is_expected.to eq(kubernetes_service) } - end - - context 'when user configured kubernetes from CI/CD > Clusters' do - let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } - let(:platform_kubernetes) { cluster.platform_kubernetes } - - it { is_expected.to eq(platform_kubernetes) } - end - end - describe '#write_repository_config' do set(:project) { create(:project, :repository) } diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 540615de117..ab6678cab38 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -272,4 +272,12 @@ describe Service do expect(service.deprecation_message).to be_nil end end + + describe '.find_by_template' do + let!(:kubernetes_service) { create(:kubernetes_service, template: true) } + + it 'returns service template' do + expect(KubernetesService.find_by_template).to eq(kubernetes_service) + end + end end From 1859dc867092c1ba31335cf67047c9b6141466b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20=C4=8Cupi=C4=87?= Date: Fri, 5 Jan 2018 00:02:26 +0100 Subject: [PATCH 41/61] Add back bottom margins for integration form --- app/views/projects/clusters/_integration_form.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/clusters/_integration_form.html.haml b/app/views/projects/clusters/_integration_form.html.haml index 1eac2c9dc1f..9d593ffc021 100644 --- a/app/views/projects/clusters/_integration_form.html.haml +++ b/app/views/projects/clusters/_integration_form.html.haml @@ -1,6 +1,6 @@ = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| = form_errors(@cluster) - .form-group + .form-group.append-bottom-20 %h5= s_('ClusterIntegration|Integration status') %p - if @cluster.enabled? @@ -10,7 +10,7 @@ = s_('ClusterIntegration|Cluster integration is enabled for this project.') - else = s_('ClusterIntegration|Cluster integration is disabled for this project.') - %label + %label.append-bottom-10 = field.hidden_field :enabled, { class: 'js-toggle-input'} %button{ type: 'button', From 7d1fdcdc836921d2f2324265752107519f47a6de Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Wed, 8 Nov 2017 15:32:12 -0600 Subject: [PATCH 42/61] Modify `LDAP::Person` to return username value based on attributes `Gitlab::LDAP::Person` did not respect the LDAP attributes username configuration and would simply return the uid value. There are cases where users would like to specify a different username field to allow more friendly GitLab usernames. For example, it's common in AD to have sAMAccountName be an employee ID like `A12345` while the local part of the email address is more human-friendly. --- .../unreleased/ldap_username_attributes.yml | 5 ++ lib/gitlab/ldap/adapter.rb | 2 +- lib/gitlab/ldap/config.rb | 2 +- lib/gitlab/ldap/person.rb | 36 +++++++-- spec/lib/gitlab/ldap/adapter_spec.rb | 10 ++- spec/lib/gitlab/ldap/person_spec.rb | 73 ++++++++++++++++++- spec/lib/gitlab/o_auth/user_spec.rb | 20 +++++ 7 files changed, 135 insertions(+), 13 deletions(-) create mode 100644 changelogs/unreleased/ldap_username_attributes.yml diff --git a/changelogs/unreleased/ldap_username_attributes.yml b/changelogs/unreleased/ldap_username_attributes.yml new file mode 100644 index 00000000000..89bbca58fc9 --- /dev/null +++ b/changelogs/unreleased/ldap_username_attributes.yml @@ -0,0 +1,5 @@ +--- +title: Modify `LDAP::Person` to return username value based on attributes +merge_request: +author: +type: fixed diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb index 0afaa2306b5..76863e77dc3 100644 --- a/lib/gitlab/ldap/adapter.rb +++ b/lib/gitlab/ldap/adapter.rb @@ -74,7 +74,7 @@ module Gitlab def user_options(fields, value, limit) options = { - attributes: Gitlab::LDAP::Person.ldap_attributes(config).compact.uniq, + attributes: Gitlab::LDAP::Person.ldap_attributes(config), base: config.base } diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index c8f19cd52d5..0d9a554fc18 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -148,7 +148,7 @@ module Gitlab def default_attributes { - 'username' => %w(uid userid sAMAccountName), + 'username' => %w(uid sAMAccountName userid), 'email' => %w(mail email userPrincipalName), 'name' => 'cn', 'first_name' => 'givenName', diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index 38d7a9ba2f5..e81cec6ba1a 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -6,6 +6,8 @@ module Gitlab # Source: http://ctogonewild.com/2009/09/03/bitmask-searches-in-ldap/ AD_USER_DISABLED = Net::LDAP::Filter.ex("userAccountControl:1.2.840.113556.1.4.803", "2") + InvalidEntryError = Class.new(StandardError) + attr_accessor :entry, :provider def self.find_by_uid(uid, adapter) @@ -29,11 +31,12 @@ module Gitlab def self.ldap_attributes(config) [ - 'dn', # Used in `dn` - config.uid, # Used in `uid` - *config.attributes['name'], # Used in `name` - *config.attributes['email'] # Used in `email` - ] + 'dn', + config.uid, + *config.attributes['name'], + *config.attributes['email'], + *config.attributes['username'] + ].compact.uniq end def self.normalize_dn(dn) @@ -60,6 +63,8 @@ module Gitlab Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" } @entry = entry @provider = provider + + validate_entry end def name @@ -71,7 +76,13 @@ module Gitlab end def username - uid + username = attribute_value(:username) + + # Depending on the attribute, multiple values may + # be returned. We need only one for username. + # Ex. `uid` returns only one value but `mail` may + # return an array of multiple email addresses. + [username].flatten.first end def email @@ -104,6 +115,19 @@ module Gitlab entry.public_send(selected_attr) # rubocop:disable GitlabSecurity/PublicSend end + + def validate_entry + allowed_attrs = self.class.ldap_attributes(config).map(&:downcase) + + # Net::LDAP::Entry transforms keys to symbols. Change to strings to compare. + entry_attrs = entry.attribute_names.map { |n| n.to_s.downcase } + invalid_attrs = entry_attrs - allowed_attrs + + if invalid_attrs.any? + raise InvalidEntryError, + "#{self.class.name} initialized with Net::LDAP::Entry containing invalid attributes(s): #{invalid_attrs}" + end + end end end end diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/ldap/adapter_spec.rb index d9ddb4326be..6132abd9b35 100644 --- a/spec/lib/gitlab/ldap/adapter_spec.rb +++ b/spec/lib/gitlab/ldap/adapter_spec.rb @@ -16,7 +16,7 @@ describe Gitlab::LDAP::Adapter do expect(adapter).to receive(:ldap_search) do |arg| expect(arg[:filter].to_s).to eq('(uid=johndoe)') expect(arg[:base]).to eq('dc=example,dc=com') - expect(arg[:attributes]).to match(%w{dn uid cn mail email userPrincipalName}) + expect(arg[:attributes]).to match(ldap_attributes) end.and_return({}) adapter.users('uid', 'johndoe') @@ -26,7 +26,7 @@ describe Gitlab::LDAP::Adapter do expect(adapter).to receive(:ldap_search).with( base: 'uid=johndoe,ou=users,dc=example,dc=com', scope: Net::LDAP::SearchScope_BaseObject, - attributes: %w{dn uid cn mail email userPrincipalName}, + attributes: ldap_attributes, filter: nil ).and_return({}) @@ -63,7 +63,7 @@ describe Gitlab::LDAP::Adapter do it 'uses the right uid attribute when non-default' do stub_ldap_config(uid: 'sAMAccountName') expect(adapter).to receive(:ldap_search).with( - hash_including(attributes: %w{dn sAMAccountName cn mail email userPrincipalName}) + hash_including(attributes: ldap_attributes) ).and_return({}) adapter.users('sAMAccountName', 'johndoe') @@ -137,4 +137,8 @@ describe Gitlab::LDAP::Adapter do end end end + + def ldap_attributes + Gitlab::LDAP::Person.ldap_attributes(Gitlab::LDAP::Config.new('ldapmain')) + end end diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb index d204050ef66..ff29d9aa5be 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -8,13 +8,16 @@ describe Gitlab::LDAP::Person do before do stub_ldap_config( options: { + 'uid' => 'uid', 'attributes' => { - 'name' => 'cn', - 'email' => %w(mail email userPrincipalName) + 'name' => 'cn', + 'email' => %w(mail email userPrincipalName), + 'username' => username_attribute } } ) end + let(:username_attribute) { %w(uid sAMAccountName userid) } describe '.normalize_dn' do subject { described_class.normalize_dn(given) } @@ -44,6 +47,34 @@ describe Gitlab::LDAP::Person do end end + describe '.ldap_attributes' do + it 'returns a compact and unique array' do + stub_ldap_config( + options: { + 'uid' => nil, + 'attributes' => { + 'name' => 'cn', + 'email' => 'mail', + 'username' => %w(uid mail memberof) + } + } + ) + config = Gitlab::LDAP::Config.new('ldapmain') + ldap_attributes = described_class.ldap_attributes(config) + + expect(ldap_attributes).to match_array(%w(dn uid cn mail memberof)) + end + end + + describe '.validate_entry' do + it 'raises InvalidEntryError' do + entry['foo'] = 'bar' + + expect { described_class.new(entry, 'ldapmain') } + .to raise_error(Gitlab::LDAP::Person::InvalidEntryError) + end + end + describe '#name' do it 'uses the configured name attribute and handles values as an array' do name = 'John Doe' @@ -72,6 +103,44 @@ describe Gitlab::LDAP::Person do end end + describe '#username' do + context 'with default uid username attribute' do + let(:username_attribute) { 'uid' } + + it 'returns the proper username value' do + attr_value = 'johndoe' + entry[username_attribute] = attr_value + person = described_class.new(entry, 'ldapmain') + + expect(person.username).to eq(attr_value) + end + end + + context 'with a different username attribute' do + let(:username_attribute) { 'sAMAccountName' } + + it 'returns the proper username value' do + attr_value = 'johndoe' + entry[username_attribute] = attr_value + person = described_class.new(entry, 'ldapmain') + + expect(person.username).to eq(attr_value) + end + end + + context 'with a non-standard username attribute' do + let(:username_attribute) { 'mail' } + + it 'returns the proper username value' do + attr_value = 'john.doe@example.com' + entry[username_attribute] = attr_value + person = described_class.new(entry, 'ldapmain') + + expect(person.username).to eq(attr_value) + end + end + end + def assert_generic_test(test_description, got, expected) test_failure_message = "Failed test description: '#{test_description}'\n\n expected: #{expected}\n got: #{got}" expect(got).to eq(expected), test_failure_message diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 6334bcd0156..45fff4c5787 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -275,6 +275,26 @@ describe Gitlab::OAuth::User do end end + context 'and a corresponding LDAP person with a non-default username' do + before do + allow(ldap_user).to receive(:uid) { uid } + allow(ldap_user).to receive(:username) { 'johndoe@example.com' } + allow(ldap_user).to receive(:email) { %w(johndoe@example.com john2@example.com) } + allow(ldap_user).to receive(:dn) { dn } + end + + context 'and no account for the LDAP user' do + it 'creates a user favoring the LDAP username and strips email domain' do + allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) + + oauth_user.save + + expect(gl_user).to be_valid + expect(gl_user.username).to eql 'johndoe' + end + end + end + context "and no corresponding LDAP person" do before do allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil) From f4c2eeb2a6885821f8b72f46deb49a55a062bd8c Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 4 Jan 2018 16:15:09 -0600 Subject: [PATCH 43/61] Fix custom name in branch creation for issue in Firefox Fix https://gitlab.com/gitlab-org/gitlab-ce/issues/41563 `event.srcElement` is non-standard, https://developer.mozilla.org/en-US/docs/Web/API/Event/srcElement --- app/assets/javascripts/create_merge_request_dropdown.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index 23425672b16..eedbd3feeb5 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -276,13 +276,13 @@ export default class CreateMergeRequestDropdown { let target; let value; - if (event.srcElement === this.branchInput) { + if (event.target === this.branchInput) { target = 'branch'; value = this.branchInput.value; - } else if (event.srcElement === this.refInput) { + } else if (event.target === this.refInput) { target = 'ref'; - value = event.srcElement.value.slice(0, event.srcElement.selectionStart) + - event.srcElement.value.slice(event.srcElement.selectionEnd); + value = event.target.value.slice(0, event.target.selectionStart) + + event.target.value.slice(event.target.selectionEnd); } else { return false; } From 8b3b28b8d81acc719701a3c2bfc05b6f7c22c4f2 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 5 Jan 2018 15:32:31 +0800 Subject: [PATCH 44/61] Just try to detect and assign once --- app/models/concerns/deployment_platform.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb index e1373455e98..89d0474a596 100644 --- a/app/models/concerns/deployment_platform.rb +++ b/app/models/concerns/deployment_platform.rb @@ -1,8 +1,9 @@ module DeploymentPlatform def deployment_platform - @deployment_platform ||= find_cluster_platform_kubernetes - @deployment_platform ||= find_kubernetes_service_integration - @deployment_platform ||= build_cluster_and_deployment_platform + @deployment_platform ||= + find_cluster_platform_kubernetes || + find_kubernetes_service_integration || + build_cluster_and_deployment_platform end private From 27a75ea1757d1c1b67bf501ec333221ed5e92d04 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Wed, 20 Dec 2017 10:01:21 +0100 Subject: [PATCH 45/61] Backport 'Rebase' feature from EE to CE When a project uses fast-forward merging strategy user has to rebase MRs to target branch before it can be merged. Now user can do rebase in UI by clicking 'Rebase' button instead of doing rebase locally. This feature was already present in EE, this is only backport of the feature to CE. Couple of changes: * removed rebase license check * renamed migration (changed timestamp) Closes #40301 --- .../components/states/mr_widget_rebase.vue | 133 +++++++++++++++++ .../vue_merge_request_widget/dependencies.js | 1 + .../mr_widget_options.js | 3 + .../services/mr_widget_service.js | 4 + .../stores/get_state_key.js | 2 + .../stores/mr_widget_store.js | 8 ++ .../stores/state_maps.js | 3 + .../projects/merge_requests_controller.rb | 17 +++ app/models/merge_request.rb | 9 +- app/models/repository.rb | 7 + app/presenters/merge_request_presenter.rb | 18 +++ app/serializers/merge_request_basic_entity.rb | 1 + .../merge_request_widget_entity.rb | 10 ++ app/services/merge_requests/rebase_service.rb | 30 ++++ .../working_copy_base_service.rb | 24 ++++ ...ge_request_fast_forward_settings.html.haml | 2 +- .../_merge_request_rebase_settings.html.haml | 2 +- app/workers/all_queues.yml | 1 + app/workers/rebase_worker.rb | 12 ++ changelogs/unreleased/40301-rebase.yml | 5 + config/routes/project.rb | 1 + ...add_rebase_commit_sha_to_merge_requests.rb | 7 + db/schema.rb | 3 +- .../merge_requests/fast_forward_merge.md | 4 +- .../merge_requests/img/ff_merge_mr.png | Bin 21380 -> 0 bytes .../merge_requests/img/ff_merge_rebase.png | Bin 0 -> 26945 bytes features/project/ff_merge_requests.feature | 17 +++ features/steps/project/ff_merge_requests.rb | 22 +++ lib/gitlab/git/operation_service.rb | 5 + .../merge_requests_controller_spec.rb | 58 ++++++++ .../schemas/entities/merge_request_basic.json | 1 + .../entities/merge_request_widget.json | 6 +- .../components/mr_widget_rebase_spec.js | 115 +++++++++++++++ spec/models/merge_request_spec.rb | 46 ++++++ spec/models/project_spec.rb | 19 ++- .../merge_request_presenter_spec.rb | 63 ++++++++ .../merge_request_widget_entity_spec.rb | 16 +++ .../merge_requests/rebase_service_spec.rb | 134 ++++++++++++++++++ .../merge_requests/show.html.haml_spec.rb | 2 + spec/workers/rebase_worker_spec.rb | 27 ++++ 40 files changed, 825 insertions(+), 13 deletions(-) create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue create mode 100644 app/services/merge_requests/rebase_service.rb create mode 100644 app/services/merge_requests/working_copy_base_service.rb create mode 100644 app/workers/rebase_worker.rb create mode 100644 changelogs/unreleased/40301-rebase.yml create mode 100644 db/migrate/20171230123729_add_rebase_commit_sha_to_merge_requests.rb delete mode 100644 doc/user/project/merge_requests/img/ff_merge_mr.png create mode 100644 doc/user/project/merge_requests/img/ff_merge_rebase.png create mode 100644 spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js create mode 100644 spec/services/merge_requests/rebase_service_spec.rb create mode 100644 spec/workers/rebase_worker_spec.rb diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue new file mode 100644 index 00000000000..09276ba2769 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -0,0 +1,133 @@ + + diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js index 5bd8b99420a..940f3d9b2d0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js +++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js @@ -32,6 +32,7 @@ export { default as UnresolvedDiscussionsState } from './components/states/mr_wi export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked'; export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed'; export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds'; +export { default as RebaseState } from './components/states/mr_widget_rebase.vue'; export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed'; export { default as CheckingState } from './components/states/mr_widget_checking'; export { default as MRWidgetStore } from './stores/mr_widget_store'; diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js index fdae06200de..2075f8e4fec 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js @@ -10,6 +10,7 @@ import { MergedState, ClosedState, MergingState, + RebaseState, WipState, ArchivedState, ConflictsState, @@ -79,6 +80,7 @@ export default { ciEnvironmentsStatusPath: store.ciEnvironmentsStatusPath, statusPath: store.statusPath, mergeActionsContentPath: store.mergeActionsContentPath, + rebasePath: store.rebasePath, }; return new MRWidgetService(endpoints); }, @@ -232,6 +234,7 @@ export default { 'mr-widget-pipeline-failed': PipelineFailedState, 'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState, 'mr-widget-auto-merge-failed': AutoMergeFailed, + 'mr-widget-rebase': RebaseState, }, template: `
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js index 7c0bbdd403f..fecbfec2214 100644 --- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js +++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js @@ -37,6 +37,10 @@ export default class MRWidgetService { return axios.get(this.endpoints.mergeActionsContentPath); } + rebase() { + return axios.post(this.endpoints.rebasePath); + } + static stopEnvironment(url) { return axios.post(url); } diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js index 2bace3311c8..f7f0c1b6cb7 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js @@ -25,6 +25,8 @@ export default function deviseState(data) { return this.mergeError ? stateKey.autoMergeFailed : stateKey.mergeWhenPipelineSucceeds; } else if (!this.canMerge) { return stateKey.notAllowedToMerge; + } else if (this.shouldBeRebased) { + return stateKey.rebase; } else if (this.canBeMerged) { return stateKey.readyToMerge; } diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 474c17ec133..ed004b3bb08 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -26,6 +26,7 @@ export default class MergeRequestStore { this.divergedCommitsCount = data.diverged_commits_count; this.pipeline = data.pipeline || {}; this.deployments = this.deployments || data.deployments || []; + this.initRebase(data); if (data.issues_links) { const links = data.issues_links; @@ -124,6 +125,13 @@ export default class MergeRequestStore { return this.state === stateKey.nothingToMerge; } + initRebase(data) { + this.canPushToSourceBranch = data.can_push_to_source_branch; + this.rebaseInProgress = data.rebase_in_progress; + this.approvalsLeft = !data.approved; + this.rebasePath = data.rebase_path; + } + static buildMetrics(metrics) { if (!metrics) { return {}; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js index de980c175fb..29d5bd4a1da 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js @@ -17,6 +17,7 @@ const stateToComponentMap = { failedToMerge: 'mr-widget-failed-to-merge', autoMergeFailed: 'mr-widget-auto-merge-failed', shaMismatch: 'mr-widget-sha-mismatch', + rebase: 'mr-widget-rebase', }; const statesToShowHelpWidget = [ @@ -29,6 +30,7 @@ const statesToShowHelpWidget = [ 'pipelineFailed', 'pipelineBlocked', 'autoMergeFailed', + 'rebase', ]; export const stateKey = { @@ -46,6 +48,7 @@ export const stateKey = { mergeWhenPipelineSucceeds: 'mergeWhenPipelineSucceeds', notAllowedToMerge: 'notAllowedToMerge', readyToMerge: 'readyToMerge', + rebase: 'rebase', }; export default { diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 6b59c8461a3..2e8a738b6d9 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -10,6 +10,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort] before_action :set_issuables_index, only: [:index] before_action :authenticate_user!, only: [:assign_related_issues] + before_action :check_user_can_push_to_source_branch!, only: [:rebase] def index @merge_requests = @issuables @@ -223,6 +224,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo render json: environments end + def rebase + RebaseWorker.perform_async(@merge_request.id, current_user.id) + + render nothing: true, status: 200 + end + protected alias_method :subscribable_resource, :merge_request @@ -322,4 +329,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo @finder_type = MergeRequestsFinder super end + + def check_user_can_push_to_source_branch! + return access_denied! unless @merge_request.source_branch_exists? + + access_check = ::Gitlab::UserAccess + .new(current_user, project: @merge_request.source_project) + .can_push_to_branch?(@merge_request.source_branch) + + access_denied! unless access_check + end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index c39789b047d..ef58816937c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -156,6 +156,13 @@ class MergeRequest < ActiveRecord::Base '!' end + def rebase_in_progress? + # The source project can be deleted + return false unless source_project + + source_project.repository.rebase_in_progress?(id) + end + # Use this method whenever you need to make sure the head_pipeline is synced with the # branch head commit, for example checking if a merge request can be merged. # For more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/40004 @@ -607,7 +614,7 @@ class MergeRequest < ActiveRecord::Base check_if_can_be_merged - can_be_merged? + can_be_merged? && !should_be_rebased? end def mergeable_state?(skip_ci_check: false) diff --git a/app/models/repository.rb b/app/models/repository.rb index b1fd981965c..4bedcbfb6a2 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1099,6 +1099,13 @@ class Repository @project.repository_storage_path end + def rebase(user, merge_request) + raw.rebase(user, merge_request.id, branch: merge_request.source_branch, + branch_sha: merge_request.source_branch_sha, + remote_repository: merge_request.target_project.repository.raw, + remote_branch: merge_request.target_branch) + end + private # TODO Generice finder, later split this on finders by Ref or Oid diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index ab4c87c0169..c6806b7cc26 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -76,6 +76,12 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated end end + def rebase_path + if !rebase_in_progress? && should_be_rebased? && user_can_push_to_source_branch? + rebase_project_merge_request_path(project, merge_request) + end + end + def target_branch_tree_path if target_branch_exists? project_tree_path(project, target_branch) @@ -152,6 +158,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated user_can_collaborate_with_project? && can_be_cherry_picked? end + def can_push_to_source_branch? + source_branch_exists? && user_can_push_to_source_branch? + end + private def conflicts @@ -174,6 +184,14 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated end.sort.to_sentence end + def user_can_push_to_source_branch? + return false unless source_branch_exists? + + ::Gitlab::UserAccess + .new(current_user, project: source_project) + .can_push_to_branch?(source_branch) + end + def user_can_collaborate_with_project? can?(current_user, :push_code, project) || (current_user && current_user.already_forked?(project)) diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb index d54a6516aed..e4aec977f01 100644 --- a/app/serializers/merge_request_basic_entity.rb +++ b/app/serializers/merge_request_basic_entity.rb @@ -4,4 +4,5 @@ class MergeRequestBasicEntity < IssuableSidebarEntity expose :merge_error expose :state expose :source_branch_exists?, as: :source_branch_exists + expose :rebase_in_progress?, as: :rebase_in_progress end diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index e905e6876c2..48cd2317f46 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -23,6 +23,16 @@ class MergeRequestWidgetEntity < IssuableEntity MergeRequestMetricsEntity.new(metrics).as_json end + expose :rebase_commit_sha + expose :rebase_in_progress?, as: :rebase_in_progress + + expose :can_push_to_source_branch do |merge_request| + presenter(merge_request).can_push_to_source_branch? + end + expose :rebase_path do |merge_request| + presenter(merge_request).rebase_path + end + # User entities expose :merge_user, using: UserEntity diff --git a/app/services/merge_requests/rebase_service.rb b/app/services/merge_requests/rebase_service.rb new file mode 100644 index 00000000000..0d5a25fa28e --- /dev/null +++ b/app/services/merge_requests/rebase_service.rb @@ -0,0 +1,30 @@ +module MergeRequests + class RebaseService < MergeRequests::WorkingCopyBaseService + def execute(merge_request) + @merge_request = merge_request + + if rebase + success + else + error('Failed to rebase. Should be done manually') + end + end + + def rebase + if merge_request.rebase_in_progress? + log_error('Rebase task canceled: Another rebase is already in progress', save_message_on_model: true) + return false + end + + rebase_sha = repository.rebase(current_user, merge_request) + + merge_request.update_attributes(rebase_commit_sha: rebase_sha) + + true + rescue => e + log_error('Failed to rebase branch:') + log_error(e.message, save_message_on_model: true) + false + end + end +end diff --git a/app/services/merge_requests/working_copy_base_service.rb b/app/services/merge_requests/working_copy_base_service.rb new file mode 100644 index 00000000000..186e05bf966 --- /dev/null +++ b/app/services/merge_requests/working_copy_base_service.rb @@ -0,0 +1,24 @@ +module MergeRequests + class WorkingCopyBaseService < MergeRequests::BaseService + attr_reader :merge_request + + def source_project + @source_project ||= merge_request.source_project + end + + def target_project + @target_project ||= merge_request.target_project + end + + def log_error(message, save_message_on_model: false) + Gitlab::GitLogger.error("#{self.class.name} error (#{merge_request.to_reference(full: true)}): #{message}") + + merge_request.update(merge_error: message) if save_message_on_model + end + + # Don't try to print expensive instance variables. + def inspect + "#<#{self.class} #{merge_request.to_reference(full: true)}>" + end + end +end diff --git a/app/views/projects/_merge_request_fast_forward_settings.html.haml b/app/views/projects/_merge_request_fast_forward_settings.html.haml index 9d357293a2f..8129c72feb2 100644 --- a/app/views/projects/_merge_request_fast_forward_settings.html.haml +++ b/app/views/projects/_merge_request_fast_forward_settings.html.haml @@ -10,4 +10,4 @@ No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. %br %span.descr - When fast-forward merge is not possible, the user must first rebase locally. + When fast-forward merge is not possible, the user is given the option to rebase. diff --git a/app/views/projects/_merge_request_rebase_settings.html.haml b/app/views/projects/_merge_request_rebase_settings.html.haml index c52e09573a6..54e0b73d24c 100644 --- a/app/views/projects/_merge_request_rebase_settings.html.haml +++ b/app/views/projects/_merge_request_rebase_settings.html.haml @@ -10,4 +10,4 @@ This way you could make sure that if this merge request would build, after merging to target branch it would also build. %br %span.descr - When fast-forward merge is not possible, the user must first rebase locally. + When fast-forward merge is not possible, the user is given the option to rebase. diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 268b7028fd9..fafd9e5ef00 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -89,6 +89,7 @@ - project_service - propagate_service_template - reactive_caching +- rebase - repository_fork - repository_import - storage_migrator diff --git a/app/workers/rebase_worker.rb b/app/workers/rebase_worker.rb new file mode 100644 index 00000000000..090987778a2 --- /dev/null +++ b/app/workers/rebase_worker.rb @@ -0,0 +1,12 @@ +class RebaseWorker + include ApplicationWorker + + def perform(merge_request_id, current_user_id) + current_user = User.find(current_user_id) + merge_request = MergeRequest.find(merge_request_id) + + MergeRequests::RebaseService + .new(merge_request.source_project, current_user) + .execute(merge_request) + end +end diff --git a/changelogs/unreleased/40301-rebase.yml b/changelogs/unreleased/40301-rebase.yml new file mode 100644 index 00000000000..1c0fc0cd8ae --- /dev/null +++ b/changelogs/unreleased/40301-rebase.yml @@ -0,0 +1,5 @@ +--- +title: Allow user to rebase merge requests. +merge_request: +author: +type: added diff --git a/config/routes/project.rb b/config/routes/project.rb index c3ad53a387f..1354c4c5537 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -96,6 +96,7 @@ constraints(ProjectUrlConstrainer.new) do post :toggle_subscription post :remove_wip post :assign_related_issues + post :rebase scope constraints: { format: nil }, action: :show do get :commits, defaults: { tab: 'commits' } diff --git a/db/migrate/20171230123729_add_rebase_commit_sha_to_merge_requests.rb b/db/migrate/20171230123729_add_rebase_commit_sha_to_merge_requests.rb new file mode 100644 index 00000000000..2ce156fa92e --- /dev/null +++ b/db/migrate/20171230123729_add_rebase_commit_sha_to_merge_requests.rb @@ -0,0 +1,7 @@ +class AddRebaseCommitShaToMergeRequests < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column :merge_requests, :rebase_commit_sha, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 778d66f16b0..740e80ccfd4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20171229225929) do +ActiveRecord::Schema.define(version: 20171230123729) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -1099,6 +1099,7 @@ ActiveRecord::Schema.define(version: 20171229225929) do t.string "merge_jid" t.boolean "discussion_locked" t.integer "latest_merge_request_diff_id" + t.string "rebase_commit_sha" end add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree diff --git a/doc/user/project/merge_requests/fast_forward_merge.md b/doc/user/project/merge_requests/fast_forward_merge.md index 085170d9f03..3cd91a185e3 100644 --- a/doc/user/project/merge_requests/fast_forward_merge.md +++ b/doc/user/project/merge_requests/fast_forward_merge.md @@ -9,7 +9,7 @@ When the fast-forward merge ([`--ff-only`][ffonly]) setting is enabled, no merge commits will be created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. -When a fast-forward merge is not possible, the user must rebase the branch manually. +When a fast-forward merge is not possible, the user is given the option to rebase. ## Use cases @@ -25,7 +25,7 @@ merge commits. In such cases, the fast-forward merge is the perfect candidate. Now, when you visit the merge request page, you will be able to accept it **only if a fast-forward merge is possible**. -![Fast forward merge request](img/ff_merge_mr.png) +![Fast forward merge request](img/ff_merge_rebase.png) If the target branch is ahead of the source branch, you need to rebase the source branch locally before you will be able to do a fast-forward merge. diff --git a/doc/user/project/merge_requests/img/ff_merge_mr.png b/doc/user/project/merge_requests/img/ff_merge_mr.png deleted file mode 100644 index 241cc99034305316ae8f29c234f5c83d7d2d62c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21380 zcmb@NWmKH8*Wi&BTAboAxVskycP;Mj#cj|+ao6JR?(XgkQXC2tD-7=Lu=C&d{j$3s zc0X*+dGb7yoZRHx$&K7!5}~XpjfzBw1Oo$uDk~$Q3Il@xf`Nfm{R9t9xwNm3g@J*G zQU0bb`TqX?^z^j5yUW7DLRg^=uxiEoCG-CN`0)Pj;b!~MOnRSOG~{lVF7O;z>v-Sf`dd#MqvM#kLq8xI(p_`AiqcAR8Q_+*7k4R>o<4qpY=*MCbHUd+*?XIt< zN-`9y)BR}PD_ZZoTgYzr<7YG|eSdxcgTYKr#r_X)5_EloXn8gydm&d*BTULrJX0kN=%VsAxuR{8cX@B;#)mL}zzi}qa z9}dP{ZY`V+_P0;(ES+x&2Mq*hvz1>w@9dojM2)w`v{p}S4R_D{o&2pG*nYgw%xF-b zv`|$)s=TlQ8C}@E+8CyHZ++iwPS>WZs%vAbH#Nx|Z|McEH}$OjOQ~GAtovnOSus{w zQlh4&di8Xh-u9Ou@HhDAPrZLLGkpw`MnsO6Muh`IQ+BtASK~k^f6v;vgI#O-;GRWX zYhG?Xn^neGxsRElUUL29pWzOl&|qe9_sue`XnXmErUb}XB|$}!tsGmNV@pX{t#rFf zO|a(Qfg;5+vq---t)B(XV+jG0j3s?5zB$cJ`C&z^BXU+T9gxxNm=3L8KTi)2@2SkM z{Cqr;ETSwd!;QJax%U1wYCm&_Z9|GJhr)PtJB+E+hx4`9 zWvq6`FCN`Hu0v7}&y8J!2r7QIu4bt&uUOA_5)`Q5RaRs5=1eTPXG}!|{M({haiWKT zQ5Ke!5LNeFJ^9lg0nd&hF`KSf41bV#rDSh{3jZ}mX+UPX8{_Dz92sgo|6ABn_CSF7 zmUO?^0S`lSoS7K^z6;sc_8grWVR1RWX-r;lvxHVj)b1ZL*h1mpmdCI^B! zkVS*(OaQ=mCBXO9P$Bx~Kw#<^$6)hlfiNfkXZEtZ=0%ylMZMe@EE?yBeeoC#+)8p_ zp9+JGzJQcub^US~-59K5f(o7^11Nr&K^5d5r2fE5Dz!in^;)krN&^E!j`5-YEz0+L z@Fh7NX=tm3H6E}Pm&0UQ>-VS)ZCIF0ECdFHidGi-V*I~EGGUS9>LY58jmEp z?bplGo8Efu?mnjOVQ(w!cQTcEzj1z_$>Zp|nf)Wt{ziLzK%iaz=Z?P0qXpw2(q=AhYVLR$>g1)%$ z6(`o2GiHVN{503V93^ za^ah4$agzKQGWC%x(PCb+)N)^&m>+xb%1HyO0MPDFlw-$#L4$aNgQ)aWbP#d6AIs1 zEP2%m)A%e-b>#9L{ndEyYe}P9O=J`m-7K^%bWUB{m-Bgu*Cf#8t@RV8$NTiebd?E7 z5&$ufhjW4ta*DxeQD6IOLQoI#)!5_}1!jNcafs3aRfMRdqe32rezaUopl`?gJEbeO zJx*zc&mE9_kI@BXtQT@P^5sLC3Jo$2@^NCUbVf4?3!1SL0KzhgYo9Zj{m?BY$=={X zv(T($7n%YM!rlC<${>Y`7Dpvonh)&*RQ|CAGAqC4f zFgq*KRBjU+Ln8W1W?=I6+}I?)9kw468KTMg$44@T9@RQE{9>I6rxk;89VQ2=X^U1o z@bL0-fM)6|8~a%+e@XcfagU}_T|cmsk}6|iPv(q{B-FgwWCkKa#%aBbHrI4#GsSO$qU)J z5Ft%SG*@W;+Zep#MvOc1C*F&9rSsK5!8Z0}o)#Dtp+wR+KfOPDJ;y*uTdmD!ze(q0 zSzyl?+V>roc{efDZ2deIHIzB|lz9o9;Zjr~RJ0fg^Iws2y%{q{EYkk5Wtj z9n&Cv@S3G$aFKk$^z~9zGiOUF?dsc3p`#C?pye5m_G;KNc(CRsKVCFB?L*WS4a?J<5kdYrv;?EVOw#?4?hnjL9mmsNiQUP!N1~NF7Pz zPr{V)(lT|&0<108{i))Jx2%eQt!JlxX69veO6hYWt5`g|f0Og8)F@5bjPujVHyrmb zTl~3EA*SyuN6Xp)oQjJ7j*VnS6RMk}S<0l$*hwx36r55a5f>>w4g1K}#`3&Btd&FT z-K}1Z@4_x~elo5w_UH3VyteLa>D4msIZn`eC;UA70#5}LUy+PDKB3$q6j~5)O`Z3o z9$k>?nP5jN$O?%lV`M;JobC_7$yn?nv6ZSAKaQ}D>-ZuUbHW8bd_b)(ijMcG1n!mHrBAGrUHD@?(JxL?R=Bz9iubkC1x7s54Mkd6`-PENXzw+Xfk z9b;udA75IyXHIi zDg~P1-dt8_QmN1%ip#{m)M|yGh$31j2}o26zENcb%PMvu@gx0VYmw2TFZ#%=Hd(Q^ ziX9EyV3JR05z_s}=yx?bDCu2Llph)yev2RLnM(P@DWtLZOH>epOHLwMIE#npx+zrW z)StlH_T-Nw$hY3(tzHFN1<=dL{%OI+y9Gg!uDLjcqPuccx3USxs=4`!@h)seb*4bV zR0A8kEbk))oDGupBfR<-yErj!KP`&wIko8A+#2x6?3&J=dg@ zPv~RzeYk+Pd3o#C{Kijws$(ZMbz}8+{^_}8`mOGDGA-Tp=E#%?c%0NvfDFNww_q`C92v9y7~?T3%7 zdGdEU#xKDD4mN;#j=Y zf0--<^o8c`D~54?T&M?GKu)e`NRJ7CiKlo&@UH$|ncWY3mPuZT$hn0<51tNujuAt1 zgQ%AwnwOM|8|KatZKY(EN*RH}t_*+OYv_piTN;~V=xkJMv9 zla}pxwGaO+WDGZ$xxe--5$XQLoww!n3EQ6#&^a6}CLC(*Dn=s-*C;u3`a`p~X>r4#(4{?U z*P;lBMoVM0uzV?KTJlXgWe0uvLwcq6LV9Ynu0dAg0lqS;-Zx~Xb=9oO{{u^yDb#i^ zXBGgsWY|L2`8*Z?$GgN~Z{HD6BX0n=Qj;z0hD=Et%^M`VW#`Rep@j#qDS)B0ESt-2 zbWdMtgVEW!6fh+&7?lZt$?zx;QuVevMSLEp5Ry`S%2eF$O0OyDxTe1YkDZ*HH1QcA zN1}}9M-W~>2+%A#3lJfF&U<0^Sq)T5Nk*6G(~L5NA$ZWnmCdr#3N;1SsHinfnB0>(iBHaLZVVJ_ve=3>ceyX)osf3m_FS-dMMP6UG6=tp6 z`;b){=3781z$U9eW&Z$?Jc+MK6{S(zWtw?mbGLW;fkR~gfVns7Ey6t5;O{v?X&%^pU|A027 z{x;(7=WRInA?%`4cDG36(p{Jf`_=0-$-(M-JV^5<7KRZ`!l0^a>*MQjw7wS@kO;sh z!%C?;HS#yQbS!N*Qbv%-NcJ|fm*@bE;KQr`OW&75hF;vpRuPmbTs zn$M`o%zYh?B_Um5ZD38XPR<}@y4+fVbGsDOw3%V9h%7m(5cPW{5$FCuq1&>U- zUU=e=f*mcxZr>Ykx*P7R*6b}vyhwap?)m4uE=@ny_(D!;X*8nii+wmCFARu{0!+?d zt(&eou9;IuFGI*vKiDACIrKa0a)*4|x@!ArAwpC|KM4VJrE1+8Qnb&5}LewW%Q%?c-(P-6QjGWzZ}LyEAp#4V_qaN__4AfZ>`=0uqtd!4RIN8j&x8qcTY+!Qp`9Ke~{nKG{^Wop~a&;XE8%lJt6M%@DQZQ+%s6q@NTzVP5iTUeBuO!} z$WBVjz{0o<`6(NzsUVv>Zru-fh`W%D;L88Ra)>E1kg0)cMN@)s;~JAkWGI9R2L8+aVveR2E1dN_Ti~> zp1sDw0)C#sNRiQw>f2_@^wwnnIq6&euKyUt=8ZjM;T8X zn2SvbNuh}pP*BMw(%s36Vsc1%h3#$kWShvU1>M7Me3srbg`l{RSw6fbFzJ6sZqG$$ z=Cf5wZ6~u_dj&|{ZT+B-J}=HrCbmsiGfDl&zpa1&G2ysG#BG*zq>=7U%XTVjNh0G;q$>~1)v7!kXf+*(0d_i*=CTwqw^hNae# z*6Y{WQ^QG27u{!-7DNc!9f5O%!R|-;y6Um{6jBQ98jgoK(|_9D3(;Ah$(t448^I^{ z^yG*FiSH~m^*`Yn2OCnPa#%qnUSeb_PV5g8iosRvAV9P1yzQzsDzF?61qc%n7-ywW zu#70H6zXPr-m6m)2kQ@ujt`uO!$l&s`dSrZ)I%hxKY-{UndHB#K&lDt%V)(o$qL1& zS7pltLTugp>rwnKXgF=-u}cI5p#Uj3ycmy2b6ko>G6Y71F{?5}$RxR&=%K?YL44x~ zyH;Z9-k*7cxS*GbDlT~RnxCWL1IUBQ;~G1=8VYC0NFb-_h}s7Bhn zp@mDi-YmEt(2M!Tk*5@+%~{&`7aW~M#KbKVfc{Kz4U;X}t$9F7Zb?=36_Dhd7257Q zV&aYxm$>)QD)=PG4dp>(wR+6+djK*BM11V_SNEp_{}e%d;%*@ZGC+NS5zEyE+QY&I zJV~uaw=!&Z_ktjx(jTMe(rm3GBXGb(h_~Yb*R-wFR1)*`7c$dV3--NZoHi8x3YI$2 zNv?|ghdrRWQY;ZDE<_v|&=xoKd~V1pN0(+UhNi%2Kmreu8SrOUxN4$zCCQe>_J(X7 zBfPIjAR+wCxY+U37qt@^A&n|ZK?KzlGWJH;$N2;DD6Wm+sxlsxpXQfX>7PGCXRk8I zwaxx7JZ1cq)|I*QkmN#7!OIRxsJR9Lm_AC93Dj)gIjkl*g|`f)AAPdHJvK+`wjj#i zz2Kf&U5$O!ABcnBA3aiVzRu{#N0|JpN`eyUs*)Ruo&N*#KBHkd7;u-{%LE{l@v!}s zx_W~BT=M9dt)$oEJdI_>m*vK6%32-D27!r*=p5f!<--;K(@qhl8JpFYdDXT<<`nS~ zb->cDNLvdBYwz{i^c=W()%jQskB0Z=moF{X6^C0|1;d zJ_Dzpf0kr`!2=xBq1mhHH-OQX~XWcGmCLOq+>Vaaey8NL>qDGgI9q+M$_VzIWer#*&s<) zn3nN(@L*F$>G}eHY)%kBt;1K&!~T~eY$YsEsQ(j!pg|{>lyod1sZ$Twq@21^4ot!_ zohU5QrP=G{YZC90jUx-k8D`Nu-SsTe>~8Ia54s6&EQ@c9#e3%jMaSbL^%{w|w$P*_{Dzr-JzU@NR*e65 zpUz~0$jR3}nC!5$!Nhkq9xf$wW?JVvgbBkY6gCwL9&!MhJri{_<^ZjXLA`Jrs!;h6oqS@;;fBXpti%|8r3Ss5Fk9P?@vj&f$9aCF|tUYq8 zwvg7jDMWO^{iyBlVx?I#soKf(5t+iL1sUgF{MjG_!+Z#)v@n2#bLS&oCA(G&-Wze- z3XquSF}ZjlRdQp@2LPPw#MWfact~^^qufGD! z&~-;QljBziZlV=?!AWTM=XwUkw8*3ORqnnnV{U=ciGETF_ zx8yu8Ka4o|O1WH|RZg)xNo}J>0z7b3TwCvN7hukx6IA3gcwV#6tO|z5a|)@c=c{}; z7^CZ}*AX73q2)xxQN?+5jb69n>Ib5L@RaTOF8RIETv*;-(R?j{NRsQ`m`!E5S@x7g zj`{?T<&UH9*HK{Knf;RuIPZ}?B+m`!t^31nNR|H8O!;7#pC0`Qd+db}jMVGuu3dg0 z-XM2v6W`+R%xk`?EIG2U67>^D`=}q4gHqnM5Cv_U3JPj!(&mY8{5P&Q2fIPE?#U-1 zIWG3-?8ZgQDnix{L%;oQU3rgeUuG`Y22(gdR*}OcBX`MPK^w7FyXL>a=)<=uSt2W( zDJ~W3Uw1TXOI@qqiaegMfsJf*o=#gDpVT5Ve~rVcMAS$0dzmL{ih9TNFnXAc8uoTi z?@J+qpnjaX^D~Q%&h)7ClkaeomwV&HA3g!Q;;%z#gh`wuqfdkOXG>d2eY_9$&lg7Q zi`xW+v)3BozU#}jzN);e<()?MAPTm{fdiE5=13*&{kmlw3rXNXU~(6b)S`;5Ux4$~ zOKJ(JQIX5W#hz?=6<3DEo91omOPR}wDQ%_|kqqwwu8fijpN%B^kq zr_(5g*%AU^P6XGATC<++R1PepO-@pbElqj<;cTQB5$Upl7glRrB2=t-X26(j`A7gS zIf?RDX5Pq^aeM83x4N#P6|+QK6D0!uj~uWehT%k)F{W=%h+m;Z)-wec1bIrr zdeUj7%F^TBG+17hZzWckObOM~KSLt0jb=hUC@jA?|BFKvg`7`@Y$PO}d%N82r~ zPJ0qZNoq<6IpHgol+OKPAdcJ;(WH0gjq+r=*VC8tfg)KF9(*|Z#o5G6cK+C2 zf#L-iNHsd#jLUZq8y*!^dEGXi(DuM&fg}xIF@La4FBf^45Kn-wBL^~$#RKv_LL-0F zF`fSaLFWGeK|7f#IS>`1l_)ea76)l2Py$7ep%DyGLSq783|1GMhR4Hs6*v672m*u7 zLBZ<4F+v_?fiec0Lq+)tH2N-;Q@_0b1^UzcyHnzI(lAI4?Sf6*wFkm$QGZVj#h~<1 z44MIDq);5GoCtvFfYMQ$1b7iG=%WAyrcf$c28E^u(1&{rHt#>Mlm-anN)3crcuN7m zJg|?!8t)s8wZm+c{;z@zPnPRrf<&+-RRU)n{l&_X5M&~lRmNqCaLc{Pboup9Q=RH? zNn91Nw9cEMm4KlYR72|qLF=uAO3H#ZiD3+u3`%C3{{LmC%1x>7V7nc`cn(@5a4zsF z5c9#H8PEW^O<7BqDQyt@@++w9Sf5m|L=u*y zD-7Ua39?plE0kLL*rUiZ2D>FbMgWC)UuDo>NM)w3===nvpozJO^R>Gc8zsBx>CWjb z?Oj#f5SW)bkG*;o9A^OOlT62)1vLW$&M%Ur88P{9M5|s+m}Pm@qc$-}%`~2M3!0bV znn|9pHdSThks26f2(r~*NmO=x@~lK6aAr#iE`_!~M@k6nJ#RR=-|c4(oXu%=PTiwF z)nq^ZKL7pfv*(3g*(0Gifq=&wkLFWGxh-ZX9wix@3=JBa8}o-mw#WUK;s2co zT}E%R;^<5=J%Nm(0F2O3DI|*a#9_}oP?qxb#*Yb9^`Gzr2dq8-yM`Ms1h6Pq{#V(> z9Lmy{J-hz*7z5{KG5@g(equah0aF8<7Y;Agd`Oqr}P6RQ;cmnt}ph z?L|@7G^eDERd8bcJq!h+ka{F-4G%&Xe#8!qi;3`h@GK%9-b*H!fP2H4WjeSuLq0)D zQ5NYgsvuUhxsEVejs&>Q0m6$|FZ(2z9~>8S(5id>?i-A82p^JVtZ+J{OsHmMYOkOi zHt`CmPDI0_&kvA5yC;vs9NkU4`)aw{)##LXfCmdie5t(j3f)oPIhTx8OQQlw5g)8& zdrbzI6)X0>z9 z*yy$`JFD&nyy3;UXsK0~T9~$`k{+4^9GX^va%{!eAPL2ChP^sSTu^^MylOG+6tVas zg=EIhcOL4PtUQ#p^?@oGqzFYRjlMdE1RE+nJ<^{#C!GU(Md!M7 zBtj<$+bH$liFR<39MqjTeh2c3Kix(74i)1tIhVGG^Xz3s1Wuv@m6Wg7!Xp8B@vN<_ z58s$NCXIJ$uln2_Yt?cdv20NfZl)YMzG6a>ij(oBhUUk$+W=CTqr6i(O~76spJxKj z?(7}Nc;L+=@`R$KnH^*T`R}H%0^NF-lh1R-arbFdAtQ!Vs2JxDK7E;LradPOd+(Je z3)odrjwEAx79p*-@(O0XWF=t(qdS<`L}%9HTo6QkDWzGMO|4jmmYJ22{{w#v$GtmM zJ`%0GO3Pb{h+Y1;=At!(?7jispOr?_bxRc#Qy&pRMQ5L7A-?>Iy zC*7?5G3MwejzEYnGeXf(8SnX_nhX8r*k1i_D{W{NU@!?12cQJayiIL~pg$qU`aEy_`^dIJIXDYZxL9>40+55~x@YQmKj!SA}R~$2%fK zKlJF3K&98q=R>D0$)@oxqU-HyGm$lq}8hpyJ*bTC@+#fYgFA{GTH# zGH_XWsngjL@A02Ql#Hj5w?{Zjb{g>FgfPQ@yqJdc<*49*Tl1liGDh+8h|>5a`rs`Y0Gzt# zQ$R_q+$RW8=cDUGPUTq)dHK+s%fBc-HQF)bOg@@A+EkWHA}BVRV$w|>#-EaI#I^x6 z0Z3kdlo;YKU^l)m)&-PRAkA*?hPl0R%ZIW#6OeMcJh-U30?Dj<$zn?Bj2|r zm*~H%A>Qf349Sw~8rpG)UEOHh`813_b66+b`8Fl(^Sk@dKQ+fS*7qo;jXrJA zt4<1@mobB)HkE^qaVAZ_`c{ZQHS);q71z6>anNI$?kPt&A zA$|96LgRwj0-;3WBx95P5;Z07*GsuNGfk z{iFIsxt2_`2`CsDX*`&jlwTuk{dGw}AM!>wnHfCqDW}_X)VXE%7>-->3lpzG0Zux#=a*=pF8==zoB0!Fb6s1PDn@T_f)QT~t zQV9!BaoIKs7Pe}@3oIcU@H$Ahk&$NQ7o*g+uKt-<{&=v(0S`J*k%&b4Wh?Z;VuT_s z9bAB?C&&*M5UNPFU{%wQmKh>{85TX1EG?BeQG0$nfv;a;i0T(9=O~5tnQQk)-hDW) z^c-xGd>BctS@UD~)P?5Vk1261j9#fmYJCZ^D}3r=ZGk^wSav5{hK z$XV*WV;q`BYSdgh_ID>%3*NiSFwPuNjGucWM~Bo@{)6YX%S!L zEM5p6dvif<+W91gc2FR-xnKibe9aoPHQCy3Qc54^ef#DT>qb(IF1DW)7UrK(j6LJ} z0W6e2c>eHNj5!cxU=ayL_hHV&QE$IT9S>tx*|Ut0S~QRbLI%u^`9iYtky`ZN!4x-& z?T3hU4z2wiHyKf8kfg9=?!6dsjYd8%H{I+*{NS|+{OXVM4UjM>#qcVtQl2DstZ!eN zBljm2Jb)%F$2zVj)NS$U_e)l$7KO!Cnmc`eBqbtXrhP7Svy|`LNAs#_O^>IRAT?!E z!Z1?ThqNzo7D$3gQd#gD{I4b(YZunbnRYZLYb3#VR@IQtCHYpTs$0YjFOXPg zI*6nMkz8}|AYOSocmrP$cOA}V^WT`1-ETceXd2PW;X{d$-w!{U=4=hbqJV}SaK7-* zn_%5vy3zEMGp37V_Ig?vH?m(>FNAxXTacz%&&lXzpT2=Lg%2_q7p6K8^ z#wRAo5dsD4--)cu=_0Vg6mO}bvuf8re)v1%6M+n@M8p7Ej)iXWeM6%P2M|uoSl0CI zq;`MFAJ2`|95xNpWV`mLVO>Q9g%*lh8>SBZVUO|u=v5)Nf{1faW+sKk?Y>f(*C~zm zIF%K~yYJk5L!SpSos@tj0Hv87Zk7o)N(Oa_fKJQ_ev86BWDmuO%ToITa+QD}mdSha zQQm@2UvQ&S3H+l#lePhDlwqB??;OY5B4{+t3F2r>B|pQdv?4s?1{w3GZKAT?^P$@e zJiU5c_Ag-w>m6UD&~V|Ze8oq?ygvcclE*ekRGyd+xFl#|vs9`vRgXvBw1sB!3J?%V z&jq#o3_DWy@!-vui{o8~JOLd7E+12T3Faa+7AI0ojy`emN(rXfV0E!kmIjXEM31t{ zAeRHaVO_Fm@GNSKZU-U8q9rTtflWf>06^FAG(;}Y+xPpA2{yQTgh54D%P4Ko6)$hQ zh}5;sSe zg~`2XC~tU2SZzDqQoS(HVSc~S)XfNmUpSwg1LrTo*@O`8f%?d*q2nW4^W8N<>~>j!66Rlh2&~w#0DoL);+ql40IsouwsGJ=)uE=!ED(4beP@1;sYy zlHeCXoel=?`p~Ir$YjSxFF^#YgGIZ!BtFO?w(R6=a+GrrVacf)7ws*SZ)I*hV#kka zZi3y<>~}Cg{WY3>A>3^P`OkKz!L^repxrF^|_mdiZvz=K2b9}qkMKR1b(0dC1p;b4OTF!rOGQSW`tIA6^#*;(Jj@jlHH@?-pC0dD7(@0;g>cRPcST%Ce?d3jl-d5^joL2Xh zJc5wuMS2qc4RV9sk~T%ZoEXIOwF$td-{$sgT)M><;~!|{@f3{`*SF$G_$kmJPNW&v zWqAg-zRyha_e9O~|54C^ZWD@n7qdBu&#kk5nLtT9dUxX&L;?_m=tI!#G;E!s%wa-F40r+wbkQ@t7`;s}eV30zP$V$y53k>BA`K zCqcQ6^P(=dyU8=g(Cw|O)(dzxHr+z_7$}o00J`|Ds05HN=LwgHyRQ7%KSMbg*T>}; zh=7=v-Q;9_D^)OVgIZB~?ZQ{lVvBZKc2C$I;Umk3?Le}94B-kBf6BL?x!z4&{L5Up z(&;>GHl&cYmLDD>bBA(jwO_5=5O=m$~7&ecP}KO)Y|XR6w%9(=G1%B>eV#0@8!`m*y`mf-!ekZo6z`Qw5p zgBJHR&hC!T?Y=?#kJ<@CVDnFG%_>P@Nc6~$?yU|f1y%!0)McLfo5tvxVI*X8pHI(< zH;8JbWk~m8>~5Z9eE#oZKA%{#+s8ps&3H+7y(g&?J)P7dRBRgm(^CJlzY3v=s=xtN zjIq1oomvVa7AHue-5OJvrba5$jz(t%H#asnH&^~?>^h#Ou6^a`$FcpbfR!m9%9hNf zWM_lhISX!T`a3avmTah@t3fD3MtuEm$6>l%5FsYU+FCLz=|q2Za`{U}tzGH3!@ycN3en`Gn;bW` z>a2Anb?m)%m`x?*kxajzxIu@Ajb4OG)WM5mky^-=qNV2X@V7Imi(VS?L_(D79$g>-UsJM@7yZdf z%M>_D#cwLmvRIFxhf@r6z3x-RJsgmYi*@I~0U6>7+}{-PznfSj*Rd(x+Y$HF?64Ti zu#cJ4b)|kss{dKsdDTB|BN9i+XKm1=Lrip#o%JS(W>nweWz_|)Z#d$)tesd(g`yT@ zO#zvsnU2qlm9S#52PLdB_g^c%HtF5)fbyWX|CA>_LjBrrSaN>-l|g^awg0aS zO`kBpY`sNUWzLdJsd`cK=-EHEZpp5m7;b|tu92?0a49_# zogGaNf(2rL8-+?ECW!E3<)`G8HaOqCf*~+YRv<%%39lcGEUllQB*Gs;^>llwe|~@a z*mf5l2Y`uHP*eoLJQP5pTMQH%n=xTd(y4(H>71p_)G*7O@*vZuM)#?uXn?=cx5O-= zRck497x4?G z^N~k`5rA^3pQhUITO+CPB8kvMXak^a>Vqb$LqGqw691nv`%HRc4G0ki==}Y5ovD&$ zD6ERSi_iSEW zwya@#ZmN9MM8m{P#$gN#4{xXkg@>0_X#*uBl46p@B_u3{$H#_euBm=MzIc6k8gx4{ zsk;68Mcl(sfr+OTVlfo8c|G+dT=A+P(lMblOs~P^Y;L4ba82z zBU5v{gEmv{eS`9O^|hx6pW=lAjFlqrxHF5tJaM4j)_AHakyLyo`nicrCMw3; zuY0d!k_7kss_bLVE+w_|i-XGiL*tF#{uvv*XIY9EoGhJ}Y4~w;P@JL{o02!k<|ogi zjds5C@c(%=6UPn-%Z!NF(5yv|T6{h`5?HDs>8|PtS4>*eJQs8QA{hQf0*||cESOA! zN{ou)!+QX5zw@4W$D!aQ&u-&>aH{jP>Bsp`mx&YoV$=6$BWd5_?^j{2#Tf{hsy44X z@|?ZLXSpx~5fhG--xao5Z>I|v7~U{eiy1UQZ}wGA>vNizLeiMS!XdrhKaF!#qrR!C z7-YW_SEJY~P9kl*WOe$%U1KQfzhMuADkm~jUOG?p#_-j2&S0XHm}0BEL_KsS&G!hA zka;YnA}*&@Mq#--UPFRmg@WgLK6{oJ_=(6QR#@p5@s1=;C$b$Wj$Sq!3zUr_I?4WBBjS$JwPpB|f)7}g||3X+o; z8ZN|&=8A*5FR^lNsm6Nly@zLUec!v`hdnC}+We(09%zc7_h(Ocu3}Sq?KsBHmghN$ z#ChS^<4i_Ze~~xAnELh^h#+6AODUV=N=4T`8j^kr8DtisqN*yYPcik}=Pz_*33G|) z0V^2)!CagSdGOt&B$%_Sl)|8+TQG-Hz?9`LUdZ8!lqV@(O5kx~#IQwfMkf(imX4NN zIY_17`jUf+h|a<$sOAPeHG3=OtJGqx>fsm6+uVQNa{HIH*=SafJulC@3#$T8TeNWd z3(A(~f}-21d)Uu-`JICBZQ8hGx-fSqKH~uUurQ+uqG78AB6-K;-)PfxPyhUaOlW5> zRh6m?Yo#tFh`N0?58Zqiw4m49gC3tv)rC844tglEB7&$}+d4aX`l>3jv=s)ABTFcI z*hl5HeNcty$yyTBy3+VAbws-{Tf^;oSf!9wpA`XHPq0%ngiNNr@TCp~?|Q!!PD&QI z8CY(S(QW#kD|nK&#bsO1MV^0PMnCMDl3Q@HcRreFC$dh?Bl0f|o22js=`-yiu3L~D z8EoCruztQb>-mrxmE-;}NhEV*y=i`mvV%!q;^b~)$1+^$5-<~4fTS%3A*#0~%dpl~ z<~HN+*geWd4EQ*QBjTK%w=l1UA?dLh*_?L{NS(!gMaq$??|TF1youJN?KX=jWm#(P zcW4o+)>}-^@YNU*;l*hc$B=A-9A#_#MQDp~T+7_2>?88Xa8cW}yYP&jo~{(d6_xDw z#RXZ8neU`7r*|Ax(R^1&iNX|TjT$@Q4b!OS>-w1Lm_(ZSa z&)@>e{Hj>!iEch{aX;#BXGUyWj2nOZ>>@*emm|72Uh0#WX^R~1IK)+L7G_zQ5~+`9 z%)~aU&YC}3;Q#1eg?iUcxIE6P7z|st1yZBj72NN!{{o9kJM z+C#`gedhmYs5bZ~nV;=bpKD&S&m_--+XfiZbqJu2g(`Uo*xnkt_doU{yi3qY>ke5ruvZJpUxCl7 zFumt2j%OT1U0pTe@g4Aj8#TG8%vKxbXbXgmyg5+$*$&yure329%z)y#8nXCR)`Z)& zrp$l{EdCOh;@otXT6{$Z(F%O!8i%hzugi##DeR)7ZfVgbX1{$qqrvIU;Q*h zjaYX3<}hBWqoN?7?AgK1WmY^#$Z$#NBC+JqyKus>;jDN)GigJEO8S6YkvNY(!|N&EO15qGGH`9A0)M1M0XEnPgNedg6n?Z=5Fs*@FCIP4sU{Rq zV@nvQOpVJsvlfINvqPMIHEYS#Zk$xtn&@`7dTPO_)NLO#(5Ex*Dl1~e>u&y!mRy`P z50qJbAcd&_A=vsd1K+67Gi5mUNsvt#p({~KW#C`CdC;G4im%@QB0?e`Ge>?N%*_4J z!Q(|Ho!0*>|0%?$>9+_MCQr`b6I3kxWX*ba-8`|10G!^eoncXd-~DpHSq z{&@kvP1Z(oLpaxla~e3i+g=3=qTY;C?>(zRsS69V30O8lt6we_WIu_}Ru zNTjh=SF2+aY0WxXGu70mgW^cxKIfC|F4}dSWae#Lwffk``R(sT`O%h*c3RA`;{jPV`f&SGui;je~f4wV3xzPHk zhT%O)%kTCX{tRZ}khS;hMioGsLjdc4p zLN&jvp&5N&0tp+2AolY#V^?eqp4A(YBU}ao8XZV6e(8?-ijd~4062Zmfly9_KF!%N zIzi)edPdWQ(($!xI;r!3WL5r7H9skeJJR>0eSWH8r=6O)nUqi+<-z(dhBH=};si^UI7I-z#U^g7 zBNy2?=R@|Fafj-yN(R{Dr5P!&DPzAw#K`&ASn7{0x)~DZl2dp#D8Pe#;mkk5)$|#T zbakU-!>o4=)>2Oho2#+}{E6W^mJTRWhW>WPIm3&}Yrg?Frn4hOYUF3|VfG$h+a4c` zavsJDiyFL$q-4v7uj2*ws`kTZ0W_LqFFQr$uu9v(Hf|~P! zzoGs}CQ3Kk7XWxt(EDoRp#$LZ?jVrgTEq`}@abf!6NGX%cR0iem)5E^Bd^7gcGM1U zJvJI)>#NZQ34ko=!u^M<1_>VbhXE@e`AmngYcu0j@deZcZCHm?h^OS_JI1vu2=i{w z0h^{N$tft|T&qe+=4rz6#Wu_iTk@vNJ_gpWTG)5)N@)CGG}g!rcMr?#Pb=n+iD`=k zv{RN8butWSX$h%Hkaljy>l#TBKl^2LT9tg>2yqhELbUQ&(~bOEIHFilpEG17t8?%i zGLvK|NX`o%D!LvJ^)iuI4n8?*#!+Z?IBEzGD4Y-I4r$n3R08KP!*k0KB`_V~$ z@tupJcyW(pOm&%)vORn*>|C@2ohddM1D%v=**$Qymt=aBu;?qUSWg<=5O&D^1+LWd z>A3>D7@^hjCO}V&3E!6p*_j^@*11sAVf=okx=tX&|95plw0?svv?9O26wPwzmOB88 zdG^iwcw{V$ugub%hX3b$h=$ng=REg;3}Y_bm^qCL$JUp|Xz?G(&G!=UCb?seFGEA~kA=V>;tLiW8#)%Pb6{j4{>>7JvokrZXQB47$eVr@ z;pb@2I_j669i!1I(Cq835F=R=c3+(ec(Kn@f0A0JrHUqr+sqDgb#1?OZo9NT9wR5zUxolt9$ ztk=OrIBo>%Z(8)Z4RtU?n#t88sVv%GK#ALKqc}fpy7t;CWu zc<~|9-2NK_ebV^lVp^*B4wyfw`2l;U7wuan!3x04k7m9AzO6EYI;#lww{U1dAE4Y9Eehs`YeIWzz65aq7g^j z1@a-;&wlY;NE$?~->xUi5MmDADaaWn{SFs=riuh}7eZfLuf9DNoGLC;d?c|$>c$!G zONcZfOS5Md%f&uO08W=>im{XsT?QP{bI0gKD7j>^bF@K0Q1~f6c7{mqO);~I6;E!O zsN3gK^zkPXfp^A9(lO&~Z#SI`p1|i#Bp(Z<&PYhSJ}=MtlceXc71HOOQ`;X-ddk$M z5E=_QW);d^3t;$H+J-0KPm-Q6eAyOh%d!d#orU3AIwGtwC}@n z+wb{;i_c)TZr6U^ZZQ8k@fX!SD~zXqg|1?4lpe*Y0N4ElnG*ergZp!F7r*JO^?>C~ z7!Tk*VnR=w4Ce3>0;>9a<0mB?at1iJ1((cKZn#M_KCu-R&4_Z|HQGf@XqEBWSC2TP zhR`G|H)jFXC%HpSGosgEL+ zVZ+JX;irv?PE0Ga9!$_NWzdoH$v~+zKFN?Ai)1OEFgRhTGy6ssBU&+z6b;RtjD}4X zuU8uT$Q%TK5}s=c2W}imx-dP<$YNzb|D8xH(iXrebWG>S9>%9DXozXvUmlWXic(x& zEWRT^&O{GJ{r;h>lqOzwDf3s#nP!@rqsDrDb$eP$YQy~gbAbq~4|Qyw2$?{RQKn%g ziGO`d&t%WKu0m2hU`PK0Qu*Cg*)`{g(S~1m^9`6t_BW#y;}CB?W11I=zx|Q!)PQfr zRBsa_%87*S{hqPvGbp!cL**;#HKW=qCoxiUMO&kWCpq-t6cp5~+Ib}0mYzI35P=*< zr^@Lz;Cxa$;QR`x-Oz5Av+^g2Q+Tz9NI!Z+2<<=U_BBWO%M+7B8RY=xr1_&J>Yt-Y zo1C9FLJeZRpaQk^!~b0ZWG%ygdDF*p&TMlTo~TeNc4vFqj{c$6VcgnRCJocWa0vxnHNNQV1;*r~Oj4o^~1oKxC?o7p7xuO~{SO z>Te4fyPp7h@CqJmE44$ics1jU5JpaX8yhUSb^|hsUw9aTE1gYoM;O>EU$#>h;E{(6J?75ot>^V+vKR~>AuEua>dD5SEm zd`Wd{M4A=28hypF`u)31lx&-o#;sr4gz}3Lk<*thXjWD}3iwuzQTd9*>#Z(ba2^*y0Vekz^?O-{E*T)(8{k+7 z1qf{dzN?}Dw!^>=>Gx8O`!L>3K+^y7;LcXYmteqZ_CFIWsLRIZ(>yVNd>{IM6aQ9S o$jOxdv&6l9xtypC{NJ5GqBl=U@TL>BeE#E5QP7mHl(mfbFJWopi2wiq diff --git a/doc/user/project/merge_requests/img/ff_merge_rebase.png b/doc/user/project/merge_requests/img/ff_merge_rebase.png new file mode 100644 index 0000000000000000000000000000000000000000..f6139f189ced3f004d4f60e49a774d0466c7cb54 GIT binary patch literal 26945 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 literal 0 HcmV?d00001 diff --git a/features/project/ff_merge_requests.feature b/features/project/ff_merge_requests.feature index 995e52f9332..39035d551d1 100644 --- a/features/project/ff_merge_requests.feature +++ b/features/project/ff_merge_requests.feature @@ -22,3 +22,20 @@ Feature: Project Ff Merge Requests Then I should see ff-only merge button When I accept this merge request Then I should see merged request + + @javascript + Scenario: I do rebase before ff-only merge + Given ff merge enabled + And rebase before merge enabled + When I visit merge request page "Bug NS-05" + Then I should see rebase button + When I press rebase button + Then I should see rebase in progress message + + @javascript + Scenario: I do rebase before regular merge + Given rebase before merge enabled + When I visit merge request page "Bug NS-05" + Then I should see rebase button + When I press rebase button + Then I should see rebase in progress message diff --git a/features/steps/project/ff_merge_requests.rb b/features/steps/project/ff_merge_requests.rb index d68fe71e16e..27efcfd65b6 100644 --- a/features/steps/project/ff_merge_requests.rb +++ b/features/steps/project/ff_merge_requests.rb @@ -17,6 +17,10 @@ class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps author: project.users.first) end + step 'merge request is mergeable' do + expect(page).to have_button 'Merge' + end + step 'I should see ff-only merge button' do expect(page).to have_content "Fast-forward merge without a merge commit" expect(page).to have_button 'Merge' @@ -45,6 +49,10 @@ class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps project.save! end + step 'I should see rebase button' do + expect(page).to have_button "Rebase" + end + step 'merge request "Bug NS-05" is rebased' do merge_request.source_branch = 'flatten-dir' merge_request.target_branch = 'improve/awesome' @@ -59,6 +67,20 @@ class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps merge_request.save! end + step 'rebase before merge enabled' do + project = merge_request.target_project + project.merge_requests_rebase_enabled = true + project.save! + end + + step 'I press rebase button' do + click_button "Rebase" + end + + step "I should see rebase in progress message" do + expect(page).to have_content("Rebase in progress") + end + def merge_request @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05") end diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb index ef5bdbaf819..3fb0e2eed93 100644 --- a/lib/gitlab/git/operation_service.rb +++ b/lib/gitlab/git/operation_service.rb @@ -97,6 +97,11 @@ module Gitlab end end + def update_branch(branch_name, newrev, oldrev) + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name + update_ref_in_hooks(ref, newrev, oldrev) + end + private # Returns [newrev, should_run_after_create, should_run_after_create_branch] diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 45c424af8c4..c8cc6b374f6 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -684,4 +684,62 @@ describe Projects::MergeRequestsController do format: :json end end + + describe 'POST #rebase' do + let(:viewer) { user } + + def post_rebase + post :rebase, namespace_id: project.namespace, project_id: project, id: merge_request + end + + def expect_rebase_worker_for(user) + expect(RebaseWorker).to receive(:perform_async).with(merge_request.id, user.id) + end + + context 'successfully' do + it 'enqeues a RebaseWorker' do + expect_rebase_worker_for(viewer) + + post_rebase + + expect(response.status).to eq(200) + end + end + + context 'with a forked project' do + let(:fork_project) { create(:project, :repository, forked_from_project: project) } + let(:fork_owner) { fork_project.owner } + + before do + merge_request.update!(source_project: fork_project) + fork_project.add_reporter(user) + end + + context 'user cannot push to source branch' do + it 'returns 404' do + expect_rebase_worker_for(viewer).never + + post_rebase + + expect(response.status).to eq(404) + end + end + + context 'user can push to source branch' do + before do + project.add_reporter(fork_owner) + + sign_in(fork_owner) + end + + it 'returns 200' do + expect_rebase_worker_for(fork_owner) + + post_rebase + + expect(response.status).to eq(200) + end + end + end + end end diff --git a/spec/fixtures/api/schemas/entities/merge_request_basic.json b/spec/fixtures/api/schemas/entities/merge_request_basic.json index 995f13381ad..f1199468d53 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_basic.json +++ b/spec/fixtures/api/schemas/entities/merge_request_basic.json @@ -9,6 +9,7 @@ "human_time_estimate": { "type": ["string", "null"] }, "human_total_time_spent": { "type": ["string", "null"] }, "merge_error": { "type": ["string", "null"] }, + "rebase_in_progress": { "type": "boolean" }, "assignee_id": { "type": ["integer", "null"] }, "subscribed": { "type": ["boolean", "null"] }, "participants": { "type": "array" } diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json index 9de27bee751..7f662098216 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_widget.json +++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json @@ -103,7 +103,11 @@ "remove_source_branch": { "type": ["boolean", "null"] }, "merge_ongoing": { "type": "boolean" }, "ff_only_enabled": { "type": ["boolean", false] }, - "should_be_rebased": { "type": "boolean" } + "should_be_rebased": { "type": "boolean" }, + "rebase_commit_sha": { "type": ["string", "null"] }, + "rebase_in_progress": { "type": "boolean" }, + "can_push_to_source_branch": { "type": "boolean" }, + "rebase_path": { "type": ["string", "null"] } }, "additionalProperties": false } diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js new file mode 100644 index 00000000000..66ecaa316c8 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js @@ -0,0 +1,115 @@ +import Vue from 'vue'; +import eventHub from '~/vue_merge_request_widget/event_hub'; +import component from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Merge request widget rebase component', () => { + let Component; + let vm; + beforeEach(() => { + Component = Vue.extend(component); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('While rebasing', () => { + it('should show progress message', () => { + vm = mountComponent(Component, { + mr: { rebaseInProgress: true }, + service: {}, + }); + + expect( + vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(), + ).toContain('Rebase in progress'); + }); + }); + + describe('With permissions', () => { + beforeEach(() => { + vm = mountComponent(Component, { + mr: { + rebaseInProgress: false, + canPushToSourceBranch: true, + }, + service: {}, + }); + }); + + it('it should render rebase button and warning message', () => { + const text = vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(); + expect(text).toContain('Fast-forward merge is not possible.'); + expect(text).toContain('Rebase the source branch onto the target branch or merge target'); + expect(text).toContain('branch into source branch to allow this merge request to be merged.'); + }); + + it('it should render error message when it fails', (done) => { + vm.rebasingError = 'Something went wrong!'; + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(), + ).toContain('Something went wrong!'); + done(); + }); + }); + }); + + describe('Without permissions', () => { + it('should render a message explaining user does not have permissions', () => { + vm = mountComponent(Component, { + mr: { + rebaseInProgress: false, + canPushToSourceBranch: false, + targetBranch: 'foo', + }, + service: {}, + }); + + const text = vm.$el.querySelector('.rebase-state-find-class-convention span').textContent.trim(); + + expect(text).toContain('Fast-forward merge is not possible.'); + expect(text).toContain('Rebase the source branch onto'); + expect(text).toContain('foo'); + expect(text).toContain('to allow this merge request to be merged.'); + }); + }); + + describe('methods', () => { + it('checkRebaseStatus', (done) => { + spyOn(eventHub, '$emit'); + vm = mountComponent(Component, { + mr: {}, + service: { + rebase() { + return Promise.resolve(); + }, + poll() { + return Promise.resolve({ + data: { + rebase_in_progress: false, + merge_error: null, + }, + }); + }, + }, + }); + + vm.rebase(); + + // Wait for the rebase request + vm.$nextTick() + // Wait for the polling request + .then(vm.$nextTick()) + // Wait for the eventHub to be called + .then(vm.$nextTick()) + .then(() => { + expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index d8ebd46faab..07b3e1c1758 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1903,4 +1903,50 @@ describe MergeRequest do end end end + + describe '#should_be_rebased?' do + let(:project) { create(:project, :repository) } + + it 'returns false for the same source and target branches' do + merge_request = create(:merge_request, source_project: project, target_project: project) + + expect(merge_request.should_be_rebased?).to be_falsey + end + end + + describe '#rebase_in_progress?' do + # Create merge request and project before we stub file calls + before do + subject + end + + it 'returns true when there is a current rebase directory' do + allow(File).to receive(:exist?).and_return(true) + allow(File).to receive(:mtime).and_return(Time.now) + + expect(subject.rebase_in_progress?).to be_truthy + end + + it 'returns false when there is no rebase directory' do + allow(File).to receive(:exist?).and_return(false) + + expect(subject.rebase_in_progress?).to be_falsey + end + + it 'returns false when the rebase directory has expired' do + allow(File).to receive(:exist?).and_return(true) + allow(File).to receive(:mtime).and_return(20.minutes.ago) + + 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) + allow(File).to receive(:exist?).and_return(true) + allow(File).to receive(:mtime).and_return(Time.now) + + expect(File).not_to have_received(:exist?) + expect(subject.rebase_in_progress?).to be_falsey + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 3c2ed043b82..13e5345ee4c 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -418,14 +418,21 @@ describe Project do end describe '#merge_method' do - it 'returns "ff" merge_method when ff is enabled' do - project = build(:project, merge_requests_ff_only_enabled: true) - expect(project.merge_method).to be :ff + using RSpec::Parameterized::TableSyntax + + where(:ff, :rebase, :method) do + true | true | :ff + true | false | :ff + false | true | :rebase_merge + false | false | :merge end - it 'returns "merge" merge_method when ff is disabled' do - project = build(:project, merge_requests_ff_only_enabled: false) - expect(project.merge_method).to be :merge + with_them do + let(:project) { build(:project, merge_requests_rebase_enabled: rebase, merge_requests_ff_only_enabled: ff) } + + subject { project.merge_method } + + it { is_expected.to eq(method) } end end diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb index 969c4753f33..e3b37739e8e 100644 --- a/spec/presenters/merge_request_presenter_spec.rb +++ b/spec/presenters/merge_request_presenter_spec.rb @@ -404,4 +404,67 @@ describe MergeRequestPresenter do .to eq("#{resource.source_branch}") end end + + describe '#rebase_path' do + before do + allow(resource).to receive(:rebase_in_progress?) { rebase_in_progress } + allow(resource).to receive(:should_be_rebased?) { should_be_rebased } + + allow_any_instance_of(Gitlab::UserAccess::RequestCacheExtension) + .to receive(:can_push_to_branch?) + .with(resource.source_branch) + .and_return(can_push_to_branch) + end + + subject do + described_class.new(resource, current_user: user).rebase_path + end + + context 'when can rebase' do + let(:rebase_in_progress) { false } + let(:can_push_to_branch) { true } + let(:should_be_rebased) { true } + + before do + allow(resource).to receive(:source_branch_exists?) { true } + end + + it 'returns path' do + is_expected + .to eq("/#{project.full_path}/merge_requests/#{resource.iid}/rebase") + end + end + + context 'when cannot rebase' do + context 'when rebase in progress' do + let(:rebase_in_progress) { true } + let(:can_push_to_branch) { true } + let(:should_be_rebased) { true } + + it 'returns nil' do + is_expected.to be_nil + end + end + + context 'when user cannot merge' do + let(:rebase_in_progress) { false } + let(:can_push_to_branch) { false } + let(:should_be_rebased) { true } + + it 'returns nil' do + is_expected.to be_nil + end + end + + context 'should not be rebased' do + let(:rebase_in_progress) { false } + let(:can_push_to_branch) { true } + let(:should_be_rebased) { false } + + it 'returns nil' do + is_expected.to be_nil + end + end + end + end end diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb index e25552eb0d8..80a271ba7fb 100644 --- a/spec/serializers/merge_request_widget_entity_spec.rb +++ b/spec/serializers/merge_request_widget_entity_spec.rb @@ -190,4 +190,20 @@ describe MergeRequestWidgetEntity do end end end + + describe 'when source project is deleted' do + let(:project) { create(:project, :repository) } + let(:fork_project) { create(:project, :repository, forked_from_project: project) } + let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: project) } + + it 'returns a blank rebase_path' do + allow(merge_request).to receive(:should_be_rebased?).and_return(true) + fork_project.destroy + merge_request.reload + + entity = described_class.new(merge_request, request: request).as_json + + expect(entity[:rebase_path]).to be_nil + end + end end diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb new file mode 100644 index 00000000000..d1b37cdd073 --- /dev/null +++ b/spec/services/merge_requests/rebase_service_spec.rb @@ -0,0 +1,134 @@ +require 'spec_helper' + +describe MergeRequests::RebaseService do + include ProjectForksHelper + + let(:user) { create(:user) } + let(:merge_request) do + create(:merge_request, + source_branch: 'feature_conflict', + target_branch: 'master') + end + let(:project) { merge_request.project } + let(:repository) { project.repository.raw } + + subject(:service) { described_class.new(project, user, {}) } + + before do + project.add_master(user) + end + + 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) + end + + it 'saves the error message' do + subject.execute(merge_request) + + expect(merge_request.reload.merge_error).to eq 'Rebase task canceled: Another rebase is already in progress' + end + + it 'returns an error' do + expect(service.execute(merge_request)).to match(status: :error, + message: 'Failed to rebase. Should be done manually') + end + end + + context 'when unexpected error occurs' do + before do + allow(repository).to receive(:run_git!).and_raise('Something went wrong') + end + + it 'saves the error message' do + subject.execute(merge_request) + + expect(merge_request.reload.merge_error).to eq 'Something went wrong' + end + + it 'returns an error' do + expect(service.execute(merge_request)).to match(status: :error, + message: 'Failed to rebase. Should be done manually') + end + end + + context 'with git command failure' do + before do + allow(repository).to receive(:run_git!).and_raise(Gitlab::Git::Repository::GitError, 'Something went wrong') + end + + it 'saves the error message' do + subject.execute(merge_request) + + expect(merge_request.reload.merge_error).to eq 'Something went wrong' + end + + it 'returns an error' do + expect(service.execute(merge_request)).to match(status: :error, + message: 'Failed to rebase. Should be done manually') + end + end + + context 'valid params' do + before do + service.execute(merge_request) + end + + it 'rebases source branch' do + parent_sha = merge_request.source_project.repository.commit(merge_request.source_branch).parents.first.sha + target_branch_sha = merge_request.target_project.repository.commit(merge_request.target_branch).sha + expect(parent_sha).to eq(target_branch_sha) + end + + it 'records the new SHA on the merge request' do + head_sha = merge_request.source_project.repository.commit(merge_request.source_branch).sha + expect(merge_request.reload.rebase_commit_sha).to eq(head_sha) + end + + it 'logs correct author and commiter' do + head_commit = merge_request.source_project.repository.commit(merge_request.source_branch) + + expect(head_commit.author_email).to eq('dmitriy.zaporozhets@gmail.com') + expect(head_commit.author_name).to eq('Dmitriy Zaporozhets') + expect(head_commit.committer_email).to eq(user.email) + expect(head_commit.committer_name).to eq(user.name) + end + + context 'git commands' do + it 'sets GL_REPOSITORY env variable when calling git commands' do + expect(repository).to receive(:popen).exactly(3) + .with(anything, anything, hash_including('GL_REPOSITORY')) + .and_return(['', 0]) + + service.execute(merge_request) + end + end + + context 'fork' do + let(:forked_project) do + fork_project(project, user, repository: true) + end + + let(:merge_request_from_fork) do + forked_project.repository.create_file( + user, + 'new-file-to-target', + '', + message: 'Add new file to target', + branch_name: 'master') + + create(:merge_request, + source_branch: 'master', source_project: forked_project, + target_branch: 'master', target_project: project) + end + + it 'rebases source branch' do + parent_sha = forked_project.repository.commit(merge_request_from_fork.source_branch).parents.first.sha + target_branch_sha = project.repository.commit(merge_request_from_fork.target_branch).sha + expect(parent_sha).to eq(target_branch_sha) + end + end + end + end +end diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb index 28d54c2fb77..264e0ce0b40 100644 --- a/spec/views/projects/merge_requests/show.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb @@ -54,6 +54,8 @@ describe 'projects/merge_requests/show.html.haml' do it 'closes the merge request if the source project does not exist' do closed_merge_request.update_attributes(state: 'open') forked_project.destroy + # Reload merge request so MergeRequest#source_project turns to `nil` + closed_merge_request.reload render diff --git a/spec/workers/rebase_worker_spec.rb b/spec/workers/rebase_worker_spec.rb new file mode 100644 index 00000000000..20aff020dbb --- /dev/null +++ b/spec/workers/rebase_worker_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe RebaseWorker, '#perform' do + context 'when rebasing an MR from a fork where upstream has protected branches' do + let(:upstream_project) { create(:project, :repository) } + let(:fork_project) { create(:project, :repository) } + + let(:merge_request) do + create(:merge_request, + source_project: fork_project, + source_branch: 'feature_conflict', + target_project: upstream_project, + target_branch: 'master') + end + + before do + create(:forked_project_link, forked_to_project: fork_project, forked_from_project: upstream_project) + end + + it 'sets the correct project for running hooks' do + expect(MergeRequests::RebaseService) + .to receive(:new).with(fork_project, merge_request.author).and_call_original + + subject.perform(merge_request, merge_request.author) + end + end +end From 304851dc90c06d770042bf3cb0af887b6f3497e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarka=20Kadlecova=CC=81?= Date: Thu, 4 Jan 2018 13:29:48 +0100 Subject: [PATCH 46/61] Refactor RelativePositioning so that it can be used by other classes --- app/models/concerns/relative_positioning.rb | 18 +++++++++++------- app/models/issue.rb | 6 ++++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index 835f26aa57b..afacdb8cb12 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -10,12 +10,12 @@ module RelativePositioning after_save :save_positionable_neighbours end - def project_ids - [project.id] + def min_relative_position + self.class.in_parents(parent_ids).minimum(:relative_position) end def max_relative_position - self.class.in_projects(project_ids).maximum(:relative_position) + self.class.in_parents(parent_ids).maximum(:relative_position) end def prev_relative_position @@ -23,7 +23,7 @@ module RelativePositioning if self.relative_position prev_pos = self.class - .in_projects(project_ids) + .in_parents(parent_ids) .where('relative_position < ?', self.relative_position) .maximum(:relative_position) end @@ -36,7 +36,7 @@ module RelativePositioning if self.relative_position next_pos = self.class - .in_projects(project_ids) + .in_parents(parent_ids) .where('relative_position > ?', self.relative_position) .minimum(:relative_position) end @@ -63,7 +63,7 @@ module RelativePositioning pos_after = before.next_relative_position if before.shift_after? - issue_to_move = self.class.in_projects(project_ids).find_by!(relative_position: pos_after) + issue_to_move = self.class.in_parents(parent_ids).find_by!(relative_position: pos_after) issue_to_move.move_after @positionable_neighbours = [issue_to_move] # rubocop:disable Gitlab/ModuleWithInstanceVariables @@ -78,7 +78,7 @@ module RelativePositioning pos_before = after.prev_relative_position if after.shift_before? - issue_to_move = self.class.in_projects(project_ids).find_by!(relative_position: pos_before) + issue_to_move = self.class.in_parents(parent_ids).find_by!(relative_position: pos_before) issue_to_move.move_before @positionable_neighbours = [issue_to_move] # rubocop:disable Gitlab/ModuleWithInstanceVariables @@ -92,6 +92,10 @@ module RelativePositioning self.relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION) end + def move_to_start + self.relative_position = position_between(min_relative_position || START_POSITION, MIN_POSITION) + end + # Indicates if there is an issue that should be shifted to free the place def shift_after? next_pos = next_relative_position diff --git a/app/models/issue.rb b/app/models/issue.rb index 4eafc1316d6..f2d111ba926 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -35,6 +35,8 @@ class Issue < ActiveRecord::Base validates :project, presence: true + alias_attribute :parent_id, :project_id + scope :in_projects, ->(project_ids) { where(project_id: project_ids) } scope :assigned, -> { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') } @@ -78,6 +80,10 @@ class Issue < ActiveRecord::Base acts_as_paranoid + class << self + alias_method :in_parents, :in_projects + end + def self.reference_prefix '#' end From 6b15784ce7d893ed509c00d9f51a4702787799ed Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Fri, 5 Jan 2018 09:41:05 +0000 Subject: [PATCH 47/61] Fix typos in a code comment --- lib/gitlab/git/blob.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index bd91125d3b6..a1755143abe 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -138,8 +138,8 @@ module Gitlab # Gitaly will think that setting the limit to 0 means unlimited, while # the client might only need the metadata and thus set the limit to 0. - # In this method we'll than set the limit to 1, but clear the byte of data - # that we got back so fot the outside world it looks like the limit was + # In this method we'll then set the limit to 1, but clear the byte of data + # that we got back so for the outside world it looks like the limit was # actually 0. req_limit = limit == 0 ? 1 : limit From 3514b7248cf00bcee8a6b3133e4e157f656d30c6 Mon Sep 17 00:00:00 2001 From: Alessio Caiazza Date: Tue, 13 Jun 2017 22:03:34 +0200 Subject: [PATCH 48/61] Add status attribute to runner api entity --- .../unreleased/feature-api_runners_online.yml | 5 ++-- doc/api/runners.md | 23 ++++++++++++++----- lib/api/entities.rb | 1 + 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/changelogs/unreleased/feature-api_runners_online.yml b/changelogs/unreleased/feature-api_runners_online.yml index f5077507e5b..08f4dd16f28 100644 --- a/changelogs/unreleased/feature-api_runners_online.yml +++ b/changelogs/unreleased/feature-api_runners_online.yml @@ -1,4 +1,5 @@ --- -title: Add online attribute to runner api entity +title: Add online and status attribute to runner api entity merge_request: 11750 -author: Alessio Caiazza +author: +type: added diff --git a/doc/api/runners.md b/doc/api/runners.md index 50981ed96bc..7495c6cdedb 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -31,7 +31,8 @@ Example response: "id": 6, "is_shared": false, "name": null, - "online": true + "online": true, + "status": "online" }, { "active": true, @@ -39,7 +40,8 @@ Example response: "id": 8, "is_shared": false, "name": null, - "online": false + "online": false, + "status": "offline" } ] ``` @@ -72,7 +74,8 @@ Example response: "id": 1, "is_shared": true, "name": null, - "online": true + "online": true, + "status": "online" }, { "active": true, @@ -81,6 +84,7 @@ Example response: "is_shared": true, "name": null, "online": false + "status": "offline" }, { "active": true, @@ -89,6 +93,7 @@ Example response: "is_shared": false, "name": null, "online": true + "status": "paused" }, { "active": true, @@ -96,7 +101,8 @@ Example response: "id": 8, "is_shared": false, "name": null, - "online": false + "online": false, + "status": "offline" } ] ``` @@ -129,6 +135,7 @@ Example response: "contacted_at": "2016-01-25T16:39:48.066Z", "name": null, "online": true, + "status": "online", "platform": null, "projects": [ { @@ -184,6 +191,7 @@ Example response: "contacted_at": "2016-01-25T16:39:48.066Z", "name": null, "online": true, + "status": "online", "platform": null, "projects": [ { @@ -336,7 +344,8 @@ Example response: "id": 8, "is_shared": false, "name": null, - "online": false + "online": false, + "status": "offline" }, { "active": true, @@ -345,6 +354,7 @@ Example response: "is_shared": true, "name": null, "online": true + "status": "paused" } ] ``` @@ -375,7 +385,8 @@ Example response: "id": 9, "is_shared": false, "name": null, - "online": true + "online": true, + "status": "online" } ``` diff --git a/lib/api/entities.rb b/lib/api/entities.rb index c612dde7f73..f5fa5fef389 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -863,6 +863,7 @@ module API expose :is_shared expose :name expose :online?, as: :online + expose :status end class RunnerDetails < Runner From 13926246020fb41599e1a7b908912bb2e61f114f Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 5 Jan 2018 11:16:18 +0100 Subject: [PATCH 49/61] add deprecation and removal issue to docs --- doc/administration/raketasks/check.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/administration/raketasks/check.md b/doc/administration/raketasks/check.md index 7dabc014bad..831b73237b6 100644 --- a/doc/administration/raketasks/check.md +++ b/doc/administration/raketasks/check.md @@ -28,6 +28,12 @@ exactly which repositories are causing the trouble. ### Check all GitLab repositories +>**Note:** +> +> - `gitlab:repo:check` has been deprecated in favour of `gitlab:git:fsck` +> - [Deprecated][ce-15931] in GitLab 10.4. +> - `gitlab:repo:check` will be removed in the future. [Removal issue][ce-41699] + This task loops through all repositories on the GitLab server and runs the 3 integrity checks described previously. @@ -76,3 +82,6 @@ The LDAP check Rake task will test the bind_dn and password credentials (if configured) and will list a sample of LDAP users. This task is also executed as part of the `gitlab:check` task, but can run independently. See [LDAP Rake Tasks - LDAP Check](ldap.md#check) for details. + +[ce-15931]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15931 +[ce-41699]: https://gitlab.com/gitlab-org/gitlab-ce/issues/41699 From 0feb2437e1318fa1b3157bf322d6759bb32bc01a Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 5 Jan 2018 10:30:57 +0000 Subject: [PATCH 50/61] Update check.md --- doc/administration/raketasks/check.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/administration/raketasks/check.md b/doc/administration/raketasks/check.md index 831b73237b6..c39cb49b1c6 100644 --- a/doc/administration/raketasks/check.md +++ b/doc/administration/raketasks/check.md @@ -30,7 +30,7 @@ exactly which repositories are causing the trouble. >**Note:** > -> - `gitlab:repo:check` has been deprecated in favour of `gitlab:git:fsck` +> - `gitlab:repo:check` has been deprecated in favor of `gitlab:git:fsck` > - [Deprecated][ce-15931] in GitLab 10.4. > - `gitlab:repo:check` will be removed in the future. [Removal issue][ce-41699] From 7c91863b43dbb4027e9b888d5064a3c1d3dabcf4 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 5 Jan 2018 10:56:09 +0000 Subject: [PATCH 51/61] Use computed prop in expand button --- app/assets/javascripts/vue_shared/components/expand_button.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue index 96991c4e268..05e48ed297f 100644 --- a/app/assets/javascripts/vue_shared/components/expand_button.vue +++ b/app/assets/javascripts/vue_shared/components/expand_button.vue @@ -35,7 +35,7 @@ type="button" v-show="isCollapsed" class="text-expander btn-blank" - aria-label="Click to Expand Text" + :aria-label="ariaLabel" @click="onClick"> ... From 5d1391b6818b61e3b7ba4742d6487382484f9643 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 5 Jan 2018 12:29:01 +0100 Subject: [PATCH 52/61] Fix specs --- spec/models/namespace_spec.rb | 10 +++++++--- spec/models/project_spec.rb | 8 ++++---- spec/services/projects/create_service_spec.rb | 2 +- .../hashed_storage/migrate_repository_service_spec.rb | 2 +- spec/services/projects/transfer_service_spec.rb | 4 ++-- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 0678cae9b93..b3f160f3119 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -250,9 +250,13 @@ describe Namespace do parent.update(path: 'mygroup_new') - expect(project_in_parent_group.repo.config['gitlab.fullpath']).to eq "mygroup_new/#{project_in_parent_group.path}" - expect(hashed_project_in_subgroup.repo.config['gitlab.fullpath']).to eq "mygroup_new/mysubgroup/#{hashed_project_in_subgroup.path}" - expect(legacy_project_in_subgroup.repo.config['gitlab.fullpath']).to eq "mygroup_new/mysubgroup/#{legacy_project_in_subgroup.path}" + expect(project_rugged(project_in_parent_group).config['gitlab.fullpath']).to eq "mygroup_new/#{project_in_parent_group.path}" + expect(project_rugged(hashed_project_in_subgroup).config['gitlab.fullpath']).to eq "mygroup_new/mysubgroup/#{hashed_project_in_subgroup.path}" + expect(project_rugged(legacy_project_in_subgroup).config['gitlab.fullpath']).to eq "mygroup_new/mysubgroup/#{legacy_project_in_subgroup.path}" + end + + def project_rugged(project) + project.repository.rugged end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index cea22bbd184..8111365bed1 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2632,7 +2632,7 @@ describe Project do project.rename_repo - expect(project.repo.config['gitlab.fullpath']).to eq(project.full_path) + expect(project.repository.rugged.config['gitlab.fullpath']).to eq(project.full_path) end end @@ -2793,7 +2793,7 @@ describe Project do it 'updates project full path in .git/config' do project.rename_repo - expect(project.repo.config['gitlab.fullpath']).to eq(project.full_path) + expect(project.repository.rugged.config['gitlab.fullpath']).to eq(project.full_path) end end @@ -3162,13 +3162,13 @@ describe Project do it 'writes full path in .git/config when key is missing' do project.write_repository_config - expect(project.repo.config['gitlab.fullpath']).to eq project.full_path + expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path end it 'updates full path in .git/config when key is present' do project.write_repository_config(gl_full_path: 'old/path') - expect { project.write_repository_config }.to change { project.repo.config['gitlab.fullpath'] }.from('old/path').to(project.full_path) + expect { project.write_repository_config }.to change { project.repository.rugged.config['gitlab.fullpath'] }.from('old/path').to(project.full_path) end it 'does not raise an error with an empty repository' do diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 1833078f37c..9a44dfde41b 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -255,7 +255,7 @@ describe Projects::CreateService, '#execute' do it 'writes project full path to .git/config' do project = create_project(user, opts) - expect(project.repo.config['gitlab.fullpath']).to eq project.full_path + expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path end def create_project(user, opts) diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb index ded864beb1d..7b536cc05cb 100644 --- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb +++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb @@ -37,7 +37,7 @@ describe Projects::HashedStorage::MigrateRepositoryService do it 'writes project full path to .git/config' do service.execute - expect(project.repo.config['gitlab.fullpath']).to eq project.full_path + expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path end end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 7377c748698..39f6388c25e 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -58,7 +58,7 @@ describe Projects::TransferService do it 'updates project full path in .git/config' do transfer_project(project, user, group) - expect(project.repo.config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}" + expect(project.repository.rugged.config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}" end end @@ -95,7 +95,7 @@ describe Projects::TransferService do it 'rolls back project full path in .git/config' do attempt_project_transfer - expect(project.repo.config['gitlab.fullpath']).to eq project.full_path + expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path end it "doesn't send move notifications" do From c5e2c0665fe7e4937689cfedaa064aa64f538c8b Mon Sep 17 00:00:00 2001 From: "Jacob Vosmaer (GitLab)" Date: Fri, 5 Jan 2018 11:31:12 +0000 Subject: [PATCH 53/61] Allow local tests to use a modified Gitaly --- doc/development/gitaly.md | 23 ++++++++++++++ lib/gitlab/setup_helper.rb | 61 ++++++++++++++++++++++++++++++++++++ lib/tasks/gitlab/gitaly.rake | 57 ++------------------------------- spec/support/test_env.rb | 7 +++++ 4 files changed, 93 insertions(+), 55 deletions(-) create mode 100644 lib/gitlab/setup_helper.rb diff --git a/doc/development/gitaly.md b/doc/development/gitaly.md index ca2048c7019..26abf967dcf 100644 --- a/doc/development/gitaly.md +++ b/doc/development/gitaly.md @@ -97,6 +97,29 @@ describe 'Gitaly Request count tests' do end ``` +## Running tests with a locally modified version of Gitaly + +Normally, gitlab-ce/ee tests use a local clone of Gitaly in `tmp/tests/gitaly` +pinned at the version specified in GITALY_SERVER_VERSION. If you want +to run tests locally against a modified version of Gitaly you can +replace `tmp/tests/gitaly` with a symlink. + +```shell +rm -rf tmp/tests/gitaly +ln -s /path/to/gitaly tmp/tests/gitaly +``` + +Make sure you run `make` in your local Gitaly directory before running +tests. Otherwise, Gitaly will fail to boot. + +If you make changes to your local Gitaly in between test runs you need +to manually run `make` again. + +Note that CI tests will not use your locally modified version of +Gitaly. To use a custom Gitaly version in CI you need to update +GITALY_SERVER_VERSION. You can use the format `=revision` to use a +non-tagged commit from https://gitlab.com/gitlab-org/gitaly in CI. + --- [Return to Development documentation](README.md) diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb new file mode 100644 index 00000000000..d01213bb6e0 --- /dev/null +++ b/lib/gitlab/setup_helper.rb @@ -0,0 +1,61 @@ +module Gitlab + module SetupHelper + class << self + # We cannot create config.toml files for all possible Gitaly configuations. + # For instance, if Gitaly is running on another machine then it makes no + # sense to write a config.toml file on the current machine. This method will + # only generate a configuration for the most common and simplest case: when + # we have exactly one Gitaly process and we are sure it is running locally + # because it uses a Unix socket. + # For development and testing purposes, an extra storage is added to gitaly, + # which is not known to Rails, but must be explicitly stubbed. + def gitaly_configuration_toml(gitaly_dir, gitaly_ruby: true) + storages = [] + address = nil + + Gitlab.config.repositories.storages.each do |key, val| + if address + if address != val['gitaly_address'] + raise ArgumentError, "Your gitlab.yml contains more than one gitaly_address." + end + elsif URI(val['gitaly_address']).scheme != 'unix' + raise ArgumentError, "Automatic config.toml generation only supports 'unix:' addresses." + else + address = val['gitaly_address'] + end + + storages << { name: key, path: val['path'] } + end + + if Rails.env.test? + storages << { name: 'test_second_storage', path: Rails.root.join('tmp', 'tests', 'second_storage').to_s } + end + + config = { socket_path: address.sub(%r{\Aunix:}, ''), storage: storages } + config[:auth] = { token: 'secret' } if Rails.env.test? + config[:'gitaly-ruby'] = { dir: File.join(gitaly_dir, 'ruby') } if gitaly_ruby + config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path } + config[:bin_dir] = Gitlab.config.gitaly.client_path + + TOML.dump(config) + end + + # rubocop:disable Rails/Output + def create_gitaly_configuration(dir, force: false) + config_path = File.join(dir, 'config.toml') + FileUtils.rm_f(config_path) if force + + File.open(config_path, File::WRONLY | File::CREAT | File::EXCL) do |f| + f.puts gitaly_configuration_toml(dir) + end + rescue Errno::EEXIST + puts "Skipping config.toml generation:" + puts "A configuration file already exists." + rescue ArgumentError => e + puts "Skipping config.toml generation:" + puts e.message + end + # rubocop:enable Rails/Output + end + end +end diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index 4d880c05f99..4507b841964 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -21,8 +21,8 @@ namespace :gitlab do command << 'BUNDLE_FLAGS=--no-deployment' if Rails.env.test? + Gitlab::SetupHelper.create_gitaly_configuration(args.dir) Dir.chdir(args.dir) do - create_gitaly_configuration # In CI we run scripts/gitaly-test-build instead of this command unless ENV['CI'].present? Bundler.with_original_env { run_command!(command) } @@ -39,60 +39,7 @@ namespace :gitlab do # Exclude gitaly-ruby configuration because that depends on the gitaly # installation directory. - puts gitaly_configuration_toml(gitaly_ruby: false) - end - - private - - # We cannot create config.toml files for all possible Gitaly configuations. - # For instance, if Gitaly is running on another machine then it makes no - # sense to write a config.toml file on the current machine. This method will - # only generate a configuration for the most common and simplest case: when - # we have exactly one Gitaly process and we are sure it is running locally - # because it uses a Unix socket. - # For development and testing purposes, an extra storage is added to gitaly, - # which is not known to Rails, but must be explicitly stubbed. - def gitaly_configuration_toml(gitaly_ruby: true) - storages = [] - address = nil - - Gitlab.config.repositories.storages.each do |key, val| - if address - if address != val['gitaly_address'] - raise ArgumentError, "Your gitlab.yml contains more than one gitaly_address." - end - elsif URI(val['gitaly_address']).scheme != 'unix' - raise ArgumentError, "Automatic config.toml generation only supports 'unix:' addresses." - else - address = val['gitaly_address'] - end - - storages << { name: key, path: val['path'] } - end - - if Rails.env.test? - storages << { name: 'test_second_storage', path: Rails.root.join('tmp', 'tests', 'second_storage').to_s } - end - - config = { socket_path: address.sub(%r{\Aunix:}, ''), storage: storages } - config[:auth] = { token: 'secret' } if Rails.env.test? - config[:'gitaly-ruby'] = { dir: File.join(Dir.pwd, 'ruby') } if gitaly_ruby - config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path } - config[:bin_dir] = Gitlab.config.gitaly.client_path - - TOML.dump(config) - end - - def create_gitaly_configuration - File.open("config.toml", File::WRONLY | File::CREAT | File::EXCL) do |f| - f.puts gitaly_configuration_toml - end - rescue Errno::EEXIST - puts "Skipping config.toml generation:" - puts "A configuration file already exists." - rescue ArgumentError => e - puts "Skipping config.toml generation:" - puts e.message + puts Gitlab::SetupHelper.gitaly_configuration_toml('', gitaly_ruby: false) end end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 1d99746b09f..664698fcbaf 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -1,4 +1,5 @@ require 'rspec/mocks' +require 'toml' module TestEnv extend self @@ -147,6 +148,9 @@ module TestEnv version: Gitlab::GitalyClient.expected_server_version, task: "gitlab:gitaly:install[#{gitaly_dir}]") do + # Always re-create config, in case it's outdated. This is fast anyway. + Gitlab::SetupHelper.create_gitaly_configuration(gitaly_dir, force: true) + start_gitaly(gitaly_dir) end end @@ -347,6 +351,9 @@ module TestEnv end def component_needs_update?(component_folder, expected_version) + # Allow local overrides of the component for tests during development + return false if Rails.env.test? && File.symlink?(component_folder) + version = File.read(File.join(component_folder, 'VERSION')).strip # Notice that this will always yield true when using branch versions From 2c47f0924fc5534035905746046ab0f5e9c99f23 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Wed, 3 Jan 2018 10:21:17 +0100 Subject: [PATCH 54/61] Add id to modal.vue to support data-toggle="modal" --- .../groups/components/item_actions.vue | 6 +- .../ide/components/new_dropdown/index.vue | 8 +- .../ide/components/new_dropdown/modal.vue | 8 +- .../ide/components/repo_commit_section.vue | 2 +- .../ide/components/repo_edit_button.vue | 2 +- .../components/delete_account_modal.vue | 111 +++++++----------- .../javascripts/profile/account/index.js | 8 ++ .../vue_shared/components/modal.vue | 30 +++-- .../vue_shared/components/recaptcha_modal.vue | 2 +- app/views/profiles/accounts/show.html.haml | 6 +- .../unreleased/winh-modal-target-id.yml | 5 + .../groups/components/item_actions_spec.js | 12 +- .../components/delete_account_modal_spec.js | 8 +- .../components/new_dropdown/index_spec.js | 13 +- .../vue_shared/components/modal_spec.js | 64 +++++++++- 15 files changed, 169 insertions(+), 116 deletions(-) create mode 100644 changelogs/unreleased/winh-modal-target-id.yml diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue index 58ba5aff7cf..b98cfcf7563 100644 --- a/app/assets/javascripts/groups/components/item_actions.vue +++ b/app/assets/javascripts/groups/components/item_actions.vue @@ -45,11 +45,9 @@ export default { onLeaveGroup() { this.modalStatus = true; }, - leaveGroup(leaveConfirmed) { + leaveGroup() { this.modalStatus = false; - if (leaveConfirmed) { - eventHub.$emit('leaveGroup', this.group, this.parentGroup); - } + eventHub.$emit('leaveGroup', this.group, this.parentGroup); }, }, }; diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index 6e67e99a70f..d475813c4f7 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -32,10 +32,10 @@ methods: { createNewItem(type) { this.modalType = type; - this.toggleModalOpen(); + this.openModal = true; }, - toggleModalOpen() { - this.openModal = !this.openModal; + hideModal() { + this.openModal = false; }, }, }; @@ -95,7 +95,7 @@ :branch-id="branch" :path="path" :parent="parent" - @toggle="toggleModalOpen" + @hide="hideModal" />
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index a0650d37690..0312f56efbd 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -43,10 +43,10 @@ type: this.type, }); - this.toggleModalOpen(); + this.hideModal(); }, - toggleModalOpen() { - this.$emit('toggle'); + hideModal() { + this.$emit('hide'); }, }, computed: { @@ -86,7 +86,7 @@ :title="modalTitle" :primary-button-label="buttonLabel" kind="success" - @toggle="toggleModalOpen" + @cancel="hideModal" @submit="createEntryInStore" >
diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue index 78be6b6e884..36ad618aa46 100644 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -1,7 +1,7 @@ diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js index 635056e0eeb..a93bc935dd0 100644 --- a/app/assets/javascripts/profile/account/index.js +++ b/app/assets/javascripts/profile/account/index.js @@ -1,7 +1,12 @@ import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; + import deleteAccountModal from './components/delete_account_modal.vue'; +Vue.use(Translate); + +const deleteAccountButton = document.getElementById('delete-account-button'); const deleteAccountModalEl = document.getElementById('delete-account-modal'); // eslint-disable-next-line no-new new Vue({ @@ -9,6 +14,9 @@ new Vue({ components: { deleteAccountModal, }, + mounted() { + deleteAccountButton.classList.remove('disabled'); + }, render(createElement) { return createElement('delete-account-modal', { props: { diff --git a/app/assets/javascripts/vue_shared/components/modal.vue b/app/assets/javascripts/vue_shared/components/modal.vue index 55f466b7b41..00089dfef38 100644 --- a/app/assets/javascripts/vue_shared/components/modal.vue +++ b/app/assets/javascripts/vue_shared/components/modal.vue @@ -3,6 +3,10 @@ export default { name: 'modal', props: { + id: { + type: String, + required: false, + }, title: { type: String, required: false, @@ -62,11 +66,11 @@ export default { }, methods: { - close() { - this.$emit('toggle', false); + emitCancel(event) { + this.$emit('cancel', event); }, - emitSubmit(status) { - this.$emit('submit', status); + emitSubmit(event) { + this.$emit('submit', event); }, }, }; @@ -75,7 +79,9 @@ export default { diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue index 8053c65d498..16d60bb2876 100644 --- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue +++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue @@ -70,7 +70,7 @@ export default { class="recaptcha-modal js-recaptcha-modal" :hide-footer="true" :title="__('Please solve the reCAPTCHA')" - @toggle="close" + @cancel="close" >

diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index f1313b79589..79e197ad08b 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -84,11 +84,13 @@ = s_('Profiles|Deleting an account has the following effects:') = render 'users/deletion_guidance', user: current_user + %button#delete-account-button.btn.btn-danger.disabled{ data: { toggle: 'modal', + target: '#delete-account-modal' } } + = s_('Profiles|Delete account') + #delete-account-modal{ data: { action_url: user_registration_path, confirm_with_password: ('true' if current_user.confirm_deletion_with_password?), username: current_user.username } } - %button.btn.btn-danger.disabled - = s_('Profiles|Delete account') - else - if @user.solo_owned_groups.present? %p diff --git a/changelogs/unreleased/winh-modal-target-id.yml b/changelogs/unreleased/winh-modal-target-id.yml new file mode 100644 index 00000000000..f8d5b72be50 --- /dev/null +++ b/changelogs/unreleased/winh-modal-target-id.yml @@ -0,0 +1,5 @@ +--- +title: Add id to modal.vue to support data-toggle="modal" +merge_request: 16189 +author: +type: other diff --git a/spec/javascripts/groups/components/item_actions_spec.js b/spec/javascripts/groups/components/item_actions_spec.js index 7a5c1da4d1d..6d6fb410859 100644 --- a/spec/javascripts/groups/components/item_actions_spec.js +++ b/spec/javascripts/groups/components/item_actions_spec.js @@ -47,18 +47,12 @@ describe('ItemActionsComponent', () => { it('should change `modalStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`', () => { spyOn(eventHub, '$emit'); vm.modalStatus = true; - vm.leaveGroup(true); + + vm.leaveGroup(); + expect(vm.modalStatus).toBeFalsy(); expect(eventHub.$emit).toHaveBeenCalledWith('leaveGroup', vm.group, vm.parentGroup); }); - - it('should change `modalStatus` prop to `false` and should NOT emit `leaveGroup` event when called with `leaveConfirmed` as `false`', () => { - spyOn(eventHub, '$emit'); - vm.modalStatus = true; - vm.leaveGroup(false); - expect(vm.modalStatus).toBeFalsy(); - expect(eventHub.$emit).not.toHaveBeenCalled(); - }); }); }); diff --git a/spec/javascripts/profile/account/components/delete_account_modal_spec.js b/spec/javascripts/profile/account/components/delete_account_modal_spec.js index 2e94948cfb2..588b61196a5 100644 --- a/spec/javascripts/profile/account/components/delete_account_modal_spec.js +++ b/spec/javascripts/profile/account/components/delete_account_modal_spec.js @@ -51,7 +51,7 @@ describe('DeleteAccountModal component', () => { Vue.nextTick() .then(() => { expect(vm.enteredPassword).toBe(input.value); - expect(submitButton).toHaveClass('disabled'); + expect(submitButton).toHaveAttr('disabled', 'disabled'); submitButton.click(); expect(form.submit).not.toHaveBeenCalled(); }) @@ -68,7 +68,7 @@ describe('DeleteAccountModal component', () => { Vue.nextTick() .then(() => { expect(vm.enteredPassword).toBe(input.value); - expect(submitButton).not.toHaveClass('disabled'); + expect(submitButton).not.toHaveAttr('disabled', 'disabled'); submitButton.click(); expect(form.submit).toHaveBeenCalled(); }) @@ -101,7 +101,7 @@ describe('DeleteAccountModal component', () => { Vue.nextTick() .then(() => { expect(vm.enteredUsername).toBe(input.value); - expect(submitButton).toHaveClass('disabled'); + expect(submitButton).toHaveAttr('disabled', 'disabled'); submitButton.click(); expect(form.submit).not.toHaveBeenCalled(); }) @@ -118,7 +118,7 @@ describe('DeleteAccountModal component', () => { Vue.nextTick() .then(() => { expect(vm.enteredUsername).toBe(input.value); - expect(submitButton).not.toHaveClass('disabled'); + expect(submitButton).not.toHaveAttr('disabled', 'disabled'); submitButton.click(); expect(form.submit).toHaveBeenCalled(); }) diff --git a/spec/javascripts/repo/components/new_dropdown/index_spec.js b/spec/javascripts/repo/components/new_dropdown/index_spec.js index b001c1655b4..6efbbf6d75e 100644 --- a/spec/javascripts/repo/components/new_dropdown/index_spec.js +++ b/spec/javascripts/repo/components/new_dropdown/index_spec.js @@ -57,15 +57,16 @@ describe('new dropdown component', () => { }); }); - describe('toggleModalOpen', () => { + describe('hideModal', () => { + beforeAll((done) => { + vm.openModal = true; + Vue.nextTick(done); + }); + it('closes modal after toggling', (done) => { - vm.toggleModalOpen(); + vm.hideModal(); Vue.nextTick() - .then(() => { - expect(vm.$el.querySelector('.modal')).not.toBeNull(); - }) - .then(vm.toggleModalOpen) .then(() => { expect(vm.$el.querySelector('.modal')).toBeNull(); }) diff --git a/spec/javascripts/vue_shared/components/modal_spec.js b/spec/javascripts/vue_shared/components/modal_spec.js index 721f4044659..fe75a86cac8 100644 --- a/spec/javascripts/vue_shared/components/modal_spec.js +++ b/spec/javascripts/vue_shared/components/modal_spec.js @@ -2,11 +2,65 @@ import Vue from 'vue'; import modal from '~/vue_shared/components/modal.vue'; import mountComponent from '../../helpers/vue_mount_component_helper'; -describe('Modal', () => { - it('does not render a primary button if no primaryButtonLabel', () => { - const modalComponent = Vue.extend(modal); - const vm = mountComponent(modalComponent); +const modalComponent = Vue.extend(modal); - expect(vm.$el.querySelector('.js-primary-button')).toBeNull(); +describe('Modal', () => { + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('props', () => { + describe('without primaryButtonLabel', () => { + beforeEach(() => { + vm = mountComponent(modalComponent, { + primaryButtonLabel: null, + }); + }); + + it('does not render a primary button', () => { + expect(vm.$el.querySelector('.js-primary-button')).toBeNull(); + }); + }); + + describe('with id', () => { + it('does not render a primary button', () => { + beforeEach(() => { + vm = mountComponent(modalComponent, { + id: 'my-modal', + }); + }); + + it('assigns the id to the modal', () => { + expect(vm.$el.querySelector('#my-modal.modal')).not.toBeNull(); + }); + + it('does not show the modal immediately', () => { + expect(vm.$el.querySelector('#my-modal.modal')).not.toHaveClass('show'); + }); + + it('does not show a backdrop', () => { + expect(vm.$el.querySelector('modal-backdrop')).toBeNull(); + }); + }); + }); + + it('works with data-toggle="modal"', (done) => { + setFixtures(` + +

+ `); + + const modalContainer = document.getElementById('modal-container'); + const modalButton = document.getElementById('modal-button'); + vm = mountComponent(modalComponent, { + id: 'my-modal', + }, modalContainer); + const modalElement = vm.$el.querySelector('#my-modal'); + $(modalElement).on('shown.bs.modal', () => done()); + + modalButton.click(); + }); }); }); From 153ea1830153b7d7c3be5ac2e7ca60486c9b2700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarka=20Kadlecova=CC=81?= Date: Fri, 5 Jan 2018 10:15:03 +0100 Subject: [PATCH 55/61] Refactor matchers for background migrations --- app/models/issue.rb | 2 +- .../prepare_untracked_uploads_spec.rb | 13 +------------ ...grate_stage_id_reference_in_background_spec.rb | 6 +++--- spec/migrations/migrate_stages_statuses_spec.rb | 6 +++--- ...e_create_gpg_key_subkeys_from_gpg_keys_spec.rb | 12 ------------ ...schedule_merge_request_diff_migrations_spec.rb | 6 +++--- ...merge_request_diff_migrations_take_two_spec.rb | 6 +++--- ...atest_merge_request_diff_id_migrations_spec.rb | 6 +++--- ...merge_request_metrics_with_events_data_spec.rb | 4 ++-- spec/migrations/track_untracked_uploads_spec.rb | 12 ------------ spec/support/background_migrations_matchers.rb | 15 ++++++++++++++- 11 files changed, 33 insertions(+), 55 deletions(-) diff --git a/app/models/issue.rb b/app/models/issue.rb index f2d111ba926..ad4a3c737ff 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -35,7 +35,7 @@ class Issue < ActiveRecord::Base validates :project, presence: true - alias_attribute :parent_id, :project_id + alias_attribute :parent_ids, :project_id scope :in_projects, ->(project_ids) { where(project_id: project_ids) } diff --git a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb index cd3f1a45270..8bb9ebe0419 100644 --- a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb +++ b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb @@ -2,21 +2,10 @@ require 'spec_helper' describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do include TrackUntrackedUploadsHelpers + include MigrationsHelpers let!(:untracked_files_for_uploads) { described_class::UntrackedFile } - matcher :be_scheduled_migration do |*expected| - match do |migration| - BackgroundMigrationWorker.jobs.any? do |job| - job['args'] == [migration, expected] - end - end - - failure_message do |migration| - "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!" - end - end - before do DatabaseCleaner.clean diff --git a/spec/migrations/migrate_stage_id_reference_in_background_spec.rb b/spec/migrations/migrate_stage_id_reference_in_background_spec.rb index 9b92f4b70b0..a837498e1b1 100644 --- a/spec/migrations/migrate_stage_id_reference_in_background_spec.rb +++ b/spec/migrations/migrate_stage_id_reference_in_background_spec.rb @@ -35,9 +35,9 @@ describe MigrateStageIdReferenceInBackground, :migration, :sidekiq do Timecop.freeze do migrate! - expect(described_class::MIGRATION).to be_scheduled_migration(2.minutes, 1, 2) - expect(described_class::MIGRATION).to be_scheduled_migration(2.minutes, 3, 3) - expect(described_class::MIGRATION).to be_scheduled_migration(4.minutes, 4, 5) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, 1, 2) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, 3, 3) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, 4, 5) expect(BackgroundMigrationWorker.jobs.size).to eq 3 end end diff --git a/spec/migrations/migrate_stages_statuses_spec.rb b/spec/migrations/migrate_stages_statuses_spec.rb index 094c9bc604e..79d2708f9ad 100644 --- a/spec/migrations/migrate_stages_statuses_spec.rb +++ b/spec/migrations/migrate_stages_statuses_spec.rb @@ -50,9 +50,9 @@ describe MigrateStagesStatuses, :migration do Timecop.freeze do migrate! - expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 1, 1) - expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 2, 2) - expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 3, 3) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, 1, 1) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, 2, 2) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, 3, 3) expect(BackgroundMigrationWorker.jobs.size).to eq 3 end end diff --git a/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb b/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb index 0e884a7d910..65ec07da31c 100644 --- a/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb +++ b/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb @@ -2,18 +2,6 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20171005130944_schedule_create_gpg_key_subkeys_from_gpg_keys') describe ScheduleCreateGpgKeySubkeysFromGpgKeys, :migration, :sidekiq do - matcher :be_scheduled_migration do |*expected| - match do |migration| - BackgroundMigrationWorker.jobs.any? do |job| - job['args'] == [migration, expected] - end - end - - failure_message do |migration| - "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!" - end - end - before do create(:gpg_key, id: 1, key: GpgHelpers::User1.public_key) create(:gpg_key, id: 2, key: GpgHelpers::User3.public_key) diff --git a/spec/migrations/schedule_merge_request_diff_migrations_spec.rb b/spec/migrations/schedule_merge_request_diff_migrations_spec.rb index 76afb6c19cf..d230f064444 100644 --- a/spec/migrations/schedule_merge_request_diff_migrations_spec.rb +++ b/spec/migrations/schedule_merge_request_diff_migrations_spec.rb @@ -24,9 +24,9 @@ describe ScheduleMergeRequestDiffMigrations, :migration, :sidekiq do Timecop.freeze do migrate! - expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 1, 1) - expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 2, 2) - expect(described_class::MIGRATION).to be_scheduled_migration(15.minutes, 4, 4) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, 1, 1) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, 2, 2) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(15.minutes, 4, 4) expect(BackgroundMigrationWorker.jobs.size).to eq 3 end end diff --git a/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb b/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb index cf323973384..1aab4ae1650 100644 --- a/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb +++ b/spec/migrations/schedule_merge_request_diff_migrations_take_two_spec.rb @@ -24,9 +24,9 @@ describe ScheduleMergeRequestDiffMigrationsTakeTwo, :migration, :sidekiq do Timecop.freeze do migrate! - expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 1, 1) - expect(described_class::MIGRATION).to be_scheduled_migration(20.minutes, 2, 2) - expect(described_class::MIGRATION).to be_scheduled_migration(30.minutes, 4, 4) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, 1, 1) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(20.minutes, 2, 2) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(30.minutes, 4, 4) expect(BackgroundMigrationWorker.jobs.size).to eq 3 end end diff --git a/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb b/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb index 158d0bc02ed..c9fdbe95d13 100644 --- a/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb +++ b/spec/migrations/schedule_merge_request_latest_merge_request_diff_id_migrations_spec.rb @@ -44,9 +44,9 @@ describe ScheduleMergeRequestLatestMergeRequestDiffIdMigrations, :migration, :si Timecop.freeze do migrate! - expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, merge_request_1.id, merge_request_1.id) - expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, merge_request_2.id, merge_request_2.id) - expect(described_class::MIGRATION).to be_scheduled_migration(15.minutes, merge_request_4.id, merge_request_4.id) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, merge_request_1.id, merge_request_1.id) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, merge_request_2.id, merge_request_2.id) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(15.minutes, merge_request_4.id, merge_request_4.id) expect(BackgroundMigrationWorker.jobs.size).to eq 3 end end diff --git a/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb b/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb index 97e089c5cb8..2e6b2cff0ab 100644 --- a/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb +++ b/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb @@ -12,10 +12,10 @@ describe SchedulePopulateMergeRequestMetricsWithEventsData, :migration, :sidekiq migrate! expect(described_class::MIGRATION) - .to be_scheduled_migration(10.minutes, mrs.first.id, mrs.second.id) + .to be_scheduled_delayed_migration(10.minutes, mrs.first.id, mrs.second.id) expect(described_class::MIGRATION) - .to be_scheduled_migration(20.minutes, mrs.third.id, mrs.third.id) + .to be_scheduled_delayed_migration(20.minutes, mrs.third.id, mrs.third.id) expect(BackgroundMigrationWorker.jobs.size).to eq(2) end diff --git a/spec/migrations/track_untracked_uploads_spec.rb b/spec/migrations/track_untracked_uploads_spec.rb index 7fe7a140e2f..fe4d5b8a279 100644 --- a/spec/migrations/track_untracked_uploads_spec.rb +++ b/spec/migrations/track_untracked_uploads_spec.rb @@ -4,18 +4,6 @@ require Rails.root.join('db', 'post_migrate', '20171103140253_track_untracked_up describe TrackUntrackedUploads, :migration, :sidekiq do include TrackUntrackedUploadsHelpers - matcher :be_scheduled_migration do - match do |migration| - BackgroundMigrationWorker.jobs.any? do |job| - job['args'] == [migration] - end - end - - failure_message do |migration| - "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!" - end - end - it 'correctly schedules the follow-up background migration' do Sidekiq::Testing.fake! do migrate! diff --git a/spec/support/background_migrations_matchers.rb b/spec/support/background_migrations_matchers.rb index 423c0e4cefc..f4127efc6ae 100644 --- a/spec/support/background_migrations_matchers.rb +++ b/spec/support/background_migrations_matchers.rb @@ -1,4 +1,4 @@ -RSpec::Matchers.define :be_scheduled_migration do |delay, *expected| +RSpec::Matchers.define :be_scheduled_delayed_migration do |delay, *expected| match do |migration| BackgroundMigrationWorker.jobs.any? do |job| job['args'] == [migration, expected] && @@ -11,3 +11,16 @@ RSpec::Matchers.define :be_scheduled_migration do |delay, *expected| 'not scheduled in expected time!' end end + +RSpec::Matchers.define :be_scheduled_migration do |*expected| + match do |migration| + BackgroundMigrationWorker.jobs.any? do |job| + args = job['args'].size == 1 ? [BackgroundMigrationWorker.jobs[0]['args'][0], []] : job['args'] + args == [migration, expected] + end + end + + failure_message do |migration| + "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!" + end +end From c4600805b58e4fc705a6733cd92fa5518a267e9f Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 5 Jan 2018 14:08:17 +0100 Subject: [PATCH 56/61] Update redis-rack to 2.0.4 Up until version 2.0.3 redis-rack included a "rake" binary that would overwrite/hijack the one provided by Rake itself. Unfortunately the binary provided by redis-rack would produce errors in many cases. See https://github.com/redis-store/redis-rack/pull/34 for more info. --- Gemfile.lock | 2 +- changelogs/unreleased/update-redis-rack.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/update-redis-rack.yml diff --git a/Gemfile.lock b/Gemfile.lock index c510a6da2d7..2a81c81b0f8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -718,7 +718,7 @@ GEM redis-store (>= 1.3, < 2) redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) - redis-rack (2.0.3) + redis-rack (2.0.4) rack (>= 1.5, < 3) redis-store (>= 1.2, < 2) redis-rails (5.0.2) diff --git a/changelogs/unreleased/update-redis-rack.yml b/changelogs/unreleased/update-redis-rack.yml new file mode 100644 index 00000000000..6e2e6e203b8 --- /dev/null +++ b/changelogs/unreleased/update-redis-rack.yml @@ -0,0 +1,5 @@ +--- +title: Update redis-rack to 2.0.4 +merge_request: +author: +type: other From 55980c7eca8e82e306fb3b8ade1f4a5b68a60e9f Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 5 Jan 2018 11:24:24 -0200 Subject: [PATCH 57/61] Remove EE only sections from docs --- doc/api/boards.md | 85 ----------------------------------------------- 1 file changed, 85 deletions(-) diff --git a/doc/api/boards.md b/doc/api/boards.md index a5f455e1c43..246de50323e 100644 --- a/doc/api/boards.md +++ b/doc/api/boards.md @@ -141,91 +141,6 @@ Example response: } ``` -## Create a board (EES-Only) - -Creates a board. - -``` -POST /projects/:id/boards -``` - -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `name` | string | yes | The name of the new board | - -```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards?name=newboard -``` - -Example response: - -```json - { - "id": 1, - "project": { - "id": 5, - "name": "Diaspora Project Site", - "name_with_namespace": "Diaspora / Diaspora Project Site", - "path": "diaspora-project-site", - "path_with_namespace": "diaspora/diaspora-project-site", - "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", - "web_url": "http://example.com/diaspora/diaspora-project-site" - }, - "name": "newboard", - "milestone": { - "id": 12 - "title": "10.0" - }, - "lists" : [ - { - "id" : 1, - "label" : { - "name" : "Testing", - "color" : "#F0AD4E", - "description" : null - }, - "position" : 1 - }, - { - "id" : 2, - "label" : { - "name" : "Ready", - "color" : "#FF0000", - "description" : null - }, - "position" : 2 - }, - { - "id" : 3, - "label" : { - "name" : "Production", - "color" : "#FF5F00", - "description" : null - }, - "position" : 3 - } - ] - } -``` - -## Delete a board (EES-Only) - -Deletes a board. - -``` -DELETE /projects/:id/boards/:board_id -``` - -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `board_id` | integer | yes | The ID of a board | - -```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1 -``` - ## List board lists Get a list of the board's lists. From 8bdc6c74e82445048d66f6bf4be9dd0db7dc4737 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 5 Jan 2018 15:32:41 +0100 Subject: [PATCH 58/61] Rephrase paragraph about e2e tests in merge requests in docs --- doc/development/testing_guide/end_to_end_tests.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/doc/development/testing_guide/end_to_end_tests.md b/doc/development/testing_guide/end_to_end_tests.md index 30efe3e3b76..abe5b06e0f0 100644 --- a/doc/development/testing_guide/end_to_end_tests.md +++ b/doc/development/testing_guide/end_to_end_tests.md @@ -20,15 +20,13 @@ You can find these nightly pipelines at [GitLab QA pipelines page][gitlab-qa-pip ### Testing code in merge requests -It is also possible to trigger build of GitLab packages and then pass these -package to GitLab QA to run tests in a [pipeline][gitlab-qa-pipelines]. +It is possible to run end-to-end tests (eventually being run within a +[GitLab QA pipeline][gitlab-qa-pipelines]) for a merge request by triggering +the `package-qa` manual action, that should be present in a merge request +widget. -Developers can trigger the `package-qa` manual action, that should be present in -the merge request widget. - -It is also possible to trigger Gitlab QA pipeline from merge requests in -Omnibus GitLab project. You can find a manual action that is similar to -`package-qa`, mentioned above, in your Omnibus-related merge requests as well. +Mmanual action that starts end-to-end tests is also available in merge requests +in Omnibus GitLab project. Below you can read more about how to use it and how does it work. From 288b276077987bc77f191d2cb93eb2f764c5c1ef Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 20 Dec 2017 14:48:09 +0100 Subject: [PATCH 59/61] Copy Mermaid graphs as GFM --- .../javascripts/behaviors/copy_as_gfm.js | 12 +++ app/assets/javascripts/render_mermaid.js | 20 +++- lib/banzai/filter/mermaid_filter.rb | 11 +-- spec/features/copy_as_gfm_spec.rb | 96 +++++++++++++++++++ spec/features/markdown_spec.rb | 2 +- spec/lib/banzai/filter/mermaid_filter_spec.rb | 4 +- 6 files changed, 131 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/behaviors/copy_as_gfm.js b/app/assets/javascripts/behaviors/copy_as_gfm.js index e7dc4ef8304..c6eca72c51b 100644 --- a/app/assets/javascripts/behaviors/copy_as_gfm.js +++ b/app/assets/javascripts/behaviors/copy_as_gfm.js @@ -74,6 +74,18 @@ const gfmRules = { return `![${el.dataset.title}](${el.getAttribute('src')})`; }, }, + MermaidFilter: { + 'svg.mermaid'(el, text) { + const sourceEl = el.querySelector('text.source'); + if (!sourceEl) return false; + + return `\`\`\`mermaid\n${CopyAsGFM.nodeToGFM(sourceEl)}\n\`\`\``; + }, + 'svg.mermaid style, svg.mermaid g'(el, text) { + // We don't want to include the content of these elements in the copied text. + return ''; + }, + }, MathFilter: { 'pre.code.math[data-math-style=display]'(el, text) { return `\`\`\`math\n${text.trim()}\n\`\`\``; diff --git a/app/assets/javascripts/render_mermaid.js b/app/assets/javascripts/render_mermaid.js index 41942c04a4e..b7cde6fb092 100644 --- a/app/assets/javascripts/render_mermaid.js +++ b/app/assets/javascripts/render_mermaid.js @@ -24,7 +24,25 @@ export default function renderMermaid($els) { }); $els.each((i, el) => { - mermaid.init(undefined, el); + const source = el.textContent; + + mermaid.init(undefined, el, (id) => { + const svg = document.getElementById(id); + + svg.classList.add('mermaid'); + + // pre > code > svg + svg.closest('pre').replaceWith(svg); + + // We need to add the original source into the DOM to allow Copy-as-GFM + // to access it. + const sourceEl = document.createElement('text'); + sourceEl.classList.add('source'); + sourceEl.setAttribute('display', 'none'); + sourceEl.textContent = source; + + svg.appendChild(sourceEl); + }); }); }).catch((err) => { Flash(`Can't load mermaid module: ${err}`); diff --git a/lib/banzai/filter/mermaid_filter.rb b/lib/banzai/filter/mermaid_filter.rb index b545b947a2c..65c131e08d9 100644 --- a/lib/banzai/filter/mermaid_filter.rb +++ b/lib/banzai/filter/mermaid_filter.rb @@ -2,16 +2,7 @@ module Banzai module Filter class MermaidFilter < HTML::Pipeline::Filter def call - doc.css('pre[lang="mermaid"]').add_class('mermaid') - doc.css('pre[lang="mermaid"]').add_class('js-render-mermaid') - - # The `` blocks are added in the lib/banzai/filter/syntax_highlight_filter.rb - # We want to keep context and consistency, so we the blocks are added for all filters. - # Details: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15107/diffs?diff_id=7962900#note_45495859 - doc.css('pre[lang="mermaid"]').each do |pre| - document = pre.at('code') - document.replace(document.content) - end + doc.css('pre[lang="mermaid"] > code').add_class('js-render-mermaid') doc end diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb index 1fcb8d5bc67..d8f1a919522 100644 --- a/spec/features/copy_as_gfm_spec.rb +++ b/spec/features/copy_as_gfm_spec.rb @@ -284,6 +284,102 @@ describe 'Copy as GFM', :js do expect(output_gfm.strip).to eq(gfm.strip) end + verify( + 'MermaidFilter: mermaid as converted from GFM to HTML', + + <<-GFM.strip_heredoc + ```mermaid + graph TD; + A-->B; + ``` + GFM + ) + + aggregate_failures('MermaidFilter: mermaid as transformed from HTML to SVG') do + gfm = <<-GFM.strip_heredoc + ```mermaid + graph TD; + A-->B; + ``` + GFM + + html = <<-HTML.strip_heredoc + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+
+ + + + + + +
A
+
+
+
+
+ + + + + + +
B
+
+
+
+
+
+
+
+ graph TD; + A-->B; + +
+ HTML + + output_gfm = html_to_gfm(html) + expect(output_gfm.strip).to eq(gfm.strip) + end + verify( 'SanitizationFilter', diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index e285befc66f..a2b78a5e021 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -71,7 +71,7 @@ describe 'GitLab Markdown' do it 'parses mermaid code block' do aggregate_failures do - expect(doc).to have_selector('pre.code.js-render-mermaid') + expect(doc).to have_selector('pre[lang=mermaid] > code.js-render-mermaid') end end diff --git a/spec/lib/banzai/filter/mermaid_filter_spec.rb b/spec/lib/banzai/filter/mermaid_filter_spec.rb index 532d25e121d..f6474c8936d 100644 --- a/spec/lib/banzai/filter/mermaid_filter_spec.rb +++ b/spec/lib/banzai/filter/mermaid_filter_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' describe Banzai::Filter::MermaidFilter do include FilterSpecHelper - it 'adds `js-render-mermaid` class to the `pre` tag' do + it 'adds `js-render-mermaid` class to the `code` tag' do doc = filter("
graph TD;\n  A-->B;\n
") - result = doc.xpath('descendant-or-self::pre').first + result = doc.css('code').first expect(result[:class]).to include('js-render-mermaid') end From 34b9cc9674554155af49c9a7fe60aaeba72bb23d Mon Sep 17 00:00:00 2001 From: Brent Greeff Date: Fri, 5 Jan 2018 15:21:53 +0000 Subject: [PATCH 60/61] API: get participants from merge_requests & issues --- ...86-get-participants-from-issues-mr-api.yml | 5 +++ doc/api/issues.md | 39 +++++++++++++++++++ doc/api/merge_requests.md | 35 +++++++++++++++++ lib/api/issues.rb | 13 +++++++ lib/api/merge_requests.rb | 10 +++++ spec/requests/api/issues_spec.rb | 12 ++++++ spec/requests/api/merge_requests_spec.rb | 6 +++ .../api/issuable_participants_examples.rb | 29 ++++++++++++++ 8 files changed, 149 insertions(+) create mode 100644 changelogs/unreleased/issues-40986-get-participants-from-issues-mr-api.yml create mode 100644 spec/support/shared_examples/requests/api/issuable_participants_examples.rb diff --git a/changelogs/unreleased/issues-40986-get-participants-from-issues-mr-api.yml b/changelogs/unreleased/issues-40986-get-participants-from-issues-mr-api.yml new file mode 100644 index 00000000000..4cac87b0cdb --- /dev/null +++ b/changelogs/unreleased/issues-40986-get-participants-from-issues-mr-api.yml @@ -0,0 +1,5 @@ +--- +title: 'API: get participants from merge_requests & issues' +merge_request: 16187 +author: Brent Greeff +type: added diff --git a/doc/api/issues.md b/doc/api/issues.md index d2fefbe68aa..da89db17cd9 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -1124,6 +1124,45 @@ Example response: ``` +## Participants on issues + +``` +GET /projects/:id/issues/:issue_iid/participants +``` + +| Attribute | Type | Required | Description | +|-------------|---------|----------|--------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `issue_iid` | integer | yes | The internal ID of a project's issue | + +```bash +curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/participants +``` + +Example response: + +```json +[ + { + "id": 1, + "name": "John Doe1", + "username": "user1", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon", + "web_url": "http://localhost/user1" + }, + { + "id": 5, + "name": "John Doe5", + "username": "user5", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/4aea8cf834ed91844a2da4ff7ae6b491?s=80&d=identicon", + "web_url": "http://localhost/user5" + } +] +``` + + ## Comments on issues Comments are done via the [notes](notes.md) resource. diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 4d3592e8f71..24afcef9a31 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -308,6 +308,41 @@ Parameters: } ``` +## Get single MR participants + +Get a list of merge request participants. + +``` +GET /projects/:id/merge_requests/:merge_request_iid/participants +``` + +Parameters: + +- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user +- `merge_request_iid` (required) - The internal ID of the merge request + + +```json +[ + { + "id": 1, + "name": "John Doe1", + "username": "user1", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon", + "web_url": "http://localhost/user1" + }, + { + "id": 2, + "name": "John Doe2", + "username": "user2", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/10fc7f102be8de7657fb4d80898bbfe3?s=80&d=identicon", + "web_url": "http://localhost/user2" + }, +] +``` + ## Get single MR commits Get a list of merge request commits. diff --git a/lib/api/issues.rb b/lib/api/issues.rb index b29c5848aef..7aa10631d53 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -277,6 +277,19 @@ module API present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project end + desc 'List participants for an issue' do + success Entities::UserBasic + end + params do + requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue' + end + get ':id/issues/:issue_iid/participants' do + issue = find_project_issue(params[:issue_iid]) + participants = ::Kaminari.paginate_array(issue.participants) + + present paginate(participants), with: Entities::UserBasic, current_user: current_user, project: user_project + end + desc 'Get the user agent details for an issue' do success Entities::UserAgentDetail end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 02f2b75ab9d..8f665b39fa8 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -185,6 +185,16 @@ module API present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project end + desc 'Get the participants of a merge request' do + success Entities::UserBasic + end + get ':id/merge_requests/:merge_request_iid/participants' do + merge_request = find_merge_request_with_access(params[:merge_request_iid]) + participants = ::Kaminari.paginate_array(merge_request.participants) + + present paginate(participants), with: Entities::UserBasic + end + desc 'Get the commits of a merge request' do success Entities::Commit end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 00d9c795619..320217f2032 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -1582,4 +1582,16 @@ describe API::Issues, :mailer do expect(json_response).to be_an Array expect(json_response.length).to eq(size) if size end + + describe 'GET projects/:id/issues/:issue_iid/participants' do + it_behaves_like 'issuable participants endpoint' do + let(:entity) { issue } + end + + it 'returns 404 if the issue is confidential' do + post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/participants", non_member) + + expect(response).to have_gitlab_http_status(404) + end + end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index ef3f610740d..0c9fbb1f187 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -500,6 +500,12 @@ describe API::MergeRequests do end end + describe 'GET /projects/:id/merge_requests/:merge_request_iid/participants' do + it_behaves_like 'issuable participants endpoint' do + let(:entity) { merge_request } + end + end + describe 'GET /projects/:id/merge_requests/:merge_request_iid/commits' do it 'returns a 200 when merge request is valid' do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/commits", user) diff --git a/spec/support/shared_examples/requests/api/issuable_participants_examples.rb b/spec/support/shared_examples/requests/api/issuable_participants_examples.rb new file mode 100644 index 00000000000..96d59e0c472 --- /dev/null +++ b/spec/support/shared_examples/requests/api/issuable_participants_examples.rb @@ -0,0 +1,29 @@ +shared_examples 'issuable participants endpoint' do + let(:area) { entity.class.name.underscore.pluralize } + + it 'returns participants' do + get api("/projects/#{project.id}/#{area}/#{entity.iid}/participants", user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(entity.participants.size) + + last_participant = entity.participants.last + expect(json_response.last['id']).to eq(last_participant.id) + expect(json_response.last['name']).to eq(last_participant.name) + expect(json_response.last['username']).to eq(last_participant.username) + end + + it 'returns a 404 when iid does not exist' do + get api("/projects/#{project.id}/#{area}/999/participants", user) + + expect(response).to have_gitlab_http_status(404) + end + + it 'returns a 404 when id is used instead of iid' do + get api("/projects/#{project.id}/#{area}/#{entity.id}/participants", user) + + expect(response).to have_gitlab_http_status(404) + end +end From 33c5630b02a783a749cc0bf63474f643652cdeeb Mon Sep 17 00:00:00 2001 From: "Lin Jen-Shin (godfat)" Date: Fri, 5 Jan 2018 16:52:06 +0000 Subject: [PATCH 61/61] Use --left-right and --max-count for counting diverging commits --- app/helpers/branches_helper.rb | 8 ++ app/models/repository.rb | 12 +-- app/views/projects/branches/_branch.html.haml | 8 +- .../40622-use-left-right-and-max-count.yml | 6 ++ lib/gitlab/git/repository.rb | 75 +++++++++++++++++-- spec/lib/gitlab/git/repository_spec.rb | 40 +++++++++- spec/models/repository_spec.rb | 9 +++ 7 files changed, 141 insertions(+), 17 deletions(-) create mode 100644 changelogs/unreleased/40622-use-left-right-and-max-count.yml diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index 686437fc99a..2641a98e29e 100644 --- a/app/helpers/branches_helper.rb +++ b/app/helpers/branches_helper.rb @@ -23,4 +23,12 @@ module BranchesHelper def protected_branch?(project, branch) ProtectedBranch.protected?(project, branch.name) end + + def diverging_count_label(count) + if count >= Repository::MAX_DIVERGING_COUNT + "#{Repository::MAX_DIVERGING_COUNT - 1}+" + else + count.to_s + end + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 4bedcbfb6a2..7b8f5794a87 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -4,6 +4,7 @@ class Repository REF_MERGE_REQUEST = 'merge-requests'.freeze REF_KEEP_AROUND = 'keep-around'.freeze REF_ENVIRONMENTS = 'environments'.freeze + MAX_DIVERGING_COUNT = 1000 RESERVED_REFS_NAMES = %W[ heads @@ -278,11 +279,12 @@ class Repository cache.fetch(:"diverging_commit_counts_#{branch.name}") do # Rugged seems to throw a `ReferenceError` when given branch_names rather # than SHA-1 hashes - number_commits_behind = raw_repository - .count_commits_between(branch.dereferenced_target.sha, root_ref_hash) - - number_commits_ahead = raw_repository - .count_commits_between(root_ref_hash, branch.dereferenced_target.sha) + number_commits_behind, number_commits_ahead = + raw_repository.count_commits_between( + root_ref_hash, + branch.dereferenced_target.sha, + left_right: true, + max_count: MAX_DIVERGING_COUNT) { behind: number_commits_behind, ahead: number_commits_ahead } end diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index acf67b83890..1da0e865a41 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -66,16 +66,16 @@ = icon("trash-o") - if branch.name != @repository.root_ref - .divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: number_commits_behind, + .divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind), default_branch: @repository.root_ref, - number_commits_ahead: number_commits_ahead } } + number_commits_ahead: diverging_count_label(number_commits_ahead) } } .graph-side .bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" } - %span.count.count-behind= number_commits_behind + %span.count.count-behind= diverging_count_label(number_commits_behind) .graph-separator .graph-side .bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" } - %span.count.count-ahead= number_commits_ahead + %span.count.count-ahead= diverging_count_label(number_commits_ahead) - if commit diff --git a/changelogs/unreleased/40622-use-left-right-and-max-count.yml b/changelogs/unreleased/40622-use-left-right-and-max-count.yml new file mode 100644 index 00000000000..c4c8f271cbe --- /dev/null +++ b/changelogs/unreleased/40622-use-left-right-and-max-count.yml @@ -0,0 +1,6 @@ +--- +title: Improve the performance for counting diverging commits. Show 999+ + if it is more than 1000 commits +merge_request: 15963 +author: +type: performance diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 17c05c44d7e..e8b1788e140 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -498,11 +498,13 @@ module Gitlab end def count_commits(options) + count_commits_options = process_count_commits_options(options) + gitaly_migrate(:count_commits) do |is_enabled| if is_enabled - count_commits_by_gitaly(options) + count_commits_by_gitaly(count_commits_options) else - count_commits_by_shelling_out(options) + count_commits_by_shelling_out(count_commits_options) end end end @@ -540,8 +542,8 @@ module Gitlab end # Counts the amount of commits between `from` and `to`. - def count_commits_between(from, to) - count_commits(ref: "#{from}..#{to}") + def count_commits_between(from, to, options = {}) + count_commits(from: from, to: to, **options) end # Returns the SHA of the most recent common ancestor of +from+ and +to+ @@ -1468,6 +1470,26 @@ module Gitlab end end + def process_count_commits_options(options) + if options[:from] || options[:to] + ref = + if options[:left_right] # Compare with merge-base for left-right + "#{options[:from]}...#{options[:to]}" + else + "#{options[:from]}..#{options[:to]}" + end + + options.merge(ref: ref) + + elsif options[:ref] && options[:left_right] + from, to = options[:ref].match(/\A([^\.]*)\.{2,3}([^\.]*)\z/)[1..2] + + options.merge(from: from, to: to) + else + options + end + end + def log_using_shell?(options) options[:path].present? || options[:disable_walk] || @@ -1690,20 +1712,59 @@ module Gitlab end def count_commits_by_gitaly(options) - gitaly_commit_client.commit_count(options[:ref], options) + if options[:left_right] + from = options[:from] + to = options[:to] + + right_count = gitaly_commit_client + .commit_count("#{from}..#{to}", options) + left_count = gitaly_commit_client + .commit_count("#{to}..#{from}", options) + + [left_count, right_count] + else + gitaly_commit_client.commit_count(options[:ref], options) + end end def count_commits_by_shelling_out(options) + cmd = count_commits_shelling_command(options) + + raw_output = IO.popen(cmd) { |io| io.read } + + process_count_commits_raw_output(raw_output, options) + end + + def count_commits_shelling_command(options) cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} rev-list] cmd << "--after=#{options[:after].iso8601}" if options[:after] cmd << "--before=#{options[:before].iso8601}" if options[:before] cmd << "--max-count=#{options[:max_count]}" if options[:max_count] + cmd << "--left-right" if options[:left_right] cmd += %W[--count #{options[:ref]}] cmd += %W[-- #{options[:path]}] if options[:path].present? + cmd + end - raw_output = IO.popen(cmd) { |io| io.read } + def process_count_commits_raw_output(raw_output, options) + if options[:left_right] + result = raw_output.scan(/\d+/).map(&:to_i) - raw_output.to_i + if result.sum != options[:max_count] + result + else # Reaching max count, right is not accurate + right_option = + process_count_commits_options(options + .except(:left_right, :from, :to) + .merge(ref: options[:to])) + + right = count_commits_by_shelling_out(right_option) + + [result.first, right] # left should be accurate in the first call + end + else + raw_output.to_i + end end def gitaly_ls_files(ref) diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index faccc2c8e00..f94234f6010 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1030,14 +1030,52 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + context 'with max_count' do + it 'returns the number of commits with path ' do + options = { ref: 'master', max_count: 5 } + + expect(repository.count_commits(options)).to eq(5) + end + end + context 'with path' do it 'returns the number of commits with path ' do - options = { ref: 'master', path: "encoding" } + options = { ref: 'master', path: 'encoding' } expect(repository.count_commits(options)).to eq(2) end end + context 'with option :from and option :to' do + it 'returns the number of commits ahead for fix-mode..fix-blob-path' do + options = { from: 'fix-mode', to: 'fix-blob-path' } + + expect(repository.count_commits(options)).to eq(2) + end + + it 'returns the number of commits ahead for fix-blob-path..fix-mode' do + options = { from: 'fix-blob-path', to: 'fix-mode' } + + expect(repository.count_commits(options)).to eq(1) + end + + context 'with option :left_right' do + it 'returns the number of commits for fix-mode...fix-blob-path' do + options = { from: 'fix-mode', to: 'fix-blob-path', left_right: true } + + expect(repository.count_commits(options)).to eq([1, 2]) + end + + context 'with max_count' do + it 'returns the number of commits with path ' do + options = { from: 'fix-mode', to: 'fix-blob-path', left_right: true, max_count: 1 } + + expect(repository.count_commits(options)).to eq([1, 1]) + end + end + end + end + context 'with max_count' do it 'returns the number of commits up to the passed limit' do options = { ref: 'master', max_count: 10, after: Time.iso8601('2013-03-03T20:15:01+00:00') } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 9a68ae086ea..48a75c9885b 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -2215,6 +2215,15 @@ describe Repository do end end + describe '#diverging_commit_counts' do + it 'returns the commit counts behind and ahead of default branch' do + result = repository.diverging_commit_counts( + repository.find_branch('fix')) + + expect(result).to eq(behind: 29, ahead: 2) + end + end + describe '#cache_method_output', :use_clean_rails_memory_store_caching do let(:fallback) { 10 }