From 249487f3322a45111c1c08118093403b621c30df Mon Sep 17 00:00:00 2001 From: Ruben Davila Date: Tue, 20 Sep 2016 18:42:31 -0500 Subject: [PATCH 01/49] Update VERSION to 8.12.0-rc6-ee --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 434d18115f7499..5b12cc2e3d94f1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.12.0-rc5-ee +8.12.0-rc6-ee -- GitLab From bcd02e0d75d7a934ee18d11643c1eaca434cf17b Mon Sep 17 00:00:00 2001 From: Ruben Davila Date: Tue, 20 Sep 2016 18:44:31 -0500 Subject: [PATCH 02/49] Update VERSION to 8.12.0-rc6 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 0fee7edd7155f5..2087f5da6c894d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.12.0-rc5 +8.12.0-rc6 -- GitLab From f60c82d0b53eb342efa6ea857deca0e191bb588b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 20 Sep 2016 17:37:37 +0200 Subject: [PATCH 03/49] Merge branch 'JonTheNiceGuy/gitlab-ce-Ubuntu-16.04-Package' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See gitlab-org/gitlab-ce!6247. Signed-off-by: Rémy Coutable --- .pkgr.yml | 12 ++++++++++++ CHANGELOG | 2 ++ 2 files changed, 14 insertions(+) diff --git a/.pkgr.yml b/.pkgr.yml index 8fc9fddf8f79d5..10bcd7bd4bdf09 100644 --- a/.pkgr.yml +++ b/.pkgr.yml @@ -3,6 +3,8 @@ group: git services: - postgres before_precompile: ./bin/pkgr_before_precompile.sh +env: + - SKIP_STORAGE_VALIDATION=true targets: debian-7: &wheezy build_dependencies: @@ -25,6 +27,16 @@ targets: - libicu52 - libpcre3 - git + ubuntu-16.04: + build_dependencies: + - libkrb5-dev + - libicu-dev + - cmake + - pkg-config + dependencies: + - libicu55 + - libpcre3 + - git centos-6: build_dependencies: - krb5-devel diff --git a/CHANGELOG b/CHANGELOG index 088f9ee44f1975..a3b427828066f2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,7 @@ v 8.12.0 (unreleased) - Prune events older than 12 months. (ritave) - Prepend blank line to `Closes` message on merge request linked to issue (lukehowell) - Fix issues/merge-request templates dropdown for forked projects + - Amends the packager.io configuration file to create a build for Ubuntu 16.04. !6247 (Jon "The Nice Guy" Spriggs) - Filter tags by name !6121 - Update gitlab shell secret file also when it is empty. !3774 (glensc) - Give project selection dropdowns responsive width, make non-wrapping. @@ -94,6 +95,7 @@ v 8.12.0 (unreleased) - Add hover state to todos !5361 (winniehell) - Fix icon alignment of star and fork buttons !5451 (winniehell) - Fix alignment of icon buttons !5887 (winniehell) + - Added Ubuntu 16.04 support for packager.io (JonTheNiceGuy) - Fix markdown help references (ClemMakesApps) - Add last commit time to repo view (ClemMakesApps) - Fix accessibility and visibility of project list dropdown button !6140 -- GitLab From 182de5941a7493c584c32b6abd7278b263bcf77e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 21 Sep 2016 12:21:59 +0200 Subject: [PATCH 04/49] Merge branch 'vitalybaev/gitlab-ce-build-page-sidebar-coverage-horizontal-padding' See !6196. --- CHANGELOG | 1 + app/assets/stylesheets/pages/builds.scss | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index a3b427828066f2..b029fdddeec2a0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -40,6 +40,7 @@ v 8.12.0 (unreleased) - Escape search term before passing it to Regexp.new !6241 (winniehell) - Fix pinned sidebar behavior in smaller viewports !6169 - Fix file permissions change when updating a file on the Gitlab UI !5979 + - Added horizontal padding on build page sidebar on code coverage block. !6196 (Vitaly Baev) - Change merge_error column from string to text type - Reduce contributions calendar data payload (ClemMakesApps) - Replace contributions calendar timezone payload with dates (ClemMakesApps) diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index c879074c7fee48..a5a260d4c8fc7f 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -109,6 +109,10 @@ width: 100%; } + .block-first { + padding: 5px 16px 11px; + } + .js-build-variable { color: $code-color; } -- GitLab From d0f5d9a1b5882aa17866860b1fa4f89845f45487 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 21 Sep 2016 18:19:44 +0000 Subject: [PATCH 05/49] Merge branch 'zj-default-setting-features' into 'master' Add default values for ProjectFeature See merge request !6447 --- app/models/project_feature.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 9c602c582bd24b..8c9534c3565f95 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -22,6 +22,12 @@ class ProjectFeature < ActiveRecord::Base belongs_to :project + default_value_for :builds_access_level, value: ENABLED, allows_nil: false + default_value_for :issues_access_level, value: ENABLED, allows_nil: false + default_value_for :merge_requests_access_level, value: ENABLED, allows_nil: false + default_value_for :snippets_access_level, value: ENABLED, allows_nil: false + default_value_for :wiki_access_level, value: ENABLED, allows_nil: false + def feature_available?(feature, user) raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature) -- GitLab From b9e02703c834a7df291294b35acb466829b0ce44 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 21 Sep 2016 15:58:12 +0000 Subject: [PATCH 06/49] Merge branch 'rs-revert-rubocop-rspec-1-7' into 'master' Revert "Merge branch 'rs-update-rubocop-rspec' into 'master'" Reverts https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6357 due to rubocop-rspec dropping support for Ruby 2.1. See https://github.com/backus/rubocop-rspec/pull/131 See merge request !6444 --- .rubocop.yml | 77 +++++++--------------------------------------------- Gemfile | 2 +- Gemfile.lock | 6 ++-- 3 files changed, 14 insertions(+), 71 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index b054675d6775b7..5bd31ccf32915f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -767,33 +767,26 @@ Rails/ScopeArgs: RSpec/AnyInstance: Enabled: false -# Check for expectations where `be(...)` can replace `eql(...)`. -RSpec/BeEql: - Enabled: false - -# Check that the first argument to the top level describe is a constant. +# Check that the first argument to the top level describe is the tested class or +# module. RSpec/DescribeClass: Enabled: false -# Checks that tests use `described_class`. -RSpec/DescribedClass: - Enabled: false - -# Checks that the second argument to `describe` specifies a method. +# Use `described_class` for tested class / module. RSpec/DescribeMethod: Enabled: false -# Checks if an example group does not include any tests. -RSpec/EmptyExampleGroup: +# Checks that the second argument to top level describe is the tested method +# name. +RSpec/DescribedClass: Enabled: false - CustomIncludeMethods: [] -# Checks for long examples. +# Checks for long example. RSpec/ExampleLength: Enabled: false Max: 5 -# Checks that example descriptions do not start with "should". +# Do not use should when describing your tests. RSpec/ExampleWording: Enabled: false CustomTransform: @@ -802,10 +795,6 @@ RSpec/ExampleWording: not: does not IgnoredWords: [] -# Checks for `expect(...)` calls containing literal values. -RSpec/ExpectActual: - Enabled: false - # Checks the file and folder naming of the spec file. RSpec/FilePath: Enabled: false @@ -817,65 +806,19 @@ RSpec/FilePath: RSpec/Focus: Enabled: true -# Checks the arguments passed to `before`, `around`, and `after`. -RSpec/HookArgument: - Enabled: false - EnforcedStyle: implicit - -# Check that a consistent implict expectation style is used. -# TODO (rspeicher): Available in rubocop-rspec 1.8.0 -# RSpec/ImplicitExpect: -# Enabled: true -# EnforcedStyle: is_expected - # Checks for the usage of instance variables. RSpec/InstanceVariable: Enabled: false -# Checks for `subject` definitions that come after `let` definitions. -RSpec/LeadingSubject: - Enabled: false - -# Checks unreferenced `let!` calls being used for test setup. -RSpec/LetSetup: - Enabled: false - -# Check that chains of messages are not being stubbed. -RSpec/MessageChain: - Enabled: false - -# Checks for consistent message expectation style. -RSpec/MessageExpectation: - Enabled: false - EnforcedStyle: allow - -# Checks for multiple top level describes. +# Checks for multiple top-level describes. RSpec/MultipleDescribes: Enabled: false -# Checks if examples contain too many `expect` calls. -RSpec/MultipleExpectations: - Enabled: false - Max: 1 - -# Checks for explicitly referenced test subjects. -RSpec/NamedSubject: - Enabled: false - -# Checks for nested example groups. -RSpec/NestedGroups: - Enabled: false - MaxNesting: 2 - -# Checks for consistent method usage for negating expectations. +# Enforces the usage of the same method on all negative message expectations. RSpec/NotToNot: EnforcedStyle: not_to Enabled: true -# Checks for stubbed test subjects. -RSpec/SubjectStub: - Enabled: false - # Prefer using verifying doubles over normal doubles. RSpec/VerifiedDoubles: Enabled: false diff --git a/Gemfile b/Gemfile index cb1c619cc64f30..9545b844835766 100644 --- a/Gemfile +++ b/Gemfile @@ -299,7 +299,7 @@ group :development, :test do gem 'spring-commands-teaspoon', '~> 0.0.2' gem 'rubocop', '~> 0.42.0', require: false - gem 'rubocop-rspec', '~> 1.7.0', require: false + gem 'rubocop-rspec', '~> 1.5.0', require: false gem 'scss_lint', '~> 0.47.0', require: false gem 'haml_lint', '~> 0.18.2', require: false gem 'simplecov', '0.12.0', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 8e26429df14e1e..7044c476b54c02 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -626,8 +626,8 @@ GEM rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) - rubocop-rspec (1.7.0) - rubocop (>= 0.42.0) + rubocop-rspec (1.5.0) + rubocop (>= 0.40.0) ruby-fogbugz (0.2.1) crack (~> 0.4) ruby-prof (0.15.9) @@ -947,7 +947,7 @@ DEPENDENCIES rspec-rails (~> 3.5.0) rspec-retry (~> 0.4.5) rubocop (~> 0.42.0) - rubocop-rspec (~> 1.7.0) + rubocop-rspec (~> 1.5.0) ruby-fogbugz (~> 0.2.1) ruby-prof (~> 0.15.9) sanitize (~> 2.0) -- GitLab From 84e6b80bd680598093433c92a316a2756fac1711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 21 Sep 2016 10:15:00 +0000 Subject: [PATCH 07/49] Merge branch 'add_spec_for_committer_hash' into 'master' Add spec covering 'committer_hash' Adds a missing spec from changes added in !5822 See merge request !6433 --- CHANGELOG | 1 + app/models/repository.rb | 2 +- lib/gitlab/git.rb | 2 ++ spec/lib/gitlab/git_spec.rb | 45 +++++++++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 spec/lib/gitlab/git_spec.rb diff --git a/CHANGELOG b/CHANGELOG index b029fdddeec2a0..5217cc95bb3961 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -29,6 +29,7 @@ v 8.12.0 (unreleased) - Instructions for enabling Git packfile bitmaps !6104 - Use Search::GlobalService.new in the `GET /projects/search/:query` endpoint - Fix long comments in diffs messing with table width + - Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman) - Fix pagination on user snippets page - Run CI builds with the permissions of users !5735 - Fix sorting of issues in API diff --git a/app/models/repository.rb b/app/models/repository.rb index 772c62a4124f81..51557228ab9ffb 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -840,7 +840,7 @@ def remove_file(user, path, message, branch, author_email: nil, author_name: nil def get_committer_and_author(user, email: nil, name: nil) committer = user_to_committer(user) - author = name && email ? Gitlab::Git::committer_hash(email: email, name: name) : committer + author = Gitlab::Git::committer_hash(email: email, name: name) || committer { author: author, diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index 3ab99360206f90..3cd515e4a3ab74 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -19,6 +19,8 @@ def branch_name(ref) end def committer_hash(email:, name:) + return if email.nil? || name.nil? + { email: email, name: name, diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb new file mode 100644 index 00000000000000..219198eff60f5d --- /dev/null +++ b/spec/lib/gitlab/git_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe Gitlab::Git, lib: true do + let(:committer_email) { FFaker::Internet.email } + + # I have to remove periods from the end of the name + # This happened when the user's name had a suffix (i.e. "Sr.") + # This seems to be what git does under the hood. For example, this commit: + # + # $ git commit --author='Foo Sr. ' -m 'Where's my trailing period?' + # + # results in this: + # + # $ git show --pretty + # ... + # Author: Foo Sr + # ... + let(:committer_name) { FFaker::Name.name.chomp("\.") } + + describe 'committer_hash' do + it "returns a hash containing the given email and name" do + committer_hash = Gitlab::Git::committer_hash(email: committer_email, name: committer_name) + + expect(committer_hash[:email]).to eq(committer_email) + expect(committer_hash[:name]).to eq(committer_name) + expect(committer_hash[:time]).to be_a(Time) + end + + context 'when email is nil' do + it "returns nil" do + committer_hash = Gitlab::Git::committer_hash(email: nil, name: committer_name) + + expect(committer_hash).to be_nil + end + end + + context 'when name is nil' do + it "returns nil" do + committer_hash = Gitlab::Git::committer_hash(email: committer_email, name: nil) + + expect(committer_hash).to be_nil + end + end + end +end -- GitLab From 8ad412e07bb957ac6491775f26c5232391edd189 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 21 Sep 2016 05:05:02 +0000 Subject: [PATCH 08/49] Merge branch '21170-cycle-analytics' into 'master' Cycle Analytics: first iteration ## What does this MR do? - Implement the first iteration of the "Cycle Analytics" feature. ## What are the relevant issue numbers? - Closes #21170 ## Screenshots ![cycle_analytics_screencast.gif](/uploads/d23c3c912caa6935fd47b53ca3a56b97/cycle_analytics.gif) ## Backend Tasks - [x] Implementation - [x] Phases - [x] Issue (Tracker) - [x] Plan (Board) - [x] Code (IDE) - [x] Test (CI) - [x] Review (MR) - [x] Staging (CD) - [x] Production (Total) - [x] Make heuristics more modular - [x] Scope to project - [x] Date range (30 days, 90 days) - [x] Access restriction - [x] Test - [x] Find a better way to test these phases - [x] Phases - [x] Issue (Tracker) - [x] Plan (Board) - [x] Code (IDE) - [x] Test (CI) - [x] Review (MR) - [x] Staging (CD) - [x] Production (Total) - [x] Test for "end case happens before start case" - [x] Consolidate helper - [x] Miniboss review - [x] Performance testing with mock data - [x] Improve performance - [x] Pre-calculate "merge requests closing issues - [x] Pre-calculate everything else - [x] Test performance against 10k issues - [x] Test all pre-calculation code - [x] Ci::Pipeline -> build start/finish - [x] Ci::Pipeline#merge_requests - [x] Issue -> record default metrics after save - [x] MergeRequest -> record default metrics after save - [x] Deployment -> Update "first_deployed_to_production_at" for MR metrics - [x] Git Push -> Update "first commit mention" for issue metrics - [x] Merge request create/update/refresh -> Update "merge requests closing issues" - [x] Remove `MergeRequestsClosingIssues` when necessary - [x] Changes to unblock Fatih - [x] Add summary data - [x] `stats` should be array - [x] Let `stats` be `null` if all `stats` are null - [x] Indexes for "merge requests closing issues" - [x] Test summary data - [x] Scope everything to project - [x] Find out why tests were passing - [x] Filter should include issues/MRs which have made it to production within the range - [x] Don't create duplicate `MergeRequestsClosingIssues` - [x] Fix tests - [x] MySQL median - [x] Assign to Douwe for review - [x] Fix conflicts - [x] Implement suggestions from Yorick's review - [x] Test on PG - [x] Test on MySQL - [x] Refactor - [x] Cleanup - [x] What happens if we have no data at all? - [x] Extract common queries to methods / scopes - [x] Remove unused queries - [x] Downtime for foreign key migrations - [x] Find a way around "if issue.metrics.present?" all over the place - [x] Find a way around "if merge_request.metrics.present?" all over the place - [x] Test migrations on a fresh database - [x] MySQL - [x] Pg - [x] Access issues - While the project is public and the visibility is set to "Everyone with access", you cannot visit the cycle analytics page when signed out. - [x] CHANGELOG - [x] Implement suggestions from Douwe's review - [x] First set of comments - [x] Second set of comments - [x] Third set of comments - [x] Fourth set of comments - [x] Make sure build is green - [ ] Make issue for "polish" - [ ] EE MR See merge request !5986 --- CHANGELOG | 1 + Gemfile | 1 + Gemfile.lock | 1 + app/assets/javascripts/cycle-analytics.js.es6 | 92 +++++++ app/assets/javascripts/dispatcher.js | 8 + .../stylesheets/pages/cycle_analytics.scss | 121 +++++++++ .../projects/cycle_analytics_controller.rb | 67 +++++ app/helpers/gitlab_routing_helper.rb | 4 + app/models/ci/pipeline.rb | 20 ++ app/models/concerns/issuable.rb | 9 + app/models/cycle_analytics.rb | 97 +++++++ app/models/cycle_analytics/summary.rb | 24 ++ app/models/deployment.rb | 34 +++ app/models/environment.rb | 4 + app/models/issue.rb | 4 + app/models/issue/metrics.rb | 21 ++ app/models/merge_request.rb | 18 +- app/models/merge_request/metrics.rb | 11 + app/models/merge_requests_closing_issues.rb | 7 + app/policies/project_policy.rb | 2 + app/services/create_deployment_service.rb | 6 +- app/services/git_push_service.rb | 8 + app/services/issuable_base_service.rb | 5 + app/services/merge_requests/create_service.rb | 1 + .../merge_requests/refresh_service.rb | 9 + app/services/merge_requests/update_service.rb | 4 + app/views/layouts/nav/_project.html.haml | 2 +- .../projects/cycle_analytics/show.html.haml | 57 ++++ app/views/projects/pipelines/_head.html.haml | 6 + .../icons/_icon_cycle_analytics_splash.svg | 1 + config/routes.rb | 2 + db/fixtures/development/17_cycle_analytics.rb | 246 ++++++++++++++++++ .../20160824124900_add_table_issue_metrics.rb | 37 +++ ...5052008_add_table_merge_request_metrics.rb | 38 +++ ...21_create_merge_requests_closing_issues.rb | 34 +++ db/schema.rb | 39 ++- lib/gitlab/database/date_time.rb | 27 ++ lib/gitlab/database/median.rb | 112 ++++++++ spec/factories/deployments.rb | 3 +- spec/models/ci/pipeline_spec.rb | 55 ++++ spec/models/cycle_analytics/code_spec.rb | 42 +++ spec/models/cycle_analytics/issue_spec.rb | 50 ++++ spec/models/cycle_analytics/plan_spec.rb | 52 ++++ .../models/cycle_analytics/production_spec.rb | 54 ++++ spec/models/cycle_analytics/review_spec.rb | 35 +++ spec/models/cycle_analytics/staging_spec.rb | 64 +++++ spec/models/cycle_analytics/summary_spec.rb | 53 ++++ spec/models/cycle_analytics/test_spec.rb | 94 +++++++ spec/models/issue/metrics_spec.rb | 55 ++++ spec/models/merge_request/metrics_spec.rb | 18 ++ .../create_deployment_service_spec.rb | 79 ++++++ spec/services/git_push_service_spec.rb | 37 +++ .../merge_requests/create_service_spec.rb | 29 +++ .../merge_requests/refresh_service_spec.rb | 52 ++++ .../merge_requests/update_service_spec.rb | 37 +++ spec/services/notification_service_spec.rb | 2 + spec/support/cycle_analytics_helpers.rb | 64 +++++ .../test_generation.rb | 161 ++++++++++++ spec/support/git_helpers.rb | 9 + 59 files changed, 2220 insertions(+), 5 deletions(-) create mode 100644 app/assets/javascripts/cycle-analytics.js.es6 create mode 100644 app/assets/stylesheets/pages/cycle_analytics.scss create mode 100644 app/controllers/projects/cycle_analytics_controller.rb create mode 100644 app/models/cycle_analytics.rb create mode 100644 app/models/cycle_analytics/summary.rb create mode 100644 app/models/issue/metrics.rb create mode 100644 app/models/merge_request/metrics.rb create mode 100644 app/models/merge_requests_closing_issues.rb create mode 100644 app/views/projects/cycle_analytics/show.html.haml create mode 100644 app/views/shared/icons/_icon_cycle_analytics_splash.svg create mode 100644 db/fixtures/development/17_cycle_analytics.rb create mode 100644 db/migrate/20160824124900_add_table_issue_metrics.rb create mode 100644 db/migrate/20160825052008_add_table_merge_request_metrics.rb create mode 100644 db/migrate/20160915042921_create_merge_requests_closing_issues.rb create mode 100644 lib/gitlab/database/date_time.rb create mode 100644 lib/gitlab/database/median.rb create mode 100644 spec/models/cycle_analytics/code_spec.rb create mode 100644 spec/models/cycle_analytics/issue_spec.rb create mode 100644 spec/models/cycle_analytics/plan_spec.rb create mode 100644 spec/models/cycle_analytics/production_spec.rb create mode 100644 spec/models/cycle_analytics/review_spec.rb create mode 100644 spec/models/cycle_analytics/staging_spec.rb create mode 100644 spec/models/cycle_analytics/summary_spec.rb create mode 100644 spec/models/cycle_analytics/test_spec.rb create mode 100644 spec/models/issue/metrics_spec.rb create mode 100644 spec/models/merge_request/metrics_spec.rb create mode 100644 spec/support/cycle_analytics_helpers.rb create mode 100644 spec/support/cycle_analytics_helpers/test_generation.rb create mode 100644 spec/support/git_helpers.rb diff --git a/CHANGELOG b/CHANGELOG index 5217cc95bb3961..240ee46043e69b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -22,6 +22,7 @@ v 8.12.0 (unreleased) - Pass the "Remember me" value to the U2F authentication form - Display stages in valid order in stages dropdown on build page - Only update projects.last_activity_at once per hour when creating a new event + - Cycle analytics (first iteration) !5986 - Remove vendor prefixes for linear-gradient CSS (ClemMakesApps) - Move pushes_since_gc from the database to Redis - Add font color contrast to external label in admin area (ClemMakesApps) diff --git a/Gemfile b/Gemfile index 9545b844835766..54d3170d811f74 100644 --- a/Gemfile +++ b/Gemfile @@ -320,6 +320,7 @@ group :test do gem 'webmock', '~> 1.21.0' gem 'test_after_commit', '~> 0.4.2' gem 'sham_rack', '~> 1.3.6' + gem 'timecop', '~> 0.8.0' end group :production do diff --git a/Gemfile.lock b/Gemfile.lock index 7044c476b54c02..3b15f3654a9cd4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -980,6 +980,7 @@ DEPENDENCIES teaspoon-jasmine (~> 2.2.0) test_after_commit (~> 0.4.2) thin (~> 1.7.0) + timecop (~> 0.8.0) turbolinks (~> 2.5.0) u2f (~> 0.2.1) uglifier (~> 2.7.2) diff --git a/app/assets/javascripts/cycle-analytics.js.es6 b/app/assets/javascripts/cycle-analytics.js.es6 new file mode 100644 index 00000000000000..afaed7c4f60b45 --- /dev/null +++ b/app/assets/javascripts/cycle-analytics.js.es6 @@ -0,0 +1,92 @@ +((global) => { + + const COOKIE_NAME = 'cycle_analytics_help_dismissed'; + + gl.CycleAnalytics = class CycleAnalytics { + constructor() { + const that = this; + + this.isHelpDismissed = $.cookie(COOKIE_NAME); + this.vue = new Vue({ + el: '#cycle-analytics', + name: 'CycleAnalytics', + created: this.fetchData(), + data: this.decorateData({ isLoading: true }), + methods: { + dismissLanding() { + that.dismissLanding(); + } + } + }); + } + + fetchData(options) { + options = options || { startDate: 30 }; + + $.ajax({ + url: $('#cycle-analytics').data('request-path'), + method: 'GET', + dataType: 'json', + contentType: 'application/json', + data: { start_date: options.startDate } + }).done((data) => { + this.vue.$data = this.decorateData(data); + this.initDropdown(); + }) + .error((data) => { + this.handleError(data); + }) + .always(() => { + this.vue.isLoading = false; + }) + } + + decorateData(data) { + data.summary = data.summary || []; + data.stats = data.stats || []; + data.isHelpDismissed = this.isHelpDismissed; + data.isLoading = data.isLoading || false; + + data.summary.forEach((item) => { + item.value = item.value || '-'; + }); + + data.stats.forEach((item) => { + item.value = item.value || '- - -'; + }) + + return data; + } + + handleError(data) { + this.vue.$data = { + hasError: true, + isHelpDismissed: this.isHelpDismissed + }; + + new Flash('There was an error while fetching cycle analytics data.', 'alert'); + } + + dismissLanding() { + this.vue.isHelpDismissed = true; + $.cookie(COOKIE_NAME, true); + } + + initDropdown() { + const $dropdown = $('.js-ca-dropdown'); + const $label = $dropdown.find('.dropdown-label'); + + $dropdown.find('li a').off('click').on('click', (e) => { + e.preventDefault(); + const $target = $(e.currentTarget); + const value = $target.data('value'); + + $label.text($target.text().trim()); + this.vue.isLoading = true; + this.fetchData({ startDate: value }); + }) + } + + } + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 99b16f7d59bd14..ddf11ecf34c74a 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -94,6 +94,11 @@ break; case "projects:merge_requests:conflicts": window.mcui = new MergeConflictResolver() + break; + case 'projects:merge_requests:index': + shortcut_handler = new ShortcutsNavigation(); + Issuable.init(); + break; case 'dashboard:activity': new Activities(); break; @@ -185,6 +190,9 @@ new gl.ProtectedBranchCreate(); new gl.ProtectedBranchEditList(); break; + case 'projects:cycle_analytics:show': + new gl.CycleAnalytics(); + break; } switch (path.first()) { case 'admin': diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss new file mode 100644 index 00000000000000..21e19c9763282f --- /dev/null +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -0,0 +1,121 @@ +#cycle-analytics { + margin: 24px auto 0; + width: 800px; + position: relative; + + .panel { + + .content-block { + padding: 24px 0; + border-bottom: none; + position: relative; + } + + .column { + text-align: center; + + .header { + font-size: 30px; + line-height: 38px; + font-weight: normal; + margin: 0; + } + + .text { + color: $layout-link-gray; + margin: 0; + } + + &:last-child { + text-align: right; + } + } + + .dropdown { + position: relative; + top: 13px; + } + } + + .bordered-box { + border: 1px solid $border-color; + @include border-radius($border-radius-default); + position: relative; + } + + .content-list { + li { + padding: 18px $gl-padding $gl-padding; + + .container-fluid { + padding: 0; + } + } + + .title-col { + p { + margin: 0; + + &.title { + line-height: 19px; + font-size: 15px; + font-weight: 600; + } + &:text { + color: #8c8c8c; + } + } + } + + .value-col { + text-align: right; + + span { + line-height: 42px; + } + } + } + + .landing { + margin-bottom: $gl-padding; + overflow: hidden; + + .dismiss-icon { + position: absolute; + right: $gl-padding; + cursor: pointer; + color: #b2b2b2; + } + + svg { + margin: 0 20px; + float: left; + width: 136px; + height: 136px; + } + + .inner-content { + width: 480px; + float: left; + + h4 { + color: $gl-text-color; + font-size: 17px; + } + + p { + color: #8c8c8c; + margin-bottom: $gl-padding; + } + } + } + + .fa-spinner { + font-size: 28px; + position: relative; + margin-left: -20px; + left: 50%; + margin-top: 36px; + } + +} diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb new file mode 100644 index 00000000000000..16a7b1fc6e2661 --- /dev/null +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -0,0 +1,67 @@ +class Projects::CycleAnalyticsController < Projects::ApplicationController + include ActionView::Helpers::DateHelper + include ActionView::Helpers::TextHelper + + before_action :authorize_read_cycle_analytics! + + def show + @cycle_analytics = CycleAnalytics.new(@project, from: parse_start_date) + + respond_to do |format| + format.html + format.json { render json: cycle_analytics_json } + end + end + + private + + def parse_start_date + case cycle_analytics_params[:start_date] + when '30' then 30.days.ago + when '90' then 90.days.ago + else 90.days.ago + end + end + + def cycle_analytics_params + return {} unless params[:cycle_analytics].present? + + { start_date: params[:cycle_analytics][:start_date] } + end + + def cycle_analytics_json + cycle_analytics_view_data = [[:issue, "Issue", "Time before an issue gets scheduled"], + [:plan, "Plan", "Time before an issue starts implementation"], + [:code, "Code", "Time until first merge request"], + [:test, "Test", "Total test time for all commits/merges"], + [:review, "Review", "Time between merge request creation and merge/close"], + [:staging, "Staging", "From merge request merge until deploy to production"], + [:production, "Production", "From issue creation until deploy to production"]] + + stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_description)| + value = @cycle_analytics.send(stage_method).presence + + stats << { + title: stage_text, + description: stage_description, + value: value && !value.zero? ? distance_of_time_in_words(value) : nil + } + stats + end + + issues = @cycle_analytics.summary.new_issues + commits = @cycle_analytics.summary.commits + deploys = @cycle_analytics.summary.deploys + + summary = [ + { title: "New Issue".pluralize(issues), value: issues }, + { title: "Commit".pluralize(commits), value: commits }, + { title: "Deploy".pluralize(deploys), value: deploys } + ] + + { + summary: summary, + stats: stats + } + end +end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index a322a90cc4e037..5b71113feb90aa 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -46,6 +46,10 @@ def project_environments_path(project, *args) namespace_project_environments_path(project.namespace, project, *args) end + def project_cycle_analytics_path(project, *args) + namespace_project_cycle_analytics_path(project.namespace, project, *args) + end + def project_builds_path(project, *args) namespace_project_builds_path(project.namespace, project, *args) end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 895eac1a258b59..663c5b1e2315a3 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -56,6 +56,16 @@ class Pipeline < ActiveRecord::Base pipeline.finished_at = Time.now end + after_transition [:created, :pending] => :running do |pipeline| + MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)). + update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil) + end + + after_transition any => [:success] do |pipeline| + MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)). + update_all(latest_build_finished_at: pipeline.finished_at) + end + before_transition do |pipeline| pipeline.update_duration end @@ -280,6 +290,16 @@ def execute_hooks project.execute_services(data, :pipeline_hooks) end + # Merge requests for which the current pipeline is running against + # the merge request's latest commit. + def merge_requests + @merge_requests ||= + begin + project.merge_requests.where(source_branch: self.ref). + select { |merge_request| merge_request.pipeline.try(:id) == self.id } + end + end + private def pipeline_data diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 22231b2e0f03a2..1650ac9fcbe22c 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -28,10 +28,13 @@ def award_emojis_loaded? loaded? && to_a.all? { |note| note.association(:award_emoji).loaded? } end end + has_many :label_links, as: :target, dependent: :destroy has_many :labels, through: :label_links has_many :todos, as: :target, dependent: :destroy + has_one :metrics + validates :author, presence: true validates :title, presence: true, length: { within: 0..255 } @@ -81,6 +84,7 @@ def award_emojis_loaded? acts_as_paranoid after_save :update_assignee_cache_counts, if: :assignee_id_changed? + after_save :record_metrics def update_assignee_cache_counts # make sure we flush the cache for both the old *and* new assignee @@ -286,4 +290,9 @@ def updated_tasks def can_move?(*) false end + + def record_metrics + metrics = self.metrics || create_metrics + metrics.record! + end end diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb new file mode 100644 index 00000000000000..be295487fd2786 --- /dev/null +++ b/app/models/cycle_analytics.rb @@ -0,0 +1,97 @@ +class CycleAnalytics + include Gitlab::Database::Median + include Gitlab::Database::DateTime + + def initialize(project, from:) + @project = project + @from = from + end + + def summary + @summary ||= Summary.new(@project, from: @from) + end + + def issue + calculate_metric(:issue, + Issue.arel_table[:created_at], + [Issue::Metrics.arel_table[:first_associated_with_milestone_at], + Issue::Metrics.arel_table[:first_added_to_board_at]]) + end + + def plan + calculate_metric(:plan, + [Issue::Metrics.arel_table[:first_associated_with_milestone_at], + Issue::Metrics.arel_table[:first_added_to_board_at]], + Issue::Metrics.arel_table[:first_mentioned_in_commit_at]) + end + + def code + calculate_metric(:code, + Issue::Metrics.arel_table[:first_mentioned_in_commit_at], + MergeRequest.arel_table[:created_at]) + end + + def test + calculate_metric(:test, + MergeRequest::Metrics.arel_table[:latest_build_started_at], + MergeRequest::Metrics.arel_table[:latest_build_finished_at]) + end + + def review + calculate_metric(:review, + MergeRequest.arel_table[:created_at], + MergeRequest::Metrics.arel_table[:merged_at]) + end + + def staging + calculate_metric(:staging, + MergeRequest::Metrics.arel_table[:merged_at], + MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) + end + + def production + calculate_metric(:production, + Issue.arel_table[:created_at], + MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) + end + + private + + def calculate_metric(name, start_time_attrs, end_time_attrs) + cte_table = Arel::Table.new("cte_table_for_#{name}") + + # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time). + # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time). + # We compute the (end_time - start_time) interval, and give it an alias based on the current + # cycle analytics stage. + interval_query = Arel::Nodes::As.new( + cte_table, + subtract_datetimes(base_query, end_time_attrs, start_time_attrs, name.to_s)) + + median_datetime(cte_table, interval_query, name) + end + + # Join table with a row for every pair (where the merge request + # closes the given issue) with issue and merge request metrics included. The metrics + # are loaded with an inner join, so issues / merge requests without metrics are + # automatically excluded. + def base_query + arel_table = MergeRequestsClosingIssues.arel_table + + # Load issues + query = arel_table.join(Issue.arel_table).on(Issue.arel_table[:id].eq(arel_table[:issue_id])). + join(Issue::Metrics.arel_table).on(Issue.arel_table[:id].eq(Issue::Metrics.arel_table[:issue_id])). + where(Issue.arel_table[:project_id].eq(@project.id)). + where(Issue.arel_table[:deleted_at].eq(nil)). + where(Issue.arel_table[:created_at].gteq(@from)) + + # Load merge_requests + query = query.join(MergeRequest.arel_table, Arel::Nodes::OuterJoin). + on(MergeRequest.arel_table[:id].eq(arel_table[:merge_request_id])). + join(MergeRequest::Metrics.arel_table). + on(MergeRequest.arel_table[:id].eq(MergeRequest::Metrics.arel_table[:merge_request_id])) + + # Limit to merge requests that have been deployed to production after `@from` + query.where(MergeRequest::Metrics.arel_table[:first_deployed_to_production_at].gteq(@from)) + end +end diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb new file mode 100644 index 00000000000000..53b2cacb131f9e --- /dev/null +++ b/app/models/cycle_analytics/summary.rb @@ -0,0 +1,24 @@ +class CycleAnalytics + class Summary + def initialize(project, from:) + @project = project + @from = from + end + + def new_issues + @project.issues.created_after(@from).count + end + + def commits + repository = @project.repository.raw_repository + + if @project.default_branch + repository.log(ref: @project.default_branch, after: @from).count + end + end + + def deploys + @project.deployments.where("created_at > ?", @from).count + end + end +end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 1e338889714ccd..07d7e19e70d898 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -42,4 +42,38 @@ def includes_commit?(commit) project.repository.is_ancestor?(commit.id, sha) end + + def update_merge_request_metrics! + return unless environment.update_merge_request_metrics? + + merge_requests = project.merge_requests. + joins(:metrics). + where(target_branch: self.ref, merge_request_metrics: { first_deployed_to_production_at: nil }). + where("merge_request_metrics.merged_at <= ?", self.created_at) + + if previous_deployment + merge_requests = merge_requests.where("merge_request_metrics.merged_at >= ?", previous_deployment.created_at) + end + + # Need to use `map` instead of `select` because MySQL doesn't allow `SELECT`ing from the same table + # that we're updating. + merge_request_ids = + if Gitlab::Database.postgresql? + merge_requests.select(:id) + elsif Gitlab::Database.mysql? + merge_requests.map(&:id) + end + + MergeRequest::Metrics. + where(merge_request_id: merge_request_ids, first_deployed_to_production_at: nil). + update_all(first_deployed_to_production_at: self.created_at) + end + + def previous_deployment + @previous_deployment ||= + project.deployments.joins(:environment). + where(environments: { name: self.environment.name }, ref: self.ref). + where.not(id: self.id). + take + end end diff --git a/app/models/environment.rb b/app/models/environment.rb index 33c9abf382a166..49e0a20640ce66 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -43,4 +43,8 @@ def includes_commit?(commit) last_deployment.includes_commit?(commit) end + + def update_merge_request_metrics? + self.name == "production" + end end diff --git a/app/models/issue.rb b/app/models/issue.rb index 788611305fec4b..abd58e0454adac 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -23,6 +23,8 @@ class Issue < ActiveRecord::Base has_many :events, as: :target, dependent: :destroy + has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all + validates :project, presence: true scope :cared, ->(user) { where(assignee_id: user) } @@ -36,6 +38,8 @@ class Issue < ActiveRecord::Base scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') } scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') } + scope :created_after, -> (datetime) { where("created_at >= ?", datetime) } + attr_spammable :title, spam_title: true attr_spammable :description, spam_description: true diff --git a/app/models/issue/metrics.rb b/app/models/issue/metrics.rb new file mode 100644 index 00000000000000..012d545c44093b --- /dev/null +++ b/app/models/issue/metrics.rb @@ -0,0 +1,21 @@ +class Issue::Metrics < ActiveRecord::Base + belongs_to :issue + + def record! + if issue.milestone_id.present? && self.first_associated_with_milestone_at.blank? + self.first_associated_with_milestone_at = Time.now + end + + if issue_assigned_to_list_label? && self.first_added_to_board_at.blank? + self.first_added_to_board_at = Time.now + end + + self.save + end + + private + + def issue_assigned_to_list_label? + issue.labels.any? { |label| label.lists.present? } + end +end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 75f48fd4ba5cb2..616efaf3c42108 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -16,6 +16,8 @@ class MergeRequest < ActiveRecord::Base has_many :events, as: :target, dependent: :destroy + has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all + serialize :merge_params, Hash after_create :ensure_merge_request_diff, unless: :importing? @@ -501,6 +503,19 @@ def project target_project end + # If the merge request closes any issues, save this information in the + # `MergeRequestsClosingIssues` model. This is a performance optimization. + # Calculating this information for a number of merge requests requires + # running `ReferenceExtractor` on each of them separately. + def cache_merge_request_closes_issues!(current_user = self.author) + transaction do + self.merge_requests_closing_issues.delete_all + closes_issues(current_user).each do |issue| + self.merge_requests_closing_issues.create!(issue: issue) + end + end + end + def closes_issue?(issue) closes_issues.include?(issue) end @@ -508,7 +523,8 @@ def closes_issue?(issue) # Return the set of issues that will be closed if this merge request is accepted. def closes_issues(current_user = self.author) if target_branch == project.default_branch - messages = commits.map(&:safe_message) << description + messages = [description] + messages.concat(commits.map(&:safe_message)) if merge_request_diff Gitlab::ClosingIssueExtractor.new(project, current_user). closed_by_message(messages.join("\n")) diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb new file mode 100644 index 00000000000000..99c49a020c974e --- /dev/null +++ b/app/models/merge_request/metrics.rb @@ -0,0 +1,11 @@ +class MergeRequest::Metrics < ActiveRecord::Base + belongs_to :merge_request + + def record! + if merge_request.merged? && self.merged_at.blank? + self.merged_at = Time.now + end + + self.save + end +end diff --git a/app/models/merge_requests_closing_issues.rb b/app/models/merge_requests_closing_issues.rb new file mode 100644 index 00000000000000..ab597c379471af --- /dev/null +++ b/app/models/merge_requests_closing_issues.rb @@ -0,0 +1,7 @@ +class MergeRequestsClosingIssues < ActiveRecord::Base + belongs_to :merge_request + belongs_to :issue + + validates :merge_request_id, uniqueness: { scope: :issue_id }, presence: true + validates :issue_id, presence: true +end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 00c4c7b1440847..be25c750d674a2 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -46,6 +46,7 @@ def guest_access! can! :create_issue can! :create_note can! :upload_file + can! :read_cycle_analytics end def reporter_access! @@ -204,6 +205,7 @@ def anonymous_rules can! :read_commit_status can! :read_container_image can! :download_code + can! :read_cycle_analytics # NOTE: may be overridden by IssuePolicy can! :read_issue diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb index e6667132e27582..799ad3e1bd0f40 100644 --- a/app/services/create_deployment_service.rb +++ b/app/services/create_deployment_service.rb @@ -4,7 +4,7 @@ class CreateDeploymentService < BaseService def execute(deployable = nil) environment = find_or_create_environment - project.deployments.create( + deployment = project.deployments.create( environment: environment, ref: params[:ref], tag: params[:tag], @@ -12,6 +12,10 @@ def execute(deployable = nil) user: current_user, deployable: deployable ) + + deployment.update_merge_request_metrics! + + deployment end private diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 948041063c0e19..c499427605adb4 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -134,6 +134,7 @@ def process_commit_messages end commit.create_cross_references!(authors[commit], closed_issues) + update_issue_metrics(commit, authors) end end @@ -186,4 +187,11 @@ def commit_user(commit) def branch_name @branch_name ||= Gitlab::Git.ref_name(params[:ref]) end + + def update_issue_metrics(commit, authors) + mentioned_issues = commit.all_references(authors[commit]).issues + + Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil). + update_all(first_mentioned_in_commit_at: commit.committed_date) + end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 4c8d93999a7049..fbce46769f7648 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -157,6 +157,10 @@ def after_create(issuable) # To be overridden by subclasses end + def after_update(issuable) + # To be overridden by subclasses + end + def update_issuable(issuable, attributes) issuable.with_transaction_returning_status do issuable.update(attributes.merge(updated_by: current_user)) @@ -182,6 +186,7 @@ def update(issuable) end handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users) + after_update(issuable) issuable.create_new_cross_references!(current_user) execute_hooks(issuable, 'update') end diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index 73247e62421b35..b0ae2dfe4ce532 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -20,6 +20,7 @@ def after_create(issuable) event_service.open_mr(issuable, current_user) notification_service.new_merge_request(issuable, current_user) todo_service.new_merge_request(issuable, current_user) + issuable.cache_merge_request_closes_issues!(current_user) end end end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 5cedd6f11d9e06..22596b4014ab3c 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -13,6 +13,7 @@ def execute(oldrev, newrev, ref) reload_merge_requests reset_merge_when_build_succeeds mark_pending_todos_done + cache_merge_requests_closing_issues # Leave a system note if a branch was deleted/added if branch_added? || branch_removed? @@ -141,6 +142,14 @@ def execute_mr_web_hooks end end + # If the merge requests closes any issues, save this information in the + # `MergeRequestsClosingIssues` model (as a performance optimization). + def cache_merge_requests_closing_issues + @project.merge_requests.where(source_branch: @branch_name).each do |merge_request| + merge_request.cache_merge_request_closes_issues!(@current_user) + end + end + def filter_merge_requests(merge_requests) merge_requests.uniq.select(&:source_project) end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 398ec47f0ea22f..f14f9e4b327960 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -77,5 +77,9 @@ def reopen_service def close_service MergeRequests::CloseService end + + def after_update(issuable) + issuable.cache_merge_request_closes_issues!(current_user) + end end end diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 8e4937b7aa0048..e44a2bfed9d769 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -47,7 +47,7 @@ Repository - if project_nav_tab? :pipelines - = nav_link(controller: [:pipelines, :builds, :environments]) do + = nav_link(controller: [:pipelines, :builds, :environments, :cycle_analytics]) do = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do %span Pipelines diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml new file mode 100644 index 00000000000000..5dcb2a17873594 --- /dev/null +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -0,0 +1,57 @@ +- @no_container = true +- page_title "Cycle Analytics" += render "projects/pipelines/head" + +#cycle-analytics{"v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project)}} + + .bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"} + = icon('times', class: 'dismiss-icon', "@click": "dismissLanding()") + = custom_icon('icon_cycle_analytics_splash') + .inner-content + %h4 + Introducing Cycle Analytics + %p + Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project. + + = link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn' + + = icon("spinner spin", "v-show" => "isLoading") + + .wrapper{"v-show" => "!isLoading && !hasError"} + .panel.panel-default + .panel-heading + Pipeline Health + + .content-block + .container-fluid + .row + .col-xs-3.column{"v-for" => "item in summary"} + %h3.header {{item.value}} + %p.text {{item.title}} + + .col-xs-3.column + .dropdown.inline.js-ca-dropdown + %button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"} + %span.dropdown-label Last 30 days + %i.fa.fa-chevron-down + %ul.dropdown-menu.dropdown-menu-align-right + %li + %a{'href' => "#", 'data-value' => '30'} + Last 30 days + %li + %a{'href' => "#", 'data-value' => '90'} + Last 90 days + + .bordered-box + %ul.content-list + %li{"v-for" => "item in stats"} + .container-fluid + .row + .col-xs-10.title-col + %p.title + {{item.title}} + %p.text + {{item.description}} + .col-xs-2.value-col + %span + {{item.value}} diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index f611ddc8f5f514..5f571499e8025a 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -19,3 +19,9 @@ = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do %span Environments + + - if can?(current_user, :read_cycle_analytics, @project) + = nav_link(controller: %w(cycle_analytics)) do + = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics' do + %span + Cycle Analytics diff --git a/app/views/shared/icons/_icon_cycle_analytics_splash.svg b/app/views/shared/icons/_icon_cycle_analytics_splash.svg new file mode 100644 index 00000000000000..eb5a962d651a45 --- /dev/null +++ b/app/views/shared/icons/_icon_cycle_analytics_splash.svg @@ -0,0 +1 @@ + diff --git a/config/routes.rb b/config/routes.rb index 068c92d1400e1f..c4eee59e7aa8f2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -780,6 +780,8 @@ resources :environments + resource :cycle_analytics, only: [:show] + resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do collection do post :cancel_all diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb new file mode 100644 index 00000000000000..e882a492757053 --- /dev/null +++ b/db/fixtures/development/17_cycle_analytics.rb @@ -0,0 +1,246 @@ +require 'sidekiq/testing' +require './spec/support/test_env' + +class Gitlab::Seeder::CycleAnalytics + def initialize(project, perf: false) + @project = project + @user = User.order(:id).last + @issue_count = perf ? 1000 : 5 + stub_git_pre_receive! + end + + # The GitLab API needn't be running for the fixtures to be + # created. Since we're performing a number of git actions + # here (like creating a branch or committing a file), we need + # to disable the `pre_receive` hook in order to remove this + # dependency on the GitLab API. + def stub_git_pre_receive! + GitHooksService.class_eval do + def run_hook(name) + [true, ''] + end + end + end + + def seed_metrics! + @issue_count.times do |index| + # Issue + Timecop.travel 5.days.from_now + title = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}" + issue = Issue.create(project: @project, title: title, author: @user) + issue_metrics = issue.metrics + + # Milestones / Labels + Timecop.travel 5.days.from_now + if index.even? + issue_metrics.first_associated_with_milestone_at = rand(6..12).hours.from_now + else + issue_metrics.first_added_to_board_at = rand(6..12).hours.from_now + end + + # Commit + Timecop.travel 5.days.from_now + issue_metrics.first_mentioned_in_commit_at = rand(6..12).hours.from_now + + # MR + Timecop.travel 5.days.from_now + branch_name = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}" + @project.repository.add_branch(@user, branch_name, 'master') + merge_request = MergeRequest.create(target_project: @project, source_project: @project, source_branch: branch_name, target_branch: 'master', title: branch_name, author: @user) + merge_request_metrics = merge_request.metrics + + # MR closing issues + Timecop.travel 5.days.from_now + MergeRequestsClosingIssues.create!(issue: issue, merge_request: merge_request) + + # Merge + Timecop.travel 5.days.from_now + merge_request_metrics.merged_at = rand(6..12).hours.from_now + + # Start build + Timecop.travel 5.days.from_now + merge_request_metrics.latest_build_started_at = rand(6..12).hours.from_now + + # Finish build + Timecop.travel 5.days.from_now + merge_request_metrics.latest_build_finished_at = rand(6..12).hours.from_now + + # Deploy to production + Timecop.travel 5.days.from_now + merge_request_metrics.first_deployed_to_production_at = rand(6..12).hours.from_now + + issue_metrics.save! + merge_request_metrics.save! + + print '.' + end + end + + def seed! + Sidekiq::Testing.inline! do + issues = create_issues + puts '.' + + # Stage 1 + Timecop.travel 5.days.from_now + add_milestones_and_list_labels(issues) + print '.' + + # Stage 2 + Timecop.travel 5.days.from_now + branches = mention_in_commits(issues) + print '.' + + # Stage 3 + Timecop.travel 5.days.from_now + merge_requests = create_merge_requests_closing_issues(issues, branches) + print '.' + + # Stage 4 + Timecop.travel 5.days.from_now + run_builds(merge_requests) + print '.' + + # Stage 5 + Timecop.travel 5.days.from_now + merge_merge_requests(merge_requests) + print '.' + + # Stage 6 / 7 + Timecop.travel 5.days.from_now + deploy_to_production(merge_requests) + print '.' + end + + print '.' + end + + private + + def create_issues + Array.new(@issue_count) do + issue_params = { + title: "Cycle Analytics: #{FFaker::Lorem.sentence(6)}", + description: FFaker::Lorem.sentence, + state: 'opened', + assignee: @project.team.users.sample + } + + Issues::CreateService.new(@project, @project.team.users.sample, issue_params).execute + end + end + + def add_milestones_and_list_labels(issues) + issues.shuffle.map.with_index do |issue, index| + Timecop.travel 12.hours.from_now + + if index.even? + issue.update(milestone: @project.milestones.sample) + else + label_name = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}" + list_label = FactoryGirl.create(:label, title: label_name, project: issue.project) + FactoryGirl.create(:list, board: FactoryGirl.create(:board, project: issue.project), label: list_label) + issue.update(labels: [list_label]) + end + + issue + end + end + + def mention_in_commits(issues) + issues.map do |issue| + Timecop.travel 12.hours.from_now + + branch_name = filename = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}" + + issue.project.repository.add_branch(@user, branch_name, 'master') + + options = { + committer: issue.project.repository.user_to_committer(@user), + author: issue.project.repository.user_to_committer(@user), + commit: { message: "Commit for ##{issue.iid}", branch: branch_name, update_ref: true }, + file: { content: "content", path: filename, update: false } + } + + commit_sha = Gitlab::Git::Blob.commit(issue.project.repository, options) + issue.project.repository.commit(commit_sha) + + + GitPushService.new(issue.project, + @user, + oldrev: issue.project.repository.commit("master").sha, + newrev: commit_sha, + ref: 'refs/heads/master').execute + + branch_name + end + end + + def create_merge_requests_closing_issues(issues, branches) + issues.zip(branches).map do |issue, branch| + Timecop.travel 12.hours.from_now + + opts = { + title: 'Cycle Analytics merge_request', + description: "Fixes #{issue.to_reference}", + source_branch: branch, + target_branch: 'master' + } + + MergeRequests::CreateService.new(issue.project, @user, opts).execute + end + end + + def run_builds(merge_requests) + merge_requests.each do |merge_request| + Timecop.travel 12.hours.from_now + + service = Ci::CreatePipelineService.new(merge_request.project, + @user, + ref: "refs/heads/#{merge_request.source_branch}") + pipeline = service.execute(ignore_skip_ci: true, save_on_errors: false) + + pipeline.run! + Timecop.travel rand(1..6).hours.from_now + pipeline.succeed! + end + end + + def merge_merge_requests(merge_requests) + merge_requests.each do |merge_request| + Timecop.travel 12.hours.from_now + + MergeRequests::MergeService.new(merge_request.project, @user).execute(merge_request) + end + end + + def deploy_to_production(merge_requests) + merge_requests.each do |merge_request| + Timecop.travel 12.hours.from_now + + CreateDeploymentService.new(merge_request.project, @user, { + environment: 'production', + ref: 'master', + tag: false, + sha: @project.repository.commit('master').sha + }).execute + end + end +end + +Gitlab::Seeder.quiet do + if ENV['SEED_CYCLE_ANALYTICS'] + Project.all.each do |project| + seeder = Gitlab::Seeder::CycleAnalytics.new(project) + seeder.seed! + end + elsif ENV['CYCLE_ANALYTICS_PERF_TEST'] + seeder = Gitlab::Seeder::CycleAnalytics.new(Project.order(:id).first, perf: true) + seeder.seed! + elsif ENV['CYCLE_ANALYTICS_POPULATE_METRICS_DIRECTLY'] + seeder = Gitlab::Seeder::CycleAnalytics.new(Project.order(:id).first, perf: true) + seeder.seed_metrics! + else + puts "Not running the cycle analytics seed file. Use the `SEED_CYCLE_ANALYTICS` environment variable to enable it." + end +end diff --git a/db/migrate/20160824124900_add_table_issue_metrics.rb b/db/migrate/20160824124900_add_table_issue_metrics.rb new file mode 100644 index 00000000000000..e9bb79b3c628f1 --- /dev/null +++ b/db/migrate/20160824124900_add_table_issue_metrics.rb @@ -0,0 +1,37 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddTableIssueMetrics < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = true + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + DOWNTIME_REASON = 'Adding foreign key' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + create_table :issue_metrics do |t| + t.references :issue, index: { name: "index_issue_metrics" }, foreign_key: { on_delete: :cascade }, null: false + + t.datetime 'first_mentioned_in_commit_at' + t.datetime 'first_associated_with_milestone_at' + t.datetime 'first_added_to_board_at' + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160825052008_add_table_merge_request_metrics.rb b/db/migrate/20160825052008_add_table_merge_request_metrics.rb new file mode 100644 index 00000000000000..e01cc5038b900e --- /dev/null +++ b/db/migrate/20160825052008_add_table_merge_request_metrics.rb @@ -0,0 +1,38 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddTableMergeRequestMetrics < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = true + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + DOWNTIME_REASON = 'Adding foreign key' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + create_table :merge_request_metrics do |t| + t.references :merge_request, index: { name: "index_merge_request_metrics" }, foreign_key: { on_delete: :cascade }, null: false + + t.datetime 'latest_build_started_at' + t.datetime 'latest_build_finished_at' + t.datetime 'first_deployed_to_production_at', index: true + t.datetime 'merged_at' + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160915042921_create_merge_requests_closing_issues.rb b/db/migrate/20160915042921_create_merge_requests_closing_issues.rb new file mode 100644 index 00000000000000..94874a853dae6e --- /dev/null +++ b/db/migrate/20160915042921_create_merge_requests_closing_issues.rb @@ -0,0 +1,34 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CreateMergeRequestsClosingIssues < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = true + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + DOWNTIME_REASON = 'Adding foreign keys' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + create_table :merge_requests_closing_issues do |t| + t.references :merge_request, foreign_key: { on_delete: :cascade }, index: true, null: false + t.references :issue, foreign_key: { on_delete: :cascade }, index: true, null: false + + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 3567908de03178..fc98694e2eb4c3 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: 20160913212128) do +ActiveRecord::Schema.define(version: 20160915081353) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -439,6 +439,17 @@ add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree + create_table "issue_metrics", force: :cascade do |t| + t.integer "issue_id", null: false + t.datetime "first_associated_with_milestone_at" + t.datetime "first_added_to_board_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.datetime "first_mentioned_in_commit_at" + end + + add_index "issue_metrics", ["issue_id"], name: "index_issue_metrics", using: :btree + create_table "issues", force: :cascade do |t| t.string "title" t.integer "assignee_id" @@ -581,6 +592,18 @@ add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", using: :btree + create_table "merge_request_metrics", force: :cascade do |t| + t.integer "merge_request_id", null: false + t.datetime "latest_build_started_at" + t.datetime "latest_build_finished_at" + t.datetime "first_deployed_to_production_at" + t.datetime "merged_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "merge_request_metrics", ["merge_request_id"], name: "index_merge_request_metrics", using: :btree + create_table "merge_requests", force: :cascade do |t| t.string "target_branch", null: false t.string "source_branch", null: false @@ -622,6 +645,16 @@ add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} + create_table "merge_requests_closing_issues", force: :cascade do |t| + t.integer "merge_request_id", null: false + t.integer "issue_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "merge_requests_closing_issues", ["issue_id"], name: "index_merge_requests_closing_issues_on_issue_id", using: :btree + add_index "merge_requests_closing_issues", ["merge_request_id"], name: "index_merge_requests_closing_issues_on_merge_request_id", using: :btree + create_table "milestones", force: :cascade do |t| t.string "title", null: false t.integer "project_id", null: false @@ -1147,8 +1180,12 @@ add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree add_foreign_key "boards", "projects" + add_foreign_key "issue_metrics", "issues", on_delete: :cascade add_foreign_key "lists", "boards" add_foreign_key "lists", "labels" + add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade + add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade + add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade add_foreign_key "personal_access_tokens", "users" add_foreign_key "protected_branch_merge_access_levels", "protected_branches" add_foreign_key "protected_branch_push_access_levels", "protected_branches" diff --git a/lib/gitlab/database/date_time.rb b/lib/gitlab/database/date_time.rb new file mode 100644 index 00000000000000..b6a89f715fdaaa --- /dev/null +++ b/lib/gitlab/database/date_time.rb @@ -0,0 +1,27 @@ +module Gitlab + module Database + module DateTime + # Find the first of the `end_time_attrs` that isn't `NULL`. Subtract from it + # the first of the `start_time_attrs` that isn't NULL. `SELECT` the resulting interval + # along with an alias specified by the `as` parameter. + # + # Note: For MySQL, the interval is returned in seconds. + # For PostgreSQL, the interval is returned as an INTERVAL type. + def subtract_datetimes(query_so_far, end_time_attrs, start_time_attrs, as) + diff_fn = if Gitlab::Database.postgresql? + Arel::Nodes::Subtraction.new( + Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs)), + Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs))) + elsif Gitlab::Database.mysql? + Arel::Nodes::NamedFunction.new( + "TIMESTAMPDIFF", + [Arel.sql('second'), + Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)), + Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs))]) + end + + query_so_far.project(diff_fn.as(as)) + end + end + end +end diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb new file mode 100644 index 00000000000000..1444d25ebc7563 --- /dev/null +++ b/lib/gitlab/database/median.rb @@ -0,0 +1,112 @@ +# https://www.periscopedata.com/blog/medians-in-sql.html +module Gitlab + module Database + module Median + def median_datetime(arel_table, query_so_far, column_sym) + median_queries = + if Gitlab::Database.postgresql? + pg_median_datetime_sql(arel_table, query_so_far, column_sym) + elsif Gitlab::Database.mysql? + mysql_median_datetime_sql(arel_table, query_so_far, column_sym) + end + + results = Array.wrap(median_queries).map do |query| + ActiveRecord::Base.connection.execute(query) + end + extract_median(results).presence + end + + def extract_median(results) + result = results.compact.first + + if Gitlab::Database.postgresql? + result = result.first.presence + median = result['median'] if result + median.to_f if median + elsif Gitlab::Database.mysql? + result.to_a.flatten.first + end + end + + def mysql_median_datetime_sql(arel_table, query_so_far, column_sym) + query = arel_table. + from(arel_table.project(Arel.sql('*')).order(arel_table[column_sym]).as(arel_table.table_name)). + project(average([arel_table[column_sym]], 'median')). + where( + Arel::Nodes::Between.new( + Arel.sql("(select @row_id := @row_id + 1)"), + Arel::Nodes::And.new( + [Arel.sql('@ct/2.0'), + Arel.sql('@ct/2.0 + 1')] + ) + ) + ). + # Disallow negative values + where(arel_table[column_sym].gteq(0)) + + [ + Arel.sql("CREATE TEMPORARY TABLE IF NOT EXISTS #{query_so_far.to_sql}"), + Arel.sql("set @ct := (select count(1) from #{arel_table.table_name});"), + Arel.sql("set @row_id := 0;"), + query.to_sql, + Arel.sql("DROP TEMPORARY TABLE IF EXISTS #{arel_table.table_name};") + ] + end + + def pg_median_datetime_sql(arel_table, query_so_far, column_sym) + # Create a CTE with the column we're operating on, row number (after sorting by the column + # we're operating on), and count of the table we're operating on (duplicated across) all rows + # of the CTE. For example, if we're looking to find the median of the `projects.star_count` + # column, the CTE might look like this: + # + # star_count | row_id | ct + # ------------+--------+---- + # 5 | 1 | 3 + # 9 | 2 | 3 + # 15 | 3 | 3 + cte_table = Arel::Table.new("ordered_records") + cte = Arel::Nodes::As.new( + cte_table, + arel_table. + project( + arel_table[column_sym].as(column_sym.to_s), + Arel::Nodes::Over.new(Arel::Nodes::NamedFunction.new("row_number", []), + Arel::Nodes::Window.new.order(arel_table[column_sym])).as('row_id'), + arel_table.project("COUNT(1)").as('ct')). + # Disallow negative values + where(arel_table[column_sym].gteq(zero_interval))) + + # From the CTE, select either the middle row or the middle two rows (this is accomplished + # by 'where cte.row_id between cte.ct / 2.0 AND cte.ct / 2.0 + 1'). Find the average of the + # selected rows, and this is the median value. + cte_table.project(average([extract_epoch(cte_table[column_sym])], "median")). + where( + Arel::Nodes::Between.new( + cte_table[:row_id], + Arel::Nodes::And.new( + [(cte_table[:ct] / Arel.sql('2.0')), + (cte_table[:ct] / Arel.sql('2.0') + 1)] + ) + ) + ). + with(query_so_far, cte). + to_sql + end + + private + + def average(args, as) + Arel::Nodes::NamedFunction.new("AVG", args, as) + end + + def extract_epoch(arel_attribute) + Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")}) + end + + # Need to cast '0' to an INTERVAL before we can check if the interval is positive + def zero_interval + Arel::Nodes::NamedFunction.new("CAST", [Arel.sql("'0' AS INTERVAL")]) + end + end + end +end diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb index 82591604fcb313..6f24bf58d14085 100644 --- a/spec/factories/deployments.rb +++ b/spec/factories/deployments.rb @@ -3,11 +3,12 @@ sha '97de212e80737a608d939f648d959671fb0a0142' ref 'master' tag false + project nil environment factory: :environment after(:build) do |deployment, evaluator| - deployment.project = deployment.environment.project + deployment.project ||= deployment.environment.project end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index f1857f846dcf6a..550a890797e5bd 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -187,6 +187,37 @@ end end + describe "merge request metrics" do + let(:project) { FactoryGirl.create :project } + let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) } + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) } + + context 'when transitioning to running' do + it 'records the build start time' do + time = Time.now + Timecop.freeze(time) { build.run } + + expect(merge_request.reload.metrics.latest_build_started_at).to be_within(1.second).of(time) + end + + it 'clears the build end time' do + build.run + + expect(merge_request.reload.metrics.latest_build_finished_at).to be_nil + end + end + + context 'when transitioning to success' do + it 'records the build end time' do + build.run + time = Time.now + Timecop.freeze(time) { build.success } + + expect(merge_request.reload.metrics.latest_build_finished_at).to be_within(1.second).of(time) + end + end + end + def create_build(name, queued_at = current, started_from = 0) create(:ci_build, name: name, @@ -468,4 +499,28 @@ def create_build(name, stage_idx) stage_idx: stage_idx) end end + + describe "#merge_requests" do + let(:project) { FactoryGirl.create :project } + let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) } + + it "returns merge requests whose `diff_head_sha` matches the pipeline's SHA" do + merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref) + + expect(pipeline.merge_requests).to eq([merge_request]) + end + + it "doesn't return merge requests whose source branch doesn't match the pipeline's ref" do + create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') + + expect(pipeline.merge_requests).to be_empty + end + + it "doesn't return merge requests whose `diff_head_sha` doesn't match the pipeline's SHA" do + create(:merge_request, source_project: project, source_branch: pipeline.ref) + allow_any_instance_of(MergeRequest).to receive(:diff_head_sha) { '97de212e80737a608d939f648d959671fb0a0142b' } + + expect(pipeline.merge_requests).to be_empty + end + end end diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb new file mode 100644 index 00000000000000..b9381e3391411e --- /dev/null +++ b/spec/models/cycle_analytics/code_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe 'CycleAnalytics#code', feature: true do + extend CycleAnalyticsHelpers::TestGeneration + + let(:project) { create(:project) } + let(:from_date) { 10.days.ago } + let(:user) { create(:user, :admin) } + subject { CycleAnalytics.new(project, from: from_date) } + + generate_cycle_analytics_spec( + phase: :code, + data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } }, + start_time_conditions: [["issue mentioned in a commit", + -> (context, data) do + context.create_commit_referencing_issue(data[:issue]) + end]], + end_time_conditions: [["merge request that closes issue is created", + -> (context, data) do + context.create_merge_request_closing_issue(data[:issue]) + end]], + post_fn: -> (context, data) do + context.merge_merge_requests_closing_issue(data[:issue]) + context.deploy_master + end) + + context "when a regular merge request (that doesn't close the issue) is created" do + it "returns nil" do + 5.times do + issue = create(:issue, project: project) + + create_commit_referencing_issue(issue) + create_merge_request_closing_issue(issue, message: "Closes nothing") + + merge_merge_requests_closing_issue(issue) + deploy_master + end + + expect(subject.code).to be_nil + end + end +end diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb new file mode 100644 index 00000000000000..e9cc71254ab466 --- /dev/null +++ b/spec/models/cycle_analytics/issue_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe 'CycleAnalytics#issue', models: true do + extend CycleAnalyticsHelpers::TestGeneration + + let(:project) { create(:project) } + let(:from_date) { 10.days.ago } + let(:user) { create(:user, :admin) } + subject { CycleAnalytics.new(project, from: from_date) } + + generate_cycle_analytics_spec( + phase: :issue, + data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } }, + start_time_conditions: [["issue created", -> (context, data) { data[:issue].save }]], + end_time_conditions: [["issue associated with a milestone", + -> (context, data) do + if data[:issue].persisted? + data[:issue].update(milestone: context.create(:milestone, project: context.project)) + end + end], + ["list label added to issue", + -> (context, data) do + if data[:issue].persisted? + data[:issue].update(label_ids: [context.create(:label, lists: [context.create(:list)]).id]) + end + end]], + post_fn: -> (context, data) do + if data[:issue].persisted? + context.create_merge_request_closing_issue(data[:issue].reload) + context.merge_merge_requests_closing_issue(data[:issue]) + context.deploy_master + end + end) + + context "when a regular label (instead of a list label) is added to the issue" do + it "returns nil" do + 5.times do + regular_label = create(:label) + issue = create(:issue, project: project) + issue.update(label_ids: [regular_label.id]) + + create_merge_request_closing_issue(issue) + merge_merge_requests_closing_issue(issue) + deploy_master + end + + expect(subject.issue).to be_nil + end + end +end diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb new file mode 100644 index 00000000000000..5b8c96dc992189 --- /dev/null +++ b/spec/models/cycle_analytics/plan_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe 'CycleAnalytics#plan', feature: true do + extend CycleAnalyticsHelpers::TestGeneration + + let(:project) { create(:project) } + let(:from_date) { 10.days.ago } + let(:user) { create(:user, :admin) } + subject { CycleAnalytics.new(project, from: from_date) } + + generate_cycle_analytics_spec( + phase: :plan, + data_fn: -> (context) do + { + issue: context.create(:issue, project: context.project), + branch_name: context.random_git_name + } + end, + start_time_conditions: [["issue associated with a milestone", + -> (context, data) do + data[:issue].update(milestone: context.create(:milestone, project: context.project)) + end], + ["list label added to issue", + -> (context, data) do + data[:issue].update(label_ids: [context.create(:label, lists: [context.create(:list)]).id]) + end]], + end_time_conditions: [["issue mentioned in a commit", + -> (context, data) do + context.create_commit_referencing_issue(data[:issue], branch_name: data[:branch_name]) + end]], + post_fn: -> (context, data) do + context.create_merge_request_closing_issue(data[:issue], source_branch: data[:branch_name]) + context.merge_merge_requests_closing_issue(data[:issue]) + context.deploy_master + end) + + context "when a regular label (instead of a list label) is added to the issue" do + it "returns nil" do + branch_name = random_git_name + label = create(:label) + issue = create(:issue, project: project) + issue.update(label_ids: [label.id]) + create_commit_referencing_issue(issue, branch_name: branch_name) + + create_merge_request_closing_issue(issue, source_branch: branch_name) + merge_merge_requests_closing_issue(issue) + deploy_master + + expect(subject.issue).to be_nil + end + end +end diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb new file mode 100644 index 00000000000000..1f5e5cab92d069 --- /dev/null +++ b/spec/models/cycle_analytics/production_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe 'CycleAnalytics#production', feature: true do + extend CycleAnalyticsHelpers::TestGeneration + + let(:project) { create(:project) } + let(:from_date) { 10.days.ago } + let(:user) { create(:user, :admin) } + subject { CycleAnalytics.new(project, from: from_date) } + + generate_cycle_analytics_spec( + phase: :production, + data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } }, + start_time_conditions: [["issue is created", -> (context, data) { data[:issue].save }]], + before_end_fn: lambda do |context, data| + context.create_merge_request_closing_issue(data[:issue]) + context.merge_merge_requests_closing_issue(data[:issue]) + end, + end_time_conditions: + [["merge request that closes issue is deployed to production", -> (context, data) { context.deploy_master }], + ["production deploy happens after merge request is merged (along with other changes)", + lambda do |context, data| + # Make other changes on master + sha = context.project.repository.commit_file(context.user, context.random_git_name, "content", "commit message", 'master', false) + context.project.repository.commit(sha) + + context.deploy_master + end]]) + + context "when a regular merge request (that doesn't close the issue) is merged and deployed" do + it "returns nil" do + 5.times do + merge_request = create(:merge_request) + MergeRequests::MergeService.new(project, user).execute(merge_request) + deploy_master + end + + expect(subject.production).to be_nil + end + end + + context "when the deployment happens to a non-production environment" do + it "returns nil" do + 5.times do + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + MergeRequests::MergeService.new(project, user).execute(merge_request) + deploy_master(environment: 'staging') + end + + expect(subject.production).to be_nil + end + end +end diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb new file mode 100644 index 00000000000000..b6e26d8f261722 --- /dev/null +++ b/spec/models/cycle_analytics/review_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe 'CycleAnalytics#review', feature: true do + extend CycleAnalyticsHelpers::TestGeneration + + let(:project) { create(:project) } + let(:from_date) { 10.days.ago } + let(:user) { create(:user, :admin) } + subject { CycleAnalytics.new(project, from: from_date) } + + generate_cycle_analytics_spec( + phase: :review, + data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } }, + start_time_conditions: [["merge request that closes issue is created", + -> (context, data) do + context.create_merge_request_closing_issue(data[:issue]) + end]], + end_time_conditions: [["merge request that closes issue is merged", + -> (context, data) do + context.merge_merge_requests_closing_issue(data[:issue]) + end]], + post_fn: -> (context, data) { context.deploy_master }) + + context "when a regular merge request (that doesn't close the issue) is created and merged" do + it "returns nil" do + 5.times do + MergeRequests::MergeService.new(project, user).execute(create(:merge_request)) + + deploy_master + end + + expect(subject.review).to be_nil + end + end +end diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb new file mode 100644 index 00000000000000..af1c4477ddb58b --- /dev/null +++ b/spec/models/cycle_analytics/staging_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe 'CycleAnalytics#staging', feature: true do + extend CycleAnalyticsHelpers::TestGeneration + + let(:project) { create(:project) } + let(:from_date) { 10.days.ago } + let(:user) { create(:user, :admin) } + subject { CycleAnalytics.new(project, from: from_date) } + + generate_cycle_analytics_spec( + phase: :staging, + data_fn: lambda do |context| + issue = context.create(:issue, project: context.project) + { issue: issue, merge_request: context.create_merge_request_closing_issue(issue) } + end, + start_time_conditions: [["merge request that closes issue is merged", + -> (context, data) do + context.merge_merge_requests_closing_issue(data[:issue]) + end ]], + end_time_conditions: [["merge request that closes issue is deployed to production", + -> (context, data) do + context.deploy_master + end], + ["production deploy happens after merge request is merged (along with other changes)", + lambda do |context, data| + # Make other changes on master + sha = context.project.repository.commit_file( + context.user, + context.random_git_name, + "content", + "commit message", + 'master', + false) + context.project.repository.commit(sha) + + context.deploy_master + end]]) + + context "when a regular merge request (that doesn't close the issue) is merged and deployed" do + it "returns nil" do + 5.times do + merge_request = create(:merge_request) + MergeRequests::MergeService.new(project, user).execute(merge_request) + deploy_master + end + + expect(subject.staging).to be_nil + end + end + + context "when the deployment happens to a non-production environment" do + it "returns nil" do + 5.times do + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + MergeRequests::MergeService.new(project, user).execute(merge_request) + deploy_master(environment: 'staging') + end + + expect(subject.staging).to be_nil + end + end +end diff --git a/spec/models/cycle_analytics/summary_spec.rb b/spec/models/cycle_analytics/summary_spec.rb new file mode 100644 index 00000000000000..743bc2da33fbeb --- /dev/null +++ b/spec/models/cycle_analytics/summary_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe CycleAnalytics::Summary, models: true do + let(:project) { create(:project) } + let(:from) { Time.now } + let(:user) { create(:user, :admin) } + subject { described_class.new(project, from: from) } + + describe "#new_issues" do + it "finds the number of issues created after the 'from date'" do + Timecop.freeze(5.days.ago) { create(:issue, project: project) } + Timecop.freeze(5.days.from_now) { create(:issue, project: project) } + + expect(subject.new_issues).to eq(1) + end + + it "doesn't find issues from other projects" do + Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project)) } + + expect(subject.new_issues).to eq(0) + end + end + + describe "#commits" do + it "finds the number of commits created after the 'from date'" do + Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') } + Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') } + + expect(subject.commits).to eq(1) + end + + it "doesn't find commits from other projects" do + Timecop.freeze(5.days.from_now) { create_commit("Test message", create(:project), user, 'master') } + + expect(subject.commits).to eq(0) + end + end + + describe "#deploys" do + it "finds the number of deploys made created after the 'from date'" do + Timecop.freeze(5.days.ago) { create(:deployment, project: project) } + Timecop.freeze(5.days.from_now) { create(:deployment, project: project) } + + expect(subject.deploys).to eq(1) + end + + it "doesn't find commits from other projects" do + Timecop.freeze(5.days.from_now) { create(:deployment, project: create(:project)) } + + expect(subject.deploys).to eq(0) + end + end +end diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb new file mode 100644 index 00000000000000..89ace0b2742783 --- /dev/null +++ b/spec/models/cycle_analytics/test_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe 'CycleAnalytics#test', feature: true do + extend CycleAnalyticsHelpers::TestGeneration + + let(:project) { create(:project) } + let(:from_date) { 10.days.ago } + let(:user) { create(:user, :admin) } + subject { CycleAnalytics.new(project, from: from_date) } + + generate_cycle_analytics_spec( + phase: :test, + data_fn: lambda do |context| + issue = context.create(:issue, project: context.project) + merge_request = context.create_merge_request_closing_issue(issue) + pipeline = context.create(:ci_pipeline, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, project: context.project) + { pipeline: pipeline, issue: issue } + end, + start_time_conditions: [["pipeline is started", -> (context, data) { data[:pipeline].run! }]], + end_time_conditions: [["pipeline is finished", -> (context, data) { data[:pipeline].succeed! }]], + post_fn: -> (context, data) do + context.merge_merge_requests_closing_issue(data[:issue]) + context.deploy_master + end) + + context "when the pipeline is for a regular merge request (that doesn't close an issue)" do + it "returns nil" do + 5.times do + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) + + pipeline.run! + pipeline.succeed! + + merge_merge_requests_closing_issue(issue) + deploy_master + end + + expect(subject.test).to be_nil + end + end + + context "when the pipeline is not for a merge request" do + it "returns nil" do + 5.times do + pipeline = create(:ci_pipeline, ref: "refs/heads/master", sha: project.repository.commit('master').sha) + + pipeline.run! + pipeline.succeed! + + deploy_master + end + + expect(subject.test).to be_nil + end + end + + context "when the pipeline is dropped (failed)" do + it "returns nil" do + 5.times do + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) + + pipeline.run! + pipeline.drop! + + merge_merge_requests_closing_issue(issue) + deploy_master + end + + expect(subject.test).to be_nil + end + end + + context "when the pipeline is cancelled" do + it "returns nil" do + 5.times do + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) + + pipeline.run! + pipeline.cancel! + + merge_merge_requests_closing_issue(issue) + deploy_master + end + + expect(subject.test).to be_nil + end + end +end diff --git a/spec/models/issue/metrics_spec.rb b/spec/models/issue/metrics_spec.rb new file mode 100644 index 00000000000000..e170b087ebcba1 --- /dev/null +++ b/spec/models/issue/metrics_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Issue::Metrics, models: true do + let(:project) { create(:project) } + + subject { create(:issue, project: project) } + + describe "when recording the default set of issue metrics on issue save" do + context "milestones" do + it "records the first time an issue is associated with a milestone" do + time = Time.now + Timecop.freeze(time) { subject.update(milestone: create(:milestone)) } + metrics = subject.metrics + + expect(metrics).to be_present + expect(metrics.first_associated_with_milestone_at).to be_within(1.second).of(time) + end + + it "does not record the second time an issue is associated with a milestone" do + time = Time.now + Timecop.freeze(time) { subject.update(milestone: create(:milestone)) } + Timecop.freeze(time + 2.hours) { subject.update(milestone: nil) } + Timecop.freeze(time + 6.hours) { subject.update(milestone: create(:milestone)) } + metrics = subject.metrics + + expect(metrics).to be_present + expect(metrics.first_associated_with_milestone_at).to be_within(1.second).of(time) + end + end + + context "list labels" do + it "records the first time an issue is associated with a list label" do + list_label = create(:label, lists: [create(:list)]) + time = Time.now + Timecop.freeze(time) { subject.update(label_ids: [list_label.id]) } + metrics = subject.metrics + + expect(metrics).to be_present + expect(metrics.first_added_to_board_at).to be_within(1.second).of(time) + end + + it "does not record the second time an issue is associated with a list label" do + time = Time.now + first_list_label = create(:label, lists: [create(:list)]) + Timecop.freeze(time) { subject.update(label_ids: [first_list_label.id]) } + second_list_label = create(:label, lists: [create(:list)]) + Timecop.freeze(time + 5.hours) { subject.update(label_ids: [second_list_label.id]) } + metrics = subject.metrics + + expect(metrics).to be_present + expect(metrics.first_added_to_board_at).to be_within(1.second).of(time) + end + end + end +end diff --git a/spec/models/merge_request/metrics_spec.rb b/spec/models/merge_request/metrics_spec.rb new file mode 100644 index 00000000000000..a79dd215d419a3 --- /dev/null +++ b/spec/models/merge_request/metrics_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe MergeRequest::Metrics, models: true do + let(:project) { create(:project) } + + subject { create(:merge_request, source_project: project) } + + describe "when recording the default set of metrics on merge request save" do + it "records the merge time" do + time = Time.now + Timecop.freeze(time) { subject.mark_as_merged } + metrics = subject.metrics + + expect(metrics).to be_present + expect(metrics.merged_at).to be_within(1.second).of(time) + end + end +end diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb index 41b897f36cd104..343b4385bf2525 100644 --- a/spec/services/create_deployment_service_spec.rb +++ b/spec/services/create_deployment_service_spec.rb @@ -169,4 +169,83 @@ end end end + + describe "merge request metrics" do + let(:params) do + { + environment: 'production', + ref: 'master', + tag: false, + sha: '97de212e80737a608d939f648d959671fb0a0142b', + } + end + + let(:merge_request) { create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: project) } + + context "while updating the 'first_deployed_to_production_at' time" do + before { merge_request.mark_as_merged } + + context "for merge requests merged before the current deploy" do + it "sets the time if the deploy's environment is 'production'" do + time = Time.now + Timecop.freeze(time) { service.execute } + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_within(1.second).of(time) + end + + it "doesn't set the time if the deploy's environment is not 'production'" do + staging_params = params.merge(environment: 'staging') + service = described_class.new(project, user, staging_params) + service.execute + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil + end + + it 'does not raise errors if the merge request does not have a metrics record' do + merge_request.metrics.destroy + + expect(merge_request.reload.metrics).to be_nil + expect { service.execute }.not_to raise_error + end + end + + context "for merge requests merged before the previous deploy" do + context "if the 'first_deployed_to_production_at' time is already set" do + it "does not overwrite the older 'first_deployed_to_production_at' time" do + # Previous deploy + time = Time.now + Timecop.freeze(time) { service.execute } + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_within(1.second).of(time) + + # Current deploy + service = described_class.new(project, user, params) + Timecop.freeze(time + 12.hours) { service.execute } + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_within(1.second).of(time) + end + end + + context "if the 'first_deployed_to_production_at' time is not already set" do + it "does not overwrite the older 'first_deployed_to_production_at' time" do + # Previous deploy + time = 5.minutes.from_now + Timecop.freeze(time) { service.execute } + + expect(merge_request.reload.metrics.merged_at).to be < merge_request.reload.metrics.first_deployed_to_production_at + + merge_request.reload.metrics.update(first_deployed_to_production_at: nil) + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil + + # Current deploy + service = described_class.new(project, user, params) + Timecop.freeze(time + 12.hours) { service.execute } + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil + end + end + end + end + end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 22724434a7f923..22991c5bc8647e 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -339,6 +339,43 @@ end end + describe "issue metrics" do + let(:issue) { create :issue, project: project } + let(:commit_author) { create :user } + let(:commit) { project.commit } + let(:commit_time) { Time.now } + + before do + project.team << [commit_author, :developer] + project.team << [user, :developer] + + allow(commit).to receive_messages( + safe_message: "this commit \n mentions #{issue.to_reference}", + references: [issue], + author_name: commit_author.name, + author_email: commit_author.email, + committed_date: commit_time + ) + + allow(project.repository).to receive(:commits_between).and_return([commit]) + end + + context "while saving the 'first_mentioned_in_commit_at' metric for an issue" do + it 'sets the metric for referenced issues' do + execute_service(project, user, @oldrev, @newrev, @ref) + + expect(issue.reload.metrics.first_mentioned_in_commit_at).to be_within(1.second).of(commit_time) + end + + it 'does not set the metric for non-referenced issues' do + non_referenced_issue = create(:issue, project: project) + execute_service(project, user, @oldrev, @newrev, @ref) + + expect(non_referenced_issue.reload.metrics.first_mentioned_in_commit_at).to be_nil + end + end + end + describe "closing issues from pushed commits containing a closing reference" do let(:issue) { create :issue, project: project } let(:other_issue) { create :issue, project: project } diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index c1e4f8bd96b019..b81428890756d6 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -83,5 +83,34 @@ } end end + + context 'while saving references to issues that the created merge request closes' do + let(:first_issue) { create(:issue, project: project) } + let(:second_issue) { create(:issue, project: project) } + + let(:opts) do + { + title: 'Awesome merge_request', + source_branch: 'feature', + target_branch: 'master', + force_remove_source_branch: '1' + } + end + + before do + project.team << [user, :master] + project.team << [assignee, :developer] + end + + it 'creates a `MergeRequestsClosingIssues` record for each issue' do + issue_closing_opts = opts.merge(description: "Closes #{first_issue.to_reference} and #{second_issue.to_reference}") + service = described_class.new(project, user, issue_closing_opts) + allow(service).to receive(:execute_hooks) + merge_request = service.execute + + issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id) + expect(issue_ids).to match_array([first_issue.id, second_issue.id]) + end + end end end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index fff86480c6d777..a162df5fc3439c 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -174,6 +174,58 @@ end end + context 'merge request metrics' do + let(:issue) { create :issue, project: @project } + let(:commit_author) { create :user } + let(:commit) { project.commit } + + before do + project.team << [commit_author, :developer] + project.team << [user, :developer] + + allow(commit).to receive_messages( + safe_message: "Closes #{issue.to_reference}", + references: [issue], + author_name: commit_author.name, + author_email: commit_author.email, + committed_date: Time.now + ) + + allow_any_instance_of(MergeRequest).to receive(:commits).and_return([commit]) + end + + context 'when the merge request is sourced from the same project' do + it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do + merge_request = create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: @project) + refresh_service = service.new(@project, @user) + allow(refresh_service).to receive(:execute_hooks) + refresh_service.execute(@oldrev, @newrev, 'refs/heads/feature') + + issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id) + expect(issue_ids).to eq([issue.id]) + end + end + + context 'when the merge request is sourced from a different project' do + it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do + forked_project = create(:project) + create(:forked_project_link, forked_to_project: forked_project, forked_from_project: @project) + + merge_request = create(:merge_request, + target_branch: 'master', + source_branch: 'feature', + target_project: @project, + source_project: forked_project) + refresh_service = service.new(@project, @user) + allow(refresh_service).to receive(:execute_hooks) + refresh_service.execute(@oldrev, @newrev, 'refs/heads/feature') + + issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id) + expect(issue_ids).to eq([issue.id]) + end + end + end + def reload_mrs @merge_request.reload @fork_merge_request.reload diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 6dfeb581975e13..33db34c0f62a3c 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -263,5 +263,42 @@ def update_merge_request(opts) end end end + + context 'while saving references to issues that the updated merge request closes' do + let(:first_issue) { create(:issue, project: project) } + let(:second_issue) { create(:issue, project: project) } + + it 'creates a `MergeRequestsClosingIssues` record for each issue' do + issue_closing_opts = { description: "Closes #{first_issue.to_reference} and #{second_issue.to_reference}" } + service = described_class.new(project, user, issue_closing_opts) + allow(service).to receive(:execute_hooks) + service.execute(merge_request) + + issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id) + expect(issue_ids).to match_array([first_issue.id, second_issue.id]) + end + + it 'removes `MergeRequestsClosingIssues` records when issues are not closed anymore' do + opts = { + title: 'Awesome merge_request', + description: "Closes #{first_issue.to_reference} and #{second_issue.to_reference}", + source_branch: 'feature', + target_branch: 'master', + force_remove_source_branch: '1' + } + + merge_request = MergeRequests::CreateService.new(project, user, opts).execute + + issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id) + expect(issue_ids).to match_array([first_issue.id, second_issue.id]) + + service = described_class.new(project, user, description: "not closing any issues") + allow(service).to receive(:execute_hooks) + service.execute(merge_request.reload) + + issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id) + expect(issue_ids).to be_empty + end + end end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index f81a58899fd819..0d152534c3843e 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -379,6 +379,7 @@ def send_notifications(*new_mentions) it "emails subscribers of the issue's labels" do subscriber = create(:user) label = create(:label, issues: [issue]) + issue.reload label.toggle_subscription(subscriber) notification.new_issue(issue, @u_disabled) @@ -399,6 +400,7 @@ def send_notifications(*new_mentions) project.team << [guest, :guest] label = create(:label, issues: [confidential_issue]) + confidential_issue.reload label.toggle_subscription(non_member) label.toggle_subscription(author) label.toggle_subscription(assignee) diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb new file mode 100644 index 00000000000000..e8e760a618739d --- /dev/null +++ b/spec/support/cycle_analytics_helpers.rb @@ -0,0 +1,64 @@ +module CycleAnalyticsHelpers + def create_commit_referencing_issue(issue, branch_name: random_git_name) + project.repository.add_branch(user, branch_name, 'master') + create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name) + end + + def create_commit(message, project, user, branch_name) + filename = random_git_name + oldrev = project.repository.commit(branch_name).sha + + options = { + committer: project.repository.user_to_committer(user), + author: project.repository.user_to_committer(user), + commit: { message: message, branch: branch_name, update_ref: true }, + file: { content: "content", path: filename, update: false } + } + + commit_sha = Gitlab::Git::Blob.commit(project.repository, options) + project.repository.commit(commit_sha) + + GitPushService.new(project, + user, + oldrev: oldrev, + newrev: commit_sha, + ref: 'refs/heads/master').execute + end + + def create_merge_request_closing_issue(issue, message: nil, source_branch: nil) + if !source_branch || project.repository.commit(source_branch).blank? + source_branch = random_git_name + project.repository.add_branch(user, source_branch, 'master') + end + + sha = project.repository.commit_file(user, random_git_name, "content", "commit message", source_branch, false) + project.repository.commit(sha) + + opts = { + title: 'Awesome merge_request', + description: message || "Fixes #{issue.to_reference}", + source_branch: source_branch, + target_branch: 'master' + } + + MergeRequests::CreateService.new(project, user, opts).execute + end + + def merge_merge_requests_closing_issue(issue) + merge_requests = issue.closed_by_merge_requests + merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user).execute(merge_request) } + end + + def deploy_master(environment: 'production') + CreateDeploymentService.new(project, user, { + environment: environment, + ref: 'master', + tag: false, + sha: project.repository.commit('master').sha + }).execute + end +end + +RSpec.configure do |config| + config.include CycleAnalyticsHelpers +end diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb new file mode 100644 index 00000000000000..8e19a6c92e2e29 --- /dev/null +++ b/spec/support/cycle_analytics_helpers/test_generation.rb @@ -0,0 +1,161 @@ +# rubocop:disable Metrics/AbcSize + +# Note: The ABC size is large here because we have a method generating test cases with +# multiple nested contexts. This shouldn't count as a violation. + +module CycleAnalyticsHelpers + module TestGeneration + # Generate the most common set of specs that all cycle analytics phases need to have. + # + # Arguments: + # + # phase: Which phase are we testing? Will call `CycleAnalytics.new.send(phase)` for the final assertion + # data_fn: A function that returns a hash, constituting initial data for the test case + # start_time_conditions: An array of `conditions`. Each condition is an tuple of `condition_name` and `condition_fn`. `condition_fn` is called with + # `context` (no lexical scope, so need to do `context.create` for factories, for example) and `data` (from the `data_fn`). + # Each `condition_fn` is expected to implement a case which consitutes the start of the given cycle analytics phase. + # end_time_conditions: An array of `conditions`. Each condition is an tuple of `condition_name` and `condition_fn`. `condition_fn` is called with + # `context` (no lexical scope, so need to do `context.create` for factories, for example) and `data` (from the `data_fn`). + # Each `condition_fn` is expected to implement a case which consitutes the end of the given cycle analytics phase. + # before_end_fn: This function is run before calling the end time conditions. Used for setup that needs to be run between the start and end conditions. + # post_fn: Code that needs to be run after running the end time conditions. + + def generate_cycle_analytics_spec(phase:, data_fn:, start_time_conditions:, end_time_conditions:, before_end_fn: nil, post_fn: nil) + combinations_of_start_time_conditions = (1..start_time_conditions.size).flat_map { |size| start_time_conditions.combination(size).to_a } + combinations_of_end_time_conditions = (1..end_time_conditions.size).flat_map { |size| end_time_conditions.combination(size).to_a } + + scenarios = combinations_of_start_time_conditions.product(combinations_of_end_time_conditions) + scenarios.each do |start_time_conditions, end_time_conditions| + context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do + context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do + it "finds the median of available durations between the two conditions" do + time_differences = Array.new(5) do |index| + data = data_fn[self] + start_time = (index * 10).days.from_now + end_time = start_time + rand(1..5).days + + start_time_conditions.each do |condition_name, condition_fn| + Timecop.freeze(start_time) { condition_fn[self, data] } + end + + # Run `before_end_fn` at the midpoint between `start_time` and `end_time` + Timecop.freeze(start_time + (end_time - start_time) / 2) { before_end_fn[self, data] } if before_end_fn + + end_time_conditions.each do |condition_name, condition_fn| + Timecop.freeze(end_time) { condition_fn[self, data] } + end + + Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn + + end_time - start_time + end + + median_time_difference = time_differences.sort[2] + expect(subject.send(phase)).to be_within(5).of(median_time_difference) + end + + context "when the data belongs to another project" do + let(:other_project) { create(:project) } + + it "returns nil" do + # Use a stub to "trick" the data/condition functions + # into using another project. This saves us from having to + # define separate data/condition functions for this particular + # test case. + allow(self).to receive(:project) { other_project } + + 5.times do + data = data_fn[self] + start_time = Time.now + end_time = rand(1..10).days.from_now + + start_time_conditions.each do |condition_name, condition_fn| + Timecop.freeze(start_time) { condition_fn[self, data] } + end + + end_time_conditions.each do |condition_name, condition_fn| + Timecop.freeze(end_time) { condition_fn[self, data] } + end + + Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn + end + + # Turn off the stub before checking assertions + allow(self).to receive(:project).and_call_original + + expect(subject.send(phase)).to be_nil + end + end + + context "when the end condition happens before the start condition" do + it 'returns nil' do + data = data_fn[self] + start_time = Time.now + end_time = start_time + rand(1..5).days + + # Run `before_end_fn` at the midpoint between `start_time` and `end_time` + Timecop.freeze(start_time + (end_time - start_time) / 2) { before_end_fn[self, data] } if before_end_fn + + end_time_conditions.each do |condition_name, condition_fn| + Timecop.freeze(start_time) { condition_fn[self, data] } + end + + start_time_conditions.each do |condition_name, condition_fn| + Timecop.freeze(end_time) { condition_fn[self, data] } + end + + Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn + + expect(subject.send(phase)).to be_nil + end + end + end + end + + context "start condition NOT PRESENT: #{start_time_conditions.map(&:first).to_sentence}" do + context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do + it "returns nil" do + 5.times do + data = data_fn[self] + end_time = rand(1..10).days.from_now + + end_time_conditions.each_with_index do |(condition_name, condition_fn), index| + Timecop.freeze(end_time + index.days) { condition_fn[self, data] } + end + + Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn + end + + expect(subject.send(phase)).to be_nil + end + end + end + + context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do + context "end condition NOT PRESENT: #{end_time_conditions.map(&:first).to_sentence}" do + it "returns nil" do + 5.times do + data = data_fn[self] + start_time = Time.now + + start_time_conditions.each do |condition_name, condition_fn| + Timecop.freeze(start_time) { condition_fn[self, data] } + end + + post_fn[self, data] if post_fn + end + + expect(subject.send(phase)).to be_nil + end + end + end + end + + context "when none of the start / end conditions are matched" do + it "returns nil" do + expect(subject.send(phase)).to be_nil + end + end + end + end +end diff --git a/spec/support/git_helpers.rb b/spec/support/git_helpers.rb new file mode 100644 index 00000000000000..93422390ef72ef --- /dev/null +++ b/spec/support/git_helpers.rb @@ -0,0 +1,9 @@ +module GitHelpers + def random_git_name + "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}" + end +end + +RSpec.configure do |config| + config.include GitHelpers +end -- GitLab From 2f54abc50f0888ef1f5d134d23a6f28f8d43d37e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 21 Sep 2016 15:22:28 +0000 Subject: [PATCH 09/49] Merge branch 'and-you-get-awards' into 'master' And Snippets get awards ## What does this MR do? Makes snippets more awesome, by making them awardables ## Why was this MR needed? Because Snippets were left behind. ## What are the relevant issue numbers? Closes #17878 See merge request !4456 --- CHANGELOG | 1 + app/assets/stylesheets/pages/snippets.scss | 7 +++ .../concerns/toggle_award_emoji.rb | 8 ++- .../projects/snippets_controller.rb | 5 +- app/controllers/snippets_controller.rb | 3 ++ app/helpers/award_emoji_helper.rb | 9 ++++ app/helpers/gitlab_routing_helper.rb | 8 +++ app/models/concerns/awardable.rb | 6 +++ app/models/concerns/issuable.rb | 4 -- app/models/note.rb | 4 -- app/models/snippet.rb | 1 + app/views/award_emoji/_awards_block.html.haml | 2 +- .../projects/snippets/_actions.html.haml | 2 +- app/views/projects/snippets/show.html.haml | 23 ++++---- app/views/snippets/show.html.haml | 2 + config/routes.rb | 18 +++---- doc/api/award_emoji.md | 15 ++++-- lib/api/award_emoji.rb | 29 +++++----- spec/controllers/snippets_controller_spec.rb | 33 +++++++++++- spec/models/snippet_spec.rb | 2 + spec/requests/api/award_emoji_spec.rb | 54 ++++++++++++++++++- 21 files changed, 184 insertions(+), 52 deletions(-) create mode 100644 app/helpers/award_emoji_helper.rb diff --git a/CHANGELOG b/CHANGELOG index 240ee46043e69b..baf99bc2732a14 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -51,6 +51,7 @@ v 8.12.0 (unreleased) - Move parsing of sidekiq ps into helper !6245 (pascalbetz) - Added go to issue boards keyboard shortcut - Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel) + - Emoji can be awarded on Snippets !4456 - Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling) - Fix blame table layout width - Fix bug where pagination is still displayed despite all todos marked as done (ClemMakesApps) diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index 5270aea4e797f3..4d5df566d9bf57 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -12,11 +12,18 @@ .snippet-file-content { border-radius: 3px; + margin-bottom: $gl-padding; + .btn-clipboard { @extend .btn; } } +.project-snippets .awards { + border-bottom: 1px solid $table-border-color; + padding-bottom: $gl-padding; +} + .snippet-title { font-size: 24px; font-weight: 600; diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb index 172d5344b7a159..3717c49f272f4d 100644 --- a/app/controllers/concerns/toggle_award_emoji.rb +++ b/app/controllers/concerns/toggle_award_emoji.rb @@ -10,7 +10,9 @@ def toggle_award_emoji if awardable.user_can_award?(current_user, name) awardable.toggle_award_emoji(name, current_user) - TodoService.new.new_award_emoji(to_todoable(awardable), current_user) + + todoable = to_todoable(awardable) + TodoService.new.new_award_emoji(todoable, current_user) if todoable render json: { ok: true } else @@ -24,8 +26,10 @@ def to_todoable(awardable) case awardable when Note awardable.noteable - else + when MergeRequest, Issue awardable + when Snippet + nil end end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 17ceefec3b86ff..e290a0eadda814 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -1,6 +1,8 @@ class Projects::SnippetsController < Projects::ApplicationController + include ToggleAwardEmoji + before_action :module_enabled - before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji] # Allow read any snippet before_action :authorize_read_project_snippet!, except: [:new, :create, :index] @@ -80,6 +82,7 @@ def raw def snippet @snippet ||= @project.snippets.find(params[:id]) end + alias_method :awardable, :snippet def authorize_read_project_snippet! return render_404 unless can?(current_user, :read_project_snippet, @snippet) diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 2a17c1f34db282..d198782138a380 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,4 +1,6 @@ class SnippetsController < ApplicationController + include ToggleAwardEmoji + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] # Allow read snippet @@ -85,6 +87,7 @@ def snippet PersonalSnippet.find(params[:id]) end end + alias_method :awardable, :snippet def authorize_read_snippet! authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet) diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb new file mode 100644 index 00000000000000..aa134cea31c6eb --- /dev/null +++ b/app/helpers/award_emoji_helper.rb @@ -0,0 +1,9 @@ +module AwardEmojiHelper + def toggle_award_url(awardable) + if @project + url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) + else + url_for([:toggle_award_emoji, awardable]) + end + end +end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 5b71113feb90aa..001319f4793c7b 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -102,6 +102,14 @@ def toggle_subscription_path(entity, *args) end end + def toggle_award_emoji_personal_snippet_path(*args) + toggle_award_emoji_snippet_path(*args) + end + + def toggle_award_emoji_namespace_project_project_snippet_path(*args) + toggle_award_emoji_namespace_project_snippet_path(*args) + end + ## Members def project_members_url(project, *args) namespace_project_project_members_url(project.namespace, project) diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index d8d4575bb4dff2..073ac4c1b65ef4 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -71,6 +71,12 @@ def user_can_award?(current_user, name) end end + def user_authored?(current_user) + author = self.respond_to?(:author) ? self.author : self.user + + author == current_user + end + def awarded_emoji?(emoji_name, current_user) award_emoji.where(name: emoji_name, user: current_user).exists? end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 1650ac9fcbe22c..ff465d2c745184 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -200,10 +200,6 @@ def user_notes_count end end - def user_authored?(user) - user == author - end - def subscribed_without_subscriptions?(user) participants(user).include?(user) end diff --git a/app/models/note.rb b/app/models/note.rb index b94e3cff2cec8f..f2656df028b2a2 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -223,10 +223,6 @@ def has_referenced_mentionables?(user) end end - def user_authored?(user) - user == author - end - def award_emoji? can_be_award_emoji? && contains_emoji_only? end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 5ec933601ac8d2..8a1730f3f36a11 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -4,6 +4,7 @@ class Snippet < ActiveRecord::Base include Participable include Referable include Sortable + include Awardable default_value_for :visibility_level, Snippet::PRIVATE diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index 02efcecc889568..fbe3ab912b615f 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -1,5 +1,5 @@ - grouped_emojis = awardable.grouped_awards(with_thumbs: inline) -.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } } +.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } } - awards_sort(grouped_emojis).each do |emoji, awards| %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } } = emoji_icon(emoji, sprite: false) diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index a5a5619fa128c8..4aa4ab46a2f16a 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -3,7 +3,7 @@ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New Snippet" do New Snippet - if can?(current_user, :update_project_snippet, @snippet) - = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-warning", title: 'Delete Snippet' do + = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do Delete - if can?(current_user, :update_project_snippet, @snippet) = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index b70fda88a799d0..9503dbded13ae4 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -2,13 +2,16 @@ = render 'shared/snippets/header' -%article.file-holder.snippet-file-content - .file-title - = blob_icon 0, @snippet.file_name - = @snippet.file_name - .file-actions - = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") - = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" - = render 'shared/snippets/blob' - -%div#notes= render "projects/notes/notes_with_form" +.project-snippets + %article.file-holder.snippet-file-content + .file-title + = blob_icon 0, @snippet.file_name + = @snippet.file_name + .file-actions + = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") + = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" + = render 'shared/snippets/blob' + + = render 'award_emoji/awards_block', awardable: @snippet, inline: true + + %div#notes= render "projects/notes/notes_with_form" diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index fa403da8f79625..cd89155c616b66 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -10,3 +10,5 @@ = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" = render 'shared/snippets/blob' + += render 'award_emoji/awards_block', awardable: @snippet, inline: true \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index c4eee59e7aa8f2..4d6ec699cbde2a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -35,6 +35,10 @@ post :approve_access_request, on: :member end + concern :awardable do + post :toggle_award_emoji, on: :member + end + namespace :ci do # CI API Ci::API::API.logger Rails.logger @@ -98,7 +102,7 @@ # # Global snippets # - resources :snippets do + resources :snippets, concerns: :awardable do member do get 'raw' end @@ -110,7 +114,6 @@ # # Invites # - resources :invites, only: [:show], constraints: { id: /[A-Za-z0-9_-]+/ } do member do post :accept @@ -662,7 +665,7 @@ end end - resources :snippets, constraints: { id: /\d+/ } do + resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do member do get 'raw' end @@ -724,7 +727,7 @@ end end - resources :merge_requests, constraints: { id: /\d+/ } do + resources :merge_requests, concerns: :awardable, constraints: { id: /\d+/ } do member do get :commits get :diffs @@ -736,7 +739,6 @@ post :cancel_merge_when_build_succeeds get :ci_status post :toggle_subscription - post :toggle_award_emoji post :remove_wip get :diff_for_path post :resolve_conflicts @@ -840,10 +842,9 @@ end end - resources :issues, constraints: { id: /\d+/ } do + resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do member do post :toggle_subscription - post :toggle_award_emoji post :mark_as_spam get :referenced_merge_requests get :related_branches @@ -871,9 +872,8 @@ resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ } - resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do + resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do member do - post :toggle_award_emoji delete :delete_attachment post :resolve delete :resolve, action: :unresolve diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md index 72ec99b7c56f3f..c464e3f3f71261 100644 --- a/doc/api/award_emoji.md +++ b/doc/api/award_emoji.md @@ -1,12 +1,13 @@ # Award Emoji -> [Introduced][ce-4575] in GitLab 8.9. +> [Introduced][ce-4575] in GitLab 8.9, Snippet support in 8.12 + An awarded emoji tells a thousand words, and can be awarded on issues, merge -requests and notes/comments. Issues, merge requests and notes are further called +requests, snippets, and notes/comments. Issues, merge requests, snippets, and notes are further called `awardables`. -## Issues and merge requests +## Issues, merge requests, and snippets ### List an awardable's award emoji @@ -15,6 +16,7 @@ Gets a list of all award emoji ``` GET /projects/:id/issues/:issue_id/award_emoji GET /projects/:id/merge_requests/:merge_request_id/award_emoji +GET /projects/:id/snippets/:snippet_id/award_emoji ``` Parameters: @@ -69,11 +71,12 @@ Example Response: ### Get single award emoji -Gets a single award emoji from an issue or merge request. +Gets a single award emoji from an issue, snippet, or merge request. ``` GET /projects/:id/issues/:issue_id/award_emoji/:award_id GET /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id +GET /projects/:id/snippets/:snippet_id/award_emoji/:award_id ``` Parameters: @@ -116,6 +119,7 @@ This end point creates an award emoji on the specified resource ``` POST /projects/:id/issues/:issue_id/award_emoji POST /projects/:id/merge_requests/:merge_request_id/award_emoji +POST /projects/:id/snippets/:snippet_id/award_emoji ``` Parameters: @@ -159,6 +163,7 @@ admins or the author of the award. Status code 200 on success, 401 if unauthoriz ``` DELETE /projects/:id/issues/:issue_id/award_emoji/:award_id DELETE /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id +DELETE /projects/:id/snippets/:snippet_id/award_emoji/:award_id ``` Parameters: @@ -197,7 +202,7 @@ Example Response: ## Award Emoji on Notes The endpoints documented above are available for Notes as well. Notes -are a sub-resource of Issues and Merge Requests. The examples below +are a sub-resource of Issues, Merge Requests, or Snippets. The examples below describe working with Award Emoji on notes for an Issue, but can be easily adapted for notes on a Merge Request. diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb index 7c22b17e4e507a..2461a783ea83ab 100644 --- a/lib/api/award_emoji.rb +++ b/lib/api/award_emoji.rb @@ -1,12 +1,12 @@ module API class AwardEmoji < Grape::API before { authenticate! } - AWARDABLES = [Issue, MergeRequest] + AWARDABLES = %w[issue merge_request snippet] resource :projects do AWARDABLES.each do |awardable_type| - awardable_string = awardable_type.to_s.underscore.pluralize - awardable_id_string = "#{awardable_type.to_s.underscore}_id" + awardable_string = awardable_type.pluralize + awardable_id_string = "#{awardable_type}_id" [ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji", ":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji" @@ -87,9 +87,7 @@ class AwardEmoji < Grape::API helpers do def can_read_awardable? - ability = "read_#{awardable.class.to_s.underscore}".to_sym - - can?(current_user, ability, awardable) + can?(current_user, read_ability(awardable), awardable) end def can_award_awardable? @@ -100,18 +98,25 @@ def awardable @awardable ||= begin if params.include?(:note_id) - noteable.notes.find(params[:note_id]) + note_id = params.delete(:note_id) + + awardable.notes.find(note_id) + elsif params.include?(:issue_id) + user_project.issues.find(params[:issue_id]) + elsif params.include?(:merge_request_id) + user_project.merge_requests.find(params[:merge_request_id]) else - noteable + user_project.snippets.find(params[:snippet_id]) end end end - def noteable - if params.include?(:issue_id) - user_project.issues.find(params[:issue_id]) + def read_ability(awardable) + case awardable + when Note + read_ability(awardable.noteable) else - user_project.merge_requests.find(params[:merge_request_id]) + :"read_#{awardable.class.to_s.underscore}" end end end diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index 2a89159c0706d2..41d263a46a4d64 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' describe SnippetsController do - describe 'GET #show' do - let(:user) { create(:user) } + let(:user) { create(:user) } + describe 'GET #show' do context 'when the personal snippet is private' do let(:personal_snippet) { create(:personal_snippet, :private, author: user) } @@ -230,4 +230,33 @@ end end end + + context 'award emoji on snippets' do + let(:personal_snippet) { create(:personal_snippet, :public, author: user) } + let(:another_user) { create(:user) } + + before do + sign_in(another_user) + end + + describe 'POST #toggle_award_emoji' do + it "toggles the award emoji" do + expect do + post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup") + end.to change { personal_snippet.award_emoji.count }.from(0).to(1) + + expect(response.status).to eq(200) + end + + it "removes the already awarded emoji" do + post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup") + + expect do + post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup") + end.to change { personal_snippet.award_emoji.count }.from(1).to(0) + + expect(response.status).to eq(200) + end + end + end end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index 0621c6a06ce68a..e6bc5296398046 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -9,12 +9,14 @@ it { is_expected.to include_module(Participable) } it { is_expected.to include_module(Referable) } it { is_expected.to include_module(Sortable) } + it { is_expected.to include_module(Awardable) } end describe 'associations' do it { is_expected.to belong_to(:author).class_name('User') } it { is_expected.to belong_to(:project) } it { is_expected.to have_many(:notes).dependent(:destroy) } + it { is_expected.to have_many(:award_emoji).dependent(:destroy) } end describe 'validation' do diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index 981a6791881058..5ad4fc4865aebf 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -3,7 +3,7 @@ describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } - let!(:project) { create(:project) } + let!(:project) { create(:empty_project) } let(:issue) { create(:issue, project: project) } let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) } let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } @@ -39,6 +39,19 @@ end end + context 'on a snippet' do + let(:snippet) { create(:project_snippet, :public, project: project) } + let!(:award) { create(:award_emoji, awardable: snippet) } + + it 'returns the awarded emoji' do + get api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(award.name) + end + end + context 'when the user has no access' do it 'returns a status code 404' do user1 = create(:user) @@ -91,6 +104,20 @@ end end + context 'on a snippet' do + let(:snippet) { create(:project_snippet, :public, project: project) } + let!(:award) { create(:award_emoji, awardable: snippet) } + + it 'returns the awarded emoji' do + get api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(award.name) + expect(json_response['awardable_id']).to eq(snippet.id) + expect(json_response['awardable_type']).to eq("Snippet") + end + end + context 'when the user has no access' do it 'returns a status code 404' do user1 = create(:user) @@ -160,6 +187,18 @@ end end end + + context 'on a snippet' do + it 'creates a new award emoji' do + snippet = create(:project_snippet, :public, project: project) + + post api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user), name: 'blowfish' + + expect(response).to have_http_status(201) + expect(json_response['name']).to eq('blowfish') + expect(json_response['user']['username']).to eq(user.username) + end + end end describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do @@ -229,6 +268,19 @@ expect(response).to have_http_status(404) end end + + context 'when the awardable is a Snippet' do + let(:snippet) { create(:project_snippet, :public, project: project) } + let!(:award) { create(:award_emoji, awardable: snippet, user: user) } + + it 'deletes the award' do + expect do + delete api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user) + end.to change { snippet.award_emoji.count }.from(1).to(0) + + expect(response).to have_http_status(200) + end + end end describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do -- GitLab From 33bb5ddf45897f50b6a72e7357520c79a1dd088a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 21 Sep 2016 13:15:41 +0000 Subject: [PATCH 10/49] Merge branch 'show-all-pipelines-from-all-diffs' into 'master' Show all pipelines from all merge_request_diffs This way we could also show pipelines from commits which were discarded due to a force push. Closes #21889 See merge request !6414 --- CHANGELOG | 1 + app/models/merge_request.rb | 21 ++++++++--- app/models/merge_request_diff.rb | 8 +++++ spec/models/merge_request_spec.rb | 59 +++++++++++++++++++++++++++---- 4 files changed, 79 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index baf99bc2732a14..8b1a1690677046 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -45,6 +45,7 @@ v 8.12.0 (unreleased) - Added horizontal padding on build page sidebar on code coverage block. !6196 (Vitaly Baev) - Change merge_error column from string to text type - Reduce contributions calendar data payload (ClemMakesApps) + - Show all pipelines for merge requests even from discarded commits !6414 - Replace contributions calendar timezone payload with dates (ClemMakesApps) - Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel) - Enable pipeline events by default !6278 diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 616efaf3c42108..c3904fd8a2e45c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -761,10 +761,23 @@ def pipeline end def all_pipelines - @all_pipelines ||= - if diff_head_sha && source_project - source_project.pipelines.order(id: :desc).where(sha: commits_sha, ref: source_branch) - end + return unless source_project + + @all_pipelines ||= begin + sha = if persisted? + all_commits_sha + else + diff_head_sha + end + + source_project.pipelines.order(id: :desc). + where(sha: sha, ref: source_branch) + end + end + + # Note that this could also return SHA from now dangling commits + def all_commits_sha + merge_request_diffs.flat_map(&:commits_sha).uniq end def merge_commit diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 18c583add88481..7362886e9f55df 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -117,6 +117,14 @@ def head_commit project.commit(head_commit_sha) end + def commits_sha + if @commits + commits.map(&:sha) + else + st_commits.map { |commit| commit[:id] } + end + end + def diff_refs return unless start_commit_sha || base_commit_sha diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 06feeb1bbba6d8..38c2a28d3e1ac3 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -495,15 +495,62 @@ end describe '#all_pipelines' do - let!(:pipelines) do - subject.merge_request_diff.commits.map do |commit| - create(:ci_empty_pipeline, project: subject.source_project, sha: commit.id, ref: subject.source_branch) + shared_examples 'returning pipelines with proper ordering' do + let!(:all_pipelines) do + subject.all_commits_sha.map do |sha| + create(:ci_empty_pipeline, + project: subject.source_project, + sha: sha, + ref: subject.source_branch) + end + end + + it 'returns all pipelines' do + expect(subject.all_pipelines).not_to be_empty + expect(subject.all_pipelines).to eq(all_pipelines.reverse) + end + end + + context 'with single merge_request_diffs' do + it_behaves_like 'returning pipelines with proper ordering' + end + + context 'with multiple irrelevant merge_request_diffs' do + before do + subject.update(target_branch: 'markdown') + end + + it_behaves_like 'returning pipelines with proper ordering' + end + + context 'with unsaved merge request' do + subject { build(:merge_request) } + + let!(:pipeline) do + create(:ci_empty_pipeline, + project: subject.project, + sha: subject.diff_head_sha, + ref: subject.source_branch) end + + it 'returns pipelines from diff_head_sha' do + expect(subject.all_pipelines).to contain_exactly(pipeline) + end + end + end + + describe '#all_commits_sha' do + let(:all_commits_sha) do + subject.merge_request_diffs.flat_map(&:commits).map(&:sha).uniq + end + + before do + subject.update(target_branch: 'markdown') end - it 'returns a pipelines from source projects with proper ordering' do - expect(subject.all_pipelines).not_to be_empty - expect(subject.all_pipelines).to eq(pipelines.reverse) + it 'returns all SHA from all merge_request_diffs' do + expect(subject.merge_request_diffs.size).to eq(2) + expect(subject.all_commits_sha).to eq(all_commits_sha) end end -- GitLab From b4dbc373044af3fada67b063366a70ebb578d5ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 21 Sep 2016 12:04:07 +0000 Subject: [PATCH 11/49] Merge branch 'limit-number-of-shown-environments' into 'master' Limit number of shown environments ## What does this MR do? This MR limits in context of Merge Request a list of shown environments. Previously we would show all environments containing the SHA of the head commit of Merge Request. However, with introducing of dynamically created environments this lead to a cases that we would show multiple review apps, for different branches, because these branches would contain a new questioned commit. This MR changes what environments we test against presence of the commit, to: 1. We look for environments with deployments to source_branch of source_project: used for deployments to per-branch environments, 2. We look for environments with deployments to target_branch of target_project: used for deployments to staging / production environments, 3. We look for environments with deployments for tags on target_project: used for staging / production environments. ## Why was this MR needed? To improve a list of returned environments when we introduced ability to create dynamic environments for review apps: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6323 See merge request !6438 --- CHANGELOG | 1 + app/helpers/gitlab_routing_helper.rb | 4 ++ app/models/merge_request.rb | 9 ++-- app/models/project.rb | 16 ++++++ .../merge_requests/widget/_heading.html.haml | 25 ++++----- spec/models/merge_request_spec.rb | 53 ++++++++++++++++--- spec/models/project_spec.rb | 41 ++++++++++++++ .../merge_requests/_heading.html.haml_spec.rb | 2 + 8 files changed, 130 insertions(+), 21 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8b1a1690677046..afbbbc043dafd0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -25,6 +25,7 @@ v 8.12.0 (unreleased) - Cycle analytics (first iteration) !5986 - Remove vendor prefixes for linear-gradient CSS (ClemMakesApps) - Move pushes_since_gc from the database to Redis + - Limit number of shown environments on Merge Request: show only environments for target_branch, source_branch and tags - Add font color contrast to external label in admin area (ClemMakesApps) - Change logo animation to CSS (ClemMakesApps) - Instructions for enabling Git packfile bitmaps !6104 diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 001319f4793c7b..670a7ca36f48f5 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -70,6 +70,10 @@ def runner_path(runner, *args) namespace_project_runner_path(@project.namespace, @project, runner, *args) end + def environment_path(environment, *args) + namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args) + end + def issue_path(entity, *args) namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args) end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index c3904fd8a2e45c..2dcf7f89bfc092 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -670,9 +670,12 @@ def mergeable_ci_state? def environments return [] unless diff_head_commit - target_project.environments.select do |environment| - environment.includes_commit?(diff_head_commit) - end + environments = source_project.environments_for( + source_branch, diff_head_commit) + environments += target_project.environments_for( + target_branch, diff_head_commit, with_tags: true) + + environments.uniq end def state_human_name diff --git a/app/models/project.rb b/app/models/project.rb index d7f20070be0bc8..7265cb55594af5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1293,6 +1293,22 @@ def reset_pushes_since_gc Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) } end + def environments_for(ref, commit, with_tags: false) + environment_ids = deployments.group(:environment_id). + select(:environment_id) + + environment_ids = + if with_tags + environment_ids.where('ref=? OR tag IS TRUE', ref) + else + environment_ids.where(ref: ref) + end + + environments.where(id: environment_ids).select do |environment| + environment.includes_commit?(commit) + end + end + private def pushes_since_gc_redis_key diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 494695a03a50e3..44e645a7e8113c 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -43,15 +43,16 @@ = icon("times-circle") Could not connect to the CI server. Please check your settings and try again. -- @merge_request.environments.each do |environment| - .mr-widget-heading - .ci_widget.ci-success - = ci_icon_for_status("success") - %span.hidden-sm - Deployed to - = succeed '.' do - = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment), class: 'environment' - - external_url = environment.external_url - - if external_url - = link_to external_url, target: '_blank' do - = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true) +- @merge_request.environments.sort_by(&:name).each do |environment| + - if can?(current_user, :read_environment, environment) + .mr-widget-heading + .ci_widget.ci-success + = ci_icon_for_status("success") + %span.hidden-sm + Deployed to + = succeed '.' do + = link_to environment.name, environment_path(environment), class: 'environment' + - external_url = environment.external_url + - if external_url + = link_to external_url, target: '_blank' do + = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true) diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 38c2a28d3e1ac3..433aba7747ba7b 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -748,16 +748,57 @@ end end - describe "#environments" do + describe '#environments' do let(:project) { create(:project) } let(:merge_request) { create(:merge_request, source_project: project) } - it 'selects deployed environments' do - environments = create_list(:environment, 3, project: project) - create(:deployment, environment: environments.first, sha: project.commit('master').id) - create(:deployment, environment: environments.second, sha: project.commit('feature').id) + context 'with multiple environments' do + let(:environments) { create_list(:environment, 3, project: project) } - expect(merge_request.environments).to eq [environments.first] + before do + create(:deployment, environment: environments.first, ref: 'master', sha: project.commit('master').id) + create(:deployment, environment: environments.second, ref: 'feature', sha: project.commit('feature').id) + end + + it 'selects deployed environments' do + expect(merge_request.environments).to contain_exactly(environments.first) + end + end + + context 'with environments on source project' do + let(:source_project) do + create(:project) do |fork_project| + fork_project.create_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) + end + end + + let(:merge_request) do + create(:merge_request, + source_project: source_project, source_branch: 'feature', + target_project: project) + end + + let(:source_environment) { create(:environment, project: source_project) } + + before do + create(:deployment, environment: source_environment, ref: 'feature', sha: merge_request.diff_head_sha) + end + + it 'selects deployed environments' do + expect(merge_request.environments).to contain_exactly(source_environment) + end + + context 'with environments on target project' do + let(:target_environment) { create(:environment, project: project) } + + before do + create(:deployment, environment: target_environment, tag: true, sha: merge_request.diff_head_sha) + end + + it 'selects deployed environments' do + expect(merge_request.environments).to contain_exactly(source_environment, target_environment) + end + end end context 'without a diff_head_commit' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index a388ff703a6354..83f61f0af0a3b5 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1647,6 +1647,47 @@ def create_build(new_pipeline = pipeline, name = 'test') end end + describe '#environments_for' do + let(:project) { create(:project) } + let(:environment) { create(:environment, project: project) } + + context 'tagged deployment' do + before do + create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id) + end + + it 'returns environment when with_tags is set' do + expect(project.environments_for('master', project.commit, with_tags: true)).to contain_exactly(environment) + end + + it 'does not return environment when no with_tags is set' do + expect(project.environments_for('master', project.commit)).to be_empty + end + + it 'does not return environment when commit is not part of deployment' do + expect(project.environments_for('master', project.commit('feature'))).to be_empty + end + end + + context 'branch deployment' do + before do + create(:deployment, environment: environment, ref: 'master', sha: project.commit.id) + end + + it 'returns environment when ref is set' do + expect(project.environments_for('master', project.commit)).to contain_exactly(environment) + end + + it 'does not environment when ref is different' do + expect(project.environments_for('feature', project.commit)).to be_empty + end + + it 'does not return environment when commit is not part of deployment' do + expect(project.environments_for('master', project.commit('feature'))).to be_empty + end + end + end + def enable_lfs allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) end diff --git a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb index 733b2dfa7ffe9c..21f49d396e7b53 100644 --- a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb @@ -15,6 +15,8 @@ assign(:merge_request, merge_request) assign(:project, project) + allow(view).to receive(:can?).and_return(true) + render end -- GitLab From 579aec79e6441d54846ba24add9dcdf6f4f6afbf Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 21 Sep 2016 11:37:51 +0000 Subject: [PATCH 12/49] Merge branch '20310-new-project-btn' into 'master' Fix new project button alignment ## What does this MR do? Increases the width of the button/search container to fit all items at smaller screen width. The left side of the row can only have a max of two tabs (All Projects, Shared Projects), so everything can still fit on one line until they resize for mobile ## Why was this MR needed? The `New project` button wrapped to next line at smaller screen width, breaking the layout ## Screenshots (if relevant) ![Screen_Shot_2016-09-09_at_11.44.27_AM](/uploads/a726208deec6623d9fb62db0a549bf38/Screen_Shot_2016-09-09_at_11.44.27_AM.png) ![Screen_Shot_2016-09-09_at_11.46.29_AM](/uploads/bd8dc911757b14c5fafc4d3849e0b242/Screen_Shot_2016-09-09_at_11.46.29_AM.png) ## What are the relevant issue numbers? Closes #20310 See merge request !6286 --- app/assets/stylesheets/framework/nav.scss | 3 +-- app/assets/stylesheets/pages/groups.scss | 13 +++++++++++++ app/views/groups/show.html.haml | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 553768b2e68de0..ea43f4afc374ba 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -99,8 +99,7 @@ .top-area { @include clearfix; - - border-bottom: 1px solid #eee; + border-bottom: 1px solid $btn-gray-hover; .nav-text { padding-top: 16px; diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index b657ca47d38637..732dc645c66d4b 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -55,3 +55,16 @@ } } } + +.groups-header { + + @media (min-width: $screen-sm-min) { + .nav-links { + width: 35%; + } + + .nav-controls { + width: 65%; + } + } +} diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 53ed4fa991d688..31db6ee0cad954 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -23,7 +23,7 @@ .cover-desc.description = markdown(@group.description, pipeline: :description) -%div{ class: container_class } +%div.groups-header{ class: container_class } .top-area %ul.nav-links %li.active -- GitLab From 16ebf6c2be700f573f3c38ce336a2e48392e6c79 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 21 Sep 2016 10:12:43 +0000 Subject: [PATCH 13/49] Merge branch 'bump-shell-to-3-6-0' into 'master' Bump GITLAB_SHELL_VERSION to 3.6.0 for SSH support for LFS. cc @rdavila See merge request !6441 --- GITLAB_SHELL_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 1545d966571dc8..40c341bdcdbe83 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -3.5.0 +3.6.0 -- GitLab From 2eb4d00459af2732858c36919eb7d029a8f92033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 21 Sep 2016 09:00:38 +0000 Subject: [PATCH 14/49] Merge branch 'post-merge-improve-of-ci-permissions' into 'master' Post-merge improve of CI permissions Improves code from !6409 See merge request !6432 --- app/controllers/jwt_controller.rb | 6 +++--- .../projects/git_http_client_controller.rb | 6 +++--- app/models/ci/build.rb | 7 +++++-- .../container_registry_authentication_service.rb | 2 +- lib/ci/mask_secret.rb | 5 +++-- spec/lib/ci/mask_secret_spec.rb | 14 +++++++++++--- spec/lib/gitlab/git_access_spec.rb | 2 +- spec/requests/git_http_spec.rb | 6 +++--- 8 files changed, 30 insertions(+), 18 deletions(-) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 06d967747545e3..34d5d99558e8c0 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -11,10 +11,8 @@ def auth service = SERVICES[params[:service]] return head :not_found unless service - @authentication_result ||= Gitlab::Auth::Result.new - result = service.new(@authentication_result.project, @authentication_result.actor, auth_params). - execute(authentication_abilities: @authentication_result.authentication_abilities) + execute(authentication_abilities: @authentication_result.authentication_abilities || []) render json: result, status: result[:http_status] end @@ -22,6 +20,8 @@ def auth private def authenticate_project_or_user + @authentication_result = Gitlab::Auth::Result.new + authenticate_with_http_basic do |login, password| @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip) diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index cbfd3cab3dd8f7..383e184d7965aa 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -32,11 +32,11 @@ def authenticate_user return # Allow access end elsif allow_kerberos_spnego_auth? && spnego_provided? - user = find_kerberos_user + kerberos_user = find_kerberos_user - if user + if kerberos_user @authentication_result = Gitlab::Auth::Result.new( - user, nil, :kerberos, Gitlab::Auth.full_authentication_abilities) + kerberos_user, nil, :kerberos, Gitlab::Auth.full_authentication_abilities) send_final_spnego_response return # Allow access diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index dd984aef3184b3..cb87b43f6be9fd 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -493,8 +493,11 @@ def build_attributes_from_config end def hide_secrets(trace) - trace = Ci::MaskSecret.mask(trace, project.runners_token) if project - trace = Ci::MaskSecret.mask(trace, token) + return unless trace + + trace = trace.dup + Ci::MaskSecret.mask!(trace, project.runners_token) if project + Ci::MaskSecret.mask!(trace, token) trace end end diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 98da6563947543..38ac66312287e2 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -5,7 +5,7 @@ class ContainerRegistryAuthenticationService < BaseService AUDIENCE = 'container_registry' def execute(authentication_abilities:) - @authentication_abilities = authentication_abilities || [] + @authentication_abilities = authentication_abilities return error('not found', 404) unless registry.enabled diff --git a/lib/ci/mask_secret.rb b/lib/ci/mask_secret.rb index 3da04edde70bcb..997377abc55fd6 100644 --- a/lib/ci/mask_secret.rb +++ b/lib/ci/mask_secret.rb @@ -1,9 +1,10 @@ module Ci::MaskSecret class << self - def mask(value, token) + def mask!(value, token) return value unless value.present? && token.present? - value.gsub(token, 'x' * token.length) + value.gsub!(token, 'x' * token.length) + value end end end diff --git a/spec/lib/ci/mask_secret_spec.rb b/spec/lib/ci/mask_secret_spec.rb index 518de76911c671..3101bed20fbab9 100644 --- a/spec/lib/ci/mask_secret_spec.rb +++ b/spec/lib/ci/mask_secret_spec.rb @@ -5,15 +5,23 @@ describe '#mask' do it 'masks exact number of characters' do - expect(subject.mask('token', 'oke')).to eq('txxxn') + expect(mask('token', 'oke')).to eq('txxxn') end it 'masks multiple occurrences' do - expect(subject.mask('token token token', 'oke')).to eq('txxxn txxxn txxxn') + expect(mask('token token token', 'oke')).to eq('txxxn txxxn txxxn') end it 'does not mask if not found' do - expect(subject.mask('token', 'not')).to eq('token') + expect(mask('token', 'not')).to eq('token') + end + + it 'does support null token' do + expect(mask('token', nil)).to eq('token') + end + + def mask(value, token) + subject.mask!(value.dup, token) end end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index ed43646330f6c9..de68e32e5b4a1c 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -343,7 +343,7 @@ def self.run_permission_checks(permissions_matrix) end context 'to private project' do - let(:project) { create(:project, :internal) } + let(:project) { create(:project) } it { expect(subject).not_to be_allowed } end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index e3922bec6893be..745166869213ea 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -335,7 +335,7 @@ def attempt_login(include_password) project.team << [user, :reporter] end - shared_examples 'can download code only from own projects' do + shared_examples 'can download code only' do it 'downloads get status 200' do clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token @@ -353,7 +353,7 @@ def attempt_login(include_password) context 'administrator' do let(:user) { create(:admin) } - it_behaves_like 'can download code only from own projects' + it_behaves_like 'can download code only' it 'downloads from other project get status 403' do clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token @@ -365,7 +365,7 @@ def attempt_login(include_password) context 'regular user' do let(:user) { create(:user) } - it_behaves_like 'can download code only from own projects' + it_behaves_like 'can download code only' it 'downloads from other project get status 404' do clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token -- GitLab From 46f36341fcbe83da6ab74396ece52091db2d75e6 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 20 Sep 2016 19:37:47 +0000 Subject: [PATCH 15/49] Merge branch 'issue_20078' into 'master' Test if issue authors can access private projects See merge request !6419 --- CHANGELOG | 1 + spec/policies/project_policy_spec.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index afbbbc043dafd0..b7ff17c9b7c7d3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -56,6 +56,7 @@ v 8.12.0 (unreleased) - Emoji can be awarded on Snippets !4456 - Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling) - Fix blame table layout width + - Spec testing if issue authors can read issues on private projects - Fix bug where pagination is still displayed despite all todos marked as done (ClemMakesApps) - Request only the LDAP attributes we need !6187 - Center build stage columns in pipeline overview (ClemMakesApps) diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index eda1cafd65e46a..a7a06744428bd8 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -33,4 +33,17 @@ it 'returns increasing permissions for each level' do expect(users_permissions).to eq(users_permissions.sort.uniq) end + + it 'does not include the read_issue permission when the issue author is not a member of the private project' do + project = create(:project, :private) + issue = create(:issue, project: project) + user = issue.author + + expect(project.team.member?(issue.author)).to eq(false) + + expect(BasePolicy.class_for(project).abilities(user, project).can_set). + not_to include(:read_issue) + + expect(Ability.allowed?(user, :read_issue, project)).to be_falsy + end end -- GitLab From 01b896f37198a9109ffd76ddb3dc1b831ba7fba1 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 20 Sep 2016 19:17:13 +0000 Subject: [PATCH 16/49] Merge branch 'doc/cycle-analytics' into 'master' Add docs on Cycle Analytics Document Cycle Analytics first iteration https://gitlab.com/gitlab-org/gitlab-ce/issues/21170 See merge request !6437 --- doc/user/project/cycle_analytics.md | 112 ++++++++++++++++++ .../img/cycle_analytics_landing_page.png | Bin 0 -> 58203 bytes doc/workflow/README.md | 1 + 3 files changed, 113 insertions(+) create mode 100644 doc/user/project/cycle_analytics.md create mode 100644 doc/user/project/img/cycle_analytics_landing_page.png diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md new file mode 100644 index 00000000000000..e1fe1d256fde08 --- /dev/null +++ b/doc/user/project/cycle_analytics.md @@ -0,0 +1,112 @@ +# Cycle Analytics + +> [Introduced][ce-5986] in GitLab 8.12. +> +> **Note:** +This the first iteration of Cycle Analytics, you can follow the following issue +to track the changes that are coming to this feature: [#20975][ce-20975]. + +Cycle Analytics measures the time it takes to go from an idea to production for +each project you have. This is achieved by not only indicating the total time it +takes to reach at that point, but the total time is broken down into the +multiple stages an idea has to pass through to be shipped. + +Cycle Analytics is that it is tightly coupled with the [GitLab flow] and +calculates a separate median for each stage. + +## Overview + +You can find the Cycle Analytics page under your project's **Pipelines > Cycle +Analytics** tab. + +![Cycle Analytics landing page](img/cycle_analytics_landing_page.png) + +You can see that there are seven stages in total: + +- **Issue** (Tracker) + - Median time from issue creation until given a milestone or list label + (first assignment, any milestone, milestone date or assignee is not required) +- **Plan** (Board) + - Median time from giving an issue a milestone or label until pushing the + first commit +- **Code** (IDE) + - Median time from the first commit until the merge request is created +- **Test** (CI) + - Total test time for all commits/merges +- **Review** (Merge Request/MR) + - Median time from merge request creation until the merge request is merged + (closed merge requests won't be taken into account) +- **Staging** (Continuous Deployment) + - Median time from when the merge request got merged until the deploy to + production (production is last stage/environment) +- **Production** (Total) + - Sum of all the above stages excluding the Test (CI) time + +## How the data is measured + +Cycle Analytics records cycle time so only data on the issues that have been +deployed to production are measured. In case you just started a new project and +you have not pushed anything to production, then you will not be able to +properly see the Cycle Analytics of your project. + +Specifically, if your CI is not set up and you have not defined a `production` +[environment], then you will not have any data. + +Below you can see in more detail what the various stages of Cycle Analytics mean. + +| **Stage** | **Description** | +| --------- | --------------- | +| Issue | Measures the median time between creating an issue and taking action to solve it, by either labeling it or adding it to a milestone, whatever comes first. The label will be tracked only if it already has an [Issue Board list][board] created for it. | +| Plan | Measures the median time between the action you took for the previous stage, and pushing the first commit to the repository. To make this change tracked, the commit needs to be pushed that contains the issue closing pattern `Closes #xxx`, where `xxx` is the number of the issue related to this commit. If the commit does not contain the issue closing pattern, it is not considered to the measure time of the stage. | +| Code | Measures the median time between pushing a first commit (previous stage) and creating a merge request related to that commit. The key to keep the process tracked is include the issue closing pattern to the description of the merge request. | +| Test | Measures the median time to run the entire pipeline for that project. It's related to the time GitLab CI takes to run every job for the commits pushed to that merge request defined in the previous stage. It is basically the start->finish time for all pipelines. `master` is not excluded. It does not attempt to track time for any particular stages. | +| Review | Measures the median time taken to review the merge request, between its creation and until it's merged. | +| Staging | Measures the median time between merging the merge request until the very first deployment of the to production. It's tracked by the [environment] set to `production` in your GitLab CI configuration. If there isn't a `production` environment, this is not tracked. | +| Production| The sum of all time taken to run the entire process, from issue creation to deploying the code to production. | + +--- + +Here's a little explanation of how this works behind the scenes: + +1. Issues and merge requests are grouped together in pairs, such that for each + `` pair, the merge request has `Fixes #xxx` for the + corresponding issue. All other issues and merge requests are **not** considered. + +1. Then the pairs are filtered out. Any merge request + that has **not** been deployed to production in the last XX days (specified + by the UI - default is 90 days) prohibits these pairs from being considered. + +1. For the remaining `` pairs, we check the information that + we need for the stages, like issue creation date, merge request merge time, + etc. + +To sum up, anything that doesn't follow the [GitLab flow] won't be tracked at all. +So, if a merge request doesn't close an issue or an issue is not labeled with a +label present in the Issue Board or assigned a milestone or a project has no +`production` environment, the Cycle Analytics dashboard won't present any data +at all. + +## Permissions + +The current permissions on the Cycle Analytics dashboard are: + +- Public projects - anyone can access +- Private/internal projects - any member (guest level and above) can access + +You can [read more about permissions][permissions] in general. + +## More resources + +Learn more about Cycle Analytics in the following resources: + +- [Cycle Analytics feature page](https://about.gitlab.com/solutions/cycle-analytics/) +- [Cycle Analytics feature preview](https://about.gitlab.com/2016/09/16/feature-preview-introducing-cycle-analytics/) +- [Cycle Analytics feature highlight](https://about.gitlab.com/2016-09-19-cycle-analytics-feature-highlight.html) + + +[ce-5986]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5986 +[ce-20975]: https://gitlab.com/gitlab-org/gitlab-ce/issues/20975 +[GitLab flow]: ../../workflow/gitlab_flow.md +[permissions]: ../permissions.md +[environment]: ../../ci/yaml/README.md#environment +[board]: issue_board.md#creating-a-new-list diff --git a/doc/user/project/img/cycle_analytics_landing_page.png b/doc/user/project/img/cycle_analytics_landing_page.png new file mode 100644 index 0000000000000000000000000000000000000000..4fa42c87395458739b63cf23eb017cc0cd400db0 GIT binary patch literal 58203 zcmeAS@N?(olHy`uVBq!ia0y~yU^Zc3V6o$1V_;z5^sAcBz@Wh3>EaktG3U+R$`a|D ziO+7|6FTlUC->2GpLw=3%hhDRTN_V4Gdm@1)0vcK7RED6j{8-23V&~y6ty;Nb#&h9 zyWMZgx~(<6M9P_14!SP#=n`>#`+MK^Z?Q{UycVhG-RP862zyjr-@veOv4apx_piF= zf6fUuI$hnnJK=nL+fdJG_z=F%zWLfE7r!YeaU7c86t zR?`)RCaC4JTzJZt?wBA z=ir?6(cypW-1hwx$_YQ0x_2JPR#j%+rBi|&`@)}rR3(0iuIJi**DU>Y>Fr#Jzt`r2 z1@~y_zy7*pN|2VGWQNAlDa)=sZTa-OzI9^$zLtsk`&eGL3Kwt7d;RnA!mnEU|A&9r zVezIX$ZKT*MDdln$3Yq;JvE+&A( z`zMZ=o?JX`kY?(PjYF3cf%STfnlDwaI z3!RUz$$!v4D?=gaMr3m3wb#CyOG9LgE(CaK`mXx4b$_R{Ra!N3{TCNz!Niwa_n)lV z3v#M6*r}XzC3Z|VC^_?u?ca+|wncL#UYxG_tGRTFUJcm8YFBOq%{3`G^Mmali!g6X zcFx+9DAFNY-#lHiF!{-B{ey+y-EO`1HM!`pW7YL|6jjHrW=ph`|NYN?f49Ec)jzk) zgW4u%oLyh5{IqIs>VLtw$B)-+XI?txQmZZ~`Y!FLdVk{K>l)5|{gc`Gudfon%xAK0 z`=s@kzklrhP`Z!h{oG@y%75|wXV}*#{xJKm@{fD|at>G4bL{t8T&H^U>df!Ux83}x zRMTER;cMA~DM7n7fRmrpYFF0y=jKcc7d>|Uk)nU-`{N^uN4}gt#N7L`-IM8T|MG33 zck-T_KiIfp(|x%qL0;l(cub}Qc_n}0EAegbTJY4fZSG#xnj4Xq9;Q9jKbCXd`oi3= z-$2F}K#X7f{>ki(jm!SJ{`e%^E_n6y%Jr94#MNmQ&Nw;$mfM#y|8-}ossp?UM~+#1;=>LJ!$F3 z?6W`~t`gk${Sl9Kwei;t#@A0-BtF}_vQ+KHyzg$VC^};H`u88W{yF_)vs>+=#B|Rz z^WVEI4xg?2qf}GMdC2Gd{DZ&0cwD>P2(n`u*bb+?(*%F??Gdvtm~vFI>|67Z!{>bd zi=^*}jBGL}eqvgos~oxB9u#Bx;23+dYwl(Kn>~DTJ0~(8otbiDX0dC?jFY{>DOT}K z-}_|bwj2Ncx@=0278f{8aRt}%-pl=OSvIMGe{tK~zp-;}o?X;FyH>)Qv!VQUeVco& z?r(4g)q*I$p4jmH!=@XaOxJwcMU&?wrXQ1U;Wdx`tW?vr!l3v^X@yMNo9q2SUMpXu zfC`sc+wScv`(bscZNXI^_F0|gN$qnj!~a|?S+J6i-&p_P?fWyo{tEEY6kQLF)bu6c z{~7kZ+dqR(UG?%px1ZZDhi6Wymo6(^W}$PY0BkM8AhhT=-}`Z~Dpj!^O+3e?MI~C1@9kdHy7wdmRrB^bM9*WJ7uZ_6Td#Af&`HWOY*&82!P`!~}skwReCK2Af&5`Tl zt>iTI-4X+J=e)bQcKPB(E(LclN0#0D?VM+K$}A-D_P^o{J2zElRxjLo+2_vRopkF4_xnL2fW!g_|Qo6KUWbI!al>z^*Z_~j(6*QM{LPg}U^apJYp6Fs)y z=G}aA&bO190h@2mxv1#+_mYu_$duzU-hRJ1xQdGdFaNf9ox3C8zkQY5Z*EaFW?!+l z)2A&=I&C}SY}$+-Hk-Hm+n!xwI`PeZ*43)@Z{8oAn0+g9`gwy5lCQJ#()P&C`4%W5 zIwiR{lhN;G?q;p~NB7!VXl;0R@$|Xy+SYIF>HK*MmTqCV#OxfYec35z`OF=^`}cRP z|9h8jMTnM$`@Bbg_KE#DnqMne|L5Jw3r3~y{|MOU)*7ha?&dQo-U;#Ui;cSke$UQ! zYTw4|s*(TZVDG+m3E8@?=;!a!qbj>pu3|KXFezcUPCRaNRN6{-?pU zw=DG4g&zNXtFhXu{h#mC75D#}WIvs~{@1e0@+RT41A?wi-fEP(YVNbm^3^tt4xicY z+lhU=b>ZIs-uQo~>QA2dwure`B39#Xphx}e_y0^bES6Gzy*r`r*VbEepIm>vQ$v~A zH|uU*`K4-wb0(()Vq;}D{eD((s9MG7>+0(LTX?=6pB1=qss5v{#amxrxE*Ww*ui-E z@q42+(KUe>_ym#x36*~ks=j{3Vb!)!M(P<|p zUrJ|xwPEASV|%``e-(8-Qm=6G+tZJql1lFEY?GZW9S|4R^HqAOMWws&&zmZL(|(*% zyppfs>R2ZzW%l*KzH@3T=C-V{-X;G&FUEd-uC)Gz7wq>Br`vyDdg1;>#V6wNzq#UG zX1bpH|EvGU-Su^>{~x`sZ~Bw_efjOnsZLjS-#@tj&sY6N;rl*S|M(p$?X14z<2BL0 z<>$_3NBlngzw!RhwebhT>)xwQk(C$Q^6qr+`^W$P{=I+XdHu`NKPKnzYpZQ;W4$S# z`^&Dx{QBZG2KI~2%Up^~{uR3D-}E;Y=cDS2b_YaGXg~Mwud%6#%ki&PsBG}xTl?bU`fqF1hSit8pFYj8@S~!c$n^t;rLN~q7dl5~ z3d@y+y0Od(x&QNGt0fFR`1o_OTl84r1n zFR_NZ{~7gd`(<^fYFXpOj3d3wYhUd7&=Q_0wt~kb(&U}!tbJx8Q%&0c*~P{6Z2HA( zQCKb^ENr&JR_@O2z~)Pz%D&i3wf?%P@a@FufOQsfzkEs-yu2OR-Sy2`;+jOFl>AY~ zO37LN{cNW;?B;WQJ*(jHKY=g10_WV{&(5}-JMTG*T;6)8$gib7K0XPDX3mV4yQ8Ol z>htFUr~Jb$PZ>X5TX(ef^pO(jrIGtSOjfzPSyn?Quj9wntk=7ozSx)@(UH!t4SUNe zA~NN$(6`mEw{3Fz!k3yhr>3Odpy%H4Wu>hX-p&4ZUiaoco4B~1M_;+_REe#)|6=ap z6X&&$Eik<>@8qxE%l~*>ckbNr!Gn2rrOaP({)P8n2wd@7c};e`{Q2_R$M!mHexCpF z^!|VG&h^K+W#fO{wRb;$H2p(`&5nDY743fSj6Xba<$rKQF=gVbz_wKO$_)wU@=KzN$aj_c`b5 za!zmgPuJGD*5CLZV)MAR+}>^vYx}vo4PNGaPY9kuwMQzbZp4Ijv^if+4gooG)#tny`O}j@ z@nNxYTU*@h4Sb6f%*ChkG{&a7*I;Y0+ER19m0WKiGRGMn=l)sP6rM z{L*9X2{Whu4ww)xw(DX+uqWfJ^(&U2o|pDh$!}k5+LY(dA2u0Y&=a0!_vHE10|M%Q zj>ONGmR;I<#m_{ycGB`^d7l>Q3sJC%>ic`Bh;f z_a-8~WtsP?DSwY8?rZ*Y$HvCw$UKInS?Y?lM!P_1#f^Q|a`SJGXYc>4{exeA4(Hx! zS}heea!>xm#~+)2`d#9o8tM37ht?+^c*V1KdG!Xi{onNKRWf4t|F{+YVZK&g`#&=q z-J|JtpCwXwpDAD9$y@6X`E}=o``@=uNsfKmxna}t@O>}7?OJ(*;pW8y0dikc zSl~G z$D5-~Uk7acx^O{Vaqaswk!RBrU%z`?AtL4%v~}`?(&O5M4oG5nbl|iEBDXq_j2uZA7X2sZQkFrzW(p)%XM$Rf7o6B zf!XnO*T35~+gSDYd|;BhIY;yQ-CyVa9}%y6S^nYGsjU|G*2i!7`!T~=QpElJp{L&t z*i^B<{>)u?-ljHGd6&P+t9qxt&p+Sze_VIr#g?dytx+Z-Pan^)D_ikwOJ2{H_`{9& z^h~aWCOSLHt=jJE&oudsMBn#)hfA0%v#&eZmp+=lg4g(>`JwtA?=5#-)~j33^u4-1 z=&Iq>(CPoQuNy9tKfWVRM17ak#+M%)EWdtWwO*`5C%f9z}b;=JtP2%94^D<+kx1S+{@X z4u#AGJ8uQ|U+VJRQSqAbn6B`S`#a>W-aX0e`$3}bd!50B7l-Oj_FZqA`9^x>HtF|Y z&z|&43;wb?q|_(ga&!4&7nJp}vVRVsQXzu1STlT*Q-G1@L9(k)~!N

=iI#7wT-#F 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

+F?9{Usj@ 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 E0CABx!~g&Q literal 0 HcmV?d00001 diff --git a/doc/workflow/README.md b/doc/workflow/README.md index dcb9c32ad58ba0..e8ecb8d8fb43b0 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -1,6 +1,7 @@ # Workflow - [Change your time zone](timezone.md) +- [Cycle Analytics](../user/project/cycle_analytics.md) - [Description templates](../user/project/description_templates.md) - [Feature branch workflow](workflow.md) - [GitLab Flow](gitlab_flow.md) -- GitLab From c525ec9bc82f3ddd325d2fa2a8850a600ca3c70b Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 20 Sep 2016 19:16:15 +0000 Subject: [PATCH 17/49] Merge branch 'slash-commands-load-fix' into 'master' Fixed slash commands not loading ## What does this MR do? Fixes an issue with slash commands not working when the autocomplete source is loading & then the new issue button is clicked. This also fixes an issue where the autocomplete source is loaded on pages where it isn't actually needed. ## What are the relevant issue numbers? Closes #21774, #21807 See merge request !6207 --- CHANGELOG | 2 ++ app/views/layouts/project.html.haml | 3 --- app/views/projects/_zen.html.haml | 3 +++ .../projects/gfm_autocomplete_load_spec.rb | 21 +++++++++++++++++++ 4 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 spec/features/projects/gfm_autocomplete_load_spec.rb diff --git a/CHANGELOG b/CHANGELOG index b7ff17c9b7c7d3..8ba27d8fe1e004 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -83,6 +83,8 @@ v 8.12.0 (unreleased) - Require confirmation when not logged in for unsubscribe links !6223 (Maximiliano Perez Coto) - Add `wiki_page_events` to project hook APIs (Ben Boeckel) - Remove Gitorious import + - Loads GFM autocomplete source only when required + - Fix issue with slash commands not loading on new issue page - Fix inconsistent background color for filter input field (ClemMakesApps) - Remove prefixes from transition CSS property (ClemMakesApps) - Add Sentry logging to API calls diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 9fe94291db749d..277eb71ea739e0 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -14,9 +14,6 @@ window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}"; window.preview_markdown_path = "#{preview_markdown_path}"; -- content_for :scripts_body do - = render "layouts/init_auto_complete" if current_user - - content_for :header_content do .js-dropdown-menu-projects .dropdown-menu.dropdown-select.dropdown-menu-projects diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index 3978fa60d663f2..cb97181b9e19ac 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -7,3 +7,6 @@ = text_area_tag attr, nil, class: classes, placeholder: placeholder %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" } = icon('compress') + +- content_for :scripts_body do + = render "layouts/init_auto_complete" if current_user && (@target_project || @project) diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb new file mode 100644 index 00000000000000..1921ea6d8aec39 --- /dev/null +++ b/spec/features/projects/gfm_autocomplete_load_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe 'GFM autocomplete loading', feature: true, js: true do + let(:project) { create(:project) } + + before do + login_as :admin + + visit namespace_project_path(project.namespace, project) + end + + it 'does not load on project#show' do + expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).to eq('') + end + + it 'loads on new issue page' do + visit new_namespace_project_issue_path(project.namespace, project) + + expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).not_to eq('') + end +end -- GitLab From 62ba6805912c45fa06aafef5f7b2be696a6758b6 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 20 Sep 2016 04:44:54 +0000 Subject: [PATCH 18/49] Merge branch 'feature/import-export-security-specs' into 'master' Import/Export security specs Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/20857 Related: https://gitlab.com/gitlab-org/gitlab-ce/issues/20821/ See merge request !1987 --- .../import_export/export_file_spec.rb | 80 +++++ .../import_export/import_file_spec.rb | 2 +- spec/lib/gitlab/import_export/all_models.yml | 175 ++++++++++ .../attribute_configuration_spec.rb | 55 ++++ .../import_export/model_configuration_spec.rb | 57 ++++ .../import_export/safe_model_attributes.yml | 310 ++++++++++++++++++ .../import_export/configuration_helper.rb | 29 ++ .../import_export/export_file_helper.rb | 133 ++++++++ 8 files changed, 840 insertions(+), 1 deletion(-) create mode 100644 spec/features/projects/import_export/export_file_spec.rb create mode 100644 spec/lib/gitlab/import_export/all_models.yml create mode 100644 spec/lib/gitlab/import_export/attribute_configuration_spec.rb create mode 100644 spec/lib/gitlab/import_export/model_configuration_spec.rb create mode 100644 spec/lib/gitlab/import_export/safe_model_attributes.yml create mode 100644 spec/support/import_export/configuration_helper.rb create mode 100644 spec/support/import_export/export_file_helper.rb diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb new file mode 100644 index 00000000000000..7e2c701e401ac5 --- /dev/null +++ b/spec/features/projects/import_export/export_file_spec.rb @@ -0,0 +1,80 @@ +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+ +# or make sure the attribute is blacklisted in the +import_export.yml+ configuration +feature 'Import/Export - project export integration test', feature: true, js: true do + include Select2Helper + include ExportFileHelper + + let(:user) { create(:admin) } + let(:export_path) { "#{Dir::tmpdir}/import_file_spec" } + let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys } + + let(:sensitive_words) { %w[pass secret token key] } + let(:safe_list) do + { + token: [ProjectHook, Ci::Trigger], + key: [Project, Ci::Variable, :yaml_variables] + } + end + let(:safe_hashes) { { yaml_variables: %w[key value public] } } + + let(:project) { setup_project } + + background do + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + end + + after do + FileUtils.rm_rf(export_path, secure: true) + end + + context 'admin user' do + before do + login_as(user) + end + + scenario 'exports a project successfully' do + visit edit_namespace_project_path(project.namespace, project) + + expect(page).to have_content('Export project') + + click_link 'Export project' + + visit edit_namespace_project_path(project.namespace, project) + + expect(page).to have_content('Download export') + + in_directory_with_expanded_export(project) do |exit_status, tmpdir| + expect(exit_status).to eq(0) + + project_json_path = File.join(tmpdir, 'project.json') + expect(File).to exist(project_json_path) + + project_hash = JSON.parse(IO.read(project_json_path)) + + sensitive_words.each do |sensitive_word| + found = find_sensitive_attributes(sensitive_word, project_hash) + + expect(found).to be_nil, failure_message(found.try(:key_found), found.try(:parent), sensitive_word) + end + end + end + + def failure_message(key_found, parent, sensitive_word) + <<-MSG + Found a new sensitive word <#{key_found}>, which is part of the hash #{parent.inspect} + + If you think this information shouldn't get exported, please exclude the model or attribute in IMPORT_EXPORT_CONFIG. + + Otherwise, please add the exception to +safe_list+ in CURRENT_SPEC using #{sensitive_word} as the key and the + correspondent hash or model as the value. + + IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file} + CURRENT_SPEC: #{__FILE__} + MSG + end + end +end diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index f707ccf4e93e01..09cd6369881fda 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'project import', feature: true, js: true do +feature 'Import/Export - project import integration test', feature: true, js: true do include Select2Helper let(:admin) { create(:admin) } diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml new file mode 100644 index 00000000000000..30968ba2d5ff72 --- /dev/null +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -0,0 +1,175 @@ +--- +issues: +- subscriptions +- award_emoji +- author +- assignee +- updated_by +- milestone +- notes +- label_links +- labels +- todos +- user_agent_detail +- moved_to +- events +events: +- author +- project +- target +notes: +- award_emoji +- project +- noteable +- author +- updated_by +- resolved_by +- todos +- events +label_links: +- target +- label +label: +- subscriptions +- project +- lists +- label_links +- issues +- merge_requests +milestone: +- project +- issues +- labels +- merge_requests +- participants +- events +snippets: +- author +- project +- notes +releases: +- project +project_members: +- created_by +- user +- source +- project +merge_requests: +- subscriptions +- award_emoji +- author +- assignee +- updated_by +- milestone +- notes +- label_links +- labels +- todos +- target_project +- source_project +- merge_user +- merge_request_diffs +- merge_request_diff +- events +merge_request_diff: +- merge_request +pipelines: +- project +- user +- statuses +- builds +- trigger_requests +statuses: +- project +- pipeline +- user +variables: +- project +triggers: +- project +- trigger_requests +deploy_keys: +- user +- deploy_keys_projects +- projects +services: +- project +- service_hook +hooks: +- project +protected_branches: +- project +- merge_access_levels +- push_access_levels +project: +- taggings +- base_tags +- tag_taggings +- tags +- creator +- group +- namespace +- board +- last_event +- services +- campfire_service +- drone_ci_service +- emails_on_push_service +- builds_email_service +- irker_service +- pivotaltracker_service +- hipchat_service +- flowdock_service +- assembla_service +- asana_service +- gemnasium_service +- slack_service +- buildkite_service +- bamboo_service +- teamcity_service +- pushover_service +- jira_service +- redmine_service +- custom_issue_tracker_service +- bugzilla_service +- gitlab_issue_tracker_service +- external_wiki_service +- forked_project_link +- forked_from_project +- forked_project_links +- forks +- merge_requests +- fork_merge_requests +- issues +- labels +- events +- milestones +- notes +- snippets +- hooks +- protected_branches +- project_members +- users +- requesters +- deploy_keys_projects +- deploy_keys +- users_star_projects +- starrers +- releases +- lfs_objects_projects +- lfs_objects +- project_group_links +- invited_groups +- todos +- notification_settings +- import_data +- commit_statuses +- pipelines +- builds +- runner_projects +- runners +- variables +- triggers +- environments +- deployments +- project_feature \ No newline at end of file diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb new file mode 100644 index 00000000000000..2ba344092cefc4 --- /dev/null +++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +# Part of the test security suite for the Import/Export feature +# Checks whether there are new attributes in models that are currently being exported as part of the +# project Import/Export feature. +# If there are new attributes, these will have to either be added to this spec in case we want them +# to be included as part of the export, or blacklist them using the import_export.yml configuration file. +# Likewise, new models added to import_export.yml, will need to be added with their correspondent attributes +# to this spec. +describe 'Import/Export attribute configuration', lib: true do + include ConfigurationHelper + + let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys } + let(:relation_names) do + names = names_from_tree(config_hash['project_tree']) + + # Remove duplicated or add missing models + # - project is not part of the tree, so it has to be added manually. + # - milestone, labels have both singular and plural versions in the tree, so remove the duplicates. + names.flatten.uniq - ['milestones', 'labels'] + ['project'] + end + + let(:safe_attributes_file) { 'spec/lib/gitlab/import_export/safe_model_attributes.yml' } + let(:safe_model_attributes) { YAML.load_file(safe_attributes_file) } + + it 'has no new columns' do + relation_names.each do |relation_name| + relation_class = relation_class_for_name(relation_name) + + expect(safe_model_attributes[relation_class.to_s]).not_to be_nil, "Expected exported class #{relation_class.to_s} to exist in safe_model_attributes" + + current_attributes = parsed_attributes(relation_name, relation_class.attribute_names) + safe_attributes = safe_model_attributes[relation_class.to_s] + new_attributes = current_attributes - safe_attributes + + expect(new_attributes).to be_empty, failure_message(relation_class.to_s, new_attributes) + end + end + + def failure_message(relation_class, new_attributes) + <<-MSG + It looks like #{relation_class}, which is exported using the project Import/Export, has new attributes: #{new_attributes.join(',')} + + Please add the attribute(s) to SAFE_MODEL_ATTRIBUTES if you consider this can be exported. + Otherwise, please blacklist the attribute(s) in IMPORT_EXPORT_CONFIG by adding it to its correspondent + model in the +excluded_attributes+ section. + + SAFE_MODEL_ATTRIBUTES: #{File.expand_path(safe_attributes_file)} + IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file} + MSG + end + + class Author < User + end +end diff --git a/spec/lib/gitlab/import_export/model_configuration_spec.rb b/spec/lib/gitlab/import_export/model_configuration_spec.rb new file mode 100644 index 00000000000000..9b492d1b9c7b4a --- /dev/null +++ b/spec/lib/gitlab/import_export/model_configuration_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +# Part of the test security suite for the Import/Export feature +# Finds if a new model has been added that can potentially be part of the Import/Export +# If it finds a new model, it will show a +failure_message+ with the options available. +describe 'Import/Export model configuration', lib: true do + include ConfigurationHelper + + let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys } + let(:model_names) do + names = names_from_tree(config_hash['project_tree']) + + # Remove duplicated or add missing models + # - project is not part of the tree, so it has to be added manually. + # - milestone, labels have both singular and plural versions in the tree, so remove the duplicates. + # - User, Author... Models we do not care about for checking models + names.flatten.uniq - ['milestones', 'labels', 'user', 'author'] + ['project'] + end + + let(:all_models_yml) { 'spec/lib/gitlab/import_export/all_models.yml' } + let(:all_models) { YAML.load_file(all_models_yml) } + let(:current_models) { setup_models } + + it 'has no new models' do + model_names.each do |model_name| + new_models = Array(current_models[model_name]) - Array(all_models[model_name]) + expect(new_models).to be_empty, failure_message(model_name.classify, new_models) + end + end + + # List of current models between models, in the format of + # {model: [model_2, model3], ...} + def setup_models + all_models_hash = {} + + model_names.each do |model_name| + model_class = relation_class_for_name(model_name) + + all_models_hash[model_name] = associations_for(model_class) - ['project'] + end + + all_models_hash + end + + def failure_message(parent_model_name, new_models) + <<-MSG + New model(s) <#{new_models.join(',')}> have been added, related to #{parent_model_name}, which is exported by + the Import/Export feature. + + If you think this model should be included in the export, please add it to IMPORT_EXPORT_CONFIG. + Definitely add it to MODELS_JSON to signal that you've handled this error and to prevent it from showing up in the future. + + MODELS_JSON: #{File.expand_path(all_models_yml)} + IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file} + MSG + end +end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml new file mode 100644 index 00000000000000..f2d272ca7e2737 --- /dev/null +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -0,0 +1,310 @@ +--- +Issue: +- id +- title +- assignee_id +- author_id +- project_id +- created_at +- updated_at +- position +- branch_name +- description +- state +- iid +- updated_by_id +- confidential +- deleted_at +- due_date +- moved_to_id +- lock_version +- milestone_id +- weight +Event: +- id +- target_type +- target_id +- title +- data +- project_id +- created_at +- updated_at +- action +- author_id +Note: +- id +- note +- noteable_type +- author_id +- created_at +- updated_at +- project_id +- attachment +- line_code +- commit_id +- noteable_id +- system +- st_diff +- updated_by_id +- type +- position +- original_position +- resolved_at +- resolved_by_id +- discussion_id +- original_discussion_id +LabelLink: +- id +- label_id +- target_id +- target_type +- created_at +- updated_at +Label: +- id +- title +- color +- project_id +- created_at +- updated_at +- template +- description +- priority +Milestone: +- id +- title +- project_id +- description +- due_date +- created_at +- updated_at +- state +- iid +ProjectSnippet: +- id +- title +- content +- author_id +- project_id +- created_at +- updated_at +- file_name +- type +- visibility_level +Release: +- id +- tag +- description +- project_id +- created_at +- updated_at +ProjectMember: +- id +- access_level +- source_id +- source_type +- user_id +- notification_level +- type +- created_at +- updated_at +- created_by_id +- invite_email +- invite_token +- invite_accepted_at +- requested_at +- expires_at +User: +- id +- username +- email +MergeRequest: +- id +- target_branch +- source_branch +- source_project_id +- author_id +- assignee_id +- title +- created_at +- updated_at +- state +- merge_status +- target_project_id +- iid +- description +- position +- locked_at +- updated_by_id +- merge_error +- merge_params +- merge_when_build_succeeds +- merge_user_id +- merge_commit_sha +- deleted_at +- in_progress_merge_commit_sha +- lock_version +- milestone_id +- approvals_before_merge +- rebase_commit_sha +MergeRequestDiff: +- id +- state +- st_commits +- merge_request_id +- created_at +- updated_at +- base_commit_sha +- real_size +- head_commit_sha +- start_commit_sha +Ci::Pipeline: +- id +- project_id +- ref +- sha +- before_sha +- push_data +- created_at +- updated_at +- tag +- yaml_errors +- committed_at +- gl_project_id +- status +- started_at +- finished_at +- duration +- user_id +CommitStatus: +- id +- project_id +- status +- finished_at +- trace +- created_at +- updated_at +- started_at +- runner_id +- coverage +- commit_id +- commands +- job_id +- name +- deploy +- options +- allow_failure +- stage +- trigger_request_id +- stage_idx +- tag +- ref +- user_id +- type +- target_url +- description +- artifacts_file +- gl_project_id +- artifacts_metadata +- erased_by_id +- erased_at +- artifacts_expire_at +- environment +- artifacts_size +- when +- yaml_variables +- queued_at +Ci::Variable: +- id +- project_id +- key +- value +- encrypted_value +- encrypted_value_salt +- encrypted_value_iv +- gl_project_id +Ci::Trigger: +- id +- token +- project_id +- deleted_at +- created_at +- updated_at +- gl_project_id +DeployKey: +- id +- user_id +- created_at +- updated_at +- key +- title +- type +- fingerprint +- public +Service: +- id +- type +- title +- project_id +- created_at +- updated_at +- active +- properties +- template +- push_events +- issues_events +- merge_requests_events +- tag_push_events +- note_events +- pipeline_events +- build_events +- category +- default +- wiki_page_events +- confidential_issues_events +ProjectHook: +- id +- url +- project_id +- created_at +- updated_at +- type +- service_id +- push_events +- issues_events +- merge_requests_events +- tag_push_events +- note_events +- pipeline_events +- enable_ssl_verification +- build_events +- wiki_page_events +- token +- group_id +- confidential_issues_events +ProtectedBranch: +- id +- project_id +- name +- created_at +- updated_at +Project: +- description +- issues_enabled +- merge_requests_enabled +- wiki_enabled +- snippets_enabled +- visibility_level +- archived +Author: +- name +ProjectFeature: +- id +- project_id +- merge_requests_access_level +- issues_access_level +- wiki_access_level +- snippets_access_level +- builds_access_level +- created_at +- updated_at \ No newline at end of file diff --git a/spec/support/import_export/configuration_helper.rb b/spec/support/import_export/configuration_helper.rb new file mode 100644 index 00000000000000..f752508d48cfae --- /dev/null +++ b/spec/support/import_export/configuration_helper.rb @@ -0,0 +1,29 @@ +module ConfigurationHelper + # Returns a list of models from hashes/arrays contained in +project_tree+ + def names_from_tree(project_tree) + project_tree.map do |branch_or_model| + branch_or_model = branch_or_model.to_s if branch_or_model.is_a?(Symbol) + + branch_or_model.is_a?(String) ? branch_or_model : names_from_tree(branch_or_model) + end + end + + def relation_class_for_name(relation_name) + relation_name = Gitlab::ImportExport::RelationFactory::OVERRIDES[relation_name.to_sym] || relation_name + relation_name.to_s.classify.constantize + end + + def parsed_attributes(relation_name, attributes) + excluded_attributes = config_hash['excluded_attributes'][relation_name] + included_attributes = config_hash['included_attributes'][relation_name] + + attributes = attributes - JSON[excluded_attributes.to_json] if excluded_attributes + attributes = attributes & JSON[included_attributes.to_json] if included_attributes + + attributes + end + + def associations_for(safe_model) + safe_model.reflect_on_all_associations.map { |assoc| assoc.name.to_s } + end +end diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb new file mode 100644 index 00000000000000..be0772d6a4a8f4 --- /dev/null +++ b/spec/support/import_export/export_file_helper.rb @@ -0,0 +1,133 @@ +require './spec/support/import_export/configuration_helper' + +module ExportFileHelper + include ConfigurationHelper + + ObjectWithParent = Struct.new(:object, :parent, :key_found) + + def setup_project + project = create(:project, :public) + + create(:release, project: project) + + issue = create(:issue, assignee: user, project: project) + snippet = create(:project_snippet, project: project) + label = create(:label, project: project) + milestone = create(:milestone, project: project) + merge_request = create(:merge_request, source_project: project, milestone: milestone) + commit_status = create(:commit_status, project: project) + + create(:label_link, label: label, target: issue) + + ci_pipeline = create(:ci_pipeline, + project: project, + sha: merge_request.diff_head_sha, + ref: merge_request.source_branch, + statuses: [commit_status]) + + create(:ci_build, pipeline: ci_pipeline, project: project) + create(:milestone, project: project) + create(:note, noteable: issue, project: project) + create(:note, noteable: merge_request, project: project) + create(:note, noteable: snippet, project: project) + create(:note_on_commit, + author: user, + project: project, + commit_id: ci_pipeline.sha) + + create(:event, target: milestone, project: project, action: Event::CREATED, author: user) + create(:project_member, :master, user: user, project: project) + create(:ci_variable, project: project) + create(:ci_trigger, project: project) + key = create(:deploy_key) + key.projects << project + create(:service, project: project) + create(:project_hook, project: project, token: 'token') + create(:protected_branch, project: project) + + project + end + + # Expands the compressed file for an exported project into +tmpdir+ + def in_directory_with_expanded_export(project) + Dir.mktmpdir do |tmpdir| + export_file = project.export_project_path + _output, exit_status = Gitlab::Popen.popen(%W{tar -zxf #{export_file} -C #{tmpdir}}) + + yield(exit_status, tmpdir) + end + end + + # Recursively finds key/values including +key+ as part of the key, inside a nested hash + def deep_find_with_parent(sensitive_key_word, object, found = nil) + sensitive_key_found = object_contains_key?(object, sensitive_key_word) + + # Returns the parent object and the object found containing a sensitive word as part of the key + if sensitive_key_found && object[sensitive_key_found] + ObjectWithParent.new(object[sensitive_key_found], object, sensitive_key_found) + elsif object.is_a?(Enumerable) + # Recursively lookup for keys containing sensitive words in a Hash or Array + object_with_parent = nil + + object.find do |*hash_or_array| + object_with_parent = deep_find_with_parent(sensitive_key_word, hash_or_array.last, found) + end + + object_with_parent + end + end + + # Return true if the hash has a key containing a sensitive word + def object_contains_key?(object, sensitive_key_word) + return false unless object.is_a?(Hash) + + object.keys.find { |key| key.include?(sensitive_key_word) } + end + + # Returns the offended ObjectWithParent object if a sensitive word is found inside a hash, + # excluding the whitelisted safe hashes. + def find_sensitive_attributes(sensitive_word, project_hash) + loop do + object_with_parent = deep_find_with_parent(sensitive_word, project_hash) + + return nil unless object_with_parent && object_with_parent.object + + if is_safe_hash?(object_with_parent.parent, sensitive_word) + # It's in the safe list, remove hash and keep looking + object_with_parent.parent.delete(object_with_parent.key_found) + else + return object_with_parent + end + + nil + end + end + + # Returns true if it's one of the excluded models in +safe_list+ + def is_safe_hash?(parent, sensitive_word) + return false unless parent && safe_list[sensitive_word.to_sym] + + # Extra attributes that appear in a model but not in the exported hash. + excluded_attributes = ['type'] + + safe_list[sensitive_word.to_sym].each do |model| + # Check whether this is a hash attribute inside a model + if model.is_a?(Symbol) + return true if (safe_hashes[model] - parent.keys).empty? + else + return true if safe_model?(model, excluded_attributes, parent) + end + end + + false + end + + # Compares model attributes with those those found in the hash + # and returns true if there is a match, ignoring some excluded attributes. + def safe_model?(model, excluded_attributes, parent) + excluded_attributes += associations_for(model) + parsed_model_attributes = parsed_attributes(model.name.underscore, model.attribute_names) + + (parsed_model_attributes - parent.keys - excluded_attributes).empty? + end +end -- GitLab From 98abf6935c8ffa93e888ad540900cbc95f302a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 21 Sep 2016 14:38:40 +0000 Subject: [PATCH 19/49] Merge branch 'fix/import-security-specs' into 'master' Fix Import/Export security specs Related https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/1987#note_83855 See merge request !1997 --- .../projects/import_export/export_file_spec.rb | 2 +- spec/lib/gitlab/import_export/all_models.yml | 8 ++++++++ .../import_export/safe_model_attributes.yml | 15 ++++++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb index 7e2c701e401ac5..27c986c5187e0f 100644 --- a/spec/features/projects/import_export/export_file_spec.rb +++ b/spec/features/projects/import_export/export_file_spec.rb @@ -15,7 +15,7 @@ let(:sensitive_words) { %w[pass secret token key] } let(:safe_list) do { - token: [ProjectHook, Ci::Trigger], + token: [ProjectHook, Ci::Trigger, CommitStatus], key: [Project, Ci::Variable, :yaml_variables] } end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 30968ba2d5ff72..2d8d1bb441cc93 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -13,6 +13,8 @@ issues: - user_agent_detail - moved_to - events +- merge_requests_closing_issues +- metrics events: - author - project @@ -71,6 +73,8 @@ merge_requests: - merge_request_diffs - merge_request_diff - events +- merge_requests_closing_issues +- metrics merge_request_diff: - merge_request pipelines: @@ -101,6 +105,10 @@ protected_branches: - project - merge_access_levels - push_access_levels +merge_access_levels: +- protected_branch +push_access_levels: +- protected_branch project: - taggings - base_tags diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index f2d272ca7e2737..7efe14545b597f 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -214,6 +214,7 @@ CommitStatus: - when - yaml_variables - queued_at +- token Ci::Variable: - id - project_id @@ -307,4 +308,16 @@ ProjectFeature: - snippets_access_level - builds_access_level - created_at -- updated_at \ No newline at end of file +- updated_at +ProtectedBranch::MergeAccessLevel: +- id +- protected_branch_id +- access_level +- created_at +- updated_at +ProtectedBranch::PushAccessLevel: +- id +- protected_branch_id +- access_level +- created_at +- updated_at -- GitLab From 3f55188d11124f2ead67b6d54984713989c24a7d Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 21 Sep 2016 20:43:29 +0200 Subject: [PATCH 20/49] fix import/export security specs after merge --- lib/gitlab/import_export/import_export.yml | 3 +++ spec/lib/gitlab/import_export/all_models.yml | 6 +++++- spec/lib/gitlab/import_export/safe_model_attributes.yml | 7 +++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 925a952156ff15..88803d76623485 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -10,6 +10,7 @@ project_tree: - milestone: - :events - snippets: + - :award_emoji - notes: :author - :releases @@ -66,6 +67,8 @@ excluded_attributes: - :milestone_id merge_requests: - :milestone_id + award_emoji: + - :awardable_id methods: statuses: diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 2d8d1bb441cc93..006569254a6c6c 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -49,6 +49,7 @@ snippets: - author - project - notes +- award_emoji releases: - project project_members: @@ -180,4 +181,7 @@ project: - triggers - environments - deployments -- project_feature \ No newline at end of file +- project_feature +award_emoji: +- awardable +- user \ No newline at end of file diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 7efe14545b597f..8bccd313d6c474 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -321,3 +321,10 @@ ProtectedBranch::PushAccessLevel: - access_level - created_at - updated_at +AwardEmoji: +- id +- user_id +- name +- awardable_type +- created_at +- updated_at -- GitLab From 3b190a118fd2709841764b199709c092f42a0025 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 21 Sep 2016 10:12:29 +0000 Subject: [PATCH 21/49] Merge branch 'ad_recursive' into 'master' Active Directory ranged member retrieval Related to gitlab-org/gitlab-ee!422 See merge request !719 --- CHANGELOG-EE | 1 + lib/ee/gitlab/ldap/adapter.rb | 25 +++-- lib/ee/gitlab/ldap/group.rb | 96 ++++++++++++++++--- spec/lib/ee/gitlab/ldap/group_spec.rb | 128 +++++++++++++++++++++----- spec/support/ee/ldap_helpers.rb | 64 ++++++++++++- 5 files changed, 270 insertions(+), 44 deletions(-) diff --git a/CHANGELOG-EE b/CHANGELOG-EE index a4be3699f5a7ee..4624c50b1f8729 100644 --- a/CHANGELOG-EE +++ b/CHANGELOG-EE @@ -7,6 +7,7 @@ v 8.12.0 (Unreleased) - Add 'Sync now' to group members page !704 - [ES] Instrument other Gitlab::Elastic classes - [ES] Fix: Elasticsearch does not find partial matches in project names + - Faster Active Directory group membership resolution !719 - [ES] Global code search - [ES] Improve logging - Fix projects with remote mirrors asynchronously destruction diff --git a/lib/ee/gitlab/ldap/adapter.rb b/lib/ee/gitlab/ldap/adapter.rb index a2aa35e25063e9..a718d94e3c8e25 100644 --- a/lib/ee/gitlab/ldap/adapter.rb +++ b/lib/ee/gitlab/ldap/adapter.rb @@ -31,13 +31,26 @@ def group(*args) groups(*args).first end - def dns_for_filter(filter) + def group_members_in_range(dn, range_start) ldap_search( - base: config.base, - filter: filter, - scope: Net::LDAP::SearchScope_WholeSubtree, - attributes: %w{dn} - ).map(&:dn) + base: dn, + scope: Net::LDAP::SearchScope_BaseObject, + attributes: ["member;range=#{range_start}-*"], + ).first + end + + def nested_groups(parent_dn) + options = { + base: config.group_base, + filter: Net::LDAP::Filter.join( + Net::LDAP::Filter.eq('objectClass', 'group'), + Net::LDAP::Filter.eq('memberOf', parent_dn) + ) + } + + ldap_search(options).map do |entry| + LDAP::Group.new(entry, self) + end end end end diff --git a/lib/ee/gitlab/ldap/group.rb b/lib/ee/gitlab/ldap/group.rb index 6ed5a548cbd6ee..09a390fa8ae194 100644 --- a/lib/ee/gitlab/ldap/group.rb +++ b/lib/ee/gitlab/ldap/group.rb @@ -39,14 +39,15 @@ def member_uids entry.memberuid end - def member_dns + def dn + entry.dn + end + + def member_dns(nested_groups_to_skip = []) dns = [] - # There's an edge-case with AD where sometimes a recursive search - # doesn't return all users at the top-level. Concat recursive results - # with regular results to be safe. See gitlab-ee#484 - if active_directory? - dns = adapter.dns_for_filter(active_directory_recursive_memberof_filter) + if active_directory? && adapter + dns.concat(active_directory_members(entry, nested_groups_to_skip)) end if (entry.respond_to? :member) && (entry.respond_to? :submember) @@ -60,20 +61,91 @@ def member_dns else Rails.logger.warn("Could not find member DNs for LDAP group #{entry.inspect}") end + dns.uniq end private - # We use the ActiveDirectory LDAP_MATCHING_RULE_IN_CHAIN matching rule; see - # http://msdn.microsoft.com/en-us/library/aa746475%28VS.85%29.aspx#code-snippet-5 - def active_directory_recursive_memberof_filter - Net::LDAP::Filter.ex("memberOf:1.2.840.113556.1.4.1941", entry.dn) - end - def entry @entry end + + # Active Directory range member methods + + def has_member_range?(entry) + member_range_attribute(entry).present? + end + + def member_range_attribute(entry) + entry.attribute_names.find { |a| a.to_s.start_with?("member;range=")}.to_s + end + + def active_directory_members(entry, nested_groups_to_skip) + require 'net/ldap/dn' + + members = [] + + # Retrieve all member pages/ranges + members.concat(ranged_members(entry)) if has_member_range?(entry) + # Process nested group members + members.concat(nested_members(nested_groups_to_skip)) + + # Clean dns of groups and users outside the base + members.reject! { |dn| nested_groups_to_skip.include?(dn) } + base = Net::LDAP::DN.new(adapter.config.base.downcase).to_a + members.select! { |dn| Net::LDAP::DN.new(dn.downcase).to_a.last(base.length) == base } + + members + end + + # AD requires use of range retrieval for groups with more than 1500 members + # cf. https://msdn.microsoft.com/en-us/library/aa367017(v=vs.85).aspx + def ranged_members(entry) + members = [] + + # Concatenate the members in the current range + members.concat(entry[member_range_attribute(entry)]) + + # Recursively concatenate members until end of ranges + if has_more_member_ranges?(entry) + next_entry = adapter.group_members_in_range(dn, next_member_range_start(entry)) + + members.concat(ranged_members(next_entry)) + end + + members + end + + # Process any AD nested groups. Use a manual process because + # the AD recursive member of filter is too slow and uses too + # much CPU on the AD server. + def nested_members(nested_groups_to_skip) + # Ignore this group if we see it again in a nested group. + # Prevents infinite loops. + nested_groups_to_skip << dn + + members = [] + nested_groups = adapter.nested_groups(dn) + + nested_groups.each do |nested_group| + next if nested_groups_to_skip.include?(nested_group.dn) + + members.concat(nested_group.member_dns(nested_groups_to_skip)) + end + + members + end + + def has_more_member_ranges?(entry) + next_member_range_start(entry).present? + end + + def next_member_range_start(entry) + match = member_range_attribute(entry).match /^member;range=\d+-(\d+|\*)$/ + + match[1].to_i + 1 if match.present? && match[1] != '*' + end end end end diff --git a/spec/lib/ee/gitlab/ldap/group_spec.rb b/spec/lib/ee/gitlab/ldap/group_spec.rb index 3ea5cb87f7fbf0..054db487245b7b 100644 --- a/spec/lib/ee/gitlab/ldap/group_spec.rb +++ b/spec/lib/ee/gitlab/ldap/group_spec.rb @@ -3,47 +3,125 @@ describe EE::Gitlab::LDAP::Group, lib: true do include LdapHelpers - let(:adapter) { ldap_adapter } + before do + stub_ldap_config(active_directory: true) + end describe '#member_dns' do - def ldif - Net::LDAP::Entry.from_single_ldif_string( - <<-EOS.strip_heredoc - dn: cn=ldap_group1,ou=groups,dc=example,dc=com - cn: ldap_group1 - description: LDAP Group 1 - member: uid=user1,ou=users,dc=example,dc=com - member: uid=user2,ou=users,dc=example,dc=com - member: uid=user3,ou=users,dc=example,dc=com - objectclass: top - objectclass: groupOfNames - EOS + let(:adapter) { ldap_adapter } + + it 'resolves the correct member_dns when member has a range' do + group_entry_page1 = ldap_group_entry_with_member_range( + [user_dn('user1'), user_dn('user2'), user_dn('user3')], + range_start: '0', + range_end: '2' ) - end + group_entry_page2 = ldap_group_entry_with_member_range( + [user_dn('user4'), user_dn('user5'), user_dn('user6')], + range_start: '3', + range_end: '*' + ) + group = EE::Gitlab::LDAP::Group.new(group_entry_page1, adapter) + stub_ldap_adapter_group_members_in_range(group_entry_page2, adapter, range_start: '3') + stub_ldap_adapter_nested_groups(group.dn, [], adapter) - let(:group) { described_class.new(ldif, adapter) } - let(:recursive_dns) do - %w( - uid=user3,ou=users,dc=example,dc=com - uid=user4,ou=users,dc=example,dc=com - uid=user5,ou=users,dc=example,dc=com + expect(group.member_dns).to match_array( + %w( + uid=user1,ou=users,dc=example,dc=com + uid=user2,ou=users,dc=example,dc=com + uid=user3,ou=users,dc=example,dc=com + uid=user4,ou=users,dc=example,dc=com + uid=user5,ou=users,dc=example,dc=com + uid=user6,ou=users,dc=example,dc=com + ) ) end - it 'concatenates recursive and regular results and returns uniq' do - allow(group).to receive(:active_directory?).and_return(true) - allow(adapter).to receive(:dns_for_filter).and_return(recursive_dns) + context 'when there are nested groups' do + let(:group1_entry) do + ldap_group_entry( + [user_dn('user1'), user_dn('user2')], + objectclass: 'group', + member_attr: 'member' + ) + end + let(:group2_entry) do + ldap_group_entry( + [user_dn('user3'), user_dn('user4')], + cn: 'ldap_group2', + objectclass: 'group', + member_attr: 'member', + member_of: group1_entry.dn + ) + end + let(:group) { EE::Gitlab::LDAP::Group.new(group1_entry, adapter) } + + it 'resolves the correct member_dns when there are nested groups' do + group3_entry = ldap_group_entry( + [user_dn('user5'), user_dn('user6')], + cn: 'ldap_group3', + objectclass: 'group', + member_attr: 'member', + member_of: group1_entry.dn + ) + nested_groups = [group2_entry, group3_entry] + stub_ldap_adapter_nested_groups(group.dn, nested_groups, adapter) + stub_ldap_adapter_nested_groups(group2_entry.dn, [], adapter) + stub_ldap_adapter_nested_groups(group3_entry.dn, [], adapter) - expect(group.member_dns) - .to match_array( + expect(group.member_dns).to match_array( %w( uid=user1,ou=users,dc=example,dc=com uid=user2,ou=users,dc=example,dc=com uid=user3,ou=users,dc=example,dc=com uid=user4,ou=users,dc=example,dc=com uid=user5,ou=users,dc=example,dc=com + uid=user6,ou=users,dc=example,dc=com ) ) + end + + it 'skips duplicate nested groups' do + group3_entry = ldap_group_entry( + [user_dn('user5'), user_dn('user6')], + cn: 'ldap_group3', + objectclass: 'group', + member_attr: 'member', + member_of: [group1_entry.dn, group2_entry.dn] + ) + nested_groups = [group2_entry, group3_entry] + stub_ldap_adapter_nested_groups(group.dn, nested_groups, adapter) + stub_ldap_adapter_nested_groups(group2_entry.dn, [group3_entry], adapter) + stub_ldap_adapter_nested_groups(group3_entry.dn, [], adapter) + + expect(adapter).to receive(:nested_groups).with(group3_entry.dn).once + + group.member_dns + end + + it 'does not include group dns or users outside of the base' do + # Spaces in the 3rd DN below are intentional to ensure we're sanitizing + # DNs before comparing and not just doing a string compare. + group3_entry = ldap_group_entry( + [ + 'cn=ldap_group2,ou=groups,dc=example,dc=com', + 'uid=foo,ou=users,dc=other,dc=com', + 'uid=bar,ou=users,dc=example , dc=com' + ], + cn: 'ldap_group3', + objectclass: 'group', + member_attr: 'member', + member_of: group1_entry.dn + ) + nested_groups = [group2_entry, group3_entry] + stub_ldap_adapter_nested_groups(group.dn, nested_groups, adapter) + stub_ldap_adapter_nested_groups(group2_entry.dn, [], adapter) + stub_ldap_adapter_nested_groups(group3_entry.dn, [], adapter) + + expect(group.member_dns).not_to include('cn=ldap_group1,ou=groups,dc=example,dc=com') + expect(group.member_dns).not_to include('uid=foo,ou=users,dc=other,dc=com') + expect(group.member_dns).to include('uid=bar,ou=users,dc=example , dc=com') + end end end end diff --git a/spec/support/ee/ldap_helpers.rb b/spec/support/ee/ldap_helpers.rb index 590a63bd0440ac..4cec6841af2239 100644 --- a/spec/support/ee/ldap_helpers.rb +++ b/spec/support/ee/ldap_helpers.rb @@ -46,7 +46,8 @@ def ldap_group_entry( members, cn: 'ldap_group1', objectclass: 'groupOfNames', - member_attr: 'uniqueMember' + member_attr: 'uniqueMember', + member_of: nil ) entry = Net::LDAP::Entry.from_single_ldif_string(<<-EOS.strip_heredoc) dn: cn=#{cn},ou=groups,dc=example,dc=com @@ -56,9 +57,70 @@ def ldap_group_entry( objectclass: #{objectclass} EOS + entry['memberOf'] = member_of if member_of members = [members].flatten entry[member_attr] = members if members.any? entry end + + # To simulate Active Directory ranged member retrieval. Create an LDAP + # group entry with any number of members in a given range. A '*' signifies + # the end of the 'pages' has been reached. + # + # Example: + # ldap_group_entry_with_member_range( + # [ 'user1', 'user2' ], + # cn: 'my_group', + # range_start: '0', + # range_end: '*' + # ) + def ldap_group_entry_with_member_range( + members_in_range, + cn: 'ldap_group1', + range_start: '0', + range_end: '*' + ) + entry = Net::LDAP::Entry.from_single_ldif_string(<<-EOS.strip_heredoc) + dn: cn=#{cn},ou=groups,dc=example,dc=com + cn: #{cn} + description: LDAP Group #{cn} + EOS + + members_in_range = [members_in_range].flatten + entry["member;range=#{range_start}-#{range_end}"] = members_in_range + entry + end + + # Stub Active Directory range member retrieval. + # + # Example: + # adapter = ::Gitlab::LDAP::Adapter.new('ldapmain', double(:ldap)) + # group_entry_page1 = ldap_group_entry_with_member_range( + # [user_dn('user1'), user_dn('user2'), user_dn('user3')], + # range_start: '0', + # range_end: '2' + # ) + # group_entry_page2 = ldap_group_entry_with_member_range( + # [user_dn('user4'), user_dn('user5'), user_dn('user6')], + # range_start: '3', + # range_end: '*' + # ) + # group = EE::Gitlab::LDAP::Group.new(group_entry_page1, adapter) + # + # stub_ldap_adapter_group_members_in_range(group_entry_page2, adapter, range_start: '3') + def stub_ldap_adapter_group_members_in_range( + entry, + adapter = ldap_adapter, + range_start: '0' + ) + allow(adapter).to receive(:group_members_in_range) + .with(entry.dn, range_start.to_i).and_return(entry) + end + + def stub_ldap_adapter_nested_groups(parent_dn, entries = [], adapter = ldap_adapter) + groups = entries.map { |entry| EE::Gitlab::LDAP::Group.new(entry, adapter) } + + allow(adapter).to receive(:nested_groups).with(parent_dn).and_return(groups) + end end end -- GitLab From ffa3e2daf713d2842fc6a43c644da711f29b0458 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 21 Sep 2016 09:56:33 +0000 Subject: [PATCH 22/49] Merge branch 'restrict_attrs_inc_ssh_pubkey' into 'master' Add ssh key attribute to return attributes I realized that https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/712 would cause a regression with the SSH public key sync. This merge request fixes that functionality. Also, this uses the `Module#prepend` method that @DouweM told me about a while ago. It ends up being a **really** slick way to override a CE method and make future merges really smooth. I like this a lot! Part of this has to be changed in CE because the `#user_attributes` method didn't exist before. I made that change here, and in CE at . @DouweM What do you think about this usage of `prepend`? cc/ @jacobvosmaer-gitlab See merge request !736 --- lib/ee/gitlab/ldap/adapter.rb | 8 +++++++- lib/gitlab/ldap/adapter.rb | 8 ++++++-- spec/lib/ee/gitlab/ldap/adapter_spec.rb | 11 +++++++++-- spec/requests/api/ldap_spec.rb | 6 +++++- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/lib/ee/gitlab/ldap/adapter.rb b/lib/ee/gitlab/ldap/adapter.rb index a718d94e3c8e25..0124285dd7f19b 100644 --- a/lib/ee/gitlab/ldap/adapter.rb +++ b/lib/ee/gitlab/ldap/adapter.rb @@ -1,7 +1,7 @@ # LDAP connection adapter EE mixin # # This module is intended to encapsulate EE-specific adapter methods -# and be included in the `Gitlab::LDAP::Adapter` class. +# and be **prepended** in the `Gitlab::LDAP::Adapter` class. module EE module Gitlab module LDAP @@ -52,6 +52,12 @@ def nested_groups(parent_dn) LDAP::Group.new(entry, self) end end + + def user_attributes + attributes = super + attributes << config.sync_ssh_keys if config.sync_ssh_keys + attributes + end end end end diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb index 68f61fa293caa3..3f84cd816c8018 100644 --- a/lib/gitlab/ldap/adapter.rb +++ b/lib/gitlab/ldap/adapter.rb @@ -5,7 +5,7 @@ module Gitlab module LDAP class Adapter - include EE::Gitlab::LDAP::Adapter + prepend EE::Gitlab::LDAP::Adapter attr_reader :provider, :ldap @@ -76,7 +76,7 @@ def ldap_search(*args) private def user_options(field, value, limit) - options = { attributes: %W(#{config.uid} cn mail dn) } + options = { attributes: user_attributes } options[:size] = limit if limit if field.to_sym == :dn @@ -104,6 +104,10 @@ def user_filter(filter = nil) filter end end + + def user_attributes + %W(#{config.uid} cn mail dn) + end end end end diff --git a/spec/lib/ee/gitlab/ldap/adapter_spec.rb b/spec/lib/ee/gitlab/ldap/adapter_spec.rb index 5296cb28fa25f8..e8ce58e419f109 100644 --- a/spec/lib/ee/gitlab/ldap/adapter_spec.rb +++ b/spec/lib/ee/gitlab/ldap/adapter_spec.rb @@ -7,9 +7,9 @@ expect(Gitlab::LDAP::Adapter).to include_module(EE::Gitlab::LDAP::Adapter) end - describe '#groups' do - let(:adapter) { ldap_adapter('ldapmain') } + let(:adapter) { ldap_adapter('ldapmain') } + describe '#groups' do before do stub_ldap_config( group_base: 'ou=groups,dc=example,dc=com', @@ -39,4 +39,11 @@ expect(results.first.member_dns).to match_array(%w(john mary)) end end + + describe '#user_attributes' do + it 'appends EE-specific attributes' do + stub_ldap_config(uid: 'uid', sync_ssh_keys: 'sshPublicKey') + expect(adapter.user_attributes).to match_array(%w(uid dn cn mail sshPublicKey)) + end + end end diff --git a/spec/requests/api/ldap_spec.rb b/spec/requests/api/ldap_spec.rb index 81790f9d44c9fa..4161989f38282f 100644 --- a/spec/requests/api/ldap_spec.rb +++ b/spec/requests/api/ldap_spec.rb @@ -2,7 +2,10 @@ describe API::API do include ApiHelpers + include LdapHelpers + let(:user) { create(:user) } + let(:adapter) { ldap_adapter } before do groups = [ @@ -10,7 +13,8 @@ OpenStruct.new(cn: 'students') ] - allow_any_instance_of(Gitlab::LDAP::Adapter).to receive_messages(groups: groups) + allow(Gitlab::LDAP::Adapter).to receive(:new).and_return(adapter) + allow(adapter).to receive_messages(groups: groups) end describe "GET /ldap/groups" do -- GitLab From a2c1856e3ee93b6a2d70bd1ebcfe9255798ed41e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 21 Sep 2016 01:28:20 +0000 Subject: [PATCH 23/49] Merge branch 'repository-size-restrictions' into 'master' Enforce repository size limit across all projects and groups, includes LFS objects in that limit. Limit can be set globally, and overridden per group, and/or project. Backend functionality is there and comprehensive tests are included, but there is still some frontend work to be done and documentation to be added. I'm submitting early for review as we are close to the release. @DouweM @dbalexandre I'd appreciate it if you both could start with the review while I finish the documentation and the missing frontend parts. /cc @JobV @regisF Fixes #559 Replaces gitlab-org/gitlab-ce!6020 ## Screenshots (see gitlab-org/gitlab-ce!6020 for more) ![Screen_Shot_2016-09-18_at_9.55.38_PM](/uploads/66eeaced1f27c7e2115feaa4775a6e99/Screen_Shot_2016-09-18_at_9.55.38_PM.png) ![Screen_Shot_2016-09-18_at_9.57.12_PM](/uploads/d811d6c184044df527bd2f81cff651ce/Screen_Shot_2016-09-18_at_9.57.12_PM.png) ![Screen_Shot_2016-09-18_at_9.58.03_PM](/uploads/a2c5b2695454dda639537304a1bcd99b/Screen_Shot_2016-09-18_at_9.58.03_PM.png) ![Screen_Shot_2016-09-19_at_1.44.19_PM](/uploads/4cc6cca7536787bde49c0b086086cbcb/Screen_Shot_2016-09-19_at_1.44.19_PM.png) See merge request !740 --- CHANGELOG-EE | 1 + .../admin/application_settings_controller.rb | 1 + app/controllers/admin/groups_controller.rb | 1 + app/controllers/groups_controller.rb | 3 +- .../projects/git_http_controller.rb | 4 +- app/controllers/projects_controller.rb | 1 + app/helpers/groups_helper.rb | 6 ++ app/helpers/lfs_helper.rb | 35 ++++++++-- app/helpers/projects_helper.rb | 14 +++- app/models/application_setting.rb | 4 ++ app/models/group.rb | 9 +++ app/models/namespace.rb | 4 ++ app/models/project.rb | 31 ++++++++ app/services/files/base_service.rb | 4 ++ app/services/merge_requests/merge_service.rb | 6 ++ .../application_settings/_form.html.haml | 5 ++ app/views/admin/groups/_form.html.haml | 2 + .../_repository_size_limit_setting.html.haml | 8 +++ app/views/groups/edit.html.haml | 2 + app/views/projects/edit.html.haml | 8 +++ .../merge_requests/widget/_open.html.haml | 2 + .../widget/open/_size_limit_reached.html.haml | 8 +++ ...tory_size_limit_to_application_settings.rb | 12 ++++ ...8_add_repository_size_limit_to_projects.rb | 12 ++++ ...add_repository_size_limit_to_namespaces.rb | 12 ++++ db/schema.rb | 3 + doc/README.md | 1 + doc/administration/repository_restrictions.md | 37 ++++++++++ lib/ee/gitlab/deltas.rb | 25 +++++++ lib/gitlab/git_access.rb | 16 ++++- lib/gitlab/repository_size_error.rb | 57 +++++++++++++++ spec/lib/gitlab/git_access_spec.rb | 18 +++++ spec/lib/gitlab/repository_size_error_spec.rb | 43 ++++++++++++ spec/models/ee/group_spec.rb | 18 +++++ spec/models/namespace_spec.rb | 12 ++++ spec/models/project_spec.rb | 70 +++++++++++++++++++ spec/requests/git_http_spec.rb | 16 +++++ spec/requests/lfs_http_spec.rb | 27 ++++++- .../merge_requests/merge_service_spec.rb | 16 +++++ 39 files changed, 540 insertions(+), 14 deletions(-) create mode 100644 app/views/groups/_repository_size_limit_setting.html.haml create mode 100644 app/views/projects/merge_requests/widget/open/_size_limit_reached.html.haml create mode 100644 db/migrate/20160829104026_add_repository_size_limit_to_application_settings.rb create mode 100644 db/migrate/20160913172608_add_repository_size_limit_to_projects.rb create mode 100644 db/migrate/20160913172737_add_repository_size_limit_to_namespaces.rb create mode 100644 doc/administration/repository_restrictions.md create mode 100644 lib/ee/gitlab/deltas.rb create mode 100644 lib/gitlab/repository_size_error.rb create mode 100644 spec/lib/gitlab/repository_size_error_spec.rb diff --git a/CHANGELOG-EE b/CHANGELOG-EE index 4624c50b1f8729..b181aada1166e3 100644 --- a/CHANGELOG-EE +++ b/CHANGELOG-EE @@ -5,6 +5,7 @@ v 8.12.0 (Unreleased) - [ES] Instrument Elasticsearch::Git::Repository - Request only the LDAP attributes we need - Add 'Sync now' to group members page !704 + - Add repository size limits and enforce them !740 - [ES] Instrument other Gitlab::Elastic classes - [ES] Fix: Elasticsearch does not find partial matches in project names - Faster Active Directory group membership resolution !719 diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 3aaa4fee4b136d..6397ffd5c5a41b 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -134,6 +134,7 @@ def application_setting_params :usage_ping_enabled, :repository_storage, :enabled_git_access_protocol, + :repository_size_limit, restricted_visibility_levels: [], import_sources: [], disabled_oauth_sign_in_sources: [] diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index aed77d0358a7b9..8b7a40e4a17ede 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -66,6 +66,7 @@ def group_params :lfs_enabled, :name, :path, + :repository_size_limit, :request_access_enabled, :visibility_level ) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 0184e38373eb4f..b39c3c21ad5d30 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -131,12 +131,13 @@ def group_params :avatar, :description, :lfs_enabled, + :membership_lock, :name, :path, :public, + :repository_size_limit, :request_access_enabled, :share_with_group_lock, - :membership_lock, :visibility_level ) end diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 662d38b10a5867..785de7e2123f3e 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -68,7 +68,9 @@ def render_http_not_allowed def render_denied if user && user.can?(:read_project, project) - render plain: 'Access denied', status: :forbidden + message = project.above_size_limit? ? access_check.message : 'Access denied' + + render plain: message, status: :forbidden else # Do not leak information about project existence render_not_found diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 16938972279adc..d1404d66afc6a6 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -331,6 +331,7 @@ def project_params :mirror, :mirror_user_id, :mirror_trigger_builds, + :repository_size_limit, :reset_approvals_on_push ) end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index ab880ed6de0874..9c8ee69e592ae5 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -41,6 +41,12 @@ def projects_lfs_status(group) end end + def size_limit_message_for_group(group) + show_lfs = group.lfs_enabled? ? 'and their respective LFS files' : '' + + "Repositories within this group #{show_lfs} will be restricted to this maximum size. Can be overridden inside each project. 0 for unlimited. Leave empty to inherit the global value." + end + def group_lfs_status(group) status = group.lfs_enabled? ? 'enabled' : 'disabled' diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb index c15ecc8f86eeab..4d2a29ecb1d74e 100644 --- a/app/helpers/lfs_helper.rb +++ b/app/helpers/lfs_helper.rb @@ -1,11 +1,13 @@ module LfsHelper + include Gitlab::Routing.url_helpers + def require_lfs_enabled! return if Gitlab.config.lfs.enabled render( json: { message: 'Git LFS is not enabled on this GitLab server, contact your admin.', - documentation_url: "#{Gitlab.config.gitlab.url}/help", + documentation_url: help_url, }, status: 501 ) @@ -16,7 +18,11 @@ def lfs_check_access! return if upload_request? && lfs_upload_access? if project.public? || (user && user.can?(:read_project, project)) - render_lfs_forbidden + if project.above_size_limit? || objects_exceed_repo_limit? + render_size_error + else + render_lfs_forbidden + end else render_lfs_not_found end @@ -38,15 +44,25 @@ def build_can_download_code? def lfs_upload_access? return false unless project.lfs_enabled? + return false if project.above_size_limit? || objects_exceed_repo_limit? has_authentication_ability?(:push_code) && can?(user, :push_code, project) end + def objects_exceed_repo_limit? + return false unless project.size_limit_enabled? + return @limit_exceeded if defined?(@limit_exceeded) + + size_of_objects = objects.sum { |o| o[:size] } + + @limit_exceeded = (project.repository_and_lfs_size + size_of_objects.to_mb) > project.actual_size_limit + end + def render_lfs_forbidden render( json: { message: 'Access forbidden. Check your access level.', - documentation_url: "#{Gitlab.config.gitlab.url}/help", + documentation_url: help_url, }, content_type: "application/vnd.git-lfs+json", status: 403 @@ -57,13 +73,24 @@ def render_lfs_not_found render( json: { message: 'Not found.', - documentation_url: "#{Gitlab.config.gitlab.url}/help", + documentation_url: help_url, }, content_type: "application/vnd.git-lfs+json", status: 404 ) end + def render_size_error + render( + json: { + message: Gitlab::RepositorySizeError.new(project).push_error, + documentation_url: help_url, + }, + content_type: "application/vnd.git-lfs+json", + status: 406 + ) + end + def storage_project @storage_project ||= begin result = project diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 8d7a4b80471ce0..235f8d8ad88a5b 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -214,6 +214,12 @@ def project_lfs_status(project) end end + def size_limit_message(project) + show_lfs = project.lfs_enabled? ? 'including files in LFS' : '' + + "The total size of this project's repository #{show_lfs} will be limited to this size. 0 for unlimited. Leave empty to inherit the group/global value." + end + def git_user_name if current_user current_user.name @@ -231,8 +237,12 @@ def git_user_email end def repository_size(project = @project) - size_in_bytes = project.repository_size * 1.megabyte - number_to_human_size(size_in_bytes, delimiter: ',', precision: 2) + size_in_bytes = project.repository_and_lfs_size * 1.megabyte + limit_in_bytes = project.actual_size_limit * 1.megabyte + + limit_text = limit_in_bytes.zero? ? '' : "/#{number_to_human_size(limit_in_bytes, delimiter: ',', precision: 2)}" + + "#{number_to_human_size(size_in_bytes, delimiter: ',', precision: 2)}#{limit_text}" end def default_url_to_repo(project = @project) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 24a33060dfbf07..da0fb09b62f506 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -63,6 +63,10 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { only_integer: true, greater_than: 0 } + validates :repository_size_limit, + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :container_registry_token_expire_delay, presence: true, numericality: { only_integer: true, greater_than: 0 } diff --git a/app/models/group.rb b/app/models/group.rb index 11b09a92783759..59718668a5296f 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -33,6 +33,9 @@ class Group < Namespace validates :avatar, file_size: { maximum: 200.kilobytes.to_i } + validates :repository_size_limit, + numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true } + mount_uploader :avatar, AvatarUploader after_create :post_create_hook @@ -199,6 +202,12 @@ def post_destroy_hook system_hook_service.execute_hooks_for(self, :destroy) end + def actual_size_limit + return current_application_settings.repository_size_limit if repository_size_limit.nil? + + repository_size_limit + end + def system_hook_service SystemHooksService.new end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 332e297840cd5b..374689af0d3a3f 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -147,6 +147,10 @@ def lfs_enabled? Gitlab.config.lfs.enabled end + def actual_size_limit + current_application_settings.repository_size_limit + end + private def repository_storage_paths diff --git a/app/models/project.rb b/app/models/project.rb index 34cd50ca563a73..77245b7999d9d5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -182,6 +182,9 @@ def set_last_activity_at presence: true, inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } } + validates :repository_size_limit, + numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true } + with_options if: :mirror? do |project| project.validates :import_url, presence: true project.validates :mirror_user, presence: true @@ -1531,6 +1534,34 @@ def reset_pushes_since_gc Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) } end + def repository_and_lfs_size + repository_size + lfs_objects.sum(:size).to_i.to_mb + end + + def above_size_limit? + return false unless size_limit_enabled? + + repository_and_lfs_size > actual_size_limit + end + + def size_to_remove + repository_and_lfs_size - actual_size_limit + end + + def actual_size_limit + return namespace.actual_size_limit if repository_size_limit.nil? + + repository_size_limit + end + + def size_limit_enabled? + actual_size_limit != 0 + end + + def changes_will_exceed_size_limit?(size_mb) + size_limit_enabled? && (size_mb > actual_size_limit || size_mb + repository_and_lfs_size > actual_size_limit) + end + private def pushes_since_gc_redis_key diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 4ccd02d15f4859..91deb36a1a98d2 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -49,6 +49,10 @@ def raise_error(message) def validate allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@target_branch) + if project.above_size_limit? + raise_error(Gitlab::RepositorySizeError.new(project).commit_error) + end + unless allowed raise_error("You are not allowed to push into this branch") end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 33e3a8daafbb62..a599a4eb0af8a7 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -18,6 +18,12 @@ def execute(merge_request) return error('Merge request is not mergeable') unless @merge_request.mergeable? + if @merge_request.target_project.above_size_limit? + message = Gitlab::RepositorySizeError.new(@merge_request.target_project).merge_error + @merge_request.update(merge_error: message) + return error(message) + end + merge_request.in_locked_state do if commit after_merge diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index df060ef59c2519..84452c402ea225 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -99,6 +99,11 @@ = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'control-label col-sm-2' .col-sm-10 = f.number_field :max_attachment_size, class: 'form-control' + .form-group + = f.label :repository_size_limit, 'Per repository size limit (MB)', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :repository_size_limit, class: 'form-control', min: 0 + %span.help-block#repository_size_limit_help_block Includes LFS objects. It can be overridden per group, or per project. 0 for unlimited .form-group = f.label :session_expire_delay, 'Session duration (minutes)', class: 'control-label col-sm-2' .col-sm-10 diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 857ca9f1f1c0e3..697cf5d68b084d 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -2,6 +2,8 @@ = form_errors(@group) = render 'shared/group_form', f: f + = render 'groups/repository_size_limit_setting', f: f + .form-group.group-description-holder = f.label :avatar, "Group avatar", class: 'control-label' .col-sm-10 diff --git a/app/views/groups/_repository_size_limit_setting.html.haml b/app/views/groups/_repository_size_limit_setting.html.haml new file mode 100644 index 00000000000000..27463737489985 --- /dev/null +++ b/app/views/groups/_repository_size_limit_setting.html.haml @@ -0,0 +1,8 @@ +- if current_user.admin? + .form-group + = f.label :repository_size_limit, class: 'control-label' do + Repository size limit (MB) + .col-sm-10 + = f.number_field :repository_size_limit, class: 'form-control', min: 0 + %span.help-block#repository_size_limit_help_block + = size_limit_message_for_group(@group) diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 596016d878a9c3..9ad943220819cf 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -6,6 +6,8 @@ = form_errors(@group) = render 'shared/group_form', f: f + = render 'repository_size_limit_setting', f: f + .form-group .col-sm-offset-2.col-sm-10 = image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160' diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index c8228c040e69f5..1489ebb04781c8 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -34,6 +34,14 @@ = visibility_level_label(@project.visibility_level) .light= visibility_level_description(@project.visibility_level, @project) + - if current_user.admin? + .form-group + = f.label :repository_size_limit, class: 'label-light' do + Repository size limit (MB) + = f.number_field :repository_size_limit, class: 'form-control', min: 0 + %span.help-block#repository_size_limit_help_block + = size_limit_message(@project) + .form-group = render 'shared/allow_request_access', form: f diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 110da69e359674..df7b82658369b9 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -11,6 +11,8 @@ = render 'projects/merge_requests/widget/open/geo' - if @project.archived? = render 'projects/merge_requests/widget/open/archived' + - elsif @project.above_size_limit? + = render 'projects/merge_requests/widget/open/size_limit_reached' - elsif @merge_request.commits.blank? = render 'projects/merge_requests/widget/open/nothing' - elsif @merge_request.branch_missing? diff --git a/app/views/projects/merge_requests/widget/open/_size_limit_reached.html.haml b/app/views/projects/merge_requests/widget/open/_size_limit_reached.html.haml new file mode 100644 index 00000000000000..25c637f101dc15 --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_size_limit_reached.html.haml @@ -0,0 +1,8 @@ +- error_messages = Gitlab::RepositorySizeError.new(@project) + +%h4.size-limit-reached + = icon("exclamation-triangle") + = error_messages.merge_error + +%p + = error_messages.more_info_message diff --git a/db/migrate/20160829104026_add_repository_size_limit_to_application_settings.rb b/db/migrate/20160829104026_add_repository_size_limit_to_application_settings.rb new file mode 100644 index 00000000000000..f934281cc9bf29 --- /dev/null +++ b/db/migrate/20160829104026_add_repository_size_limit_to_application_settings.rb @@ -0,0 +1,12 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddRepositorySizeLimitToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :application_settings, :repository_size_limit, :integer, default: 0 + end +end diff --git a/db/migrate/20160913172608_add_repository_size_limit_to_projects.rb b/db/migrate/20160913172608_add_repository_size_limit_to_projects.rb new file mode 100644 index 00000000000000..636f3b661182b9 --- /dev/null +++ b/db/migrate/20160913172608_add_repository_size_limit_to_projects.rb @@ -0,0 +1,12 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddRepositorySizeLimitToProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :projects, :repository_size_limit, :integer + end +end diff --git a/db/migrate/20160913172737_add_repository_size_limit_to_namespaces.rb b/db/migrate/20160913172737_add_repository_size_limit_to_namespaces.rb new file mode 100644 index 00000000000000..23955d8ab121a0 --- /dev/null +++ b/db/migrate/20160913172737_add_repository_size_limit_to_namespaces.rb @@ -0,0 +1,12 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddRepositorySizeLimitToNamespaces < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :namespaces, :repository_size_limit, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index b29e1c36c56151..725bd6603e8928 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -100,6 +100,7 @@ t.boolean "usage_ping_enabled", default: true, null: false t.boolean "koding_enabled" t.string "koding_url" + t.integer "repository_size_limit", default: 0 end create_table "approvals", force: :cascade do |t| @@ -739,6 +740,7 @@ t.datetime "ldap_sync_last_successful_update_at" t.datetime "ldap_sync_last_sync_at" t.boolean "lfs_enabled" + t.integer "repository_size_limit" end add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree @@ -959,6 +961,7 @@ t.boolean "has_external_wiki" t.boolean "repository_read_only" t.boolean "lfs_enabled" + t.integer "repository_size_limit" end add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree diff --git a/doc/README.md b/doc/README.md index 7499a486ddeb89..2ed6be047b8665 100644 --- a/doc/README.md +++ b/doc/README.md @@ -68,6 +68,7 @@ - [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability. - [Container Registry](administration/container_registry.md) Configure Docker Registry with GitLab. - [Multiple mountpoints for the repositories storage](administration/repository_storages.md) Define multiple repository storage paths to distribute the storage load. +- [Repository restrictions](administration/repository_restrictions.md) Define size restrictions for your repositories to limit the space they occupy in your storage device. Includes LFS objects. ## Contributor documentation diff --git a/doc/administration/repository_restrictions.md b/doc/administration/repository_restrictions.md new file mode 100644 index 00000000000000..41fea4d6090e9a --- /dev/null +++ b/doc/administration/repository_restrictions.md @@ -0,0 +1,37 @@ +# Repository size restrictions + +> Introduced with GitLab Enterprise Edition 8.12 + +Repositories within your GitLab instance can grow quickly, specially if you are +using LFS. Their size can grow exponentially and eat up your storage device quite +quickly. + +In order to avoid this from happening, you can set a hard limit for your repositories. +You can set this limit globally, per group, or per project, with per project limits +taking the highest priority. + +These settings can be found within each project, or group settings and within +the Application Settings for the global value. + +Setting the limit to `0` means there is no restrictions. + +# Restrictions + +When a project has reached its size limit, you will not be able to push to it, +create new merge request, or merge existing ones. You will still be able to create +new issues, and clone the project. + +Uploading LFS objects will also be denied. + +In order to lift these restrictions, the administrator of the GitLab instance +needs to increase the limit on the particular project that exceeded it. + + +# Limitations + +The first push of a new project cannot be checked for size as of now, so the first +push will allow you to upload more than the limit dictates, but every subsequent +push will be denied. + +LFS objects, however, can be checked on first push and **will** be rejected if the +sum of their sizes exceeds the maximum allowed repository size. \ No newline at end of file diff --git a/lib/ee/gitlab/deltas.rb b/lib/ee/gitlab/deltas.rb new file mode 100644 index 00000000000000..deda13dc59df93 --- /dev/null +++ b/lib/ee/gitlab/deltas.rb @@ -0,0 +1,25 @@ +module EE + module Gitlab + module Deltas + def self.delta_size_check(change, repo) + size_of_deltas = 0 + + begin + tree_a = repo.lookup(change[:oldrev]) + tree_b = repo.lookup(change[:newrev]) + diff = tree_a.diff(tree_b) + + diff.each_delta do |d| + new_file_size = d.deleted? ? 0 : ::Gitlab::Git::Blob.raw(repo, d.new_file[:oid]).size + + size_of_deltas += new_file_size + end + + size_of_deltas + rescue Rugged::OdbError, Rugged::ReferenceError, Rugged::InvalidError + size_of_deltas + end + end + end + end +end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 8ece7c1491e83a..fb6c4fbbb231f8 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -104,9 +104,9 @@ def user_push_access_check(changes) return build_status_object(true) end - unless project.repository.exists? - return build_status_object(false, "A repository for this project does not exist yet.") - end + return build_status_object(false, "A repository for this project does not exist yet.") unless project.repository.exists? + + return build_status_object(false, Gitlab::RepositorySizeError.new(project).push_error) if project.above_size_limit? if ::License.block_changes? message = ::LicenseHelper.license_message(signed_in: true, is_admin: (user && user.is_admin?)) @@ -115,6 +115,8 @@ def user_push_access_check(changes) changes_list = Gitlab::ChangesList.new(changes) + push_size_in_bytes = 0 + # Iterate over all changes to find if user allowed all of them to be applied changes_list.each do |change| status = change_access_check(change) @@ -122,6 +124,14 @@ def user_push_access_check(changes) # If user does not have access to make at least one change - cancel all push return status end + + if project.size_limit_enabled? + push_size_in_bytes += EE::Gitlab::Deltas.delta_size_check(change, project.repository) + end + end + + if project.changes_will_exceed_size_limit?(push_size_in_bytes.to_mb) + return build_status_object(false, Gitlab::RepositorySizeError.new(project).new_changes_error) end build_status_object(true) diff --git a/lib/gitlab/repository_size_error.rb b/lib/gitlab/repository_size_error.rb new file mode 100644 index 00000000000000..91f8a6a3cb1f30 --- /dev/null +++ b/lib/gitlab/repository_size_error.rb @@ -0,0 +1,57 @@ +module Gitlab + class RepositorySizeError < StandardError + include ActionView::Helpers + + attr_reader :project + + def initialize(project) + @project = project + end + + def to_s + "The size of this repository (#{current_size}) exceeds the limit of #{limit} by #{size_to_remove}." + end + + def commit_error + "Your changes could not be committed, #{base_message}" + end + + def merge_error + "This merge request cannot be merged, #{base_message}" + end + + def push_error + "Your push has been rejected, #{base_message}. #{more_info_message}" + end + + def new_changes_error + "Your push to this repository would cause it to exceed the size limit of #{limit} so it has been rejected. #{more_info_message}" + end + + def more_info_message + 'Please contact your GitLab administrator for more information.' + end + + private + + def base_message + "because this repository has exceeded its size limit of #{limit} by #{size_to_remove}" + end + + def current_size + format_number(project.repository_and_lfs_size) + end + + def limit + format_number(project.actual_size_limit) + end + + def size_to_remove + format_number(project.size_to_remove) + end + + def format_number(number) + number_to_human_size(number * 1.megabyte, delimiter: ',', precision: 2) + end + end +end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 8674b70cc55194..7a948fc6c2010a 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -618,6 +618,24 @@ def self.run_permission_checks(permissions_matrix) expect(access.push_access_check('cfe32cf61b73a0d5e9f13e774abde7ff789b1660 913c66a37b4a45b9769037c55c2d238bd0942d2e refs/heads/master')).to be_allowed end end + + describe 'repository size restrictions' do + before do + project.update_attribute(:repository_size_limit, 50) + end + + it 'returns false when blob is too big' do + allow_any_instance_of(Gitlab::Git::Blob).to receive(:size).and_return(100.megabytes.to_i) + + expect(access.push_access_check('cfe32cf61b73a0d5e9f13e774abde7ff789b1660 913c66a37b4a45b9769037c55c2d238bd0942d2e refs/heads/master')).not_to be_allowed + end + + it 'returns true when blob is just right' do + allow_any_instance_of(Gitlab::Git::Blob).to receive(:size).and_return(2.megabytes.to_i) + + expect(access.push_access_check('cfe32cf61b73a0d5e9f13e774abde7ff789b1660 913c66a37b4a45b9769037c55c2d238bd0942d2e refs/heads/master')).to be_allowed + end + end end end diff --git a/spec/lib/gitlab/repository_size_error_spec.rb b/spec/lib/gitlab/repository_size_error_spec.rb new file mode 100644 index 00000000000000..473f2abf9e0cd5 --- /dev/null +++ b/spec/lib/gitlab/repository_size_error_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Gitlab::RepositorySizeError, lib: true do + let(:project) { create(:empty_project, repository_size: 15) } + let(:message) { Gitlab::RepositorySizeError.new(project) } + let(:base_message) { 'because this repository has exceeded its size limit of 10 MB by 5 MB' } + + before do + allow(project).to receive(:actual_size_limit).and_return(10) + end + + describe 'error messages' do + describe '#to_s' do + it 'returns the correct message' do + expect(message.to_s).to eq('The size of this repository (15 MB) exceeds the limit of 10 MB by 5 MB.') + end + end + + describe '#commit_error' do + it 'returns the correct message' do + expect(message.commit_error).to eq("Your changes could not be committed, #{base_message}") + end + end + + describe '#merge_error' do + it 'returns the correct message' do + expect(message.merge_error).to eq("This merge request cannot be merged, #{base_message}") + end + end + + describe '#push_error' do + it 'returns the correct message' do + expect(message.push_error).to eq("Your push has been rejected, #{base_message}. #{message.more_info_message}") + end + end + + describe '#new_changes_error' do + it 'returns the correct message' do + expect(message.new_changes_error).to eq("Your push to this repository would cause it to exceed the size limit of 10 MB so it has been rejected. #{message.more_info_message}") + end + end + end +end diff --git a/spec/models/ee/group_spec.rb b/spec/models/ee/group_spec.rb index f8916d61bf6c3a..62d3bdbcd89681 100644 --- a/spec/models/ee/group_spec.rb +++ b/spec/models/ee/group_spec.rb @@ -85,4 +85,22 @@ expect { group.mark_ldap_sync_as_failed('Error') }.not_to raise_error end end + + describe '#actual_size_limit' do + let(:group) { build(:group) } + + before do + allow_any_instance_of(ApplicationSetting).to receive(:repository_size_limit).and_return(50) + end + + it 'returns the value set globally' do + expect(group.actual_size_limit).to eq(50) + end + + it 'returns the value set locally' do + group.update_attribute(:repository_size_limit, 75) + + expect(group.actual_size_limit).to eq(75) + end + end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 544920d18240b4..0bebbef109e386 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -87,6 +87,18 @@ end end + describe '#actual_size_limit' do + let(:namespace) { build(:namespace) } + + before do + allow_any_instance_of(ApplicationSetting).to receive(:repository_size_limit).and_return(50) + end + + it 'returns the correct size limit' do + expect(namespace.actual_size_limit).to eq(50) + end + end + describe :rm_dir do let!(:project) { create(:project, namespace: namespace) } let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.path) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 9fb87fcc068927..df3fbe134a614b 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -482,6 +482,76 @@ end end + describe 'repository size restrictions' do + let(:project) { build(:empty_project) } + + before do + allow_any_instance_of(ApplicationSetting).to receive(:repository_size_limit).and_return(50) + end + + describe '#changes_will_exceed_size_limit?' do + before do + allow(project).to receive(:repository_and_lfs_size).and_return(49) + end + it 'returns true when changes go over' do + expect(project.changes_will_exceed_size_limit?(5)).to be_truthy + end + end + + describe '#actual_size_limit' do + it 'returns the limit set in the application settings' do + expect(project.actual_size_limit).to eq(50) + end + + it 'returns the value set in the group' do + group = create(:group, repository_size_limit: 100) + project.update_attribute(:namespace_id, group.id) + + expect(project.actual_size_limit).to eq(100) + end + + it 'returns the value set locally' do + project.update_attribute(:repository_size_limit, 75) + + expect(project.actual_size_limit).to eq(75) + end + end + + describe '#size_limit_enabled?' do + it 'returns false when disabled' do + project.update_attribute(:repository_size_limit, 0) + + expect(project.size_limit_enabled?).to be_falsey + end + + it 'returns true when a limit is set' do + project.update_attribute(:repository_size_limit, 75) + + expect(project.size_limit_enabled?).to be_truthy + end + end + + describe '#above_size_limit?' do + it 'returns true when above the limit' do + allow(project).to receive(:repository_and_lfs_size).and_return(100) + + expect(project.above_size_limit?).to be_truthy + end + + it 'returns false when not over the limit' do + expect(project.above_size_limit?).to be_falsey + end + end + + describe '#size_to_remove' do + it 'returns the correct value' do + allow(project).to receive(:repository_and_lfs_size).and_return(100) + + expect(project.size_to_remove).to eq(50) + end + end + end + describe '#default_issues_tracker?' do let(:project) { create(:project) } let(:ext_project) { create(:redmine_project) } diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index e8df2d9aa08e07..ede59449b87d4f 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -222,6 +222,22 @@ end end + context "when repository is above size limit" do + let(:env) { { user: user.username, password: user.password } } + + before do + project.team << [user, :master] + end + + it 'responds with status 403' do + allow_any_instance_of(Project).to receive(:above_size_limit?).and_return(true) + + upload(path, env) do |response| + expect(response).to have_http_status(403) + end + end + end + context "when username and password are provided" do let(:env) { { user: user.username, password: 'nope' } } diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 09e4e265dd15b9..319d8f089f564f 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -634,7 +634,7 @@ { 'operation' => 'upload', 'objects' => [ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078 + 'size' => 157507855 }] } end @@ -646,10 +646,31 @@ it 'responds with upload hypermedia link' do expect(json_response['objects']).to be_kind_of(Array) expect(json_response['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897") - expect(json_response['objects'].first['size']).to eq(1575078) - expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078") + expect(json_response['objects'].first['size']).to eq(157507855) + expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/157507855") expect(json_response['objects'].first['actions']['upload']['header']).to eq('Authorization' => authorization) end + + context 'and project is above the limit' do + let(:update_lfs_permissions) do + allow_any_instance_of(Project).to receive(:above_size_limit?).and_return(true) + end + + it 'responds with status 406' do + expect(response).to have_http_status(406) + end + end + + context 'and project will go over the limit' do + let(:update_lfs_permissions) do + allow_any_instance_of(Project).to receive_messages(actual_size_limit: 145, size_limit_enabled?: true) + end + + it 'responds with status 406' do + expect(response).to have_http_status(406) + expect(json_response['documentation_url']).to include('/help') + end + end end context 'when pushing one new and one existing lfs object' do diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 1d5c562eb8e610..5550683eb79ebc 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -38,6 +38,22 @@ end end + context 'project has exceeded size limit' do + let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') } + + before do + allow(project).to receive(:above_size_limit?).and_return(true) + + perform_enqueued_jobs do + service.execute(merge_request) + end + end + + it 'returns the correct error message' do + expect(merge_request.merge_error).to include('This merge request cannot be merged') + end + end + context 'remove source branch by author' do let(:service) do merge_request.merge_params['force_remove_source_branch'] = '1' -- GitLab From 4ce37039179fab6bf42d44903e324e0c5c294563 Mon Sep 17 00:00:00 2001 From: Ruben Davila Date: Wed, 21 Sep 2016 18:52:26 -0500 Subject: [PATCH 24/49] Add EE related models/associations to the spec. --- spec/lib/gitlab/import_export/all_models.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 006569254a6c6c..55f37f5ab0d458 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -76,6 +76,8 @@ merge_requests: - events - merge_requests_closing_issues - metrics +- approvals +- approvers merge_request_diff: - merge_request pipelines: @@ -107,8 +109,10 @@ protected_branches: - merge_access_levels - push_access_levels merge_access_levels: +- user - protected_branch push_access_levels: +- user - protected_branch project: - taggings @@ -182,6 +186,16 @@ project: - environments - deployments - project_feature +- mirror_user +- push_rule +- jenkins_service +- jenkins_deprecated_service +- index_status +- approvers +- pages_domains +- audit_events +- remote_mirrors +- path_locks award_emoji: - awardable -- user \ No newline at end of file +- user -- GitLab From 302f9e68c1c174e9cfbb7d5c427f3c02bd547afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila=20Santos?= Date: Thu, 22 Sep 2016 00:08:08 +0000 Subject: [PATCH 25/49] Revert "Merge branch '22364-rails-cache-redis-connection-pool' into 'master'" This reverts merge request !6429 --- CHANGELOG | 1 - config/application.rb | 4 ---- 2 files changed, 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8ba27d8fe1e004..bdd98d8de8b99a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -171,7 +171,6 @@ v 8.12.0 (unreleased) - Add notification_settings API calls !5632 (mahcsig) - Remove duplication between project builds and admin builds view !5680 (Katarzyna Kobierska Ula Budziszewska) - Fix URLs with anchors in wiki !6300 (houqp) - - Use a ConnectionPool for Rails.cache on Sidekiq servers - Deleting source project with existing fork link will close all related merge requests !6177 (Katarzyna Kobierska Ula Budziszeska) - Return 204 instead of 404 for /ci/api/v1/builds/register.json if no builds are scheduled for a runner !6225 - Fix Gitlab::Popen.popen thread-safety issue diff --git a/config/application.rb b/config/application.rb index 8166b6003f6b6f..4792f6670a8176 100644 --- a/config/application.rb +++ b/config/application.rb @@ -116,10 +116,6 @@ class Application < Rails::Application redis_config_hash = Gitlab::Redis.params redis_config_hash[:namespace] = Gitlab::Redis::CACHE_NAMESPACE redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever - if Sidekiq.server? # threaded context - redis_config_hash[:pool_size] = Sidekiq.options[:concurrency] + 5 - redis_config_hash[:pool_timeout] = 1 - end config.cache_store = :redis_store, redis_config_hash config.active_record.raise_in_transactional_callbacks = true -- GitLab From e7714e560fe6844d033af8000f37a86182d40088 Mon Sep 17 00:00:00 2001 From: Ruben Davila Date: Wed, 21 Sep 2016 20:22:30 -0500 Subject: [PATCH 26/49] Add missing attributes for import/export projects in EE. --- spec/lib/gitlab/import_export/safe_model_attributes.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 8bccd313d6c474..e0910f4ba6dcd6 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -114,6 +114,8 @@ ProjectMember: - invite_accepted_at - requested_at - expires_at +- ldap +- override User: - id - username @@ -315,12 +317,14 @@ ProtectedBranch::MergeAccessLevel: - access_level - created_at - updated_at +- user_id ProtectedBranch::PushAccessLevel: - id - protected_branch_id - access_level - created_at - updated_at +- user_id AwardEmoji: - id - user_id -- GitLab From d356d1cda452ad7262d7a3dcb98a5f1844d71d81 Mon Sep 17 00:00:00 2001 From: Ruben Davila Date: Wed, 21 Sep 2016 21:27:50 -0500 Subject: [PATCH 27/49] Update VERSION to 8.12.0-rc7-ee --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 5b12cc2e3d94f1..a86f57cf3ca353 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.12.0-rc6-ee +8.12.0-rc7-ee -- GitLab From e614a34d9c2c1aa131423ddac9d05e2046f578c3 Mon Sep 17 00:00:00 2001 From: Ruben Davila Date: Wed, 21 Sep 2016 21:29:55 -0500 Subject: [PATCH 28/49] Update VERSION to 8.12.0-rc7 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 2087f5da6c894d..21e2292cdcb137 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.12.0-rc6 +8.12.0-rc7 -- GitLab From ad7d9951a8cb343dbf7a785589a3b841e66e2814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 22 Sep 2016 11:24:51 +0200 Subject: [PATCH 29/49] Remove "(unreleased)" from the CHANGELOG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- CHANGELOG | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index bdd98d8de8b99a..324bec403bdf8b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ Please view this file on the master branch, on stable branches it's out of date. -v 8.12.0 (unreleased) +v 8.12.1 (unreleased) + +v 8.12.0 - Update the rouge gem to 2.0.6, which adds highlighting support for JSX, Prometheus, and others. !6251 - Only check :can_resolve permission if the note is resolvable - Bump fog-aws to v0.11.0 to support ap-south-1 region -- GitLab From edb68433b1a9162d9cd4741bcc3b2c8924ebc42f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 22 Sep 2016 11:26:37 +0200 Subject: [PATCH 30/49] Remove "(unreleased)" from CHANGELOG-EE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- CHANGELOG-EE | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG-EE b/CHANGELOG-EE index b181aada1166e3..8830836bd7d607 100644 --- a/CHANGELOG-EE +++ b/CHANGELOG-EE @@ -1,5 +1,8 @@ Please view this file on the master branch, on stable branches it's out of date. -v 8.12.0 (Unreleased) + +v 8.12.1 (unreleased) + +v 8.12.0 - Include more data in EE usage ping - Reduce UPDATE queries when moving between import states on projects - [ES] Instrument Elasticsearch::Git::Repository -- GitLab From 3b890186dc002b45b9f8177a9f5d4e049d8a9979 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 21 Sep 2016 22:54:10 +0000 Subject: [PATCH 31/49] Merge branch 'reorganize_sections_in_admin_settings' into 'master' Reorganize sections in Admin area settings ## What does this MR do? Some settings were under the "wrong" category. This is an attempt to provide proper categories and move relevant settings there. ## What are the relevant issue numbers? Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/20126 See merge request !5449 --- .../application_settings/_form.html.haml | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index d929364fc96534..0d79ca7dc52097 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -49,28 +49,6 @@ = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control') %span.help-block#clone-protocol-help Allow only the selected protocols to be used for Git access. - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :version_check_enabled do - = f.check_box :version_check_enabled - Version check enabled - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :email_author_in_body do - = f.check_box :email_author_in_body - Include author name in notification email body - .help-block - Some email servers do not support overriding the email sender name. - Enable this option to include the name of the author of the issue, - merge request or comment in the email body instead. - .form-group - = f.label :admin_notification_email, class: 'control-label col-sm-2' - .col-sm-10 - = f.text_field :admin_notification_email, class: 'form-control' - .help-block - Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area. %fieldset %legend Account and Limit Settings @@ -340,6 +318,15 @@ Generate API key at %a{ href: 'http://www.akismet.com', target: 'blank'} http://www.akismet.com + %fieldset + %legend Abuse reports + .form-group + = f.label :admin_notification_email, 'Abuse reports notification email', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :admin_notification_email, class: 'form-control' + .help-block + Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area. + %fieldset %legend Error Reporting and Logging %p @@ -407,6 +394,29 @@ = succeed "." do = link_to "Koding administration documentation", help_page_path("administration/integration/koding") + %fieldset + %legend Usage statistics + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :version_check_enabled do + = f.check_box :version_check_enabled + Version check enabled + .help-block + Let GitLab inform you when an update is available. + + %fieldset + %legend Email + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :email_author_in_body do + = f.check_box :email_author_in_body + Include author name in notification email body + .help-block + Some email servers do not support overriding the email sender name. + Enable this option to include the name of the author of the issue, + merge request or comment in the email body instead. .form-actions = f.submit 'Save', class: 'btn btn-save' -- GitLab From 1892b4e8f00fa0f331bcbd1271a6e3e41c7e83a8 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 22 Sep 2016 08:10:42 +0000 Subject: [PATCH 32/49] Merge branch 'docs/issue-closing-pattern' into 'master' Change location and refactor issue closing pattern documentation ## Moving docs to a new location? See the guidelines: http://docs.gitlab.com/ce/development/doc_styleguide.html#changing-document-location - [x] Make sure the old link is not removed and has its contents replaced with a link to the new location. - [x] Make sure internal links pointing to the document in question are not broken. - [x] Search and replace any links referring to old docs in GitLab Rails app, specifically under the `app/views/` directory. - [x] If working on CE, submit an MR to EE with the changes as well. See merge request !6466 --- doc/README.md | 4 +- doc/administration/issue_closing_pattern.md | 49 +++++++++++++++++ doc/customization/issue_closing.md | 41 +------------- doc/gitlab-basics/create-issue.md | 4 +- doc/intro/README.md | 2 +- .../project/issues/automatic_issue_closing.md | 55 +++++++++++++++++++ doc/user/project/repository/web_editor.md | 2 +- doc/workflow/README.md | 1 + 8 files changed, 114 insertions(+), 44 deletions(-) create mode 100644 doc/administration/issue_closing_pattern.md create mode 100644 doc/user/project/issues/automatic_issue_closing.md diff --git a/doc/README.md b/doc/README.md index 254394eb63e7e0..dd0eb97489e480 100644 --- a/doc/README.md +++ b/doc/README.md @@ -29,9 +29,9 @@ - [Install](install/README.md) Requirements, directory structures and installation from source. - [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components. - [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter. -- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages. +- [Issue closing pattern](administration/issue_closing_pattern.md) Customize how to close an issue from commit messages. - [Koding](administration/integration/koding.md) Set up Koding to use with GitLab. -- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars. +- [Libravatar](customization/libravatar.md) Use Libravatar instead of Gravatar for user avatars. - [Log system](administration/logs.md) Log system. - [Environment Variables](administration/environment_variables.md) to configure GitLab. - [Operations](operations/README.md) Keeping GitLab up and running. diff --git a/doc/administration/issue_closing_pattern.md b/doc/administration/issue_closing_pattern.md new file mode 100644 index 00000000000000..28e1fd4e12e2d0 --- /dev/null +++ b/doc/administration/issue_closing_pattern.md @@ -0,0 +1,49 @@ +# Issue closing pattern + +>**Note:** +This is the administration documentation. +There is a separate [user documentation] on issue closing pattern. + +When a commit or merge request resolves one or more issues, it is possible to +automatically have these issues closed when the commit or merge request lands +in the project's default branch. + +## Change the issue closing pattern + +In order to change the pattern you need to have access to the server that GitLab +is installed on. + +The default pattern can be located in [gitlab.yml.example] under the +"Automatic issue closing" section. + +> **Tip:** +You are advised to use http://rubular.com to test the issue closing pattern. +Because Rubular doesn't understand `%{issue_ref}`, you can replace this by +`#\d+` when testing your patterns, which matches only local issue references like `#123`. + +**For Omnibus installations** + +1. Open `/etc/gitlab/gitlab.rb` with your editor. +1. Change the value of `gitlab_rails['issue_closing_pattern']` to a regular + expression of your liking: + + ```ruby + gitlab_rails['issue_closing_pattern'] = "((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)" + ``` +1. [Reconfigure] GitLab for the changes to take effect. + +**For installations from source** + +1. Open `gitlab.yml` with your editor. +1. Change the value of `issue_closing_pattern`: + + ```yaml + issue_closing_pattern: "((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)" + ``` + +1. [Restart] GitLab for the changes to take effect. + +[gitlab.yml.example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example +[reconfigure]: restart_gitlab.md#omnibus-gitlab-reconfigure +[restart]: restart_gitlab.md#installations-from-source +[user documentation]: ../user/project/issues/automatic_issue_closing.md diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md index 4620bb2dcde683..31164ccd465633 100644 --- a/doc/customization/issue_closing.md +++ b/doc/customization/issue_closing.md @@ -1,39 +1,4 @@ -# Issue closing pattern +This document was split into: -When a commit or merge request resolves one or more issues, it is possible to automatically have these issues closed when the commit or merge request lands in the project's default branch. - -If a commit message or merge request description contains a sentence matching the regular expression below, all issues referenced from -the matched text will be closed. This happens when the commit is pushed to a project's default branch, or when a commit or merge request is merged into there. - -When not specified, the default `issue_closing_pattern` as shown below will be used: - -```bash -((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+) -``` - -Here, `%{issue_ref}` is a complex regular expression defined inside GitLab, that matches a reference to a local issue (`#123`), cross-project issue (`group/project#123`) or a link to an issue (`https://gitlab.example.com/group/project/issues/123`). - -For example: - -``` -git commit -m "Awesome commit message (Fix #20, Fixes #21 and Closes group/otherproject#22). This commit is also related to #17 and fixes #18, #19 and https://gitlab.example.com/group/otherproject/issues/23." -``` - -will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed to, as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as it does not match the pattern. It also works with multiline commit messages. - -Tip: you can test this closing pattern at [http://rubular.com][1]. Use this site -to test your own patterns. -Because Rubular doesn't understand `%{issue_ref}`, you can replace this by `#\d+` in testing, which matches only local issue references like `#123`. - -## Change the pattern - -For Omnibus installs you can change the default pattern in `/etc/gitlab/gitlab.rb`: - -``` -issue_closing_pattern: '((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)' -``` - -For manual installs you can customize the pattern in [gitlab.yml][0] using the `issue_closing_pattern` key. - -[0]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example -[1]: http://rubular.com/r/Xmbexed1OJ +- [administration/issue_closing_pattern.md](../administration/issue_closing_pattern.md). +- [user/project/issues/automatic_issue_closing](../user/project/issues/automatic_issue_closing.md). diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md index 5221d85b661027..da9a165b8f5f77 100644 --- a/doc/gitlab-basics/create-issue.md +++ b/doc/gitlab-basics/create-issue.md @@ -1,6 +1,6 @@ # How to create an Issue in GitLab -The Issue Tracker is a good place to add things that need to be improved or solved in a project. +The Issue Tracker is a good place to add things that need to be improved or solved in a project. To create an Issue, sign in to GitLab. @@ -24,4 +24,4 @@ You may assign the Issue to a user, add a milestone and add labels (they are all ![Submit new issue](basicsimages/submit_new_issue.png) -Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](http://docs.gitlab.com/ce/customization/issue_closing.html). +Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](../user/project/issues/automatic_issue_closing.md). diff --git a/doc/intro/README.md b/doc/intro/README.md index 71fef50ceb45a3..1790b2b761f40a 100644 --- a/doc/intro/README.md +++ b/doc/intro/README.md @@ -22,7 +22,7 @@ Create merge requests and review code. - [Fork a project and contribute to it](../workflow/forking_workflow.md) - [Create a new merge request](../gitlab-basics/add-merge-request.md) -- [Automatically close issues from merge requests](../customization/issue_closing.md) +- [Automatically close issues from merge requests](../user/project/issues/automatic_issue_closing.md) - [Automatically merge when your builds succeed](../user/project/merge_requests/merge_when_build_succeeds.md) - [Revert any commit](../user/project/merge_requests/revert_changes.md) - [Cherry-pick any commit](../user/project/merge_requests/cherry_pick_changes.md) diff --git a/doc/user/project/issues/automatic_issue_closing.md b/doc/user/project/issues/automatic_issue_closing.md new file mode 100644 index 00000000000000..d6f3a7d5555f5e --- /dev/null +++ b/doc/user/project/issues/automatic_issue_closing.md @@ -0,0 +1,55 @@ +# Automatic issue closing + +>**Note:** +This is the user docs. In order to change the default issue closing pattern, +follow the steps in the [administration docs]. + +When a commit or merge request resolves one or more issues, it is possible to +automatically have these issues closed when the commit or merge request lands +in the project's default branch. + +If a commit message or merge request description contains a sentence matching +a certain regular expression, all issues referenced from the matched text will +be closed. This happens when the commit is pushed to a project's **default** +branch, or when a commit or merge request is merged into it. + +## Default closing pattern value + +When not specified, the default issue closing pattern as shown below will be +used: + +```bash +((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+) +``` + +Note that `%{issue_ref}` is a complex regular expression defined inside GitLab's +source code that can match a reference to 1) a local issue (`#123`), +2) a cross-project issue (`group/project#123`) or 3) a link to an issue +(`https://gitlab.example.com/group/project/issues/123`). + +--- + +This translates to the following keywords: + +- Close, Closes, Closed, Closing, close, closes, closed, closing +- Fix, Fixes, Fixed, Fixing, fix, fixes, fixed, fixing +- Resolve, Resolves, Resolved, Resolving, resolve, resolves, resolved, resolving + +--- + +For example the following commit message: + +``` +Awesome commit message + +Fix #20, Fixes #21 and Closes group/otherproject#22. +This commit is also related to #17 and fixes #18, #19 +and https://gitlab.example.com/group/otherproject/issues/23. +``` + +will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed +to, as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as +it does not match the pattern. It works with multi-line commit messages as well +as one-liners when used with `git commit -m`. + +[administration docs]: ../../../administration/issue_closing_pattern.md diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md index 7c041d019bb429..993c6bfb7e95ac 100644 --- a/doc/user/project/repository/web_editor.md +++ b/doc/user/project/repository/web_editor.md @@ -172,4 +172,4 @@ you commit the changes you will be taken to a new merge request form. ![New file button](basicsimages/file_button.png) [ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808 -[issue closing pattern]: ../customization/issue_closing.md +[issue closing pattern]: ../user/project/issues/automatic_issue_closing.md diff --git a/doc/workflow/README.md b/doc/workflow/README.md index e8ecb8d8fb43b0..2d9bfbc062902a 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -1,5 +1,6 @@ # Workflow +- [Automatic issue closing](../user/project/issues/automatic_issue_closing.md) - [Change your time zone](timezone.md) - [Cycle Analytics](../user/project/cycle_analytics.md) - [Description templates](../user/project/description_templates.md) -- GitLab From 209cecf77b4a98ddf42a8f637e07c04e0732cfa3 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 22 Sep 2016 08:23:05 +0000 Subject: [PATCH 33/49] Merge branch 'docs/cycle-analytics' into 'master' Fix typos in cycle analytics docs See merge request !6467 --- doc/user/project/cycle_analytics.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md index e1fe1d256fde08..abef80e7914177 100644 --- a/doc/user/project/cycle_analytics.md +++ b/doc/user/project/cycle_analytics.md @@ -6,7 +6,7 @@ This the first iteration of Cycle Analytics, you can follow the following issue to track the changes that are coming to this feature: [#20975][ce-20975]. -Cycle Analytics measures the time it takes to go from an idea to production for +Cycle Analytics measures the time it takes to go from [an idea to production] for each project you have. This is achieved by not only indicating the total time it takes to reach at that point, but the total time is broken down into the multiple stages an idea has to pass through to be shipped. @@ -32,7 +32,7 @@ You can see that there are seven stages in total: - **Code** (IDE) - Median time from the first commit until the merge request is created - **Test** (CI) - - Total test time for all commits/merges + - Median total test time for all commits/merges - **Review** (Merge Request/MR) - Median time from merge request creation until the merge request is merged (closed merge requests won't be taken into account) @@ -57,11 +57,11 @@ Below you can see in more detail what the various stages of Cycle Analytics mean | **Stage** | **Description** | | --------- | --------------- | | Issue | Measures the median time between creating an issue and taking action to solve it, by either labeling it or adding it to a milestone, whatever comes first. The label will be tracked only if it already has an [Issue Board list][board] created for it. | -| Plan | Measures the median time between the action you took for the previous stage, and pushing the first commit to the repository. To make this change tracked, the commit needs to be pushed that contains the issue closing pattern `Closes #xxx`, where `xxx` is the number of the issue related to this commit. If the commit does not contain the issue closing pattern, it is not considered to the measure time of the stage. | -| Code | Measures the median time between pushing a first commit (previous stage) and creating a merge request related to that commit. The key to keep the process tracked is include the issue closing pattern to the description of the merge request. | +| Plan | Measures the median time between the action you took for the previous stage, and pushing the first commit to the repository. To make this change tracked, the pushed commit needs to contain the [issue closing pattern], for example `Closes #xxx`, where `xxx` is the number of the issue related to this commit. If the commit does not contain the issue closing pattern, it is not considered to the measurement time of the stage. | +| Code | Measures the median time between pushing a first commit (previous stage) and creating a merge request related to that commit. The key to keep the process tracked is include the [issue closing pattern] to the description of the merge request. | | Test | Measures the median time to run the entire pipeline for that project. It's related to the time GitLab CI takes to run every job for the commits pushed to that merge request defined in the previous stage. It is basically the start->finish time for all pipelines. `master` is not excluded. It does not attempt to track time for any particular stages. | | Review | Measures the median time taken to review the merge request, between its creation and until it's merged. | -| Staging | Measures the median time between merging the merge request until the very first deployment of the to production. It's tracked by the [environment] set to `production` in your GitLab CI configuration. If there isn't a `production` environment, this is not tracked. | +| Staging | Measures the median time between merging the merge request until the very first deployment to production. It's tracked by the [environment] set to `production` in your GitLab CI configuration. If there isn't a `production` environment, this is not tracked. | | Production| The sum of all time taken to run the entire process, from issue creation to deploying the code to production. | --- @@ -101,7 +101,7 @@ Learn more about Cycle Analytics in the following resources: - [Cycle Analytics feature page](https://about.gitlab.com/solutions/cycle-analytics/) - [Cycle Analytics feature preview](https://about.gitlab.com/2016/09/16/feature-preview-introducing-cycle-analytics/) -- [Cycle Analytics feature highlight](https://about.gitlab.com/2016-09-19-cycle-analytics-feature-highlight.html) +- [Cycle Analytics feature highlight](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/) [ce-5986]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5986 @@ -110,3 +110,5 @@ Learn more about Cycle Analytics in the following resources: [permissions]: ../permissions.md [environment]: ../../ci/yaml/README.md#environment [board]: issue_board.md#creating-a-new-list +[idea to production]: https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab +[issue closing pattern]: issues/automatic_issue_closing.md -- GitLab From d7bd7baaa25d750f3608c3b270c05dd1d12470dd Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 22 Sep 2016 09:32:16 +0000 Subject: [PATCH 34/49] Merge branch 'sh-bump-gitlab-shell-8.12' into 'master' Bump gitlab-shell upgrade version to 3.6.0 for 8.12 [ci skip] Closes #22442 See merge request !6469 --- doc/update/8.11-to-8.12.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md index 686c7e8e7b5c6a..076696f565b8af 100644 --- a/doc/update/8.11-to-8.12.md +++ b/doc/update/8.11-to-8.12.md @@ -70,7 +70,7 @@ sudo -u git -H git checkout 8-12-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all --tags -sudo -u git -H git checkout v3.5.0 +sudo -u git -H git checkout v3.6.0 ``` ### 6. Update gitlab-workhorse -- GitLab From a8a5a77e541ff4800bc1402641a9939c8615f02b Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 21 Sep 2016 22:55:08 +0000 Subject: [PATCH 35/49] Merge branch 'reorganize_sections_in_admin_settings' into 'master' Reorganize sections in Admin area settings From https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5449 See merge request !749 --- .../application_settings/_form.html.haml | 77 +++++++++------- .../admin_area/settings/usage_statistics.md | 87 +++++++++++++++++++ 2 files changed, 131 insertions(+), 33 deletions(-) create mode 100644 doc/user/admin_area/settings/usage_statistics.md diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 84452c402ea225..9beb8b0644b364 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -49,39 +49,6 @@ = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control') %span.help-block#clone-protocol-help Allow only the selected protocols to be used for Git access. - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :version_check_enabled do - = f.check_box :version_check_enabled - Version check enabled - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :usage_ping_enabled do - = f.check_box :usage_ping_enabled - Usage ping enabled - .container - .help-block - Every week GitLab will report license usage back to GitLab, Inc. - Disable this option if you do not want this to occur. This is the JSON payload that will be sent: - %pre.usage-data.js-syntax-highlight.code.highlight{ "data-endpoint" => usage_data_admin_application_settings_path(format: :html) } - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :email_author_in_body do - = f.check_box :email_author_in_body - Include author name in notification email body - .help-block - Some email servers do not support overriding the email sender name. - Enable this option to include the name of the author of the issue, - merge request or comment in the email body instead. - .form-group - = f.label :admin_notification_email, class: 'control-label col-sm-2' - .col-sm-10 - = f.text_field :admin_notification_email, class: 'form-control' - .help-block - Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area. %fieldset %legend Account and Limit Settings @@ -369,6 +336,15 @@ Generate API key at %a{ href: 'http://www.akismet.com', target: 'blank'} http://www.akismet.com + %fieldset + %legend Abuse reports + .form-group + = f.label :admin_notification_email, 'Abuse reports notification email', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :admin_notification_email, class: 'form-control' + .help-block + Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area. + %fieldset %legend Error Reporting and Logging %p @@ -464,6 +440,41 @@ = succeed "." do = link_to "Koding administration documentation", help_page_path("administration/integration/koding") + %fieldset + %legend Usage statistics + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :version_check_enabled do + = f.check_box :version_check_enabled + Version check enabled + .help-block + Let GitLab inform you when an update is available. + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :usage_ping_enabled do + = f.check_box :usage_ping_enabled + Usage ping enabled + = link_to icon('question-circle'), help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-data") + .container + .help-block + Every week GitLab will report license usage back to GitLab, Inc. + Disable this option if you do not want this to occur. This is the JSON payload that will be sent: + %pre.usage-data.js-syntax-highlight.code.highlight{ "data-endpoint" => usage_data_admin_application_settings_path(format: :html) } + + %fieldset + %legend Email + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :email_author_in_body do + = f.check_box :email_author_in_body + Include author name in notification email body + .help-block + Some email servers do not support overriding the email sender name. + Enable this option to include the name of the author of the issue, + merge request or comment in the email body instead. .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md new file mode 100644 index 00000000000000..70dea71d3c79cd --- /dev/null +++ b/doc/user/admin_area/settings/usage_statistics.md @@ -0,0 +1,87 @@ +# Usage statistics + +GitLab Inc. will periodically collect information about your instance in order +to perform various actions. + +All statistics are opt-in and you can always disable them from the admin panel. + +## Version check + +GitLab can inform you when an update is available and the importance of it. + +No information other than the GitLab version and the instance's domain name +are collected. + +In the **Overview** tab you can see if your GitLab version is up to date. There +are three cases: 1) you are up to date (green), 2) there is an update available +(yellow) and 3) your version is vulnerable and a security fix is released (red). + +In any case, you will see a message informing you of the state and the +importance of the update. + +If enabled, the version status will also be shown in the help page (`/help`) +for all signed in users. + +## Usage data + +> [Introduced][ee-557] in GitLab Enterprise Edition 8.10. More statistics +[were added][ee-735] in GitLab Enterprise Edition 8.12. + +GitLab Inc. can collect non-sensitive information about how Enterprise Edition +customers use their GitLab instance upon the activation of a ping feature +located in the admin panel (`/admin/application_settings`). + +You can see the **exact** JSON payload that your instance sends to GitLab Inc. +in the "Usage statistics" section of the admin panel. + +Nothing qualitative is collected. Only quantitative. Meaning, no project name, +author name, nature of comments, name of labels, etc. + +This is done mainly for the following reasons: + +- to have a better understanding on how our users use our product +- to provide more tools for the customer success team to help customers onboard + better. + +The total number of the following is sent back to GitLab Inc.: + +- Comments +- Groups +- Users +- Projects +- Issues +- Labels +- CI builds +- Snippets +- Milestones +- Todos +- Pushes +- Merge requests +- Environments +- Triggers +- Deploy keys +- Pages +- Project Services +- Issue Boards +- CI Runners +- Deployments +- Geo Nodes +- LDAP Groups +- LDAP Keys +- LDAP Users +- LFS objects +- Protected branches +- Releases +- Remote mirrors +- Web hooks + +## Privacy policy + +GitLab Inc. does **not** collect any sensitive information, like project names +or the content of the comments. GitLab Inc. does not disclose or otherwise make +available any of the data collected on a customer specific basis. + +Read more in about the [Privacy policy](https://about.gitlab.com/privacy). + +[ee-557]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/557 +[ee-735]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/735 -- GitLab From d49f410fb292f01278540dcc0758e5f0b883f35e Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 21 Sep 2016 22:39:40 +0000 Subject: [PATCH 36/49] Merge branch 'doc/repo-restrictions-refactor' into 'master' Refactor repo restrictions docs See merge request !750 --- .../application_settings/_form.html.haml | 7 +++- doc/administration/repository_restrictions.md | 37 ---------------- .../settings/account_and_limit_settings.md | 42 +++++++++++++++++++ 3 files changed, 47 insertions(+), 39 deletions(-) delete mode 100644 doc/administration/repository_restrictions.md create mode 100644 doc/user/admin_area/settings/account_and_limit_settings.md diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 9beb8b0644b364..fa23e2dee16e17 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -67,10 +67,13 @@ .col-sm-10 = f.number_field :max_attachment_size, class: 'form-control' .form-group - = f.label :repository_size_limit, 'Per repository size limit (MB)', class: 'control-label col-sm-2' + = f.label :repository_size_limit, class: 'control-label col-sm-2' do + Size limit per repository (MB) .col-sm-10 = f.number_field :repository_size_limit, class: 'form-control', min: 0 - %span.help-block#repository_size_limit_help_block Includes LFS objects. It can be overridden per group, or per project. 0 for unlimited + %span.help-block#repository_size_limit_help_block + Includes LFS objects. It can be overridden per group, or per project. 0 for unlimited. + = link_to icon('question-circle'), help_page_path("user/admin_area/settings/account_and_limit_settings") .form-group = f.label :session_expire_delay, 'Session duration (minutes)', class: 'control-label col-sm-2' .col-sm-10 diff --git a/doc/administration/repository_restrictions.md b/doc/administration/repository_restrictions.md deleted file mode 100644 index 41fea4d6090e9a..00000000000000 --- a/doc/administration/repository_restrictions.md +++ /dev/null @@ -1,37 +0,0 @@ -# Repository size restrictions - -> Introduced with GitLab Enterprise Edition 8.12 - -Repositories within your GitLab instance can grow quickly, specially if you are -using LFS. Their size can grow exponentially and eat up your storage device quite -quickly. - -In order to avoid this from happening, you can set a hard limit for your repositories. -You can set this limit globally, per group, or per project, with per project limits -taking the highest priority. - -These settings can be found within each project, or group settings and within -the Application Settings for the global value. - -Setting the limit to `0` means there is no restrictions. - -# Restrictions - -When a project has reached its size limit, you will not be able to push to it, -create new merge request, or merge existing ones. You will still be able to create -new issues, and clone the project. - -Uploading LFS objects will also be denied. - -In order to lift these restrictions, the administrator of the GitLab instance -needs to increase the limit on the particular project that exceeded it. - - -# Limitations - -The first push of a new project cannot be checked for size as of now, so the first -push will allow you to upload more than the limit dictates, but every subsequent -push will be denied. - -LFS objects, however, can be checked on first push and **will** be rejected if the -sum of their sizes exceeds the maximum allowed repository size. \ No newline at end of file diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md new file mode 100644 index 00000000000000..d30d26333de470 --- /dev/null +++ b/doc/user/admin_area/settings/account_and_limit_settings.md @@ -0,0 +1,42 @@ +# Account and limit settings + +## Repository size limit + +> [Introduced][ee-740] in GitLab Enterprise Edition 8.12. + +Repositories within your GitLab instance can grow quickly, especially if you are +using LFS. Their size can grow exponentially and eat up your storage device quite +quickly. + +In order to avoid this from happening, you can set a hard limit for your +repositories' size. This limit can be set globally, per group, or per project, +with per project limits taking the highest priority. + +Only a GitLab administrator can set those limits. Setting the limit to `0` means +there are no restrictions. + +These settings can be found within each project's settings, in a group's +settings and in the Application Settings area for the global value +(`/admin/application_settings`). + +### Repository size restrictions + +When a project has reached its size limit, you will not be able to push to it, +create a new merge request, or merge existing ones. You will still be able to +create new issues, and clone the project though. + +Uploading LFS objects will also be denied. + +In order to lift these restrictions, the administrator of the GitLab instance +needs to increase the limit on the particular project that exceeded it. + +### Current limitations for the repository size check + +The first push of a new project cannot be checked for size as of now, so the first +push will allow you to upload more than the limit dictates, but every subsequent +push will be denied. + +LFS objects, however, can be checked on first push and **will** be rejected if the +sum of their sizes exceeds the maximum allowed repository size. + +[ee-740]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/740 -- GitLab From 373ad85aae740adbb08e9901097a5029dce13fef Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 22 Sep 2016 08:10:53 +0000 Subject: [PATCH 37/49] Merge branch 'docs/issue-closing-pattern' into 'master' Change location and refactor issue closing pattern documentation Split into user and administrator docs: - administration/issue_closing_pattern.md - user/project/issues/automatic_issue_closing.md From https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6466 See merge request !753 --- doc/README.md | 5 +- doc/administration/issue_closing_pattern.md | 49 +++++++++++++++++ doc/customization/issue_closing.md | 41 +------------- doc/gitlab-basics/create-issue.md | 4 +- doc/intro/README.md | 2 +- .../project/issues/automatic_issue_closing.md | 55 +++++++++++++++++++ doc/user/project/repository/web_editor.md | 2 +- doc/workflow/README.md | 1 + 8 files changed, 115 insertions(+), 44 deletions(-) create mode 100644 doc/administration/issue_closing_pattern.md create mode 100644 doc/user/project/issues/automatic_issue_closing.md diff --git a/doc/README.md b/doc/README.md index 2ed6be047b8665..7a1bc5140c6521 100644 --- a/doc/README.md +++ b/doc/README.md @@ -39,9 +39,10 @@ - [Install](install/README.md) Requirements, directory structures and installation from source. - [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, LDAP and Twitter. - [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components. -- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages. +- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter. +- [Issue closing pattern](administration/issue_closing_pattern.md) Customize how to close an issue from commit messages. - [Koding](administration/integration/koding.md) Set up Koding to use with GitLab. -- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars. +- [Libravatar](customization/libravatar.md) Use Libravatar instead of Gravatar for user avatars. - [Log system](administration/logs.md) Log system. - [Environment Variables](administration/environment_variables.md) to configure GitLab. - [Operations](operations/README.md) Keeping GitLab up and running. diff --git a/doc/administration/issue_closing_pattern.md b/doc/administration/issue_closing_pattern.md new file mode 100644 index 00000000000000..28e1fd4e12e2d0 --- /dev/null +++ b/doc/administration/issue_closing_pattern.md @@ -0,0 +1,49 @@ +# Issue closing pattern + +>**Note:** +This is the administration documentation. +There is a separate [user documentation] on issue closing pattern. + +When a commit or merge request resolves one or more issues, it is possible to +automatically have these issues closed when the commit or merge request lands +in the project's default branch. + +## Change the issue closing pattern + +In order to change the pattern you need to have access to the server that GitLab +is installed on. + +The default pattern can be located in [gitlab.yml.example] under the +"Automatic issue closing" section. + +> **Tip:** +You are advised to use http://rubular.com to test the issue closing pattern. +Because Rubular doesn't understand `%{issue_ref}`, you can replace this by +`#\d+` when testing your patterns, which matches only local issue references like `#123`. + +**For Omnibus installations** + +1. Open `/etc/gitlab/gitlab.rb` with your editor. +1. Change the value of `gitlab_rails['issue_closing_pattern']` to a regular + expression of your liking: + + ```ruby + gitlab_rails['issue_closing_pattern'] = "((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)" + ``` +1. [Reconfigure] GitLab for the changes to take effect. + +**For installations from source** + +1. Open `gitlab.yml` with your editor. +1. Change the value of `issue_closing_pattern`: + + ```yaml + issue_closing_pattern: "((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)" + ``` + +1. [Restart] GitLab for the changes to take effect. + +[gitlab.yml.example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example +[reconfigure]: restart_gitlab.md#omnibus-gitlab-reconfigure +[restart]: restart_gitlab.md#installations-from-source +[user documentation]: ../user/project/issues/automatic_issue_closing.md diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md index 4620bb2dcde683..31164ccd465633 100644 --- a/doc/customization/issue_closing.md +++ b/doc/customization/issue_closing.md @@ -1,39 +1,4 @@ -# Issue closing pattern +This document was split into: -When a commit or merge request resolves one or more issues, it is possible to automatically have these issues closed when the commit or merge request lands in the project's default branch. - -If a commit message or merge request description contains a sentence matching the regular expression below, all issues referenced from -the matched text will be closed. This happens when the commit is pushed to a project's default branch, or when a commit or merge request is merged into there. - -When not specified, the default `issue_closing_pattern` as shown below will be used: - -```bash -((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+) -``` - -Here, `%{issue_ref}` is a complex regular expression defined inside GitLab, that matches a reference to a local issue (`#123`), cross-project issue (`group/project#123`) or a link to an issue (`https://gitlab.example.com/group/project/issues/123`). - -For example: - -``` -git commit -m "Awesome commit message (Fix #20, Fixes #21 and Closes group/otherproject#22). This commit is also related to #17 and fixes #18, #19 and https://gitlab.example.com/group/otherproject/issues/23." -``` - -will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed to, as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as it does not match the pattern. It also works with multiline commit messages. - -Tip: you can test this closing pattern at [http://rubular.com][1]. Use this site -to test your own patterns. -Because Rubular doesn't understand `%{issue_ref}`, you can replace this by `#\d+` in testing, which matches only local issue references like `#123`. - -## Change the pattern - -For Omnibus installs you can change the default pattern in `/etc/gitlab/gitlab.rb`: - -``` -issue_closing_pattern: '((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)' -``` - -For manual installs you can customize the pattern in [gitlab.yml][0] using the `issue_closing_pattern` key. - -[0]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example -[1]: http://rubular.com/r/Xmbexed1OJ +- [administration/issue_closing_pattern.md](../administration/issue_closing_pattern.md). +- [user/project/issues/automatic_issue_closing](../user/project/issues/automatic_issue_closing.md). diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md index 5221d85b661027..da9a165b8f5f77 100644 --- a/doc/gitlab-basics/create-issue.md +++ b/doc/gitlab-basics/create-issue.md @@ -1,6 +1,6 @@ # How to create an Issue in GitLab -The Issue Tracker is a good place to add things that need to be improved or solved in a project. +The Issue Tracker is a good place to add things that need to be improved or solved in a project. To create an Issue, sign in to GitLab. @@ -24,4 +24,4 @@ You may assign the Issue to a user, add a milestone and add labels (they are all ![Submit new issue](basicsimages/submit_new_issue.png) -Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](http://docs.gitlab.com/ce/customization/issue_closing.html). +Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](../user/project/issues/automatic_issue_closing.md). diff --git a/doc/intro/README.md b/doc/intro/README.md index 71fef50ceb45a3..1790b2b761f40a 100644 --- a/doc/intro/README.md +++ b/doc/intro/README.md @@ -22,7 +22,7 @@ Create merge requests and review code. - [Fork a project and contribute to it](../workflow/forking_workflow.md) - [Create a new merge request](../gitlab-basics/add-merge-request.md) -- [Automatically close issues from merge requests](../customization/issue_closing.md) +- [Automatically close issues from merge requests](../user/project/issues/automatic_issue_closing.md) - [Automatically merge when your builds succeed](../user/project/merge_requests/merge_when_build_succeeds.md) - [Revert any commit](../user/project/merge_requests/revert_changes.md) - [Cherry-pick any commit](../user/project/merge_requests/cherry_pick_changes.md) diff --git a/doc/user/project/issues/automatic_issue_closing.md b/doc/user/project/issues/automatic_issue_closing.md new file mode 100644 index 00000000000000..d6f3a7d5555f5e --- /dev/null +++ b/doc/user/project/issues/automatic_issue_closing.md @@ -0,0 +1,55 @@ +# Automatic issue closing + +>**Note:** +This is the user docs. In order to change the default issue closing pattern, +follow the steps in the [administration docs]. + +When a commit or merge request resolves one or more issues, it is possible to +automatically have these issues closed when the commit or merge request lands +in the project's default branch. + +If a commit message or merge request description contains a sentence matching +a certain regular expression, all issues referenced from the matched text will +be closed. This happens when the commit is pushed to a project's **default** +branch, or when a commit or merge request is merged into it. + +## Default closing pattern value + +When not specified, the default issue closing pattern as shown below will be +used: + +```bash +((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+) +``` + +Note that `%{issue_ref}` is a complex regular expression defined inside GitLab's +source code that can match a reference to 1) a local issue (`#123`), +2) a cross-project issue (`group/project#123`) or 3) a link to an issue +(`https://gitlab.example.com/group/project/issues/123`). + +--- + +This translates to the following keywords: + +- Close, Closes, Closed, Closing, close, closes, closed, closing +- Fix, Fixes, Fixed, Fixing, fix, fixes, fixed, fixing +- Resolve, Resolves, Resolved, Resolving, resolve, resolves, resolved, resolving + +--- + +For example the following commit message: + +``` +Awesome commit message + +Fix #20, Fixes #21 and Closes group/otherproject#22. +This commit is also related to #17 and fixes #18, #19 +and https://gitlab.example.com/group/otherproject/issues/23. +``` + +will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed +to, as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as +it does not match the pattern. It works with multi-line commit messages as well +as one-liners when used with `git commit -m`. + +[administration docs]: ../../../administration/issue_closing_pattern.md diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md index 7c041d019bb429..993c6bfb7e95ac 100644 --- a/doc/user/project/repository/web_editor.md +++ b/doc/user/project/repository/web_editor.md @@ -172,4 +172,4 @@ you commit the changes you will be taken to a new merge request form. ![New file button](basicsimages/file_button.png) [ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808 -[issue closing pattern]: ../customization/issue_closing.md +[issue closing pattern]: ../user/project/issues/automatic_issue_closing.md diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 4d2bed6a87b1a7..f922bcf28972d3 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -1,5 +1,6 @@ # Workflow +- [Automatic issue closing](../user/project/issues/automatic_issue_closing.md) - [Change your time zone](timezone.md) - [Cycle Analytics](../user/project/cycle_analytics.md) - [Description templates](../user/project/description_templates.md) -- GitLab From c28ead1e0974775accf4ef202af6d0d2d7c3a196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 22 Sep 2016 09:55:04 +0000 Subject: [PATCH 38/49] Merge branch 'update-db-schema' into 'master' Update db/schema.rb per most recent migrations See merge request !6446 --- db/schema.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index fc98694e2eb4c3..59b3e2377077af 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: 20160915081353) do +ActiveRecord::Schema.define(version: 20160915042921) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -158,9 +158,9 @@ t.text "commands" t.integer "job_id" t.string "name" - t.boolean "deploy", default: false + t.boolean "deploy", default: false t.text "options" - t.boolean "allow_failure", default: false, null: false + t.boolean "allow_failure", default: false, null: false t.string "stage" t.integer "trigger_request_id" t.integer "stage_idx" @@ -441,11 +441,11 @@ create_table "issue_metrics", force: :cascade do |t| t.integer "issue_id", null: false + t.datetime "first_mentioned_in_commit_at" t.datetime "first_associated_with_milestone_at" t.datetime "first_added_to_board_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.datetime "first_mentioned_in_commit_at" end add_index "issue_metrics", ["issue_id"], name: "index_issue_metrics", using: :btree @@ -602,6 +602,7 @@ t.datetime "updated_at", null: false end + add_index "merge_request_metrics", ["first_deployed_to_production_at"], name: "index_merge_request_metrics_on_first_deployed_to_production_at", using: :btree add_index "merge_request_metrics", ["merge_request_id"], name: "index_merge_request_metrics", using: :btree create_table "merge_requests", force: :cascade do |t| -- GitLab From f4f16c9e0183fd6ccf472fabfa261df5b31cee5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 22 Sep 2016 10:25:26 +0000 Subject: [PATCH 39/49] Merge branch 'bpj-merge-request-diff-fixes' into 'master' Fixups for Frontend for Merge Request Diff ## What does this MR do? Makes a few revisions to https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6343, as per feedback from @DouweM. Specifically, it removes a duplicate in the changelog, removes colons in dropdowns, uses icon helper, and fixes the 'Show original' link path. ## Why was this MR needed? https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6343 was already merged and this is a needed patch. ## What are the relevant issue numbers? https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6343 See merge request !6448 --- CHANGELOG | 1 - app/views/discussions/_jump_to_next.html.haml | 4 +--- .../projects/merge_requests/show/_versions.html.haml | 10 +++++----- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 324bec403bdf8b..d9d77aba744a89 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -178,7 +178,6 @@ v 8.12.0 - Fix Gitlab::Popen.popen thread-safety issue - Add specs to removing project (Katarzyna Kobierska Ula Budziszewska) - Clean environment variables when running git hooks - - Add UX improvements for merge request version diffs - Fix Import/Export issues importing protected branches and some specific models - Fix non-master branch readme display in tree view - Add UX improvements for merge request version diffs diff --git a/app/views/discussions/_jump_to_next.html.haml b/app/views/discussions/_jump_to_next.html.haml index da970792b4d04a..7ed09dd1a9874b 100644 --- a/app/views/discussions/_jump_to_next.html.haml +++ b/app/views/discussions/_jump_to_next.html.haml @@ -1,4 +1,3 @@ -- diff_notes_disabled = (@merge_request_diff.latest? && !!@start_sha) if @merge_request_diff - discussion = local_assigns.fetch(:discussion, nil) - if current_user %jump-to-discussion{ "inline-template" => true, ":discussion-id" => "'#{discussion.try(:id)}'" } @@ -6,6 +5,5 @@ %button.btn.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion", title: "Jump to next unresolved discussion", "aria-label" => "Jump to next unresolved discussion", - data: { container: "body" }, - disabled: diff_notes_disabled } + data: { container: "body" }} = custom_icon("next_discussion") diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml index 8f7b5d1543e9b1..33e56d5417ff6a 100644 --- a/app/views/projects/merge_requests/show/_versions.html.haml +++ b/app/views/projects/merge_requests/show/_versions.html.haml @@ -12,9 +12,9 @@ %span.caret %ul.dropdown-menu.dropdown-menu-selectable .dropdown-title - %span Version: + %span Version %button.dropdown-title-button.dropdown-menu-close - %i.fa.fa-times.dropdown-menu-close-icon + = icon('times', class: 'dropdown-menu-close-icon') - @merge_request_diffs.each do |merge_request_diff| %li = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do @@ -40,9 +40,9 @@ %span.caret %ul.dropdown-menu.dropdown-menu-selectable .dropdown-title - %span Compared with: + %span Compared with %button.dropdown-title-button.dropdown-menu-close - %i.fa.fa-times.dropdown-menu-close-icon + = icon('times', class: 'dropdown-menu-close-icon') - @comparable_diffs.each do |merge_request_diff| %li = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff, merge_request_diff.head_commit_sha), class: ('is-active' if merge_request_diff == @start_version) do @@ -67,4 +67,4 @@ Comments are disabled because you're comparing two versions of this merge request. - else Comments are disabled because you're viewing an old version of this merge request. - = link_to 'Show latest version', merge_request_version_path(@project, @merge_request, @merge_request_diff), class: 'btn btn-sm' + = link_to 'Show latest version', diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-sm' -- GitLab From ebfb2ea21b4210895711601dd12568557c4b8f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 22 Sep 2016 10:14:46 +0000 Subject: [PATCH 40/49] Merge branch 'dz-improve-mr-versions-doc' into 'master' Update merge request versions documentation with new screenshots For #13570 based on https://gitlab.com/gitlab-org/gitlab-ce/issues/21427 See merge request !6454 --- .../merge_requests/img/versions-compare.png | Bin 0 -> 68722 bytes .../merge_requests/img/versions-dropdown.png | Bin 0 -> 60587 bytes .../project/merge_requests/img/versions.png | Bin 35001 -> 171413 bytes doc/user/project/merge_requests/versions.md | 6 +++++- 4 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 doc/user/project/merge_requests/img/versions-compare.png create mode 100644 doc/user/project/merge_requests/img/versions-dropdown.png diff --git a/doc/user/project/merge_requests/img/versions-compare.png b/doc/user/project/merge_requests/img/versions-compare.png new file mode 100644 index 0000000000000000000000000000000000000000..890cae7768cc9fc0814855963ffd9d914ad4c577 GIT binary patch literal 68722 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 Y^zv!rv<*JOz`(%Z>FVdQ&MBb@03nYw%>V!Z literal 0 HcmV?d00001 diff --git a/doc/user/project/merge_requests/img/versions-dropdown.png b/doc/user/project/merge_requests/img/versions-dropdown.png new file mode 100644 index 0000000000000000000000000000000000000000..9bab9304e147aa4bbc7fafd022bd18e78ecffa72 GIT binary patch literal 60587 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

&}&+}f=`Nwqr=dJ-ND&39d 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 N44$rjF6*2UngF&Q^tAv0 diff --git a/doc/user/project/merge_requests/versions.md b/doc/user/project/merge_requests/versions.md index a6aa4b47835cb3..2805fdf635c49d 100644 --- a/doc/user/project/merge_requests/versions.md +++ b/doc/user/project/merge_requests/versions.md @@ -7,14 +7,18 @@ of merge request diff is created. When you visit a merge request that contains more than one pushes, you can select and compare the versions of those merge request diffs. +![Merge Request Versions](img/versions.png) + By default, the latest version of changes is shown. However, you can select an older one from version dropdown. -![Merge Request Versions](img/versions.png) +![Merge Request Versions](img/versions-dropdown.png) You can also compare the merge request version with older one to see what is changed since then. +![Merge Request Versions](img/versions-compare.png) + Please note that comments are disabled while viewing outdated merge versions or comparing to versions other than base. -- GitLab From b48d3893d07d81267d6e8fc013b871df2185735a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 22 Sep 2016 10:03:46 +0000 Subject: [PATCH 41/49] Merge branch '22421-fix-issuable-counter-when-more-than-one-label-is-selected' into 'master' Hotfix the issuable counters when filtering by multiple labels This is an ugly fix, but it make the counters work when multiple labels are selected so I think we should include it in 8.12, and try to find a proper fix afterward. Closes #22421 See merge request !6455 --- app/helpers/application_helper.rb | 15 ++++++++++++-- spec/features/issues/filter_by_labels_spec.rb | 20 ++++++++++++------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index ed41bf04fc0104..2163a437c4877b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -286,8 +286,19 @@ def state_filters_text_for(state, records) } state_title = titles[state] || state.to_s.humanize - count = records.public_send(state).size - html = content_tag :span, state_title + records_with_state = records.public_send(state) + + # When filtering by multiple labels, the result of query.count is a Hash + # of the form { issuable_id1 => N, issuable_id2 => N }, where N is the + # number of labels selected. The ugly "trick" is to load the issuables + # as an array and get the size of the array... + # We should probably try to solve this properly in the future. + # See https://gitlab.com/gitlab-org/gitlab-ce/issues/22414 + label_names = Array(params.fetch(:label_name, [])) + records_with_state = records_with_state.to_a if label_names.many? + + count = records_with_state.size + html = content_tag :span, state_title if count.present? html += " " diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb index 908b18e5339cdf..7e2abd759e15f7 100644 --- a/spec/features/issues/filter_by_labels_spec.rb +++ b/spec/features/issues/filter_by_labels_spec.rb @@ -6,20 +6,19 @@ let(:project) { create(:project, :public) } let!(:user) { create(:user)} let!(:label) { create(:label, project: project) } + let(:bug) { create(:label, project: project, title: 'bug') } + let(:feature) { create(:label, project: project, title: 'feature') } + let(:enhancement) { create(:label, project: project, title: 'enhancement') } + let(:issue1) { create(:issue, title: "Bugfix1", project: project) } + let(:issue2) { create(:issue, title: "Bugfix2", project: project) } + let(:issue3) { create(:issue, title: "Feature1", project: project) } before do - bug = create(:label, project: project, title: 'bug') - feature = create(:label, project: project, title: 'feature') - enhancement = create(:label, project: project, title: 'enhancement') - - issue1 = create(:issue, title: "Bugfix1", project: project) issue1.labels << bug - issue2 = create(:issue, title: "Bugfix2", project: project) issue2.labels << bug issue2.labels << enhancement - issue3 = create(:issue, title: "Feature1", project: project) issue3.labels << feature project.team << [user, :master] @@ -159,6 +158,13 @@ wait_for_ajax end + it 'shows a correct "Open" counter' do + page.within '.issues-state-filters' do + expect(page).not_to have_content "{#{issue2.id} => 1}" + expect(page).to have_content "Open 1" + end + end + it 'shows issue "Bugfix2" in issues list' do expect(page).to have_content "Bugfix2" end -- GitLab From 465e0e483ef08ba2c0b00307b94aa2ebddcdb667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 22 Sep 2016 10:37:55 +0000 Subject: [PATCH 42/49] Merge branch 'fix-pipeline-for-empty-merge-request-diff' into 'master' Fix pipeline error when trying to read empty merge request diff When a user pushed something which resulted an empty merge request diff, `st_commits` would be `nil`. Therefore we also need to check if there exists `st_commits`. We could tell this from: ``` ruby def commits @commits ||= load_commits(st_commits || []) end ``` and ``` ruby def save_commits new_attributes = {} commits = compare.commits if commits.present? commits = Commit.decorate(commits, merge_request.source_project).reverse new_attributes[:st_commits] = dump_commits(commits) end update_columns_serialized(new_attributes) end ``` Closes #22438 See merge request !6470 --- app/models/merge_request_diff.rb | 6 +++++- spec/models/merge_request_diff_spec.rb | 22 +++++++++++++++++++++ spec/models/merge_request_spec.rb | 27 ++++++++++++++++++++------ 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 7362886e9f55df..36b8b70870bc79 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -30,6 +30,10 @@ def self.select_without_diff select(column_names - ['st_diffs']) end + def st_commits + super || [] + end + # Collect information about commits and diff from repository # and save it to the database as serialized data def save_git_content @@ -83,7 +87,7 @@ def raw_diffs(options = {}) end def commits - @commits ||= load_commits(st_commits || []) + @commits ||= load_commits(st_commits) end def reload_commits diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index e5b185dc3f642a..530a7def553935 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -64,5 +64,27 @@ end end end + + describe '#commits_sha' do + shared_examples 'returning all commits SHA' do + it 'returns all commits SHA' do + commits_sha = subject.commits_sha + + expect(commits_sha).to eq(subject.commits.map(&:sha)) + end + end + + context 'when commits were loaded' do + before do + subject.commits + end + + it_behaves_like 'returning all commits SHA' + end + + context 'when commits were not loaded' do + it_behaves_like 'returning all commits SHA' + end + end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 433aba7747ba7b..12df6adde44bf2 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -517,7 +517,7 @@ context 'with multiple irrelevant merge_request_diffs' do before do - subject.update(target_branch: 'markdown') + subject.update(target_branch: 'v1.0.0') end it_behaves_like 'returning pipelines with proper ordering' @@ -544,13 +544,28 @@ subject.merge_request_diffs.flat_map(&:commits).map(&:sha).uniq end - before do - subject.update(target_branch: 'markdown') + shared_examples 'returning all SHA' do + it 'returns all SHA from all merge_request_diffs' do + expect(subject.merge_request_diffs.size).to eq(2) + expect(subject.all_commits_sha).to eq(all_commits_sha) + end + end + + context 'with a completely different branch' do + before do + subject.update(target_branch: 'v1.0.0') + end + + it_behaves_like 'returning all SHA' end - it 'returns all SHA from all merge_request_diffs' do - expect(subject.merge_request_diffs.size).to eq(2) - expect(subject.all_commits_sha).to eq(all_commits_sha) + context 'with a branch having no difference' do + before do + subject.update(target_branch: 'v1.1.0') + subject.reload # make sure commits were not cached + end + + it_behaves_like 'returning all SHA' end end -- GitLab From 8c273ccda22501f65f69f431283e5320d5433abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 22 Sep 2016 13:28:35 +0000 Subject: [PATCH 43/49] Merge branch 'search-field-ignores' into 'master' Intercept issues search form submit to preserve filters. ## What does this MR do? Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/896 by intercepting manual search form submission and redirecting it to use existing logic (now factored out into `executeSearch`). ## Why was this MR needed? Manual form submission (keying in 'enter') in issues search did not preserve applied filters. ## What are the relevant issue numbers? https://gitlab.com/gitlab-org/gitlab-ce/issues/896 See merge request !6054 --- CHANGELOG | 1 + app/assets/javascripts/issuable.js.es6 | 41 +++++++++++++++----------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d9d77aba744a89..9b0abfd7968830 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,7 @@ v 8.12.0 - Fix note form hint showing slash commands supported for commits. - Make push events have equal vertical spacing. - API: Ensure invitees are not returned in Members API. + - Preserve applied filters on issues search. - Add two-factor recovery endpoint to internal API !5510 - Pass the "Remember me" value to the U2F authentication form - Display stages in valid order in stages dropdown on build page diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index 81d89a48227f77..73e2664e9c0b08 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -15,25 +15,32 @@ return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <%- label.title %> <% }); %>'); }, initSearch: function() { - this.timer = null; - return $('#issuable_search').off('keyup').on('keyup', function() { - clearTimeout(this.timer); - return this.timer = setTimeout(function() { - var $form, $input, $search; - $search = $('#issuable_search'); - $form = $('.js-filter-form'); - $input = $("input[name='" + ($search.attr('name')) + "']", $form); - if ($input.length === 0) { - $form.append(""); - } else { - $input.val($search.val()); - } - if ($search.val() !== '') { - return Issuable.filterResults($form); - } - }, 500); + // `immediate` param set to false debounces on the `trailing` edge, lets user finish typing + const debouncedExecSearch = _.debounce(Issuable.executeSearch, 500, false); + + $('#issuable_search').off('keyup').on('keyup', debouncedExecSearch); + + // ensures existing filters are preserved when manually submitted + $('#issue_search_form').on('submit', (e) => { + e.preventDefault(); + debouncedExecSearch(e); }); }, + executeSearch: function(e) { + const $search = $('#issuable_search'); + const $searchName = $search.attr('name'); + const $searchValue = $search.val(); + const $filtersForm = $('.js-filter-form'); + const $input = $(`input[name='${$searchName}']`, $filtersForm); + + if (!$input.length) { + $filtersForm.append(``); + } else { + $input.val($searchValue); + } + + Issuable.filterResults($filtersForm); + }, initLabelFilterRemove: function() { return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) { var $button; -- GitLab From 8bb964c5f7cd8808033fb5b15c2a00f249b44d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 22 Sep 2016 12:52:54 +0000 Subject: [PATCH 44/49] Merge branch 'hotfix-gl-dropdown-field-undefined' into 'master' Fixes protected branches not removing active item ## What does this MR do? Fixes an problem where protected branches weren't getting their active item removed on a second click because they dont have a field value. ## Why was this MR needed? Protected branches could not have their clicked items removed once clicked. See merge request !6440 --- app/assets/javascripts/gl_dropdown.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index c05cda25bbd996..1b6db641200ccd 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -608,27 +608,28 @@ } } field = []; - fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName(selectedObject) : this.options.fieldName; value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id; if (isInput) { field = $(this.el); } else if(value) { field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']"); } - if (field.length && el.hasClass(ACTIVE_CLASS)) { + if (el.hasClass(ACTIVE_CLASS)) { el.removeClass(ACTIVE_CLASS); - if (isInput) { - field.val(''); - } else { - field.remove(); + if (field && field.length) { + if (isInput) { + field.val(''); + } else { + field.remove(); + } } } else if (el.hasClass(INDETERMINATE_CLASS)) { el.addClass(ACTIVE_CLASS); el.removeClass(INDETERMINATE_CLASS); - if (field.length && value == null) { + if (field && field.length && value == null) { field.remove(); } - if (!field.length && fieldName) { + if ((!field || !field.length) && fieldName) { this.addInput(fieldName, value, selectedObject); } } else { @@ -638,15 +639,15 @@ this.dropdown.parent().find("input[name='" + fieldName + "']").remove(); } } - if (field.length && value == null) { + if (field && field.length && value == null) { field.remove(); } // Toggle active class for the tick mark el.addClass(ACTIVE_CLASS); if (value != null) { - if (!field.length && fieldName) { + if ((!field || !field.length) && fieldName) { this.addInput(fieldName, value, selectedObject); - } else if (field.length) { + } else if (field && field.length) { field.val(value).trigger('change'); } } @@ -796,4 +797,4 @@ }); }; -}).call(this); \ No newline at end of file +}).call(this); -- GitLab From 507313120d5cd880fc29a7d8ecb1ba943ab8ea34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 22 Sep 2016 11:19:29 +0000 Subject: [PATCH 45/49] Merge branch '22417-api-fork-fix' into 'master' API: Return 404 when trying to fork to unaccessible namespace Closes #22417 See merge request !6452 --- lib/api/projects.rb | 4 +++- spec/requests/api/fork_spec.rb | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 5eb83c2c8f827e..6d99617b56ffa8 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -207,7 +207,9 @@ def map_public_to_visibility_level(attrs) if namespace_id.present? namespace = Namespace.find_by(id: namespace_id) || Namespace.find_by_path_or_name(namespace_id) - not_found!('Target Namespace') unless namespace + unless namespace && can?(current_user, :create_projects, namespace) + not_found!('Target Namespace') + end attrs[:namespace] = namespace end diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb index 06e3a2183c0328..34f84f789525dc 100644 --- a/spec/requests/api/fork_spec.rb +++ b/spec/requests/api/fork_spec.rb @@ -94,7 +94,7 @@ it 'fails if trying to fork to another user when not admin' do post api("/projects/fork/#{project.id}", user2), namespace: admin.namespace.id - expect(response).to have_http_status(409) + expect(response).to have_http_status(404) end it 'fails if trying to fork to non-existent namespace' do @@ -114,7 +114,7 @@ it 'fails to fork to not owned group' do post api("/projects/fork/#{project.id}", user2), namespace: group.name - expect(response).to have_http_status(409) + expect(response).to have_http_status(404) end it 'forks to not owned group when admin' do -- GitLab From 493037d5ba6f4bedcd15375fbc20de0208833c35 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Thu, 22 Sep 2016 13:31:04 +0000 Subject: [PATCH 46/49] Merge branch 'fix-mr-version-dropdowns' into 'master' fix dropdowns for mr-versions ## What does this MR do? Change markup of mr version dropdowns to be in line with ui guidelines so that the dropdown content is scrollable. ## Why was this MR needed? Dropdowns were not scrolling. ## Does this MR meet the acceptance criteria? - [ ] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG) entry added - [ ] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md) - [ ] API support added - Tests - [ ] Added for this feature/bug - [ ] All builds are passing - [ ] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) - [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides) - [ ] Branch has no merge conflicts with `master` (if you do - rebase it please) - [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) ## What are the relevant issue numbers? https://gitlab.com/gitlab-org/gitlab-ce/issues/21427 cc @brycepj @dzaporozhets See merge request !6460 --- .../merge_requests/show/_versions.html.haml | 72 ++++++++++--------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml index 33e56d5417ff6a..49819519759e6b 100644 --- a/app/views/projects/merge_requests/show/_versions.html.haml +++ b/app/views/projects/merge_requests/show/_versions.html.haml @@ -10,23 +10,25 @@ - else version #{version_index(@merge_request_diff)} %span.caret - %ul.dropdown-menu.dropdown-menu-selectable + .dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-title - %span Version - %button.dropdown-title-button.dropdown-menu-close + %span Version: + %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} = icon('times', class: 'dropdown-menu-close-icon') - - @merge_request_diffs.each do |merge_request_diff| - %li - = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do - %strong - - if merge_request_diff.latest? - latest version - - else - version #{version_index(merge_request_diff)} - .monospace #{short_sha(merge_request_diff.head_commit_sha)} - %small - #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)}, - = time_ago_with_tooltip(merge_request_diff.created_at) + .dropdown-content + %ul + - @merge_request_diffs.each do |merge_request_diff| + %li + = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do + %strong + - if merge_request_diff.latest? + latest version + - else + version #{version_index(merge_request_diff)} + .monospace #{short_sha(merge_request_diff.head_commit_sha)} + %small + #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)}, + = time_ago_with_tooltip(merge_request_diff.created_at) - if @merge_request_diff.base_commit_sha and @@ -38,27 +40,29 @@ - else #{@merge_request.target_branch} %span.caret - %ul.dropdown-menu.dropdown-menu-selectable + .dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-title - %span Compared with - %button.dropdown-title-button.dropdown-menu-close + %span Compared with: + %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} = icon('times', class: 'dropdown-menu-close-icon') - - @comparable_diffs.each do |merge_request_diff| - %li - = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff, merge_request_diff.head_commit_sha), class: ('is-active' if merge_request_diff == @start_version) do - %strong - - if merge_request_diff.latest? - latest version - - else - version #{version_index(merge_request_diff)} - .monospace #{short_sha(merge_request_diff.head_commit_sha)} - %small - = time_ago_with_tooltip(merge_request_diff.created_at) - %li - = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do - %strong - #{@merge_request.target_branch} (base) - .monospace #{short_sha(@merge_request_diff.base_commit_sha)} + .dropdown-content + %ul + - @comparable_diffs.each do |merge_request_diff| + %li + = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff, merge_request_diff.head_commit_sha), class: ('is-active' if merge_request_diff == @start_version) do + %strong + - if merge_request_diff.latest? + latest version + - else + version #{version_index(merge_request_diff)} + .monospace #{short_sha(merge_request_diff.head_commit_sha)} + %small + = time_ago_with_tooltip(merge_request_diff.created_at) + %li + = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do + %strong + #{@merge_request.target_branch} (base) + .monospace #{short_sha(@merge_request_diff.base_commit_sha)} - unless @merge_request_diff.latest? && !@start_sha .comments-disabled-notif.content-block -- GitLab From e53ecac15f87b9e12eab7b1bd5af870199eecd51 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 22 Sep 2016 14:22:16 +0000 Subject: [PATCH 47/49] Merge branch 'revert-6455-and-4960' into 'master' Revert the "accurate issuable counts in issuable list" feature ## Why was this MR needed? !6455 introduced a performance killer, so we revert it until we find a proper solution that's not killing performance. ## What are the relevant issue numbers? Revert !6455 and !4960. See merge request !6476 --- .../concerns/issuable_collections.rb | 12 ------- app/controllers/concerns/issues_action.rb | 2 -- .../concerns/merge_requests_action.rb | 2 -- app/controllers/projects/issues_controller.rb | 2 -- .../projects/merge_requests_controller.rb | 2 -- app/helpers/application_helper.rb | 24 +++++++------- app/views/shared/issuable/_nav.html.haml | 12 +++---- spec/features/dashboard_issues_spec.rb | 9 ------ spec/features/issues/filter_by_labels_spec.rb | 20 ++++-------- spec/features/issues/filter_issues_spec.rb | 32 ------------------- 10 files changed, 23 insertions(+), 94 deletions(-) diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 4a447735fa7625..b5e79099e39590 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -13,18 +13,10 @@ def issues_collection issues_finder.execute end - def all_issues_collection - IssuesFinder.new(current_user, filter_params_all).execute - end - def merge_requests_collection merge_requests_finder.execute end - def all_merge_requests_collection - MergeRequestsFinder.new(current_user, filter_params_all).execute - end - def issues_finder @issues_finder ||= issuable_finder_for(IssuesFinder) end @@ -62,10 +54,6 @@ def filter_params @filter_params end - def filter_params_all - @filter_params_all ||= filter_params.merge(state: 'all', sort: nil) - end - def set_default_scope params[:scope] = 'all' if params[:scope].blank? end diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb index eced9d9d6784a3..b89fb94be6ea7f 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issues_action.rb @@ -10,8 +10,6 @@ def issues .preload(:author, :project) .page(params[:page]) - @all_issues = all_issues_collection.non_archived - respond_to do |format| format.html format.atom { render layout: false } diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb index 729763169e28e8..a1b0eee37f91a5 100644 --- a/app/controllers/concerns/merge_requests_action.rb +++ b/app/controllers/concerns/merge_requests_action.rb @@ -9,7 +9,5 @@ def merge_requests .non_archived .preload(:author, :target_project) .page(params[:page]) - - @all_merge_requests = all_merge_requests_collection.non_archived end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 19b8b1576c4dc4..3eb13a121bfc44 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -28,8 +28,6 @@ def index @labels = @project.labels.where(title: params[:label_name]) - @all_issues = all_issues_collection - respond_to do |format| format.html format.atom { render layout: false } diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index e972376df4c0d8..935417d4ae82fa 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -37,8 +37,6 @@ def index @labels = @project.labels.where(title: params[:label_name]) - @all_merge_requests = all_merge_requests_collection - respond_to do |format| format.html format.json do diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 2163a437c4877b..1df430e6279123 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -280,25 +280,23 @@ def path_to_key(key, admin = false) end end - def state_filters_text_for(state, records) + def state_filters_text_for(entity, project) titles = { opened: "Open" } - state_title = titles[state] || state.to_s.humanize - records_with_state = records.public_send(state) + entity_title = titles[entity] || entity.to_s.humanize - # When filtering by multiple labels, the result of query.count is a Hash - # of the form { issuable_id1 => N, issuable_id2 => N }, where N is the - # number of labels selected. The ugly "trick" is to load the issuables - # as an array and get the size of the array... - # We should probably try to solve this properly in the future. - # See https://gitlab.com/gitlab-org/gitlab-ce/issues/22414 - label_names = Array(params.fetch(:label_name, [])) - records_with_state = records_with_state.to_a if label_names.many? + count = + if project.nil? + nil + elsif current_controller?(:issues) + project.issues.visible_to_user(current_user).send(entity).count + elsif current_controller?(:merge_requests) + project.merge_requests.send(entity).count + end - count = records_with_state.size - html = content_tag :span, state_title + html = content_tag :span, entity_title if count.present? html += " " diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index fb592c2b1e2448..1d9b09a5ef1429 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -1,27 +1,25 @@ %ul.nav-links.issues-state-filters - if defined?(type) && type == :merge_requests - page_context_word = 'merge requests' - - records = @all_merge_requests - else - page_context_word = 'issues' - - records = @all_issues %li{class: ("active" if params[:state] == 'opened')} = link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do - #{state_filters_text_for(:opened, records)} + #{state_filters_text_for(:opened, @project)} - if defined?(type) && type == :merge_requests %li{class: ("active" if params[:state] == 'merged')} = link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do - #{state_filters_text_for(:merged, records)} + #{state_filters_text_for(:merged, @project)} %li{class: ("active" if params[:state] == 'closed')} = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do - #{state_filters_text_for(:closed, records)} + #{state_filters_text_for(:closed, @project)} - else %li{class: ("active" if params[:state] == 'closed')} = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do - #{state_filters_text_for(:closed, records)} + #{state_filters_text_for(:closed, @project)} %li{class: ("active" if params[:state] == 'all')} = link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do - #{state_filters_text_for(:all, records)} + #{state_filters_text_for(:all, @project)} diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb index fc914022a59957..3fb1cb37544717 100644 --- a/spec/features/dashboard_issues_spec.rb +++ b/spec/features/dashboard_issues_spec.rb @@ -21,9 +21,6 @@ click_link 'No Milestone' - page.within '.issues-state-filters' do - expect(page).to have_selector('.active .badge', text: '1') - end expect(page).to have_selector('.issue', count: 1) end @@ -32,9 +29,6 @@ click_link 'Any Milestone' - page.within '.issues-state-filters' do - expect(page).to have_selector('.active .badge', text: '2') - end expect(page).to have_selector('.issue', count: 2) end @@ -45,9 +39,6 @@ click_link milestone.title end - page.within '.issues-state-filters' do - expect(page).to have_selector('.active .badge', text: '1') - end expect(page).to have_selector('.issue', count: 1) end end diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb index 7e2abd759e15f7..908b18e5339cdf 100644 --- a/spec/features/issues/filter_by_labels_spec.rb +++ b/spec/features/issues/filter_by_labels_spec.rb @@ -6,19 +6,20 @@ let(:project) { create(:project, :public) } let!(:user) { create(:user)} let!(:label) { create(:label, project: project) } - let(:bug) { create(:label, project: project, title: 'bug') } - let(:feature) { create(:label, project: project, title: 'feature') } - let(:enhancement) { create(:label, project: project, title: 'enhancement') } - let(:issue1) { create(:issue, title: "Bugfix1", project: project) } - let(:issue2) { create(:issue, title: "Bugfix2", project: project) } - let(:issue3) { create(:issue, title: "Feature1", project: project) } before do + bug = create(:label, project: project, title: 'bug') + feature = create(:label, project: project, title: 'feature') + enhancement = create(:label, project: project, title: 'enhancement') + + issue1 = create(:issue, title: "Bugfix1", project: project) issue1.labels << bug + issue2 = create(:issue, title: "Bugfix2", project: project) issue2.labels << bug issue2.labels << enhancement + issue3 = create(:issue, title: "Feature1", project: project) issue3.labels << feature project.team << [user, :master] @@ -158,13 +159,6 @@ wait_for_ajax end - it 'shows a correct "Open" counter' do - page.within '.issues-state-filters' do - expect(page).not_to have_content "{#{issue2.id} => 1}" - expect(page).to have_content "Open 1" - end - end - it 'shows issue "Bugfix2" in issues list' do expect(page).to have_content "Bugfix2" end diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 72f39e2fbcaf73..d1501c9791adc0 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -230,10 +230,6 @@ expect(page).to have_selector('.issue', count: 2) end - page.within '.issues-state-filters' do - expect(page).to have_selector('.active .badge', text: '2') - end - click_button 'Label' page.within '.labels-filter' do click_link 'bug' @@ -243,10 +239,6 @@ page.within '.issues-list' do expect(page).to have_selector('.issue', count: 1) end - - page.within '.issues-state-filters' do - expect(page).to have_selector('.active .badge', text: '1') - end end it 'filters by text and milestone' do @@ -256,10 +248,6 @@ expect(page).to have_selector('.issue', count: 2) end - page.within '.issues-state-filters' do - expect(page).to have_selector('.active .badge', text: '2') - end - click_button 'Milestone' page.within '.milestone-filter' do click_link '8' @@ -268,10 +256,6 @@ page.within '.issues-list' do expect(page).to have_selector('.issue', count: 1) end - - page.within '.issues-state-filters' do - expect(page).to have_selector('.active .badge', text: '1') - end end it 'filters by text and assignee' do @@ -281,10 +265,6 @@ expect(page).to have_selector('.issue', count: 2) end - page.within '.issues-state-filters' do - expect(page).to have_selector('.active .badge', text: '2') - end - click_button 'Assignee' page.within '.dropdown-menu-assignee' do click_link user.name @@ -293,10 +273,6 @@ page.within '.issues-list' do expect(page).to have_selector('.issue', count: 1) end - - page.within '.issues-state-filters' do - expect(page).to have_selector('.active .badge', text: '1') - end end it 'filters by text and author' do @@ -306,10 +282,6 @@ expect(page).to have_selector('.issue', count: 2) end - page.within '.issues-state-filters' do - expect(page).to have_selector('.active .badge', text: '2') - end - click_button 'Author' page.within '.dropdown-menu-author' do click_link user.name @@ -318,10 +290,6 @@ page.within '.issues-list' do expect(page).to have_selector('.issue', count: 1) end - - page.within '.issues-state-filters' do - expect(page).to have_selector('.active .badge', text: '1') - end end end end -- GitLab From 5e3f42d37700d1f974ae3f1075da8d87f36e88ae Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 22 Sep 2016 14:01:09 +0000 Subject: [PATCH 48/49] Merge branch 'ci-permissions-documentation' into 'master' Describe how the recent changes of CI permissions affect builds ## What does this MR do? This describes how the CI permission changes See merge request !6451 --- doc/ci/README.md | 2 + doc/ci/triggers/README.md | 4 + doc/user/permissions.md | 30 ++ .../project/new_ci_build_permissions_model.md | 289 ++++++++++++++++++ 4 files changed, 325 insertions(+) create mode 100644 doc/user/project/new_ci_build_permissions_model.md diff --git a/doc/ci/README.md b/doc/ci/README.md index 10ce4ac8940024..341bc85a16abf0 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -16,5 +16,7 @@ - [Trigger builds through the API](triggers/README.md) - [Build artifacts](../user/project/builds/artifacts.md) - [User permissions](../user/permissions.md#gitlab-ci) +- [Build permissions](../user/permissions.md#build-permissions) - [API](../api/ci/README.md) - [CI services (linked docker containers)](services/README.md) +- [**New CI build permissions model**](../user/project/new_ci_build_permissions_model.md) Read about what changed in GitLab 8.12 and how that affects your builds. There's a new way to access your Git submodules and LFS objects in builds. diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index 6c6767fea0b14e..b78422f6d0e9aa 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -2,6 +2,10 @@ > [Introduced][ci-229] in GitLab CE 7.14. +> **Note**: +GitLab 8.12 has a completely redesigned build permissions system. +Read all about the [new model and its implications][../../user/project/new_ci_build_permissions_model.md#build-triggers]. + Triggers can be used to force a rebuild of a specific branch, tag or commit, with an API call. diff --git a/doc/user/permissions.md b/doc/user/permissions.md index f1b75298180723..12d5b8f8744702 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -138,3 +138,33 @@ instance and project. In addition, all admins can use the admin interface under | Add shared runners | | | | ✓ | | See events in the system | | | | ✓ | | Admin interface | | | | ✓ | + +### Build permissions + +> Changed in GitLab 8.12. + +GitLab 8.12 has a completely redesigned build permissions system. +Read all about the [new model and its implications][new-mod]. + +This table shows granted privileges for builds triggered by specific types of +users: + +| Action | Guest, Reporter | Developer | Master | Admin | +|---------------------------------------------|-----------------|-------------|----------|--------| +| Run CI build | | ✓ | ✓ | ✓ | +| Clone source and LFS from current project | | ✓ | ✓ | ✓ | +| Clone source and LFS from public projects | | ✓ | ✓ | ✓ | +| Clone source and LFS from internal projects | | ✓ [^3] | ✓ [^3] | ✓ | +| Clone source and LFS from private projects | | ✓ [^4] | ✓ [^4] | ✓ [^4] | +| Push source and LFS | | | | | +| Pull container images from current project | | ✓ | ✓ | ✓ | +| Pull container images from public projects | | ✓ | ✓ | ✓ | +| Pull container images from internal projects| | ✓ [^3] | ✓ [^3] | ✓ | +| Pull container images from private projects | | ✓ [^4] | ✓ [^4] | ✓ [^4] | +| Push container images to current project | | ✓ | ✓ | ✓ | +| Push container images to other projects | | | | | + +[^3]: Only if user is not external one. +[^4]: Only if user is a member of the project. +[ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994 +[new-mod]: project/new_ci_build_permissions_model.md diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md new file mode 100644 index 00000000000000..e73f60023b5fbe --- /dev/null +++ b/doc/user/project/new_ci_build_permissions_model.md @@ -0,0 +1,289 @@ +# New CI build permissions model + +> Introduced in GitLab 8.12. + +GitLab 8.12 has a completely redesigned [build permissions] system. You can find +all discussion and all our concerns when choosing the current approach in issue +[#18994](https://gitlab.com/gitlab-org/gitlab-ce/issues/18994). + +--- + +Builds permissions should be tightly integrated with the permissions of a user +who is triggering a build. + +The reasons to do it like that are: + +- We already have a permissions system in place: group and project membership + of users. +- We already fully know who is triggering a build (using `git push`, using the + web UI, executing triggers). +- We already know what user is allowed to do. +- We use the user permissions for builds that are triggered by the user. +- It opens a lot of possibilities to further enforce user permissions, like + allowing only specific users to access runners or use secure variables and + environments. +- It is simple and convenient that your build can access everything that you + as a user have access to. +- Short living unique tokens are now used, granting access for time of the build + and maximizing security. + +With the new behavior, any build that is triggered by the user, is also marked +with their permissions. When a user does a `git push` or changes files through +the web UI, a new pipeline will be usually created. This pipeline will be marked +as created be the pusher (local push or via the UI) and any build created in this +pipeline will have the permissions of the pusher. + +This allows us to make it really easy to evaluate the access for all projects +that have Git submodules or are using container images that the pusher would +have access too. **The permission is granted only for time that build is running. +The access is revoked after the build is finished.** + +## Types of users + +It is important to note that we have a few types of users: + +- **Administrators**: CI builds created by Administrators will not have access + to all GitLab projects, but only to projects and container images of projects + that the administrator is a member of.That means that if a project is either + public or internal users have access anyway, but if a project is private, the + Administrator will have to be a member of it in order to have access to it + via another project's build. + +- **External users**: CI builds created by [external users][ext] will have + access only to projects to which user has at least reporter access. This + rules out accessing all internal projects by default, + +This allows us to make the CI and permission system more trustworthy. +Let's consider the following scenario: + +1. You are an employee of a company. Your company has a number of internal tools + hosted in private repositories and you have multiple CI builds that make use + of these repositories. + +2. You invite a new [external user][ext]. CI builds created by that user do not + have access to internal repositories, because the user also doesn't have the + access from within GitLab. You as an employee have to grant explicit access + for this user. This allows us to prevent from accidental data leakage. + +## Build token + +A unique build token is generated for each build and it allows the user to +access all projects that would be normally accessible to the user creating that +build. + +We try to make sure that this token doesn't leak by: + +1. Securing all API endpoints to not expose the build token. +1. Masking the build token from build logs. +1. Allowing to use the build token **only** when build is running. + +However, this brings a question about the Runners security. To make sure that +this token doesn't leak, you should also make sure that you configure +your Runners in the most possible secure way, by avoiding the following: + +1. Any usage of Docker's `privileged` mode is risky if the machines are re-used. +1. Using the `shell` executor since builds run on the same machine. + +By using an insecure GitLab Runner configuration, you allow the rogue developers +to steal the tokens of other builds. + +## Debugging problems + +With the new permission model in place, there may be times that your build will +fail. This is most likely because your project tries to access other project's +sources, and you don't have the appropriate permissions. In the build log look +for information about 403 or forbidden access messages + +As an Administrator, you can verify that the user is a member of the group or +project they're trying to have access to, and you can impersonate the user to +retry the failing build in order to verify that everything is correct. + +## Build triggers + +[Build triggers][triggers] do not support the new permission model. +They continue to use the old authentication mechanism where the CI build +can access only its own sources. We plan to remove that limitation in one of +the upcoming releases. + +## Before GitLab 8.12 + +In versions before GitLab 8.12, all CI builds would use the CI Runner's token +to checkout project sources. + +The project's Runner's token was a token that you could find under the +project's **Settings > CI/CD Pipelines** and was limited to access only that +project. +It could be used for registering new specific Runners assigned to the project +and to checkout project sources. +It could also be used with the GitLab Container Registry for that project, +allowing pulling and pushing Docker images from within the CI build. + +--- + +GitLab would create a special checkout URL like: + +``` +https://gitlab-ci-token:/gitlab.com/gitlab-org/gitlab-ce.git +``` + +And then the users could also use it in their CI builds all Docker related +commands to interact with GitLab Container Registry. For example: + +``` +docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.gitlab.com +``` + +Using single token had multiple security implications: + +- The token would be readable to anyone who had developer access to a project + that could run CI builds, allowing the developer to register any specific + Runner for that project. +- The token would allow to access only the project's sources, forbidding from + accessing any other projects. +- The token was not expiring and was multi-purpose: used for checking out sources, + for registering specific runners and for accessing a project's container + registry with read-write permissions. + +All the above led to a new permission model for builds that was introduced +with GitLab 8.12. + +## Making use of the new CI build permissions model + +With the new build permission model, there is now an easy way to access all +dependent source code in a project. That way, we can: + +1. Access a project's Git submodules +1. Access private container images +1. Access project's and submodule LFS objects + +Let's see how that works with Git submodules and private Docker images hosted on +the container registry. + +## Git submodules + +> +It often happens that while working on one project, you need to use another +project from within it; perhaps it’s a library that a third party developed or +you’re developing a project separately and are using it in multiple parent +projects. +A common issue arises in these scenarios: you want to be able to treat the two +projects as separate yet still be able to use one from within the other. +> +_Excerpt from the [Git website][git-scm] about submodules._ + +If dealing with submodules, your project will probably have a file named +`.gitmodules`. And this is how it usually looks like: + +``` +[submodule "tools"] + path = tools + url = git@gitlab.com/group/tools.git +``` + +> **Note:** +If you are **not** using GitLab 8.12 or higher, you would need to work your way +around this issue in order to access the sources of `gitlab.com/group/tools` +(e.g., use [SSH keys](../ssh_keys/README.md)). +> +With GitLab 8.12 onward, your permissions are used to evaluate what a CI build +can access. More information about how this system works can be found in the +[Build permissions model](../../user/permissions.md#builds-permissions). + +To make use of the new changes, you have to update your `.gitmodules` file to +use a relative URL. + +Let's consider the following example: + +1. Your project is located at `https://gitlab.com/secret-group/my-project`. +1. To checkout your sources you usually use an SSH address like + `git@gitlab.com:secret-group/my-project.git`. +1. Your project depends on `https://gitlab.com/group/tools`. +1. You have the `.gitmodules` file with above content. + +Since Git allows the usage of relative URLs for your `.gitmodules` configuration, +this easily allows you to use HTTP for cloning all your CI builds and SSH +for all your local checkouts. + +For example, if you change the `url` of your `tools` dependency, from +`git@gitlab.com/group/tools.git` to `../../group/tools.git`, this will instruct +Git to automatically deduce the URL that should be used when cloning sources. +Whether you use HTTP or SSH, Git will use that same channel and it will allow +to make all your CI builds use HTTPS (because GitLab CI uses HTTPS for cloning +your sources), and all your local clones will continue using SSH. + +Given the above explanation, your `.gitmodules` file should eventually look +like this: + +``` +[submodule "tools"] + path = tools + url = ../../group/tools.git +``` + +However, you have to explicitly tell GitLab CI to clone your submodules as this +is not done automatically. You can achieve that by adding a `before_script` +section to your `.gitlab-ci.yml`: + +``` +before_script: + - git submodule update --init --recursive + +test: + script: + - run-my-tests +``` + +This will make GitLab CI initialize (fetch) and update (checkout) all your +submodules recursively. + +In case your environment or your Docker image doesn't have Git installed, +you have to either ask your Administrator or install the missing dependency +yourself: + +``` +# Debian / Ubuntu +before_script: + - apt-get update -y + - apt-get install -y git-core + - git submodule update --init --recursive + +# CentOS / RedHat +before_script: + - yum install git + - git submodule update --init --recursive + +# Alpine +before_script: + - apk add -U git + - git submodule update --init --recursive +``` + +### Container Registry + +With the update permission model we also extended the support for accessing +Container Registries for private projects. + +> **Note:** +As GitLab Runner 1.6 doesn't yet incorporate the introduced changes for +permissions, this makes the `image:` directive to not work with private projects +automatically. The manual configuration by an Administrator is required to use +private images. We plan to remove that limitation in one of the upcoming releases. + +Your builds can access all container images that you would normally have access +to. The only implication is that you can push to the Container Registry of the +project for which the build is triggered. + +This is how an example usage can look like: + +``` +test: + script: + - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY + - docker pull $CI_REGISTRY/group/other-project:latest + - docker run $CI_REGISTRY/group/other-project:latest +``` + +[git-scm]: https://git-scm.com/book/en/v2/Git-Tools-Submodules +[build permissions]: ../permissions.md#builds-permissions +[ext]: ../permissions.md#external-users +[triggers]: ../../ci/triggers/README.md -- GitLab From 0af27537204bab036c66e545f3bc6bc50ccdc8cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 22 Sep 2016 17:17:29 +0200 Subject: [PATCH 49/49] Update VERSION to 8.12.0-ee --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index a86f57cf3ca353..59449079c0323d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.12.0-rc7-ee +8.12.0-ee -- GitLab