diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3c75ad325f46ac8e428270e469e7ae1e90f6c303..a3ac317b55df71dc0b19749d7d9eb78f629f6d53 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,130 +1,190 @@
Please view this file on the master branch, on stable branches it's out of date.
+## 8.13.3 (2016-11-02)
+
+- Removes any symlinks before importing a project export file. CVE-2016-9086
+- Fixed Import/Export foreign key issue to do with project members.
+
+## 8.13.2 (2016-10-31)
+
+- Fix encoding issues on pipeline commits. !6832
+- Use Hash rocket syntax to fix cycle analytics under Ruby 2.1. !6977
+- Modify GitHub importer to be retryable. !7003
+- Fix refs dropdown selection with special characters. !7061
+- Fix horizontal padding for highlight blocks. !7062
+- Pass user instance to `Labels::FindOrCreateService` or `skip_authorization: true`. !7093
+- Fix builds dropdown overlapping bug. !7124
+- Fix applying labels for GitHub-imported MRs. !7139
+- Fix importing MR comments from GitHub. !7139
+- Fix project member access for group links. !7144
+- API: Fix booleans not recognized as such when using the `to_boolean` helper. !7149
+- Fix and improve `Sortable.highest_label_priority`. !7165
+- Fixed sticky merge request tabs when sidebar is pinned. !7167
+- Only remove right connector of first build of last stage. !7179
+
+## 8.13.1 (2016-10-25)
+
+- Fix branch protection API. !6215
+- Fix hidden pipeline graph on commit and MR page. !6895
+- Fix Cycle analytics not showing correct data when filtering by date. !6906
+- Ensure custom provider tab labels don't break layout. !6993
+- Fix issue boards user link when in subdirectory. !7018
+- Refactor and add new environment functionality to CI yaml reference. !7026
+- Fix typo in project settings that prevents users from enabling container registry. !7037
+- Fix events order in `users/:id/events` endpoint. !7039
+- Remove extra line for empty issue description. !7045
+- Don't append issue/MR templates to any existing text. !7050
+- Fix error in generating labels. !7055
+- Stop clearing the database cache on `rake cache:clear`. !7056
+- Only show register tab if signup enabled. !7058
+- Expire and build repository cache after project import. !7064
+- Fix bug where labels would be assigned to issues that were moved. !7065
+- Fix reply-by-email not working due to queue name mismatch. !7068
+- Fix 404 for group pages when GitLab setup uses relative url. !7071
+- Fix `User#to_reference`. !7088
+- Reduce overhead of `LabelFinder` by avoiding `#presence` call. !7094
+- Fix unauthorized users dragging on issue boards. !7096
+- Only schedule `ProjectCacheWorker` jobs when needed. !7099
+
## 8.13.0 (2016-10-22)
- - Fix save button on project pipeline settings page. (!6955)
- - Avoid race condition when asynchronously removing expired artifacts. (!6881)
- - Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675)
- - Respond with 404 Not Found for non-existent tags (Linus Thiel)
- - Truncate long labels with ellipsis in labels page
- - Improve tabbing usability for sign in page (ClemMakesApps)
- - Enforce TrailingSemicolon and EmptyLineBetweenBlocks in scss-lint
- - Adding members no longer silently fails when there is extra whitespace
- - Update runner version only when updating contacted_at
- - Add link from system note to compare with previous version
- - Use gitlab-shell v3.6.6
- - Ability to resolve merge request conflicts with editor !6374
- - Add `/projects/visible` API endpoint (Ben Boeckel)
- - Fix centering of custom header logos (Ashley Dumaine)
- - ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
- - Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun)
- - Cancelled pipelines could be retried. !6927
- - Updating verbiage on git basics to be more intuitive
- - Clarify documentation for Runners API (Gennady Trafimenkov)
- - The instrumentation for Banzai::Renderer has been restored
- - Change user & group landing page routing from /u/:username to /:username
- - Added documentation for .gitattributes files
- - Move Pipeline Metrics to separate worker
- - AbstractReferenceFilter caches project_refs on RequestStore when active
- - Replaced the check sign to arrow in the show build view. !6501
- - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
- - Fix Error 500 when viewing old merge requests with bad diff data
- - Create a new /templates namespace for the /licenses, /gitignores and /gitlab_ci_ymls API endpoints. !5717 (tbalthazar)
- - Speed-up group milestones show page
- - Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps)
- - Extract project#update_merge_requests and SystemHooks to its own worker from GitPushService
- - Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs)
- - Add tag shortcut from the Commit page. !6543
- - Keep refs for each deployment
- - Allow browsing branches that end with '.atom'
- - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
- - Replace unique keyframes mixin with keyframe mixin with specific names (ClemMakesApps)
- - Add more tests for calendar contribution (ClemMakesApps)
- - Update Gitlab Shell to fix some problems with moving projects between storages
- - Cache rendered markdown in the database, rather than Redis
- - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
- - Simplify Mentionable concern instance methods
- - API: Ability to retrieve version information (Robert Schilling)
- - Fix permission for setting an issue's due date
- - API: Multi-file commit !6096 (mahcsig)
- - Unicode emoji are now converted to images
- - Revert "Label list shows all issues (opened or closed) with that label"
- - Expose expires_at field when sharing project on API
- - Fix VueJS template tags being rendered in code comments
- - Added copy file path button to merge request diff files
- - Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
- - Add Issue Board API support (andrebsguedes)
- - Allow the Koding integration to be configured through the API
- - Add new issue button to each list on Issues Board
- - Execute specific named route method from toggle_award_url helper method
- - Added soft wrap button to repository file/blob editor
- - Update namespace validation to forbid reserved names (.git and .atom) (Will Starms)
- - Show the time ago a merge request was deployed to an environment
- - Add RTL support to markdown renderer (Ebrahim Byagowi)
- - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
- - Fix todos page mobile viewport layout (ClemMakesApps)
- - Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps)
- - Remove redundant mixins (ClemMakesApps)
- - Added 'Download' button to the Snippets page (Justin DiPierro)
- - Add visibility level to project repository
- - Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
- - Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
- - Fix that manual jobs would no longer block jobs in the next stage. !6604
- - Add configurable email subject suffix (Fu Xu)
- - Use defined colour for a language when available !6748 (nilsding)
- - Added tooltip to fork count on project show page. (Justin DiPierro)
- - Use a ConnectionPool for Rails.cache on Sidekiq servers
- - Replace `alias_method_chain` with `Module#prepend`
- - Enable GitLab Import/Export for non-admin users.
- - Preserve label filters when sorting !6136 (Joseph Frazier)
- - MergeRequest#new form load diff asynchronously
- - Only update issuable labels if they have been changed
- - Take filters in account in issuable counters. !6496
- - Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
- - Append issue template to existing description !6149 (Joseph Frazier)
- - Trending projects now only show public projects and the list of projects is cached for a day
- - Memoize Gitlab Shell's secret token (!6599, Justin DiPierro)
- - Revoke button in Applications Settings underlines on hover.
- - Use higher size on Gitlab::Redis connection pool on Sidekiq servers
- - Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
- - Revert avoid touching file system on Build#artifacts?
- - Stop using a Redis lease when updating the project activity timestamp whenever a new event is created
- - Add disabled delete button to protected branches (ClemMakesApps)
- - Add broadcast messages and alerts below sub-nav
- - Better empty state for Groups view
- - API: New /users/:id/events endpoint
- - Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
- - Replace bootstrap caret with fontawesome caret (ClemMakesApps)
- - Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
- - Add organization field to user profile
- - Change user pages routing from /u/:username/PATH to /users/:username/PATH. Old routes will redirect to the new ones for the time being.
- - Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts)
- - Fix deploy status responsiveness error !6633
- - Make searching for commits case insensitive
- - Fix resolved discussion display in side-by-side diff view !6575
- - Optimize GitHub importing for speed and memory
- - API: expose pipeline data in builds API (!6502, Guilherme Salazar)
- - Notify the Merger about merge after successful build (Dimitris Karakasilis)
- - Reorder issue and merge request titles to show IDs first. !6503 (Greg Laubenstein)
- - Reduce queries needed to find users using their SSH keys when pushing commits
- - Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska)
- - Fix broken repository 500 errors in project list
- - Fix the diff in the merge request view when converting a symlink to a regular file
- - Fix Pipeline list commit column width should be adjusted
- - Close todos when accepting merge requests via the API !6486 (tonygambone)
- - Ability to batch assign issues relating to a merge request to the author. !5725 (jamedjo)
- - Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
- - Retouch environments list and deployments list
- - Add multiple command support for all label related slash commands !6780 (barthc)
- - Add Container Registry on/off status to Admin Area !6638 (the-undefined)
- - Allow empty merge requests !6384 (Artem Sidorenko)
- - Grouped pipeline dropdown is a scrollable container
- - Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi)
- - Fixes padding in all clipboard icons that have .btn class
- - Fix a typo in doc/api/labels.md
- - API: all unknown routing will be handled with 404 Not Found
- - Add docs for request profiling
- - Make guests unable to view MRs on private projects
+- Fix save button on project pipeline settings page. (!6955)
+- All Sidekiq workers now use their own queue
+- Avoid race condition when asynchronously removing expired artifacts. (!6881)
+- Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675)
+- Respond with 404 Not Found for non-existent tags (Linus Thiel)
+- Truncate long labels with ellipsis in labels page
+- Improve tabbing usability for sign in page (ClemMakesApps)
+- Enforce TrailingSemicolon and EmptyLineBetweenBlocks in scss-lint
+- Adding members no longer silently fails when there is extra whitespace
+- Update runner version only when updating contacted_at
+- Add link from system note to compare with previous version
+- Use gitlab-shell v3.6.6
+- Ignore references to internal issues when using external issues tracker
+- Ability to resolve merge request conflicts with editor !6374
+- Add `/projects/visible` API endpoint (Ben Boeckel)
+- Fix centering of custom header logos (Ashley Dumaine)
+- Keep around commits only pipeline creation as pipeline data doesn't change over time
+- Update duration at the end of pipeline
+- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
+- Add group level labels. (!6425)
+- Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun)
+- Cancelled pipelines could be retried. !6927
+- Updating verbiage on git basics to be more intuitive
+- Fix project_feature record not generated on project creation
+- Clarify documentation for Runners API (Gennady Trafimenkov)
+- The instrumentation for Banzai::Renderer has been restored
+- Change user & group landing page routing from /u/:username to /:username
+- Added documentation for .gitattributes files
+- Move Pipeline Metrics to separate worker
+- AbstractReferenceFilter caches project_refs on RequestStore when active
+- Replaced the check sign to arrow in the show build view. !6501
+- Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
+- ProjectCacheWorker updates caches at most once per 15 minutes per project
+- Fix Error 500 when viewing old merge requests with bad diff data
+- Create a new /templates namespace for the /licenses, /gitignores and /gitlab_ci_ymls API endpoints. !5717 (tbalthazar)
+- Fix viewing merged MRs when the source project has been removed !6991
+- Speed-up group milestones show page
+- Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps)
+- Extract project#update_merge_requests and SystemHooks to its own worker from GitPushService
+- Fix discussion thread from emails for merge requests. !7010
+- Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs)
+- Add tag shortcut from the Commit page. !6543
+- Keep refs for each deployment
+- Allow browsing branches that end with '.atom'
+- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
+- Replace unique keyframes mixin with keyframe mixin with specific names (ClemMakesApps)
+- Add more tests for calendar contribution (ClemMakesApps)
+- Update Gitlab Shell to fix some problems with moving projects between storages
+- Cache rendered markdown in the database, rather than Redis
+- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
+- Simplify Mentionable concern instance methods
+- API: Ability to retrieve version information (Robert Schilling)
+- Fix permission for setting an issue's due date
+- API: Multi-file commit !6096 (mahcsig)
+- Unicode emoji are now converted to images
+- Revert "Label list shows all issues (opened or closed) with that label"
+- Expose expires_at field when sharing project on API
+- Fix VueJS template tags being rendered in code comments
+- Added copy file path button to merge request diff files
+- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
+- Add Issue Board API support (andrebsguedes)
+- Allow the Koding integration to be configured through the API
+- Add new issue button to each list on Issues Board
+- Execute specific named route method from toggle_award_url helper method
+- Added soft wrap button to repository file/blob editor
+- Update namespace validation to forbid reserved names (.git and .atom) (Will Starms)
+- Show the time ago a merge request was deployed to an environment
+- Add RTL support to markdown renderer (Ebrahim Byagowi)
+- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
+- Fix todos page mobile viewport layout (ClemMakesApps)
+- Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps)
+- Remove redundant mixins (ClemMakesApps)
+- Added 'Download' button to the Snippets page (Justin DiPierro)
+- Add visibility level to project repository
+- Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
+- Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
+- Fix that manual jobs would no longer block jobs in the next stage. !6604
+- Add configurable email subject suffix (Fu Xu)
+- Use defined colour for a language when available !6748 (nilsding)
+- Added tooltip to fork count on project show page. (Justin DiPierro)
+- Use a ConnectionPool for Rails.cache on Sidekiq servers
+- Replace `alias_method_chain` with `Module#prepend`
+- Enable GitLab Import/Export for non-admin users.
+- Preserve label filters when sorting !6136 (Joseph Frazier)
+- MergeRequest#new form load diff asynchronously
+- Only update issuable labels if they have been changed
+- Take filters in account in issuable counters. !6496
+- Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
+- Append issue template to existing description !6149 (Joseph Frazier)
+- Trending projects now only show public projects and the list of projects is cached for a day
+- Memoize Gitlab Shell's secret token (!6599, Justin DiPierro)
+- Revoke button in Applications Settings underlines on hover.
+- Use higher size on Gitlab::Redis connection pool on Sidekiq servers
+- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
+- Revert avoid touching file system on Build#artifacts?
+- Stop using a Redis lease when updating the project activity timestamp whenever a new event is created
+- Add disabled delete button to protected branches (ClemMakesApps)
+- Add broadcast messages and alerts below sub-nav
+- Better empty state for Groups view
+- API: New /users/:id/events endpoint
+- Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
+- Replace bootstrap caret with fontawesome caret (ClemMakesApps)
+- Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
+- Add organization field to user profile
+- Change user pages routing from /u/:username/PATH to /users/:username/PATH. Old routes will redirect to the new ones for the time being.
+- Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts)
+- Fix deploy status responsiveness error !6633
+- Make searching for commits case insensitive
+- Fix resolved discussion display in side-by-side diff view !6575
+- Optimize GitHub importing for speed and memory
+- API: expose pipeline data in builds API (!6502, Guilherme Salazar)
+- Notify the Merger about merge after successful build (Dimitris Karakasilis)
+- Reduce queries needed to find users using their SSH keys when pushing commits
+- Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska)
+- Fix broken repository 500 errors in project list
+- Fix the diff in the merge request view when converting a symlink to a regular file
+- Fix Pipeline list commit column width should be adjusted
+- Close todos when accepting merge requests via the API !6486 (tonygambone)
+- Ability to batch assign issues relating to a merge request to the author. !5725 (jamedjo)
+- Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
+- Retouch environments list and deployments list
+- Add multiple command support for all label related slash commands !6780 (barthc)
+- Add Container Registry on/off status to Admin Area !6638 (the-undefined)
+- Add Nofollow for uppercased scheme in external urls !6820 (the-undefined)
+- Allow empty merge requests !6384 (Artem Sidorenko)
+- Grouped pipeline dropdown is a scrollable container
+- Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi)
+- Fixes padding in all clipboard icons that have .btn class
+- Fix a typo in doc/api/labels.md
+- Fix double-escaping in activities tab (Alexandre Maia)
+- API: all unknown routing will be handled with 404 Not Found
+- Add docs for request profiling
+- Delete dynamic environments
+- Fix buggy iOS tooltip layering behavior.
+- Make guests unable to view MRs on private projects
+- Fix broken Project API docs (Takuya Noguchi)
+- Migrate invalid project members (owner -> master)
## 8.12.7
@@ -250,6 +310,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Changed MR widget build status to pipeline status !6335
- Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
- Enable pipeline events by default !6278
+ - Add pipeline email service !6019
- 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)
diff --git a/Gemfile b/Gemfile
index 05166b6a828b5ef2812512122ea141158bb6d993..46245ab62d1162f4aeab67e080902e2d0328bbeb 100644
--- a/Gemfile
+++ b/Gemfile
@@ -29,7 +29,7 @@ gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.0'
gem 'omniauth-google-oauth2', '~> 0.4.1'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
-gem 'omniauth-saml', '~> 1.6.0'
+gem 'omniauth-saml', '~> 1.7.0'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index a9892d1c13006a44750907d8a79c92a6926631eb..442184b92288796443faf5ab6a0a9faff339f486 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -473,9 +473,9 @@ GEM
omniauth-oauth2 (1.3.1)
oauth2 (~> 1.0)
omniauth (~> 1.2)
- omniauth-saml (1.6.0)
+ omniauth-saml (1.7.0)
omniauth (~> 1.3)
- ruby-saml (~> 1.3)
+ ruby-saml (~> 1.4)
omniauth-shibboleth (1.2.1)
omniauth (>= 1.0.0)
omniauth-twitter (1.2.1)
@@ -635,7 +635,7 @@ GEM
crack (~> 0.4)
ruby-prof (0.16.2)
ruby-progressbar (1.8.1)
- ruby-saml (1.3.0)
+ ruby-saml (1.4.1)
nokogiri (>= 1.5.10)
ruby_parser (3.8.2)
sexp_processor (~> 4.1)
@@ -915,7 +915,7 @@ DEPENDENCIES
omniauth-gitlab (~> 1.0.0)
omniauth-google-oauth2 (~> 0.4.1)
omniauth-kerberos (~> 0.3.0)
- omniauth-saml (~> 1.6.0)
+ omniauth-saml (~> 1.7.0)
omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0)
omniauth_crowd (~> 2.2.0)
diff --git a/VERSION b/VERSION
index dff4cd02d5f7af909de3806954736a8bfde01775..3b3df401908836ca727ed885c8e0a5fbf544434c 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.13.0-pre
+8.13.3
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif
new file mode 100644
index 0000000000000000000000000000000000000000..3f4ef31947bc4a53d6b2d243d6e9f2c975c84b77
Binary files /dev/null and b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif
new file mode 100644
index 0000000000000000000000000000000000000000..387628f831c2ef2d14863a2e30bde7c6f3e5bd85
Binary files /dev/null and b/app/assets/images/mailers/ci_pipeline_notif_v1/gitlab-logo.gif differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-branch-gray.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-branch-gray.gif
new file mode 100644
index 0000000000000000000000000000000000000000..5f8f8ca143c16c801220f5fc40be68a39c0e8cb3
Binary files /dev/null and b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-branch-gray.gif differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif
new file mode 100644
index 0000000000000000000000000000000000000000..27a55b1d61fd88ef897d0021de7570288e5db0e6
Binary files /dev/null and b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gif
new file mode 100644
index 0000000000000000000000000000000000000000..8fe3281d2f6dc2fe80c94290a2fe73c93bfdd06e
Binary files /dev/null and b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-commit-gray.gif differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif
new file mode 100644
index 0000000000000000000000000000000000000000..4260e312929d8fa0e9aacda888f99c5d86bc75b0
Binary files /dev/null and b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif differ
diff --git a/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red.gif b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red.gif
new file mode 100644
index 0000000000000000000000000000000000000000..6de166ce0a2643e020fce46cedccfac3e078c5e3
Binary files /dev/null and b/app/assets/images/mailers/ci_pipeline_notif_v1/icon-x-red.gif differ
diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6
index 4e309e480b012da46bdae847e46cbce82742a761..2d5c6ade0536e3e2007988fecb64c740f237e98a 100644
--- a/app/assets/javascripts/blob/template_selector.js.es6
+++ b/app/assets/javascripts/blob/template_selector.js.es6
@@ -68,14 +68,10 @@
// To be implemented on the extending class
// e.g.
// Api.gitignoreText item.name, @requestFileSuccess.bind(@)
- requestFileSuccess(file, { skipFocus, append } = {}) {
+ requestFileSuccess(file, { skipFocus } = {}) {
const oldValue = this.editor.getValue();
let newValue = file.content;
- if (append && oldValue.length && oldValue !== newValue) {
- newValue = oldValue + '\n\n' + newValue;
- }
-
this.editor.setValue(newValue, 1);
if (!skipFocus) this.editor.focus();
@@ -99,4 +95,3 @@
global.TemplateSelector = TemplateSelector;
})(window.gl || ( window.gl = {}));
-
diff --git a/app/assets/javascripts/cycle_analytics.js.es6 b/app/assets/javascripts/cycle_analytics.js.es6
index cd9886ba58d0124b4c24079e4359976ef269dedb..20791bab942a9b23ae8cefd74dc96af787d4f920 100644
--- a/app/assets/javascripts/cycle_analytics.js.es6
+++ b/app/assets/javascripts/cycle_analytics.js.es6
@@ -1,3 +1,5 @@
+//= require vue
+
((global) => {
const COOKIE_NAME = 'cycle_analytics_help_dismissed';
@@ -34,7 +36,11 @@
method: 'GET',
dataType: 'json',
contentType: 'application/json',
- data: { start_date: options.startDate }
+ data: {
+ cycle_analytics: {
+ start_date: options.startDate
+ }
+ }
}).done((data) => {
this.decorateData(data);
this.initDropdown();
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 73691f40c74e6d45f04ea39f125f1abed85c8593..a1fe57562fa7d7744112f7083033505a52a5b716 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -117,6 +117,9 @@
new ZenMode();
shortcut_handler = new ShortcutsNavigation();
break;
+ case 'projects:commit:builds':
+ new gl.Pipelines();
+ break;
case 'projects:commits:show':
case 'projects:activity':
shortcut_handler = new ShortcutsNavigation();
@@ -168,6 +171,8 @@
shortcut_handler = new ShortcutsNavigation();
new ShortcutsBlob(true);
break;
+ case 'groups:labels:new':
+ case 'groups:labels:edit':
case 'projects:labels:new':
case 'projects:labels:edit':
new Labels();
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index f1e719937c719ebd9bba8aca52666a7b0f606c4a..b4f6e70f694a1ee3ebd2a866b76e5acfa82bd33c 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -266,7 +266,7 @@
},
fieldName: $dropdown.data('field-name'),
id: function(label) {
- if (label.id <= 0) return;
+ if (label.id <= 0) return label.title;
if ($dropdown.hasClass('js-issuable-form-dropdown')) {
return label.id;
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index fd21aa1fefa5a710bd788759d4e9c4e22d2a3f14..3dde979185b2f9f01b00217c46f4fb856cf89e9f 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -282,6 +282,7 @@
document.querySelector("div#builds").innerHTML = data.html;
gl.utils.localTimeAgo($('.js-timeago', 'div#builds'));
_this.buildsLoaded = true;
+ if (!this.pipelines) this.pipelines = new gl.Pipelines();
return _this.scrollToElement("#builds");
};
})(this)
@@ -388,28 +389,25 @@
// So we dont affix the tabs on these
if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return;
- var tabsWidth = $tabs.outerWidth(),
- $diffTabs = $('#diff-notes-app'),
- offsetTop = $tabs.offset().top - ($('.navbar-fixed-top').height() + $('.layout-nav').height());
+ var $diffTabs = $('#diff-notes-app'),
+ $fixedNav = $('.navbar-fixed-top'),
+ $layoutNav = $('.layout-nav');
$tabs.off('affix.bs.affix affix-top.bs.affix')
.affix({
offset: {
- top: offsetTop
+ top: function () {
+ var tabsTop = $diffTabs.offset().top - $tabs.height();
+ tabsTop = tabsTop - ($fixedNav.height() + $layoutNav.height());
+
+ return tabsTop;
+ }
}
}).on('affix.bs.affix', function () {
- $tabs.css({
- left: $tabs.offset().left,
- width: tabsWidth
- });
$diffTabs.css({
marginTop: $tabs.height()
});
}).on('affix-top.bs.affix', function () {
- $tabs.css({
- left: '',
- width: ''
- });
$diffTabs.css({
marginTop: ''
});
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
index fcadc4bc515291742c4d98de07e8754f41d7ad16..3ff6851d59b77e3adc43f8cde187e7fb5d2359b7 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -17,6 +17,12 @@
View on <%- external_url_formatted %>
+
+
+
+ Stop environment
+
+
`;
@@ -205,6 +211,11 @@
if ($(`.mr-state-widget #${ environment.id }`).length) return;
const $template = $(DEPLOYMENT_TEMPLATE);
if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove();
+
+ if (!environment.stop_url) {
+ $('.js-stop-env-link', $template).remove();
+ }
+
if (environment.deployed_at && environment.deployed_at_formatted) {
environment.deployed_at = $.timeago(environment.deployed_at) + '.';
} else {
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index a6c015299a0db796a48753ebb6884cfb04424ae2..af0530b7159b96ff91842596acd93f86ecf63641 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -82,7 +82,7 @@
if (ref.header != null) {
return $('
').addClass('dropdown-header').text(ref.header);
} else {
- link = $('').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
+ link = $('').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', ref);
return $('').append(link);
}
},
diff --git a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
similarity index 100%
rename from app/assets/javascripts/protected_branch_access_dropdown.js.es6
rename to app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
similarity index 100%
rename from app/assets/javascripts/protected_branch_create.js.es6
rename to app/assets/javascripts/protected_branches/protected_branch_create.js.es6
diff --git a/app/assets/javascripts/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
similarity index 100%
rename from app/assets/javascripts/protected_branch_dropdown.js.es6
rename to app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
similarity index 100%
rename from app/assets/javascripts/protected_branch_edit.js.es6
rename to app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
diff --git a/app/assets/javascripts/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
similarity index 100%
rename from app/assets/javascripts/protected_branch_edit_list.js.es6
rename to app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
new file mode 100644
index 0000000000000000000000000000000000000000..15b3affd4696bc6533e42a36ff8168b351f137c1
--- /dev/null
+++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
@@ -0,0 +1 @@
+/*= require_tree . */
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6
index bd4e3c3d00dfb1f62b3fa060d0e5b40b43064a0d..fa1b79c84158d8007f134fabaa8f8544669b96be 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6
@@ -32,24 +32,22 @@
this.currentTemplate = currentTemplate;
if (err) return; // Error handled by global AJAX error handler
this.stopLoadingSpinner();
- this.setInputValueToTemplateContent(true);
+ this.setInputValueToTemplateContent();
});
return;
}
- setInputValueToTemplateContent(append) {
+ setInputValueToTemplateContent() {
// `this.requestFileSuccess` sets the value of the description input field
- // to the content of the template selected. If `append` is true, the
- // template content will be appended to the previous value of the field,
- // separated by a blank line if the previous value is non-empty.
+ // to the content of the template selected.
if (this.titleInput.val() === '') {
// If the title has not yet been set, focus the title input and
// skip focusing the description input by setting `true` as the
// `skipFocus` option to `requestFileSuccess`.
- this.requestFileSuccess(this.currentTemplate, {skipFocus: true, append});
+ this.requestFileSuccess(this.currentTemplate, {skipFocus: true});
this.titleInput.focus();
} else {
- this.requestFileSuccess(this.currentTemplate, {skipFocus: false, append});
+ this.requestFileSuccess(this.currentTemplate, {skipFocus: false});
}
return;
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 81e4e264560aa1dc83c801b84e2ae82af68681aa..800e2dba0186d787f30e4a55782f763e6a6a027c 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -372,3 +372,5 @@ table {
margin-right: -$gl-padding;
border-top: 1px solid $border-color;
}
+
+.hide-bottom-border { border-bottom: none !important; }
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 13c1bbf0359819c30845961efe4403807060e282..f49d7b92a00923d8f3bedf9f2ec03df6e6ff842e 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -167,7 +167,6 @@
*/
&.code {
padding: 0;
- -webkit-overflow-scrolling: auto; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
}
}
}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 8bb047db2ddbc4543a90b2a99ec2385720cf458f..7baa4296abf6e29883228a3f682cbd82b7e2ddac 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -27,3 +27,15 @@ body {
.container-limited {
max-width: $fixed-layout-width;
}
+
+
+/* The following prevents side effects related to iOS Safari's implementation of -webkit-overflow-scrolling: touch,
+which is applied to the body by jquery.nicescroling plugin to force hardware acceleration for momentum scrolling. Side
+effects are commonly related to inconsisent z-index behavior (e.g. tooltips). By applying the following to direct children
+of the body element here, we negate cascading side effects but allow momentum scrolling to be applied to the body */
+
+.navbar,
+.page-gutter,
+.page-with-sidebar {
+ -webkit-overflow-scrolling: auto;
+}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index ec52f326eb9b915c10d7013501466e3901c04628..c54f7b2757519d48f38a7627f3f128f4e8f4c72f 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -164,6 +164,18 @@
padding-left: $sidebar_width;
}
}
+
+ .merge-request-tabs-holder.affix {
+ @media (min-width: $sidebar-breakpoint) {
+ left: $sidebar_width;
+ }
+ }
+
+ &.right-sidebar-expanded {
+ .line-resolve-all-container {
+ display: none;
+ }
+ }
}
header.header-sidebar-pinned {
@@ -185,6 +197,10 @@ header.header-sidebar-pinned {
@media (min-width: $screen-sm-min) {
padding-right: $sidebar_collapsed_width;
+
+ .merge-request-tabs-holder.affix {
+ right: $sidebar_collapsed_width;
+ }
}
.sidebar-collapsed-icon {
@@ -207,6 +223,10 @@ header.header-sidebar-pinned {
@media (min-width: $screen-md-min) {
padding-right: $gutter_width;
+
+ .merge-request-tabs-holder.affix {
+ right: $gutter_width;
+ }
}
&.with-overlay {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 7690d65de8e86595b4d3c64085dff2498a2c6d56..eafe84570a8b03bff566b0b9415d81791df1edde 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -16,6 +16,7 @@ $white-light: #fff;
$white-normal: #ededed;
$white-dark: #ececec;
+$gray-lightest: #fdfdfd;
$gray-light: #fafafa;
$gray-lighter: #f9f9f9;
$gray-normal: #f5f5f5;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 6e81c12aa55119f41b8679c2b37887c80307aa75..d8fabbdcebea7c5ac533d52a60ccddfaa4a43e49 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -1,4 +1,3 @@
-lex
[v-cloak] {
display: none;
}
@@ -132,7 +131,7 @@ lex
}
.board-blank-state {
- height: 100%;
+ height: calc(100% - 49px);
padding: $gl-padding;
background-color: #fff;
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 2fbf0cf34bfd7df2a9a7cec44516a8033b83e63b..d6a55fbd464bb98e26baf7b411272a6372a4a985 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -195,7 +195,7 @@
.build-job {
position: relative;
- .fa {
+ .fa-arrow-right {
position: absolute;
left: 15px;
top: 20px;
@@ -205,14 +205,23 @@
&.active {
font-weight: bold;
- .fa {
+ .fa-arrow-right {
display: block;
}
}
+ &.retried {
+ background-color: $gray-lightest;
+ }
+
&:hover {
background-color: $row-hover;
}
+
+ .fa-refresh {
+ font-size: 13px;
+ margin-left: 3px;
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index bdc82a8f0f5dd47febf0d6154972b0ee7e1278cd..fe6421f8b3ffdfbf0946b6b939fc84b882fa4bae 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -52,7 +52,6 @@
background: #fff;
color: #333;
border-radius: 0 0 3px 3px;
- -webkit-overflow-scrolling: auto;
.unfold {
cursor: pointer;
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 820cc0fc991a54f8773e376b451419b153a6f2af..12ee0a5dc3ddd437bcee6835060a8318651a7d60 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -38,6 +38,14 @@
color: $gl-dark-link-color;
}
+ .stop-env-link {
+ color: $table-text-gray;
+
+ .stop-env-icon {
+ font-size: 14px;
+ }
+ }
+
.deployment {
.build-column {
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 9bac6d463551c2cbb4190e9d4cdc073a11d2f574..397f89f501a564c9b9604f9b3a042fa3bc1e9b38 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -66,7 +66,21 @@
text-overflow: ellipsis;
vertical-align: middle;
max-width: 100%;
- }
+ }
+ }
+
+ .label-type {
+ display: block;
+ margin-bottom: 10px;
+ margin-left: 50px;
+
+ @media (min-width: $screen-sm-min) {
+ display: inline-block;
+ width: 100px;
+ margin-left: 10px;
+ margin-bottom: 0;
+ vertical-align: middle;
+ }
}
.label-description {
@@ -209,6 +223,13 @@
}
.label-subscribe-button {
+ .label-subscribe-button-icon {
+ &[disabled] {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+ }
+
.label-subscribe-button-loading {
display: none;
}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index e6d9be5185d404739db61a6ca2eccc913c243ec5..9496234c77364db8660f26c7084c0b34cb50116a 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -53,6 +53,7 @@
margin: 0 0 10px;
}
+
.login-footer {
margin-top: 10px;
@@ -142,6 +143,7 @@
&:not(.active) {
background-color: $gray-light;
+ border-left: 1px solid $border-color;
}
a {
@@ -169,6 +171,31 @@
}
}
+ // Ldap configurations may need more tabs & the tab labels are user generated (arbitrarily long).
+ // These styles prevent this from breaking the layout, and only applied when providers are configured.
+
+ .new-session-tabs.custom-provider-tabs {
+ flex-wrap: wrap;
+
+ li {
+ min-width: 85px;
+ flex-basis: auto;
+
+ // This styles tab elements that have wrapped to a second line. We cannot easily predict when this will happen.
+ // We are making somewhat of an assumption about the configuration here: that users do not have more than
+ // 3 LDAP servers configured (in addition to standard login) and they are not using especially long names for any
+ // of them. If either condition is false, this will work as expected. If both are true, there may be a missing border
+ // above one of the bottom row elements. If you know a better way, please implement it!
+ &:nth-child(n+5) {
+ border-top: 1px solid $border-color;
+ }
+ }
+
+ a {
+ font-size: 16px;
+ }
+ }
+
.form-control {
&:active, &:focus {
@@ -202,6 +229,7 @@
.login-page {
.col-sm-5.pull-right {
float: none !important;
+ margin-bottom: 45px;
}
}
}
@@ -243,6 +271,27 @@
}
.navless-container {
- padding: 65px; // height of footer + bottom padding of email confirmation link
+ padding: 65px 15px; // height of footer + bottom padding of email confirmation link
+
+ @media (max-width: $screen-xs-max) {
+ padding: 0 15px 65px;
+ }
+ }
+}
+
+// For sign in pane only, to improve tab order, the following removes the submit button from
+// normal document flow and pins it to the bottom of the form. For context, see !6867 & !6928
+
+.login-box {
+ .new_user {
+ position: relative;
+ padding-bottom: 35px;
+ }
+
+ .move-submit-down {
+ position: absolute;
+ width: 100%;
+ bottom: 0;
}
}
+
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 101472278e26f177635d74f2535386ddd9a80374..33f097222749c8329967dc8c3ce2220171164d9d 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -183,6 +183,15 @@
.ci-coverage {
float: right;
}
+
+ .stop-env-container {
+ color: $gl-text-color;
+ float: right;
+
+ a {
+ color: $gl-text-color;
+ }
+ }
}
.mr_source_commit,
@@ -429,11 +438,18 @@
}
}
-.merge-request-tabs {
- background-color: #fff;
+.merge-request-tabs-holder {
+ background-color: $white-light;
&.affix {
top: 100px;
- z-index: 9;
+ left: 0;
+ z-index: 10;
+ transition: right .15s;
+ }
+
+ &:not(.affix) .container-fluid {
+ padding-left: 0;
+ padding-right: 0;
}
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 7b71876b8226ec300ccfba954da4c7c976ea3291..8ce03df64fae1f46cfe3489935c87058bce22853 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -23,10 +23,17 @@
.table.ci-table {
min-width: 1200px;
- .branch-commit {
- width: 33%;
+ .pipeline-id {
+ color: $black;
}
+ .branch-commit {
+ width: 30%;
+
+ .branch-name {
+ max-width: 195px;
+ }
+ }
}
}
@@ -73,6 +80,10 @@
border-top-width: 1px;
}
+ .build.retried {
+ background-color: $gray-lightest;
+ }
+
.commit-link {
.ci-status {
@@ -88,6 +99,15 @@
}
}
+ .avatar {
+ margin-left: 0;
+ float: none;
+ }
+
+ .api {
+ color: $code-color;
+ }
+
.branch-commit {
.branch-name {
@@ -109,12 +129,11 @@
.fa {
font-size: 12px;
- color: $table-text-gray;
+ color: $gl-text-color;
}
.commit-id {
color: $gl-link-color;
- margin-right: 8px;
}
.commit-title {
@@ -125,10 +144,6 @@
text-overflow: ellipsis;
}
- .avatar {
- margin-left: 0;
- }
-
.label {
margin-right: 4px;
}
@@ -144,17 +159,11 @@
.icon-container {
display: inline-block;
- text-align: right;
- width: 15px;
+ width: 10px;
- .fa {
- position: relative;
- right: 3px;
- }
-
- svg {
- position: relative;
- right: 1px;
+ &.commit-icon {
+ width: 15px;
+ text-align: center;
}
}
@@ -360,10 +369,6 @@
&:hover {
background-color: $gray-lighter;
-
- .dropdown-menu-toggle {
- background-color: transparent;
- }
}
&.playable {
@@ -393,6 +398,15 @@
}
}
+ .tooltip {
+ white-space: nowrap;
+
+ .tooltip-inner {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+
.ci-status-text {
width: 135px;
white-space: nowrap;
@@ -410,6 +424,7 @@
}
.dropdown-menu-toggle {
+ background-color: transparent;
border: none;
width: auto;
padding: 0;
@@ -555,7 +570,7 @@
.build {
// Remove right connecting horizontal line from first build in last stage
&:first-child {
- &::after, &::before {
+ &::after {
border: none;
}
}
@@ -634,6 +649,10 @@
&.pipelines {
+ .ci-table {
+ min-width: 900px;
+ }
+
.content-list.pipelines {
overflow: auto;
}
diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb
index 7c37f3155dac8f0435bd02ac297a505f0b1d15da..37a1a23178eb78f26d214322b893a9fde5f04d91 100644
--- a/app/controllers/admin/services_controller.rb
+++ b/app/controllers/admin/services_controller.rb
@@ -26,14 +26,10 @@ class Admin::ServicesController < Admin::ApplicationController
private
def services_templates
- templates = []
-
- Service.available_services_names.each do |service_name|
+ Service.available_services_names.map do |service_name|
service_template = service_name.concat("_service").camelize.constantize
- templates << service_template.where(template: true).first_or_create
+ service_template.where(template: true).first_or_create
end
-
- templates
end
def service
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index bb32bc502e65b249c3062d31a935491e3dd6aea3..be86fa106f8b4df8793c1bfcbd7e305d8644b6f0 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -2,6 +2,7 @@ module IssuableActions
extend ActiveSupport::Concern
included do
+ before_action :labels, only: [:show, :new, :edit]
before_action :authorize_destroy_issuable!, only: :destroy
before_action :authorize_admin_issuable!, only: :bulk_update
end
@@ -25,6 +26,10 @@ module IssuableActions
private
+ def labels
+ @labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
+ end
+
def authorize_destroy_issuable!
unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable)
return access_denied!
diff --git a/app/controllers/dashboard/labels_controller.rb b/app/controllers/dashboard/labels_controller.rb
index 2a88350a4cabeb5d90ab1a87680caf3cec899409..d5031da867af6ac49fdebbe14b26b35507a259f9 100644
--- a/app/controllers/dashboard/labels_controller.rb
+++ b/app/controllers/dashboard/labels_controller.rb
@@ -1,9 +1,9 @@
class Dashboard::LabelsController < Dashboard::ApplicationController
def index
- labels = Label.where(project_id: projects).select(:id, :title, :color).uniq(:title)
+ labels = LabelsFinder.new(current_user).execute
respond_to do |format|
- format.json { render json: labels }
+ format.json { render json: labels.as_json(only: [:id, :title, :color]) }
end
end
end
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..29528b2cfaae4cb27cf0c05ddc4ad190691e3366
--- /dev/null
+++ b/app/controllers/groups/labels_controller.rb
@@ -0,0 +1,92 @@
+class Groups::LabelsController < Groups::ApplicationController
+ before_action :label, only: [:edit, :update, :destroy]
+ before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy]
+ before_action :save_previous_label_path, only: [:edit]
+
+ respond_to :html
+
+ def index
+ respond_to do |format|
+ format.html do
+ @labels = @group.labels.page(params[:page])
+ end
+
+ format.json do
+ available_labels = LabelsFinder.new(current_user, group_id: @group.id).execute
+ render json: available_labels.as_json(only: [:id, :title, :color])
+ end
+ end
+ end
+
+ def new
+ @label = @group.labels.new
+ @previous_labels_path = previous_labels_path
+ end
+
+ def create
+ @label = @group.labels.create(label_params)
+
+ if @label.valid?
+ redirect_to group_labels_path(@group)
+ else
+ render :new
+ end
+ end
+
+ def edit
+ @previous_labels_path = previous_labels_path
+ end
+
+ def update
+ if @label.update_attributes(label_params)
+ redirect_back_or_group_labels_path
+ else
+ render :edit
+ end
+ end
+
+ def destroy
+ @label.destroy
+
+ respond_to do |format|
+ format.html do
+ redirect_to group_labels_path(@group), notice: 'Label was removed'
+ end
+ format.js
+ end
+ end
+
+ protected
+
+ def authorize_admin_labels!
+ return render_404 unless can?(current_user, :admin_label, @group)
+ end
+
+ def authorize_read_labels!
+ return render_404 unless can?(current_user, :read_label, @group)
+ end
+
+ def label
+ @label ||= @group.labels.find(params[:id])
+ end
+
+ def label_params
+ params.require(:label).permit(:title, :description, :color)
+ end
+
+ def redirect_back_or_group_labels_path(options = {})
+ redirect_to previous_labels_path, options
+ end
+
+ def previous_labels_path
+ session.fetch(:previous_labels_path, fallback_path)
+ end
+
+ def fallback_path
+ group_labels_path(@group)
+ end
+
+ def save_previous_label_path
+ session[:previous_labels_path] = URI(request.referer || '').path
+ end
+end
diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb
index 3ec173abcdbdacac98c13a0ec80e1c986b5164ab..36d246d185bbe78289eaf94505dd80010508b85c 100644
--- a/app/controllers/import/gitlab_projects_controller.rb
+++ b/app/controllers/import/gitlab_projects_controller.rb
@@ -2,8 +2,8 @@ class Import::GitlabProjectsController < Import::BaseController
before_action :verify_gitlab_project_import_enabled
def new
- @namespace_id = project_params[:namespace_id]
- @namespace_name = Namespace.find(project_params[:namespace_id]).name
+ @namespace = Namespace.find(project_params[:namespace_id])
+ return render_404 unless current_user.can?(:create_projects, @namespace)
@path = project_params[:path]
end
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index 71eb56aed0b54a397ad52592ce7d7b90ba88ea0e..a2b01ff43dc7839c0ebd1babdd7dd8d9dfc4e9c2 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -72,10 +72,10 @@ module Projects
def serialize_as_json(resource)
resource.as_json(
+ labels: true,
only: [:iid, :title, :confidential],
include: {
- assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
- labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
+ assignee: { only: [:id, :name, :username], methods: [:avatar_url] }
})
end
end
diff --git a/app/controllers/projects/boards/lists_controller.rb b/app/controllers/projects/boards/lists_controller.rb
index 76ae41319c403836755d3b44e9965a1f4366f1f7..67e3c9add81ea3078d6175774130aebecb397ac9 100644
--- a/app/controllers/projects/boards/lists_controller.rb
+++ b/app/controllers/projects/boards/lists_controller.rb
@@ -76,9 +76,8 @@ module Projects
resource.as_json(
only: [:id, :list_type, :position],
methods: [:title],
- include: {
- label: { only: [:id, :title, :description, :color, :priority] }
- })
+ label: true
+ )
end
end
end
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 3b2e35a7a0545eb89fd1ad6605b365d9cb069cff..fbe391fc58cd949ac6104c12401797a74d0b1e63 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -47,7 +47,9 @@ class Projects::BuildsController < Projects::ApplicationController
def trace
respond_to do |format|
format.json do
- render json: @build.trace_with_state(params[:state].presence).merge!(id: @build.id, status: @build.status)
+ state = params[:state].presence
+ render json: @build.trace_with_state(state: state).
+ merge!(id: @build.id, status: @build.status)
end
end
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 58678f96879b160a8cb6d5dac34d453ac648a1bf..ea22b2dcc15a5e7aff6c5822141af82e8f1e6d6e 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -2,11 +2,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController
layout 'project'
before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create]
- before_action :authorize_update_environment!, only: [:edit, :update, :destroy]
- before_action :environment, only: [:show, :edit, :update, :destroy]
+ before_action :authorize_create_deployment!, only: [:stop]
+ before_action :authorize_update_environment!, only: [:edit, :update]
+ before_action :environment, only: [:show, :edit, :update, :stop]
def index
- @environments = project.environments
+ @scope = params[:scope]
+ @all_environments = project.environments
+ @environments =
+ if @scope == 'stopped'
+ @all_environments.stopped
+ else
+ @all_environments.available
+ end
end
def show
@@ -38,14 +46,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
- def destroy
- if @environment.destroy
- flash[:notice] = 'Environment was successfully removed.'
- else
- flash[:alert] = 'Failed to remove environment.'
- end
+ def stop
+ return render_404 unless @environment.stoppable?
- redirect_to namespace_project_environments_path(project.namespace, project)
+ new_action = @environment.stop!(current_user)
+ redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action])
end
private
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 96041b0764741c2c1b94c9c23cf91055f59c3882..cb649264146f3eb077bbc25906086a05bcc02a7d 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -26,7 +26,9 @@ class Projects::IssuesController < Projects::ApplicationController
@issues = issues_collection
@issues = @issues.page(params[:page])
- @labels = @project.labels.where(title: params[:label_name])
+ if params[:label_name].present?
+ @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
+ end
respond_to do |format|
format.html
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index a6626df4826844693b435055e669f899c53a0730..4f855134368f6c0d3234dd3d08484403b0e6ba71 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -3,21 +3,22 @@ class Projects::LabelsController < Projects::ApplicationController
before_action :module_enabled
before_action :label, only: [:edit, :update, :destroy]
+ before_action :find_labels, only: [:index, :set_priorities, :remove_priority]
before_action :authorize_read_label!
- before_action :authorize_admin_labels!, only: [
- :new, :create, :edit, :update, :generate, :destroy, :remove_priority, :set_priorities
- ]
+ before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update,
+ :generate, :destroy, :remove_priority,
+ :set_priorities]
respond_to :js, :html
def index
- @labels = @project.labels.unprioritized.page(params[:page])
- @prioritized_labels = @project.labels.prioritized
+ @prioritized_labels = @available_labels.prioritized(@project)
+ @labels = @available_labels.unprioritized(@project).page(params[:page])
respond_to do |format|
format.html
format.json do
- render json: @project.labels
+ render json: @available_labels.as_json(only: [:id, :title, :color])
end
end
end
@@ -36,7 +37,7 @@ class Projects::LabelsController < Projects::ApplicationController
end
else
respond_to do |format|
- format.html { render 'new' }
+ format.html { render :new }
format.json { render json: { message: @label.errors.messages }, status: 400 }
end
end
@@ -49,7 +50,7 @@ class Projects::LabelsController < Projects::ApplicationController
if @label.update_attributes(label_params)
redirect_to namespace_project_labels_path(@project.namespace, @project)
else
- render 'edit'
+ render :edit
end
end
@@ -68,6 +69,7 @@ class Projects::LabelsController < Projects::ApplicationController
def destroy
@label.destroy
+ @labels = find_labels
respond_to do |format|
format.html do
@@ -80,20 +82,24 @@ class Projects::LabelsController < Projects::ApplicationController
def remove_priority
respond_to do |format|
- if label.update_attribute(:priority, nil)
+ label = @available_labels.find(params[:id])
+
+ if label.unprioritize!(project)
format.json { render json: label }
else
- message = label.errors.full_messages.uniq.join('. ')
- format.json { render json: { message: message }, status: :unprocessable_entity }
+ format.json { head :unprocessable_entity }
end
end
end
def set_priorities
Label.transaction do
- params[:label_ids].each_with_index do |label_id, index|
- label = @project.labels.find_by_id(label_id)
- label.update_attribute(:priority, index) if label
+ available_labels_ids = @available_labels.where(id: params[:label_ids]).pluck(:id)
+ label_ids = params[:label_ids].select { |id| available_labels_ids.include?(id.to_i) }
+
+ label_ids.each_with_index do |label_id, index|
+ label = @available_labels.find(label_id)
+ label.prioritize!(project, index)
end
end
@@ -119,6 +125,10 @@ class Projects::LabelsController < Projects::ApplicationController
end
alias_method :subscribable_resource, :label
+ def find_labels
+ @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute.includes(:priorities)
+ end
+
def authorize_admin_labels!
return render_404 unless can?(current_user, :admin_label, @project)
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index a39b47b6d9586d7327a6579427c55e7c6671f3f8..2ee53f7cedacedd2eaf9298d6d1ba735a5aa003e 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -40,7 +40,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(:target_project)
- @labels = @project.labels.where(title: params[:label_name])
+ if params[:label_name].present?
+ labels_params = { project_id: @project.id, title: params[:label_name] }
+ @labels = LabelsFinder.new(current_user, labels_params).execute
+ end
respond_to do |format|
format.html
@@ -395,7 +398,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
status ||= "preparing"
else
- ci_service = @merge_request.source_project.ci_service
+ ci_service = @merge_request.source_project.try(:ci_service)
status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service
if ci_service.respond_to?(:commit_coverage)
@@ -422,10 +425,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController
project = environment.project
deployment = environment.first_deployment_for(@merge_request.diff_head_commit)
+ stop_url =
+ if environment.stoppable? && can?(current_user, :create_deployment, environment)
+ stop_namespace_project_environment_path(project.namespace, project, environment)
+ end
+
{
id: environment.id,
name: environment.name,
url: namespace_project_environment_path(project.namespace, project, environment),
+ stop_url: stop_url,
external_url: environment.external_url,
external_url_formatted: environment.formatted_external_url,
deployed_at: deployment.try(:created_at),
@@ -483,13 +492,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@noteable = @merge_request
@commits_count = @merge_request.commits.count
- @pipeline = @merge_request.pipeline
- @statuses = @pipeline.statuses.relevant if @pipeline
-
if @merge_request.locked_long_ago?
@merge_request.unlock_mr
@merge_request.close
end
+
+ define_pipelines_vars
end
# Discussion tab data is rendered on html responses of actions
@@ -517,7 +525,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_widget_vars
@pipeline = @merge_request.pipeline
- @pipelines = [@pipeline].compact
end
def define_commit_vars
@@ -544,6 +551,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
)
end
+ def define_pipelines_vars
+ @pipelines = @merge_request.all_pipelines
+
+ if @pipelines.present?
+ @pipeline = @pipelines.first
+ @statuses = @pipeline.statuses.relevant
+ end
+ end
+
def define_new_vars
@noteable = @merge_request
@@ -559,10 +575,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
- @pipeline = @merge_request.pipeline
- @statuses = @pipeline.statuses.relevant if @pipeline
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
+
+ @labels = LabelsFinder.new(current_user, project_id: @project.id).execute
+
+ define_pipelines_vars
end
def invalid_mr
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 9f1704281009553c9f38f49db8121b113b45bb7e..e27986ef95b382e70ac7120a81aca5f553b1c82d 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -124,15 +124,12 @@ class IssuableFinder
def labels
return @labels if defined?(@labels)
- if labels? && !filter_by_no_label?
- @labels = Label.where(title: label_names)
-
- if projects
- @labels = @labels.where(project: projects)
+ @labels =
+ if labels? && !filter_by_no_label?
+ LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute
+ else
+ Label.none
end
- else
- @labels = Label.none
- end
end
def assignee?
@@ -274,8 +271,10 @@ class IssuableFinder
items = items.without_label
else
items = items.with_label(label_names, params[:sort])
+
if projects
- items = items.where(labels: { project_id: projects })
+ label_ids = LabelsFinder.new(current_user, project_ids: projects).execute.select(:id)
+ items = items.where(labels: { id: label_ids })
end
end
end
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..865f093f04a1edf6d4fed16c693858fce031ab97
--- /dev/null
+++ b/app/finders/labels_finder.rb
@@ -0,0 +1,93 @@
+class LabelsFinder < UnionFinder
+ def initialize(current_user, params = {})
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute(skip_authorization: false)
+ @skip_authorization = skip_authorization
+ items = find_union(label_ids, Label)
+ items = with_title(items)
+ sort(items)
+ end
+
+ private
+
+ attr_reader :current_user, :params, :skip_authorization
+
+ def label_ids
+ label_ids = []
+
+ if project
+ label_ids << project.group.labels if project.group.present?
+ label_ids << project.labels
+ else
+ label_ids << Label.where(group_id: projects.group_ids)
+ label_ids << Label.where(project_id: projects.select(:id))
+ end
+
+ label_ids
+ end
+
+ def sort(items)
+ items.reorder(title: :asc)
+ end
+
+ def with_title(items)
+ return items if title.nil?
+ return items.none if title.blank?
+
+ items.where(title: title)
+ end
+
+ def group_id
+ params[:group_id].presence
+ end
+
+ def project_id
+ params[:project_id].presence
+ end
+
+ def projects_ids
+ params[:project_ids]
+ end
+
+ def title
+ params[:title] || params[:name]
+ end
+
+ def project
+ return @project if defined?(@project)
+
+ if project_id
+ @project = find_project
+ else
+ @project = nil
+ end
+
+ @project
+ end
+
+ def find_project
+ if skip_authorization
+ Project.find_by(id: project_id)
+ else
+ available_projects.find_by(id: project_id)
+ end
+ end
+
+ def projects
+ return @projects if defined?(@projects)
+
+ @projects = skip_authorization ? Project.all : available_projects
+ @projects = @projects.in_namespace(group_id) if group_id
+ @projects = @projects.where(id: projects_ids) if projects_ids
+ @projects = @projects.reorder(nil)
+
+ @projects
+ end
+
+ def available_projects
+ @available_projects ||= ProjectsFinder.new.execute(current_user)
+ end
+end
diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb
index 493f14f6f9dc6eae40ff3a8be6f742ec4f63aff6..592ffe7b89f0ce8b22b3533ed46f56bde1683217 100644
--- a/app/helpers/award_emoji_helper.rb
+++ b/app/helpers/award_emoji_helper.rb
@@ -4,7 +4,7 @@ module AwardEmojiHelper
if awardable.is_a?(Note)
# We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (6.5x)
- toggle_award_emoji_namespace_project_note_url(namespace_id: @project.namespace_id, project_id: @project.id, id: awardable.id)
+ toggle_award_emoji_namespace_project_note_url(namespace_id: @project.namespace, project_id: @project, id: awardable.id)
else
url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
end
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index b7247ffa8b205df0b8afe5d5b355d8dd209b340e..38c586ccd31e154720d191258f99ffe400a55e9f 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -5,7 +5,7 @@ module BoardsHelper
{
endpoint: namespace_project_boards_path(@project.namespace, @project),
board_id: board.id,
- disabled: !can?(current_user, :admin_list, @project),
+ disabled: "#{!can?(current_user, :admin_list, @project)}",
issue_link_base: namespace_project_issues_path(@project.namespace, @project)
}
end
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f3aaff9140de0e7122030504b2028b2d7c199bbc
--- /dev/null
+++ b/app/helpers/builds_helper.rb
@@ -0,0 +1,8 @@
+module BuildsHelper
+ def sidebar_build_class(build, current_build)
+ build_class = ''
+ build_class += ' active' if build == current_build
+ build_class += ' retried' if build.retried?
+ build_class
+ end
+end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index bfedcb1c42b62714b6ac6945589f721a10fed85d..f8ded05c31a4efe93ed21072178a3360764bec16 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -154,7 +154,7 @@ module EventsHelper
end
def event_commit_title(message)
- escape_once(truncate(message.split("\n").first, length: 70))
+ (message.split("\n").first || "").truncate(70)
rescue
"--broken encoding"
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 670a7ca36f48f586faef7d14f34ed15b4901d5c7..bccf64d1aac3154d6c3eaeb57ce5afa3817ec76e 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -94,6 +94,22 @@ module GitlabRoutingHelper
namespace_project_merge_request_url(entity.project.namespace, entity.project, entity, *args)
end
+ def pipeline_url(pipeline, *args)
+ namespace_project_pipeline_url(pipeline.project.namespace, pipeline.project, pipeline.id, *args)
+ end
+
+ def pipeline_build_url(pipeline, build, *args)
+ namespace_project_build_url(pipeline.project.namespace, pipeline.project, build.id, *args)
+ end
+
+ def commits_url(entity, *args)
+ namespace_project_commits_url(entity.project.namespace, entity.project, entity.ref, *args)
+ end
+
+ def commit_url(entity, *args)
+ namespace_project_commit_url(entity.project.namespace, entity.project, entity.sha, *args)
+ end
+
def project_snippet_url(entity, *args)
namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args)
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 692fadd505f89ed03b6167ac922f0ac234c4c6e3..03b2db1bc9136f68b826bfeea79e6e4ffa0287d5 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -124,6 +124,10 @@ module IssuablesHelper
end
end
+ def issuable_filters_present
+ params[:search] || params[:author_id] || params[:assignee_id] || params[:milestone_title] || params[:label_name]
+ end
+
def issuables_count_for_state(issuable_type, state)
issuables_finder = public_send("#{issuable_type}_finder")
issuables_finder.params[:state] = state
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index b9f3d6c75c2882f311af60d5fe9caf3cd2fa6f0f..221a84b042fc07c41ca39166046a5f2a04af70af 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -4,9 +4,8 @@ module LabelsHelper
# Link to a Label
#
# label - Label object to link to
- # project - Project object which will be used as the context for the label's
- # link. If omitted, defaults to `@project`, or the label's own
- # project.
+ # subject - Project/Group object which will be used as the context for the
+ # label's link. If omitted, defaults to the label's own group/project.
# type - The type of item the link will point to (:issue or
# :merge_request). If omitted, defaults to :issue.
# block - An optional block that will be passed to `link_to`, forming the
@@ -15,15 +14,14 @@ module LabelsHelper
#
# Examples:
#
- # # Allow the generated link to use the label's own project
+ # # Allow the generated link to use the label's own subject
# link_to_label(label)
#
- # # Force the generated link to use @project
- # @project = Project.first
- # link_to_label(label)
+ # # Force the generated link to use a provided group
+ # link_to_label(label, subject: Group.last)
#
# # Force the generated link to use a provided project
- # link_to_label(label, project: Project.last)
+ # link_to_label(label, subject: Project.last)
#
# # Force the generated link to point to merge requests instead of issues
# link_to_label(label, type: :merge_request)
@@ -32,9 +30,8 @@ module LabelsHelper
# link_to_label(label) { "My Custom Label Text" }
#
# Returns a String
- def link_to_label(label, project: nil, type: :issue, tooltip: true, css_class: nil, &block)
- project ||= @project || label.project
- link = label_filter_path(project, label, type: type)
+ def link_to_label(label, subject: nil, type: :issue, tooltip: true, css_class: nil, &block)
+ link = label_filter_path(subject || label.subject, label, type: type)
if block_given?
link_to link, class: css_class, &block
@@ -43,15 +40,40 @@ module LabelsHelper
end
end
- def label_filter_path(project, label, type: issue)
- send("namespace_project_#{type.to_s.pluralize}_path",
- project.namespace,
- project,
- label_name: [label.name])
+ def label_filter_path(subject, label, type: :issue)
+ case subject
+ when Group
+ send("#{type.to_s.pluralize}_group_path",
+ subject,
+ label_name: [label.name])
+ when Project
+ send("namespace_project_#{type.to_s.pluralize}_path",
+ subject.namespace,
+ subject,
+ label_name: [label.name])
+ end
+ end
+
+ def edit_label_path(label)
+ case label
+ when GroupLabel then edit_group_label_path(label.group, label)
+ when ProjectLabel then edit_namespace_project_label_path(label.project.namespace, label.project, label)
+ end
+ end
+
+ def destroy_label_path(label)
+ case label
+ when GroupLabel then group_label_path(label.group, label)
+ when ProjectLabel then namespace_project_label_path(label.project.namespace, label.project, label)
+ end
end
- def project_label_names
- @project.labels.pluck(:title)
+ def toggle_subscription_data(label)
+ return unless label.is_a?(ProjectLabel)
+
+ {
+ url: toggle_subscription_namespace_project_label_path(label.project.namespace, label.project, label)
+ }
end
def render_colored_label(label, label_suffix = '', tooltip: true)
@@ -68,8 +90,8 @@ module LabelsHelper
span.html_safe
end
- def render_colored_cross_project_label(label, tooltip: true)
- label_suffix = label.project.name_with_namespace
+ def render_colored_cross_project_label(label, source_project = nil, tooltip: true)
+ label_suffix = source_project ? source_project.name_with_namespace : label.project.name_with_namespace
label_suffix = " in #{escape_once(label_suffix)}"
render_colored_label(label, label_suffix, tooltip: tooltip)
end
@@ -115,7 +137,10 @@ module LabelsHelper
end
def labels_filter_path
+ return group_labels_path(@group, :json) if @group
+
project = @target_project || @project
+
if project
namespace_project_labels_path(project.namespace, project, :json)
else
@@ -124,11 +149,24 @@ module LabelsHelper
end
def label_subscription_status(label)
- label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
+ case label
+ when GroupLabel then 'Subscribing to group labels is currently not supported.'
+ when ProjectLabel then label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
+ end
end
def label_subscription_toggle_button_text(label)
- label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
+ case label
+ when GroupLabel then 'Subscribing to group labels is currently not supported.'
+ when ProjectLabel then label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
+ end
+ end
+
+ def label_deletion_confirm_text(label)
+ case label
+ when GroupLabel then 'Remove this label? This will affect all projects within the group. Are you sure?'
+ when ProjectLabel then 'Remove this label? Are you sure?'
+ end
end
# Required for Banzai::Filter::LabelReferenceFilter
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 249cb44e9d5fbb9235f2f076f97da8f25df41f38..a6659ea2fd1d0806d5da53b4bc7b7c51561e8df2 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -86,11 +86,15 @@ module MergeRequestsHelper
end
def source_branch_with_namespace(merge_request)
- branch = link_to(merge_request.source_branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch))
+ namespace = merge_request.source_project_namespace
+ branch = merge_request.source_branch
+
+ if merge_request.source_branch_exists?
+ namespace = link_to(namespace, project_path(merge_request.source_project))
+ branch = link_to(branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch))
+ end
if merge_request.for_fork?
- namespace = link_to(merge_request.source_project_namespace,
- project_path(merge_request.source_project))
namespace + ":" + branch
else
branch
diff --git a/app/mailers/.gitkeep b/app/mailers/.gitkeep
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb
new file mode 100644
index 0000000000000000000000000000000000000000..601c8b5cd621843f44867888dd04b6d262910ba2
--- /dev/null
+++ b/app/mailers/emails/pipelines.rb
@@ -0,0 +1,43 @@
+module Emails
+ module Pipelines
+ def pipeline_success_email(pipeline, to)
+ pipeline_mail(pipeline, to, 'succeeded')
+ end
+
+ def pipeline_failed_email(pipeline, to)
+ pipeline_mail(pipeline, to, 'failed')
+ end
+
+ private
+
+ def pipeline_mail(pipeline, to, status)
+ @project = pipeline.project
+ @pipeline = pipeline
+ @merge_request = pipeline.merge_requests.first
+ add_headers
+
+ mail(to: to, subject: pipeline_subject(status), skip_premailer: true) do |format|
+ format.html { render layout: false }
+ format.text
+ end
+ end
+
+ def add_headers
+ add_project_headers
+ add_pipeline_headers
+ end
+
+ def add_pipeline_headers
+ headers['X-GitLab-Pipeline-Id'] = @pipeline.id
+ headers['X-GitLab-Pipeline-Ref'] = @pipeline.ref
+ headers['X-GitLab-Pipeline-Status'] = @pipeline.status
+ end
+
+ def pipeline_subject(status)
+ commit = @pipeline.short_sha
+ commit << " in #{@merge_request.to_reference}" if @merge_request
+
+ subject("Pipeline ##{@pipeline.id} has #{status} for #{@pipeline.ref}", commit)
+ end
+ end
+end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 2444702104ebf8d1a4655fb838ccbbe9630d24ef..eca6ec297671855184a909c84101df558684c26c 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -7,6 +7,7 @@ class Notify < BaseMailer
include Emails::Projects
include Emails::Profile
include Emails::Builds
+ include Emails::Pipelines
include Emails::Members
add_template_helper MergeRequestsHelper
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 87475119b23766e41d94996669e36c68002c7aa1..a6b606d13de40434763d18c9047ceb917f946adc 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -133,13 +133,17 @@ module Ci
latest_builds.where('stage_idx < ?', stage_idx)
end
- def trace_html
- trace_with_state[:html] || ''
+ def trace_html(**args)
+ trace_with_state(**args)[:html] || ''
end
- def trace_with_state(state = nil)
- trace_with_state = Ci::Ansi2html::convert(trace, state) if trace.present?
- trace_with_state || {}
+ def trace_with_state(state: nil, last_lines: nil)
+ trace_ansi = trace(last_lines: last_lines)
+ if trace_ansi.present?
+ Ci::Ansi2html.convert(trace_ansi, state)
+ else
+ {}
+ end
end
def timeout
@@ -222,9 +226,10 @@ module Ci
raw_trace.present?
end
- def raw_trace
+ def raw_trace(last_lines: nil)
if File.exist?(trace_file_path)
- File.read(trace_file_path)
+ Gitlab::Ci::TraceReader.new(trace_file_path).
+ read(last_lines: last_lines)
else
# backward compatibility
read_attribute :trace
@@ -239,8 +244,8 @@ module Ci
project.ci_id && File.exist?(old_path_to_trace)
end
- def trace
- hide_secrets(raw_trace)
+ def trace(last_lines: nil)
+ hide_secrets(raw_trace(last_lines: last_lines))
end
def trace_length
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index e75fe6c222bf7dd37cf91e61f438b4a9f0b0e839..d5c1e03b461eb4d0a209d66c71136807fc732100 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -19,7 +19,7 @@ module Ci
validates_presence_of :status, unless: :importing?
validate :valid_commit_sha, unless: :importing?
- after_save :keep_around_commits, unless: :importing?
+ after_create :keep_around_commits, unless: :importing?
delegate :stages, to: :statuses
@@ -59,9 +59,6 @@ module Ci
before_transition any => [:success, :failed, :canceled] do |pipeline|
pipeline.finished_at = Time.now
- end
-
- before_transition do |pipeline|
pipeline.update_duration
end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index 9f64f76721d46463f29b3bb9b1b1a9109f83b378..ef3e73a4072d0cdc3ed0b77e16709d8d35a45055 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -5,6 +5,7 @@ module HasStatus
STARTED_STATUSES = %w[running success failed skipped]
ACTIVE_STATUSES = %w[pending running]
COMPLETED_STATUSES = %w[success failed canceled]
+ ORDERED_STATUSES = %w[failed pending running canceled success skipped]
class_methods do
def status_sql
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index c4b42ad82c7a043811db98fc535fa940a35251c8..17c3b526c97fc80e36da3a971eabbf1b0dd862c9 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -145,8 +145,14 @@ module Issuable
end
def order_labels_priority(excluded_labels: [])
- condition_field = "#{table_name}.id"
- highest_priority = highest_label_priority(name, condition_field, excluded_labels: excluded_labels).to_sql
+ params = {
+ target_type: name,
+ target_column: "#{table_name}.id",
+ project_column: "#{table_name}.#{project_foreign_key}",
+ excluded_labels: excluded_labels
+ }
+
+ highest_priority = highest_label_priority(params).to_sql
select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
group(arel_table[:id]).
@@ -230,18 +236,6 @@ module Issuable
labels.order('title ASC').pluck(:title)
end
- def remove_labels
- labels.delete_all
- end
-
- def add_labels_by_names(label_names)
- label_names.each do |label_name|
- label = project.labels.create_with(color: Label::DEFAULT_COLOR).
- find_or_create_by(title: label_name.strip)
- self.labels << label
- end
- end
-
# Convert this Issuable class name to a format usable by Ability definitions
#
# Examples:
diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb
index 5a7b36070e79e8fe43c3bcddb3ac12cd27efbe81..7fd0905ee818d4e41109b006864d9f0af6680c3f 100644
--- a/app/models/concerns/protected_branch_access.rb
+++ b/app/models/concerns/protected_branch_access.rb
@@ -1,6 +1,11 @@
module ProtectedBranchAccess
extend ActiveSupport::Concern
+ included do
+ scope :master, -> { where(access_level: Gitlab::Access::MASTER) }
+ scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) }
+ end
+
def humanize
self.class.human_access_levels[self.access_level]
end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 1ebecd86af9b4d961fa1d42e29ccc77d75d1a137..7edb0acd56c75d9f48187e3aa6ee2339b835c3a3 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -38,14 +38,21 @@ module Sortable
private
- def highest_label_priority(object_types, condition_field, excluded_labels: [])
- query = Label.select(Label.arel_table[:priority].minimum).
+ def highest_label_priority(target_type_column: nil, target_type: nil, target_column:, project_column:, excluded_labels: [])
+ query = Label.select(LabelPriority.arel_table[:priority].minimum).
+ left_join_priorities.
joins(:label_links).
- where(label_links: { target_type: object_types }).
- where("label_links.target_id = #{condition_field}").
+ where("label_priorities.project_id = #{project_column}").
+ where("label_links.target_id = #{target_column}").
reorder(nil)
- query.where.not(title: excluded_labels) if excluded_labels.present?
+ if target_type_column
+ query = query.where("label_links.target_type = #{target_type_column}")
+ else
+ query = query.where(label_links: { target_type: target_type })
+ end
+
+ query = query.where.not(title: excluded_labels) if excluded_labels.present?
query
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 3d9902d496e837741a4789a00379bf32c6828dcf..91d85c2279bf2da830caaf083315d7232a99ef7c 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base
delegate :name, to: :environment, prefix: true
- after_save :create_ref
+ after_create :create_ref
def commit
project.commit(sha)
@@ -34,7 +34,7 @@ class Deployment < ActiveRecord::Base
end
def manual_actions
- deployable.try(:other_actions)
+ @manual_actions ||= deployable.try(:other_actions)
end
def includes_commit?(commit)
@@ -84,6 +84,17 @@ class Deployment < ActiveRecord::Base
take
end
+ def stop_action
+ return nil unless on_stop.present?
+ return nil unless manual_actions
+
+ @stop_action ||= manual_actions.find_by(name: on_stop)
+ end
+
+ def stoppable?
+ stop_action.present?
+ end
+
def formatted_deployment_time
created_at.to_time.in_time_zone.to_s(:medium)
end
@@ -91,6 +102,6 @@ class Deployment < ActiveRecord::Base
private
def ref_path
- File.join(environment.ref_path, 'deployments', id.to_s)
+ File.join(environment.ref_path, 'deployments', iid.to_s)
end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index d970bc0a0052c9bc18f37b76d2b27730efc60db8..73f415c0ef07953c891cfe05abe303bca0aa7cdf 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -19,6 +19,24 @@ class Environment < ActiveRecord::Base
allow_nil: true,
addressable_url: true
+ delegate :stop_action, to: :last_deployment, allow_nil: true
+
+ scope :available, -> { with_state(:available) }
+ scope :stopped, -> { with_state(:stopped) }
+
+ state_machine :state, initial: :available do
+ event :start do
+ transition stopped: :available
+ end
+
+ event :stop do
+ transition available: :stopped
+ end
+
+ state :available
+ state :stopped
+ end
+
def last_deployment
deployments.last
end
@@ -53,8 +71,8 @@ class Environment < ActiveRecord::Base
return nil unless ref
- deployment_id = ref.split('/').last
- deployments.find(deployment_id)
+ deployment_iid = ref.split('/').last
+ deployments.find_by(iid: deployment_iid)
end
def ref_path
@@ -66,4 +84,14 @@ class Environment < ActiveRecord::Base
external_url.gsub(/\A.*?:\/\//, '')
end
+
+ def stoppable?
+ available? && stop_action.present?
+ end
+
+ def stop!(current_user)
+ return unless stoppable?
+
+ stop_action.play(current_user)
+ end
end
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index b7894c99846040345641c5bd16ccb52441b943c5..fd9a8c1b8b7fa84d79e7d6adc5b400636ba4b6b3 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -29,11 +29,6 @@ class ExternalIssue
@project
end
- # Pattern used to extract `JIRA-123` issue references from text
- def self.reference_pattern
- @reference_pattern ||= %r{(?\b([A-Z][A-Z0-9_]+-)\d+)}
- end
-
def to_reference(_from_project = nil)
id
end
diff --git a/app/models/group.rb b/app/models/group.rb
index a2f88cca82890321aef782bd9f334445947cd089..6865e61071813810b9c3c29d611e1c1105ddfe51 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -19,6 +19,7 @@ class Group < Namespace
has_many :project_group_links, dependent: :destroy
has_many :shared_projects, through: :project_group_links, source: :project
has_many :notification_settings, dependent: :destroy, as: :source
+ has_many :labels, class_name: 'GroupLabel'
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :visibility_level_allowed_by_projects
@@ -67,7 +68,7 @@ class Group < Namespace
end
def web_url
- Gitlab::Routing.url_helpers.group_url(self)
+ Gitlab::Routing.url_helpers.group_canonical_url(self)
end
def human_name
diff --git a/app/models/group_label.rb b/app/models/group_label.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a698b532d19a41f0dcb10fcec6236385ed352220
--- /dev/null
+++ b/app/models/group_label.rb
@@ -0,0 +1,11 @@
+class GroupLabel < Label
+ belongs_to :group
+
+ validates :group, presence: true
+
+ alias_attribute :subject, :group
+
+ def to_reference(source_project = nil, target_project = nil, format: :id)
+ super(source_project, target_project, format: format)
+ end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index abd58e0454adac3f40326cb62f638deccb737823..133a5993815134fcc92d9d8c6160f8d38c1cdfe7 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -138,6 +138,10 @@ class Issue < ActiveRecord::Base
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end
+ def self.project_foreign_key
+ 'project_id'
+ end
+
def self.sort(method, excluded_labels: [])
case method.to_s
when 'due_date_asc' then order_due_date_asc
@@ -274,4 +278,16 @@ class Issue < ActiveRecord::Base
def check_for_spam?
project.public?
end
+
+ def as_json(options = {})
+ super(options).tap do |json|
+ if options.has_key?(:labels)
+ json[:labels] = labels.as_json(
+ project: project,
+ only: [:id, :title, :description, :color],
+ methods: [:text_color]
+ )
+ end
+ end
+ end
end
diff --git a/app/models/label.rb b/app/models/label.rb
index e8e12e2904ee5c9775a188f8b148c5f4d5618574..149fd98ecb3b730dd7c700b3ae391c9340228790 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -15,34 +15,49 @@ class Label < ActiveRecord::Base
default_value_for :color, DEFAULT_COLOR
- belongs_to :project
-
has_many :lists, dependent: :destroy
+ has_many :priorities, class_name: 'LabelPriority'
has_many :label_links, dependent: :destroy
has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
validates :color, color: true, allow_blank: false
- validates :project, presence: true, unless: Proc.new { |service| service.template? }
# Don't allow ',' for label titles
- validates :title,
- presence: true,
- format: { with: /\A[^,]+\z/ },
- uniqueness: { scope: :project_id }
-
- before_save :nullify_priority
+ validates :title, presence: true, format: { with: /\A[^,]+\z/ }
+ validates :title, uniqueness: { scope: [:group_id, :project_id] }
default_scope { order(title: :asc) }
- scope :templates, -> { where(template: true) }
+ scope :templates, -> { where(template: true) }
+ scope :with_title, ->(title) { where(title: title) }
+
+ def self.prioritized(project)
+ joins(:priorities)
+ .where(label_priorities: { project_id: project })
+ .reorder('label_priorities.priority ASC, labels.title ASC')
+ end
+
+ def self.unprioritized(project)
+ labels = Label.arel_table
+ priorities = LabelPriority.arel_table
+
+ label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin).
+ on(labels[:id].eq(priorities[:label_id]).and(priorities[:project_id].eq(project.id))).
+ join_sources
- def self.prioritized
- where.not(priority: nil).reorder(:priority, :title)
+ joins(label_priorities).where(priorities[:priority].eq(nil))
end
- def self.unprioritized
- where(priority: nil)
+ def self.left_join_priorities
+ labels = Label.arel_table
+ priorities = LabelPriority.arel_table
+
+ label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin).
+ on(labels[:id].eq(priorities[:label_id])).
+ join_sources
+
+ joins(label_priorities)
end
alias_attribute :name, :title
@@ -77,6 +92,44 @@ class Label < ActiveRecord::Base
nil
end
+ def open_issues_count(user = nil, project = nil)
+ issues_count(user, project_id: project.try(:id) || project_id, state: 'opened')
+ end
+
+ def closed_issues_count(user = nil, project = nil)
+ issues_count(user, project_id: project.try(:id) || project_id, state: 'closed')
+ end
+
+ def open_merge_requests_count(user = nil, project = nil)
+ merge_requests_count(user, project_id: project.try(:id) || project_id, state: 'opened')
+ end
+
+ def prioritize!(project, value)
+ label_priority = priorities.find_or_initialize_by(project_id: project.id)
+ label_priority.priority = value
+ label_priority.save!
+ end
+
+ def unprioritize!(project)
+ priorities.where(project: project).delete_all
+ end
+
+ def priority(project)
+ priorities.find_by(project: project).try(:priority)
+ end
+
+ def template?
+ template
+ end
+
+ def text_color
+ LabelsHelper.text_color_for_bg(self.color)
+ end
+
+ def title=(value)
+ write_attribute(:title, sanitize_title(value)) if value.present?
+ end
+
##
# Returns the String necessary to reference this Label in Markdown
#
@@ -84,49 +137,47 @@ class Label < ActiveRecord::Base
#
# Examples:
#
- # Label.first.to_reference # => "~1"
- # Label.first.to_reference(format: :name) # => "~\"bug\""
- # Label.first.to_reference(project) # => "gitlab-org/gitlab-ce~1"
+ # Label.first.to_reference # => "~1"
+ # Label.first.to_reference(format: :name) # => "~\"bug\""
+ # Label.first.to_reference(project1, project2) # => "gitlab-org/gitlab-ce~1"
#
# Returns a String
#
- def to_reference(from_project = nil, format: :id)
+ def to_reference(source_project = nil, target_project = nil, format: :id)
format_reference = label_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
- if cross_project_reference?(from_project)
- project.to_reference + reference
+ if cross_project_reference?(source_project, target_project)
+ source_project.to_reference + reference
else
reference
end
end
- def open_issues_count(user = nil)
- issues.visible_to_user(user).opened.count
- end
-
- def closed_issues_count(user = nil)
- issues.visible_to_user(user).closed.count
+ def as_json(options = {})
+ super(options).tap do |json|
+ json[:priority] = priority(options[:project]) if options.has_key?(:project)
+ end
end
- def open_merge_requests_count
- merge_requests.opened.count
- end
+ private
- def template?
- template
+ def cross_project_reference?(source_project, target_project)
+ source_project && target_project && source_project != target_project
end
- def text_color
- LabelsHelper::text_color_for_bg(self.color)
+ def issues_count(user, params = {})
+ IssuesFinder.new(user, params.reverse_merge(label_name: title, scope: 'all'))
+ .execute
+ .count
end
- def title=(value)
- write_attribute(:title, sanitize_title(value)) if value.present?
+ def merge_requests_count(user, params = {})
+ MergeRequestsFinder.new(user, params.reverse_merge(label_name: title, scope: 'all'))
+ .execute
+ .count
end
- private
-
def label_format_reference(format = :id)
raise StandardError, 'Unknown format' unless [:id, :name].include?(format)
@@ -137,10 +188,6 @@ class Label < ActiveRecord::Base
end
end
- def nullify_priority
- self.priority = nil if priority.blank?
- end
-
def sanitize_title(value)
CGI.unescapeHTML(Sanitize.clean(value.to_s))
end
diff --git a/app/models/label_priority.rb b/app/models/label_priority.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5b85e0b6533644ea7d886b315f0e42a9278de975
--- /dev/null
+++ b/app/models/label_priority.rb
@@ -0,0 +1,8 @@
+class LabelPriority < ActiveRecord::Base
+ belongs_to :project
+ belongs_to :label
+
+ validates :project, :label, :priority, presence: true
+ validates :label_id, uniqueness: { scope: :project_id }
+ validates :priority, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+end
diff --git a/app/models/list.rb b/app/models/list.rb
index eb87decdbc803705671408861990277a76e0355d..065d75bd1dc6bbd3aace6d3cc34ccbc60ac6e5b1 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -26,6 +26,17 @@ class List < ActiveRecord::Base
label? ? label.name : list_type.humanize
end
+ def as_json(options = {})
+ super(options).tap do |json|
+ if options.has_key?(:label)
+ json[:label] = label.as_json(
+ project: board.project,
+ only: [:id, :title, :description, :color]
+ )
+ end
+ end
+ end
+
private
def can_be_destroyed
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 8c6905a442de644369cab82bc85961fd9977200e..c476a3bb14ed1d22fdf1283708a605168086c34f 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -137,6 +137,10 @@ class MergeRequest < ActiveRecord::Base
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end
+ def self.project_foreign_key
+ 'target_project_id'
+ end
+
# Returns all the merge requests from an ActiveRecord:Relation.
#
# This method uses a UNION as it usually operates on the result of
@@ -322,21 +326,17 @@ class MergeRequest < ActiveRecord::Base
def validate_fork
return true unless target_project && source_project
return true if target_project == source_project
- return true unless forked_source_project_missing?
+ return true unless source_project_missing?
errors.add :validate_fork,
'Source project is not a fork of the target project'
end
def closed_without_fork?
- closed? && forked_source_project_missing?
- end
-
- def closed_without_source_project?
- closed? && !source_project
+ closed? && source_project_missing?
end
- def forked_source_project_missing?
+ def source_project_missing?
return false unless for_fork?
return true unless source_project
@@ -344,9 +344,7 @@ class MergeRequest < ActiveRecord::Base
end
def reopenable?
- return false if closed_without_fork? || closed_without_source_project? || merged?
-
- closed?
+ closed? && !source_project_missing? && source_branch_exists?
end
def ensure_merge_request_diff
@@ -658,7 +656,7 @@ class MergeRequest < ActiveRecord::Base
end
def has_ci?
- source_project.ci_service && commits.any?
+ source_project.try(:ci_service) && commits.any?
end
def branch_missing?
@@ -690,12 +688,9 @@ class MergeRequest < ActiveRecord::Base
@environments ||=
begin
- 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
+ envs = target_project.environments_for(target_branch, diff_head_commit, with_tags: true)
+ envs.concat(source_project.environments_for(source_branch, diff_head_commit)) if source_project
+ envs.uniq
end
end
@@ -787,21 +782,21 @@ class MergeRequest < ActiveRecord::Base
def all_pipelines
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
+ @all_pipelines ||= source_project.pipelines
+ .where(sha: all_commits_sha, ref: source_branch)
+ .order(id: :desc)
end
# Note that this could also return SHA from now dangling commits
+ #
def all_commits_sha
- merge_request_diffs.flat_map(&:commits_sha).uniq
+ if persisted?
+ merge_request_diffs.flat_map(&:commits_sha).uniq
+ elsif compare_commits
+ compare_commits.to_a.reverse.map(&:id)
+ else
+ [diff_head_sha]
+ end
end
def merge_commit
diff --git a/app/models/project.rb b/app/models/project.rb
index db3088677d8d2d9f6d5ca21bfb73a230026d5287..a5c1d5c9e31185dc18b90001271d7db033d5dc20 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -32,8 +32,8 @@ class Project < ActiveRecord::Base
default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
after_create :ensure_dir_exist
+ after_create :create_project_feature, unless: :project_feature
after_save :ensure_dir_exist, if: :namespace_id_changed?
- after_initialize :setup_project_feature
# set last_activity_at to the same as created_at
after_create :set_last_activity_at
@@ -76,6 +76,7 @@ class Project < ActiveRecord::Base
has_one :drone_ci_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy
has_one :builds_email_service, dependent: :destroy
+ has_one :pipelines_email_service, dependent: :destroy
has_one :irker_service, dependent: :destroy
has_one :pivotaltracker_service, dependent: :destroy
has_one :hipchat_service, dependent: :destroy
@@ -106,7 +107,7 @@ class Project < ActiveRecord::Base
# Merge requests from source project should be kept when source project was removed
has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest
has_many :issues, dependent: :destroy
- has_many :labels, dependent: :destroy
+ has_many :labels, dependent: :destroy, class_name: 'ProjectLabel'
has_many :services, dependent: :destroy
has_many :events, dependent: :destroy
has_many :milestones, dependent: :destroy
@@ -387,6 +388,10 @@ class Project < ActiveRecord::Base
Project.count
end
end
+
+ def group_ids
+ joins(:namespace).where(namespaces: { type: 'Group' }).pluck(:namespace_id)
+ end
end
def lfs_enabled?
@@ -663,6 +668,10 @@ class Project < ActiveRecord::Base
end
end
+ def issue_reference_pattern
+ issues_tracker.reference_pattern
+ end
+
def default_issues_tracker?
!external_issue_tracker
end
@@ -718,7 +727,7 @@ class Project < ActiveRecord::Base
if template.nil?
# If no template, we should create an instance. Ex `create_gitlab_ci_service`
- self.send :"create_#{service_name}_service"
+ public_send("create_#{service_name}_service")
else
Service.create_from_template(self.id, template)
end
@@ -728,10 +737,8 @@ class Project < ActiveRecord::Base
def create_labels
Label.templates.each do |label|
- label = label.dup
- label.template = nil
- label.project_id = self.id
- label.save
+ params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
+ Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true)
end
end
@@ -1292,7 +1299,7 @@ class Project < ActiveRecord::Base
environment_ids.where(ref: ref)
end
- environments.where(id: environment_ids).select do |environment|
+ environments.available.where(id: environment_ids).select do |environment|
environment.includes_commit?(commit)
end
end
@@ -1303,11 +1310,6 @@ class Project < ActiveRecord::Base
"projects/#{id}/pushes_since_gc"
end
- # Prevents the creation of project_feature record for every project
- def setup_project_feature
- build_project_feature unless project_feature
- end
-
def default_branch_protected?
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
diff --git a/app/models/project_label.rb b/app/models/project_label.rb
new file mode 100644
index 0000000000000000000000000000000000000000..33c2b6177155755c6d4aacf64cca323869429980
--- /dev/null
+++ b/app/models/project_label.rb
@@ -0,0 +1,34 @@
+class ProjectLabel < Label
+ MAX_NUMBER_OF_PRIORITIES = 1
+
+ belongs_to :project
+
+ validates :project, presence: true
+
+ validate :permitted_numbers_of_priorities
+ validate :title_must_not_exist_at_group_level
+
+ delegate :group, to: :project, allow_nil: true
+
+ alias_attribute :subject, :project
+
+ def to_reference(target_project = nil, format: :id)
+ super(project, target_project, format: format)
+ end
+
+ private
+
+ def title_must_not_exist_at_group_level
+ return unless group.present? && title_changed?
+
+ if group.labels.with_title(self.title).exists?
+ errors.add(:title, :label_already_exists_at_group_level, group: group.name)
+ end
+ end
+
+ def permitted_numbers_of_priorities
+ if priorities && priorities.size > MAX_NUMBER_OF_PRIORITIES
+ errors.add(:priorities, 'Number of permitted priorities exceeded')
+ end
+ end
+end
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index fa66e5864b8b002efd92d17aa737acc96a947a48..201b94b065ba899d607c4d9ce98782c413e72e30 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -43,7 +43,7 @@ class BuildsEmailService < Service
end
def can_test?
- project.builds.count > 0
+ project.builds.any?
end
def disabled_title
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index d1df6d0292f84c0da475268a696d665d469a3f39..b26ddd518d779f82456e4a31fe1c0a920838114d 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -3,6 +3,12 @@ class IssueTrackerService < Service
default_value_for :category, 'issue_tracker'
+ # Pattern used to extract links from comments
+ # Override this method on services that uses different patterns
+ def reference_pattern
+ @reference_pattern ||= %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?\d+)}
+ end
+
def default?
default
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 97bcbacf2b942f025ba1a4d4df5a245157716729..f81b66fd21971080007043ea5eb2db82c5f9466f 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -13,6 +13,11 @@ class JiraService < IssueTrackerService
before_update :reset_password
+ # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
+ def reference_pattern
+ @reference_pattern ||= %r{(?\b([A-Z][A-Z0-9_]+-)\d+)}
+ end
+
def reset_password
# don't reset the password if a new one is provided
if api_url_changed? && !password_touched?
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ec3c1bc85ee3f092f073c1482e282923125cfa0c
--- /dev/null
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -0,0 +1,96 @@
+class PipelinesEmailService < Service
+ prop_accessor :recipients
+ boolean_accessor :add_pusher
+ boolean_accessor :notify_only_broken_pipelines
+ validates :recipients,
+ presence: true,
+ if: ->(s) { s.activated? && !s.add_pusher? }
+
+ def initialize_properties
+ self.properties ||= { notify_only_broken_pipelines: true }
+ end
+
+ def title
+ 'Pipelines emails'
+ end
+
+ def description
+ 'Email the pipelines status to a list of recipients.'
+ end
+
+ def to_param
+ 'pipelines_email'
+ end
+
+ def supported_events
+ %w[pipeline]
+ end
+
+ def execute(data, force: false)
+ return unless supported_events.include?(data[:object_kind])
+ return unless force || should_pipeline_be_notified?(data)
+
+ all_recipients = retrieve_recipients(data)
+
+ return unless all_recipients.any?
+
+ pipeline = Ci::Pipeline.find(data[:object_attributes][:id])
+ Ci::SendPipelineNotificationService.new(pipeline).execute(all_recipients)
+ end
+
+ def can_test?
+ project.pipelines.any?
+ end
+
+ def disabled_title
+ 'Please setup a pipeline on your repository.'
+ end
+
+ def test_data(project, user)
+ data = Gitlab::DataBuilder::Pipeline.build(project.pipelines.last)
+ data[:user] = user.hook_attrs
+ data
+ end
+
+ def fields
+ [
+ { type: 'textarea',
+ name: 'recipients',
+ placeholder: 'Emails separated by comma' },
+ { type: 'checkbox',
+ name: 'add_pusher',
+ label: 'Add pusher to recipients list' },
+ { type: 'checkbox',
+ name: 'notify_only_broken_pipelines' },
+ ]
+ end
+
+ def test(data)
+ result = execute(data, force: true)
+
+ { success: true, result: result }
+ rescue StandardError => error
+ { success: false, result: error }
+ end
+
+ def should_pipeline_be_notified?(data)
+ case data[:object_attributes][:status]
+ when 'success'
+ !notify_only_broken_pipelines?
+ when 'failed'
+ true
+ else
+ false
+ end
+ end
+
+ def retrieve_recipients(data)
+ all_recipients = recipients.to_s.split(',').reject(&:blank?)
+
+ if add_pusher? && data[:user].try(:[], :email)
+ all_recipients << data[:user][:email]
+ end
+
+ all_recipients
+ end
+end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 79d041d2775e8ab5c9f8f7d00a2c5b2667df3770..a6e911df9bd3d625996bf0534d9651b353d76ff5 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -125,14 +125,8 @@ class ProjectTeam
max_member_access(user.id) == Gitlab::Access::MASTER
end
- def member?(user, min_member_access = nil)
- member = !!find_member(user.id)
-
- if min_member_access
- member && max_member_access(user.id) >= min_member_access
- else
- member
- end
+ def member?(user, min_member_access = Gitlab::Access::GUEST)
+ max_member_access(user.id) >= min_member_access
end
def human_max_access(user_id)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 72e473871fab899450ee5f4200882000700dc19b..b6653d18530d1cf7dadf66453d2678a2c1a66288 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -416,6 +416,17 @@ class Repository
@exists = nil
end
+ # expire cache that doesn't depend on repository data (when expiring)
+ def expire_content_cache
+ expire_tags_cache
+ expire_tag_count_cache
+ expire_branches_cache
+ expire_branch_count_cache
+ expire_root_ref_cache
+ expire_emptiness_caches
+ expire_exists_cache
+ end
+
# Runs code after a repository has been created.
def after_create
expire_exists_cache
@@ -431,14 +442,7 @@ class Repository
expire_cache if exists?
- # expire cache that don't depend on repository data (when expiring)
- expire_tags_cache
- expire_tag_count_cache
- expire_branches_cache
- expire_branch_count_cache
- expire_root_ref_cache
- expire_emptiness_caches
- expire_exists_cache
+ expire_content_cache
repository_event(:remove_repository)
end
@@ -470,14 +474,13 @@ class Repository
end
def before_import
- expire_emptiness_caches
- expire_exists_cache
+ expire_content_cache
end
# Runs code after a repository has been forked/imported.
def after_import
- expire_emptiness_caches
- expire_exists_cache
+ expire_content_cache
+ build_cache
end
# Runs code after a new commit has been pushed.
diff --git a/app/models/service.rb b/app/models/service.rb
index 66c804f2b06c9d9809e3528f917f7395084803d3..625fbc483029c000890adb1dec5ce9f6fa8ddb00 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -196,12 +196,13 @@ class Service < ActiveRecord::Base
end
def self.available_services_names
- %w(
+ %w[
asana
assembla
bamboo
buildkite
builds_email
+ pipelines_email
bugzilla
campfire
custom_issue_tracker
@@ -218,7 +219,7 @@ class Service < ActiveRecord::Base
redmine
slack
teamcity
- )
+ ]
end
def self.create_from_template(project_id, template)
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 6ae9956ade5f61ea16aa2318a2dbd9614bcb2812..f5ade1cc293cb0175ec157e6d29826d60d606261 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -52,7 +52,13 @@ class Todo < ActiveRecord::Base
# Todos with highest priority first then oldest todos
# Need to order by created_at last because of differences on Mysql and Postgres when joining by type "Merge_request/Issue"
def order_by_labels_priority
- highest_priority = highest_label_priority(["Issue", "MergeRequest"], "todos.target_id").to_sql
+ params = {
+ target_type_column: "todos.target_type",
+ target_column: "todos.target_id",
+ project_column: "todos.project_id"
+ }
+
+ highest_priority = highest_label_priority(params).to_sql
select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')).
diff --git a/app/models/user.rb b/app/models/user.rb
index f367f4616fbdb0e10deaa87452e866ab7ba662ef..9181db40eb4a689d832bfc0631c027063cc627e4 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -309,7 +309,7 @@ class User < ActiveRecord::Base
username
end
- def to_reference(_from_project = nil)
+ def to_reference(_from_project = nil, _target_project = nil)
"#{self.class.reference_prefix}#{username}"
end
diff --git a/app/policies/group_label_policy.rb b/app/policies/group_label_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7b34aa182eb4edc6cf461354b42ae189e588b514
--- /dev/null
+++ b/app/policies/group_label_policy.rb
@@ -0,0 +1,5 @@
+class GroupLabelPolicy < BasePolicy
+ def rules
+ delegate! @subject.group
+ end
+end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 97ff62339683be16508b4fb8361da693167c7b03..b65fb68cd88b93ce6e5e293fe91595f6c3d88ade 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -19,6 +19,7 @@ class GroupPolicy < BasePolicy
if master
can! :create_projects
can! :admin_milestones
+ can! :admin_label
end
# Only group owner and administrators can admin group
diff --git a/app/policies/project_label_policy.rb b/app/policies/project_label_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b12b4c5166b3dd7066718a6de24e24202f4ea940
--- /dev/null
+++ b/app/policies/project_label_policy.rb
@@ -0,0 +1,5 @@
+class ProjectLabelPolicy < BasePolicy
+ def rules
+ delegate! @subject.project
+ end
+end
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
index abc7aeece39eafc276c0aa42a6b712d7f9b1a1bf..fe0d762ccd26d80ce58c56c2db502c6ed2e7f7ea 100644
--- a/app/services/boards/lists/create_service.rb
+++ b/app/services/boards/lists/create_service.rb
@@ -3,7 +3,7 @@ module Boards
class CreateService < BaseService
def execute(board)
List.transaction do
- label = project.labels.find(params[:label_id])
+ label = available_labels.find(params[:label_id])
position = next_position(board)
create_list(board, label, position)
@@ -12,6 +12,10 @@ module Boards
private
+ def available_labels
+ LabelsFinder.new(current_user, project_id: project.id).execute
+ end
+
def next_position(board)
max_position = board.lists.movable.maximum(:position)
max_position.nil? ? 0 : max_position.succ
diff --git a/app/services/boards/lists/generate_service.rb b/app/services/boards/lists/generate_service.rb
index d8048f1c67edb1d74a3ee78b4806d1e206f175ea..939f9bfd068cd5a56ed4141f4a53c482bd9c464e 100644
--- a/app/services/boards/lists/generate_service.rb
+++ b/app/services/boards/lists/generate_service.rb
@@ -19,8 +19,7 @@ module Boards
end
def find_or_create_label(params)
- project.labels.create_with(color: params[:color])
- .find_or_create_by(name: params[:name])
+ ::Labels::FindOrCreateService.new(current_user, project, params).execute
end
def label_params
diff --git a/app/services/ci/send_pipeline_notification_service.rb b/app/services/ci/send_pipeline_notification_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ceb182801f7f5a788acb0ac475172897578b0a62
--- /dev/null
+++ b/app/services/ci/send_pipeline_notification_service.rb
@@ -0,0 +1,19 @@
+module Ci
+ class SendPipelineNotificationService
+ attr_reader :pipeline
+
+ def initialize(new_pipeline)
+ @pipeline = new_pipeline
+ end
+
+ def execute(recipients)
+ email_template = "pipeline_#{pipeline.status}_email"
+
+ return unless Notify.respond_to?(email_template)
+
+ recipients.each do |to|
+ Notify.public_send(email_template, pipeline, to).deliver_later
+ end
+ end
+ end
+end
diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb
index ff9a8310a8c64ab83915ae9e8a6078fab5b3ca08..8ae15ad32f4a06dfb384b53f280d9b989a8f001c 100644
--- a/app/services/create_deployment_service.rb
+++ b/app/services/create_deployment_service.rb
@@ -6,7 +6,13 @@ class CreateDeploymentService < BaseService
ActiveRecord::Base.transaction do
@deployable = deployable
- @environment = prepare_environment
+
+ @environment = environment
+ @environment.external_url = expanded_url if expanded_url
+ @environment.fire_state_event(action)
+
+ return unless @environment.save
+ return if @environment.stopped?
deploy.tap do |deployment|
deployment.update_merge_request_metrics!
@@ -27,13 +33,12 @@ class CreateDeploymentService < BaseService
tag: params[:tag],
sha: params[:sha],
user: current_user,
- deployable: @deployable)
+ deployable: @deployable,
+ on_stop: options[:on_stop])
end
- def prepare_environment
- project.environments.find_or_create_by(name: expanded_name) do |environment|
- environment.external_url = expanded_url
- end
+ def environment
+ @environment ||= project.environments.find_or_create_by(name: expanded_name)
end
def expanded_name
@@ -61,4 +66,8 @@ class CreateDeploymentService < BaseService
def variables
params[:variables] || []
end
+
+ def action
+ options[:action] || 'start'
+ end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 57d521f2fead02ec54ce8a7d311f28fed3325aca..bb92cd80cc9bf5df48d7b24d6afb4142e6468f3e 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -80,17 +80,18 @@ class IssuableBaseService < BaseService
def filter_labels_in_param(key)
return if params[key].to_a.empty?
- params[key] = project.labels.where(id: params[key]).pluck(:id)
+ params[key] = available_labels.where(id: params[key]).pluck(:id)
end
def find_or_create_label_ids
labels = params.delete(:labels)
return unless labels
- params[:label_ids] = labels.split(",").map do |label_name|
- project.labels.create_with(color: Label::DEFAULT_COLOR)
- .find_or_create_by(title: label_name.strip)
- .id
+ params[:label_ids] = labels.split(',').map do |label_name|
+ service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip)
+ label = service.execute
+
+ label.id
end
end
@@ -111,6 +112,10 @@ class IssuableBaseService < BaseService
new_label_ids
end
+ def available_labels
+ LabelsFinder.new(current_user, project_id: @project.id).execute
+ end
+
def merge_slash_commands_into_params!(issuable)
description, command_params =
SlashCommands::InterpretService.new(project, current_user).
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index ab667456db772ba02de888860d307acaa918b264..a2a5f57d069db4907b1d34fb41b3f61b5106363a 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -52,8 +52,12 @@ module Issues
end
def cloneable_label_ids
- @new_project.labels
- .where(title: @old_issue.labels.pluck(:title)).pluck(:id)
+ params = {
+ project_id: @new_project.id,
+ title: @old_issue.labels.pluck(:title)
+ }
+
+ LabelsFinder.new(current_user, params).execute.pluck(:id)
end
def cloneable_milestone_id
diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d622f9edd3334da3b208b95f6fee942115fc524e
--- /dev/null
+++ b/app/services/labels/find_or_create_service.rb
@@ -0,0 +1,36 @@
+module Labels
+ class FindOrCreateService
+ def initialize(current_user, project, params = {})
+ @current_user = current_user
+ @project = project
+ @params = params.dup
+ end
+
+ def execute(skip_authorization: false)
+ @skip_authorization = skip_authorization
+ find_or_create_label
+ end
+
+ private
+
+ attr_reader :current_user, :project, :params, :skip_authorization
+
+ def available_labels
+ @available_labels ||= LabelsFinder.new(
+ current_user,
+ project_id: project.id
+ ).execute(skip_authorization: skip_authorization)
+ end
+
+ def find_or_create_label
+ new_label = available_labels.find_by(title: title)
+ new_label ||= project.labels.create(params)
+
+ new_label
+ end
+
+ def title
+ params[:title] || params[:name]
+ end
+ end
+end
diff --git a/app/services/labels/transfer_service.rb b/app/services/labels/transfer_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..514679ed29d02d6450a60a1194901e9ddab2cf88
--- /dev/null
+++ b/app/services/labels/transfer_service.rb
@@ -0,0 +1,78 @@
+# Labels::TransferService class
+#
+# User for recreate the missing group labels at project level
+#
+module Labels
+ class TransferService
+ def initialize(current_user, old_group, project)
+ @current_user = current_user
+ @old_group = old_group
+ @project = project
+ end
+
+ def execute
+ return unless old_group.present?
+
+ Label.transaction do
+ labels_to_transfer.find_each do |label|
+ new_label_id = find_or_create_label!(label)
+
+ next if new_label_id == label.id
+
+ update_label_links(group_labels_applied_to_issues, old_label_id: label.id, new_label_id: new_label_id)
+ update_label_links(group_labels_applied_to_merge_requests, old_label_id: label.id, new_label_id: new_label_id)
+ update_label_priorities(old_label_id: label.id, new_label_id: new_label_id)
+ end
+ end
+ end
+
+ private
+
+ attr_reader :current_user, :old_group, :project
+
+ def labels_to_transfer
+ label_ids = []
+ label_ids << group_labels_applied_to_issues.select(:id)
+ label_ids << group_labels_applied_to_merge_requests.select(:id)
+
+ union = Gitlab::SQL::Union.new(label_ids)
+
+ Label.where("labels.id IN (#{union.to_sql})").reorder(nil).uniq
+ end
+
+ def group_labels_applied_to_issues
+ Label.joins(:issues).
+ where(
+ issues: { project_id: project.id },
+ labels: { type: 'GroupLabel', group_id: old_group.id }
+ )
+ end
+
+ def group_labels_applied_to_merge_requests
+ Label.joins(:merge_requests).
+ where(
+ merge_requests: { target_project_id: project.id },
+ labels: { type: 'GroupLabel', group_id: old_group.id }
+ )
+ end
+
+ def find_or_create_label!(label)
+ params = label.attributes.slice('title', 'description', 'color')
+ new_label = FindOrCreateService.new(current_user, project, params).execute
+
+ new_label.id
+ end
+
+ def update_label_links(labels, old_label_id:, new_label_id:)
+ LabelLink.joins(:label).
+ merge(labels).
+ where(label_id: old_label_id).
+ update_all(label_id: new_label_id)
+ end
+
+ def update_label_priorities(old_label_id:, new_label_id:)
+ LabelPriority.where(project_id: project.id, label_id: old_label_id).
+ update_all(label_id: new_label_id)
+ end
+ end
+end
diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb
index 416aee2ab51d94e5a3575ff6b5934cb8ed8e5f0e..c13f289f61e1bd33de0ade3bb2b7da116fda05fc 100644
--- a/app/services/members/approve_access_request_service.rb
+++ b/app/services/members/approve_access_request_service.rb
@@ -4,17 +4,25 @@ module Members
attr_accessor :source
+ # source - The source object that respond to `#requesters` (i.g. project or group)
+ # current_user - The user that performs the access request approval
+ # params - A hash of parameters
+ # :user_id - User ID used to retrieve the access requester
+ # :id - Member ID used to retrieve the access requester
+ # :access_level - Optional access level set when the request is accepted
def initialize(source, current_user, params = {})
@source = source
@current_user = current_user
- @params = params
+ @params = params.slice(:user_id, :id, :access_level)
end
- def execute
+ # opts - A hash of options
+ # :force - Bypass permission check: current_user can be nil in that case
+ def execute(opts = {})
condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
access_requester = source.requesters.find_by!(condition)
- raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester)
+ raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester, opts)
access_requester.access_level = params[:access_level] if params[:access_level]
access_requester.accept_request
@@ -24,8 +32,11 @@ module Members
private
- def can_update_access_requester?(access_requester)
- access_requester && can?(current_user, action_member_permission(:update, access_requester), access_requester)
+ def can_update_access_requester?(access_requester, opts = {})
+ access_requester && (
+ opts[:force] ||
+ can?(current_user, action_member_permission(:update, access_requester), access_requester)
+ )
end
end
end
diff --git a/app/services/merge_requests/assign_issues_service.rb b/app/services/merge_requests/assign_issues_service.rb
index f636e5fec4f6ae9995e6560b1f1b0201629acea8..066efa1acc3dd7dae9b345dc384794424cbae448 100644
--- a/app/services/merge_requests/assign_issues_service.rb
+++ b/app/services/merge_requests/assign_issues_service.rb
@@ -4,7 +4,7 @@ module MergeRequests
@assignable_issues ||= begin
if current_user == merge_request.author
closes_issues.select do |issue|
- !issue.assignee_id? && can?(current_user, :admin_issue, issue)
+ !issue.is_a?(ExternalIssue) && !issue.assignee_id? && can?(current_user, :admin_issue, issue)
end
else
[]
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index b037780c431624f8ca9e0ed4e6abe90265bed696..ab9056a32508b54335b2d8632f0f242438d07a4b 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -11,14 +11,14 @@ module MergeRequests
def execute(merge_request)
@merge_request = merge_request
- return error('Merge request is not mergeable') unless @merge_request.mergeable?
+ return log_merge_error('Merge request is not mergeable', true) unless @merge_request.mergeable?
merge_request.in_locked_state do
if commit
after_merge
success
else
- error('Can not merge changes')
+ log_merge_error('Can not merge changes', true)
end
end
end
@@ -46,8 +46,8 @@ module MergeRequests
merge_request.update(merge_error: e.message)
false
rescue StandardError => e
- merge_request.update(merge_error: "Something went wrong during merge")
- Rails.logger.error(e.message)
+ merge_request.update(merge_error: "Something went wrong during merge: #{e.message}")
+ log_merge_error(e.message)
false
ensure
merge_request.update(in_progress_merge_commit_sha: nil)
@@ -65,5 +65,17 @@ module MergeRequests
def branch_deletion_user
@merge_request.force_remove_source_branch? ? @merge_request.author : current_user
end
+
+ def log_merge_error(message, http_error = false)
+ Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{message}")
+
+ error(message) if http_error
+ end
+
+ def merge_request_info
+ project = merge_request.project
+
+ "#{project.to_reference}#{merge_request.to_reference}"
+ end
end
end
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index f578f8dbea23b3d3de2c67aed2179c69cb047d64..015f282892117513f8999f2c0e27de40d8a46483 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -13,7 +13,7 @@ module Projects
end
def labels
- @project.labels.select([:title, :color])
+ LabelsFinder.new(current_user, project_id: project.id).execute.select([:title, :color])
end
def commands(noteable, type)
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index e466ffa60eb6241b0fa7f9aacb451366d9aeca4d..d7221fe993c145059191eabe78eb43b1d18b8087 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -29,7 +29,7 @@ module Projects
if unknown_url?
# In this case, we only want to import issues, not a repository.
create_repository
- else
+ elsif !project.repository_exists?
import_repository
end
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index bc7f8bf433b6ed5f77bfdc8310fb0d0fa92c9b50..28470f59807cb48c578d77e354b22aadbfe6aa86 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -28,6 +28,7 @@ module Projects
Project.transaction do
old_path = project.path_with_namespace
old_namespace = project.namespace
+ old_group = project.group
new_path = File.join(new_namespace.try(:path) || '', project.path)
if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present?
@@ -57,6 +58,9 @@ module Projects
# Move wiki repo also if present
gitlab_shell.mv_repository(project.repository_storage_path, "#{old_path}.wiki", "#{new_path}.wiki")
+ # Move missing group labels to project
+ Labels::TransferService.new(current_user, old_group, project).execute
+
# clear project cached events
project.reset_events_cache
diff --git a/app/services/protected_branches/api_create_service.rb b/app/services/protected_branches/api_create_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f2040dfa03a86825c90e52d4d0b8867de0c63a76
--- /dev/null
+++ b/app/services/protected_branches/api_create_service.rb
@@ -0,0 +1,29 @@
+# The protected branches API still uses the `developers_can_push` and `developers_can_merge`
+# flags for backward compatibility, and so performs translation between that format and the
+# internal data model (separate access levels). The translation code is non-trivial, and so
+# lives in this service.
+module ProtectedBranches
+ class ApiCreateService < BaseService
+ def execute
+ push_access_level =
+ if params.delete(:developers_can_push)
+ Gitlab::Access::DEVELOPER
+ else
+ Gitlab::Access::MASTER
+ end
+
+ merge_access_level =
+ if params.delete(:developers_can_merge)
+ Gitlab::Access::DEVELOPER
+ else
+ Gitlab::Access::MASTER
+ end
+
+ @params.merge!(push_access_levels_attributes: [{ access_level: push_access_level }],
+ merge_access_levels_attributes: [{ access_level: merge_access_level }])
+
+ service = ProtectedBranches::CreateService.new(@project, @current_user, @params)
+ service.execute
+ end
+ end
+end
diff --git a/app/services/protected_branches/api_update_service.rb b/app/services/protected_branches/api_update_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..050cb3b738b6cc92993cd4ddafe0607391bff69a
--- /dev/null
+++ b/app/services/protected_branches/api_update_service.rb
@@ -0,0 +1,47 @@
+# The protected branches API still uses the `developers_can_push` and `developers_can_merge`
+# flags for backward compatibility, and so performs translation between that format and the
+# internal data model (separate access levels). The translation code is non-trivial, and so
+# lives in this service.
+module ProtectedBranches
+ class ApiUpdateService < BaseService
+ def execute(protected_branch)
+ @developers_can_push = params.delete(:developers_can_push)
+ @developers_can_merge = params.delete(:developers_can_merge)
+
+ @protected_branch = protected_branch
+
+ protected_branch.transaction do
+ delete_redundant_access_levels
+
+ case @developers_can_push
+ when true
+ params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }])
+ when false
+ params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }])
+ end
+
+ case @developers_can_merge
+ when true
+ params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }])
+ when false
+ params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }])
+ end
+
+ service = ProtectedBranches::UpdateService.new(@project, @current_user, @params)
+ service.execute(protected_branch)
+ end
+ end
+
+ private
+
+ def delete_redundant_access_levels
+ unless @developers_can_merge.nil?
+ @protected_branch.merge_access_levels.destroy_all
+ end
+
+ unless @developers_can_push.nil?
+ @protected_branch.push_access_levels.destroy_all
+ end
+ end
+ end
+end
diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb
index e4ae3dec8aa94eb34a8aedc3917374e63cf04341..5a81194a5f49d389c259fd3bf1bf4df794c1b969 100644
--- a/app/services/slash_commands/interpret_service.rb
+++ b/app/services/slash_commands/interpret_service.rb
@@ -116,8 +116,10 @@ module SlashCommands
desc 'Add label(s)'
params '~label1 ~"label 2"'
condition do
+ available_labels = LabelsFinder.new(current_user, project_id: project.id).execute
+
current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
- project.labels.any?
+ available_labels.any?
end
command :label do |labels_param|
label_ids = find_label_ids(labels_param)
@@ -248,7 +250,7 @@ module SlashCommands
def find_label_ids(labels_param)
label_ids_by_reference = extract_references(labels_param, :label).map(&:id)
- labels_ids_by_name = @project.labels.where(name: labels_param.split).select(:id)
+ labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id)
label_ids_by_reference | labels_ids_by_name
end
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index b760b42fde061f86b1e2c9bdcfdaec83927c8c33..37bb6a3b0e088eb7caeb1d2291a72abae93cd457 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -75,4 +75,4 @@
- @runners.each do |runner|
= render "admin/runners/runner", runner: runner
- = paginate @runners
+ = paginate @runners, theme: "gitlab"
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 10fea1996aa2337a8e87924c9794bab30c69cb55..73038164056f7be693c666305358da2b631f3f8b 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -67,7 +67,7 @@
= form_for [:admin, project.namespace.becomes(Namespace), project, project.runner_projects.new] do |f|
= f.hidden_field :runner_id, value: @runner.id
= f.submit 'Enable', class: 'btn btn-xs'
- = paginate @projects
+ = paginate @projects, theme: "gitlab"
.col-md-6
%h4 Recent builds served by this Runner
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index a96b579c59329c1c20b11ef9680e342799ed9208..525e7d99d7188a4e3c79cf96fbed2aefe0eb6c82 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -5,6 +5,8 @@
%div.form-group
= f.label :password
= f.password_field :password, class: "form-control bottom", required: true, title: "This field is required."
+ %div.submit-container.move-submit-down
+ = f.submit "Sign in", class: "btn btn-save"
- if devise_mapping.rememberable?
.remember-me.checkbox
%label{for: "user_remember_me"}
@@ -12,5 +14,3 @@
%span Remember me
.pull-right
= link_to "Forgot your password?", new_password_path(resource_name)
- %div.submit-container
- = f.submit "Sign in", class: "btn btn-save"
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index 0e865b807c1bb07567a1426024bc828a116a85de..fd77cdbee2e186a6e7e6916e6c98c2e56bc2c543 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -10,7 +10,7 @@
= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'edit_user show-gl-field-errors' }) do |f|
- resource_params = params[resource_name].presence || params
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
- .form-group
+ %div
= f.label 'Two-Factor Authentication code', name: :otp_attempt
= f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.'
%p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index a057f126c4548c4a7141036b10798fca1aa52d5b..1e957f0935f369b8df870aee0217a2ff75b53727 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -1,4 +1,4 @@
-%ul.new-session-tabs.nav-links.nav-tabs
+%ul.new-session-tabs.nav-links.nav-tabs{ class: ('custom-provider-tabs' if form_based_providers.any?) }
- if crowd_enabled?
%li.active
= link_to "Crowd", "#crowd", 'data-toggle' => 'tab'
diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml
index 79b1d447a929cfad0f5df344e56a24f9f165e7fd..05246303fb6ee17d10e604e695e94c3b8829a77f 100644
--- a/app/views/devise/shared/_tabs_normal.html.haml
+++ b/app/views/devise/shared/_tabs_normal.html.haml
@@ -1,5 +1,6 @@
%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist'}
%li.active{ role: 'presentation' }
%a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab'} Sign in
- %li{ role: 'presentation'}
- %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab'} Register
+ - if signin_enabled? && signup_enabled?
+ %li{ role: 'presentation'}
+ %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab'} Register
diff --git a/app/views/groups/labels/destroy.js.haml b/app/views/groups/labels/destroy.js.haml
new file mode 100644
index 0000000000000000000000000000000000000000..3dfbfc77c0d4c18f6c77fa2201fb3ce57e09ac21
--- /dev/null
+++ b/app/views/groups/labels/destroy.js.haml
@@ -0,0 +1,2 @@
+- if @group.labels.empty?
+ $('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
diff --git a/app/views/groups/labels/edit.html.haml b/app/views/groups/labels/edit.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..836981fc6fde122c2870805077854085432cf259
--- /dev/null
+++ b/app/views/groups/labels/edit.html.haml
@@ -0,0 +1,7 @@
+- page_title 'Edit', @label.name, 'Labels'
+
+%h3.page-title
+ Edit Label
+%hr
+
+= render 'shared/labels/form', url: group_label_path(@group, @label), back_path: @previous_labels_path
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..70783a634099e540157bb2a43ff969d7942ca19a
--- /dev/null
+++ b/app/views/groups/labels/index.html.haml
@@ -0,0 +1,20 @@
+- page_title 'Labels'
+
+.top-area.adjust
+ .nav-text
+ Labels can be applied to issues and merge requests. Group labels are available for any project within the group.
+
+ .nav-controls
+ - if can?(current_user, :admin_label, @group)
+ = link_to new_group_label_path(@group), class: "btn btn-new" do
+ New label
+
+.labels
+ .other-labels
+ - if @labels.present?
+ %ul.content-list.manage-labels-list.js-other-labels
+ = render partial: 'shared/label', collection: @labels, as: :label
+ = paginate @labels, theme: 'gitlab'
+ - else
+ .nothing-here-block
+ No labels created yet.
diff --git a/app/views/groups/labels/new.html.haml b/app/views/groups/labels/new.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..2be87460b1d9f55be352edcad39b21672474c121
--- /dev/null
+++ b/app/views/groups/labels/new.html.haml
@@ -0,0 +1,8 @@
+- page_title 'New Label'
+- header_title group_title(@group, 'Labels', group_labels_path(@group))
+
+%h3.page-title
+ New Label
+%hr
+
+= render 'shared/labels/form', url: group_labels_path, back_path: @previous_labels_path
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index 44e2653ca4affb4c37bba944f977b5d101a97c00..767dffb55891bb12b1ae8fdcb82bc1128abcf8b4 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -9,12 +9,12 @@
%p
Project will be imported as
%strong
- #{@namespace_name}/#{@path}
+ #{@namespace.name}/#{@path}
%p
To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here.
.form-group
- = hidden_field_tag :namespace_id, @namespace_id
+ = hidden_field_tag :namespace_id, @namespace.id
= hidden_field_tag :path, @path
= label_tag :file, class: 'control-label' do
%span GitLab project export
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index 27ac1760166fe75cbfb8e0e68196d4ef0f28fa09..f7edb47b6663d15d306f6d914a51fedf7573d58b 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -13,6 +13,10 @@
= link_to activity_group_path(@group), title: 'Activity' do
%span
Activity
+ = nav_link(controller: [:group, :labels]) do
+ = link_to group_labels_path(@group), title: 'Labels' do
+ %span
+ Labels
= nav_link(controller: [:group, :milestones]) do
= link_to group_milestones_path(@group), title: 'Milestones' do
%span
diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..ec02c2e2d9051899a1cc1265e67d1e3a57c4faf0
--- /dev/null
+++ b/app/views/notify/pipeline_failed_email.html.haml
@@ -0,0 +1,177 @@
+
+%html{lang: "en"}
+ %head
+ %meta{content: "text/html; charset=UTF-8", "http-equiv": "Content-Type"}/
+ %meta{content: "width=device-width, initial-scale=1", name: "viewport"}/
+ %meta{content: "IE=edge", "http-equiv": "X-UA-Compatible"}/
+ %title= message.subject
+ :css
+ /* CLIENT-SPECIFIC STYLES */
+ body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
+ table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
+ img { -ms-interpolation-mode: bicubic; }
+
+ /* iOS BLUE LINKS */
+ a[x-apple-data-detectors] {
+ color: inherit !important;
+ text-decoration: none !important;
+ font-size: inherit !important;
+ font-family: inherit !important;
+ font-weight: inherit !important;
+ line-height: inherit !important;
+ }
+
+ /* ANDROID MARGIN HACK */
+ body { margin:0 !important; }
+ div[style*="margin: 16px 0"] { margin:0 !important; }
+
+ @media only screen and (max-width: 639px) {
+ body, #body {
+ min-width: 320px !important;
+ }
+ table.wrapper {
+ width: 100% !important;
+ min-width: 320px !important;
+ }
+ table.wrapper > tbody > tr > td {
+ border-left: 0 !important;
+ border-right: 0 !important;
+ border-radius: 0 !important;
+ padding-left: 10px !important;
+ padding-right: 10px !important;
+ }
+ }
+ %body{style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"}
+ %table#body{border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;"}
+ %tbody
+ %tr.line
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;"}
+ %tr.header
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
+ %img{alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55"}/
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"}
+ %table.wrapper{border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"}
+ %table.content{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;"}
+ %tbody
+ %tr.alert
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;background-color:#d22f57;color:#ffffff;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;"}
+ %img{alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;"}
+ Your pipeline has failed.
+ %tr.spacer
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"}
+
+ %tr.section
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"}
+ %table.info{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"} Project
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;"}
+ - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
+ - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
+ %a.muted{href: namespace_url, style: "color:#333333;text-decoration:none;"}
+ = namespace_name
+ \/
+ %a.muted{href: project_url(@project), style: "color:#333333;text-decoration:none;"}
+ = @project.name
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Branch
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
+ %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ %a.muted{href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;"}
+ = @pipeline.ref
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Commit
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
+ %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ %a{href: commit_url(@pipeline), style: "color:#3084bb;text-decoration:none;"}
+ = @pipeline.short_sha
+ - if @merge_request
+ in
+ %a{href: merge_request_url(@merge_request), style: "color:#3084bb;text-decoration:none;"}
+ = @merge_request.to_reference
+ .commit{style: "color:#5c5c5c;font-weight:300;"}
+ = @pipeline.git_commit_message.truncate(50)
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Author
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ - commit = @pipeline.commit
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
+ %img.avatar{height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ - if commit.author
+ %a.muted{href: user_url(commit.author), style: "color:#333333;text-decoration:none;"}
+ = commit.author.name
+ - else
+ %span
+ = commit.author_name
+ %tr.spacer
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"}
+
+ - failed = @pipeline.statuses.latest.failed
+ %tr.pre-section
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;"}
+ Pipeline
+ %a{href: pipeline_url(@pipeline), style: "color:#3084bb;text-decoration:none;"}
+ = "\##{@pipeline.id}"
+ had
+ = failed.size
+ failed
+ = "#{'build'.pluralize(failed.size)}."
+ %tr.warning
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;"}
+ Logs may contain sensitive data. Please consider before forwarding this email.
+ %tr.section
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;"}
+ %table.builds{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;"}
+ %tbody
+ - failed.each do |build|
+ %tr.build-state
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;"}
+ %img{alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;"}
+ = build.stage
+ %td{align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"}
+ %a{href: pipeline_build_url(@pipeline, build), style: "color:#3084bb;text-decoration:none;"}
+ = build.name
+ %tr.build-log
+ %td{colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;"}
+ %pre{style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;"}
+ = build.trace_html(last_lines: 10).html_safe
+ %tr.footer
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
+ %img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/
+ %div
+ %a{href: profile_notifications_url, style: "color:#3084bb;text-decoration:none;"} Manage all notifications
+ ·
+ %a{href: help_url, style: "color:#3084bb;text-decoration:none;"} Help
+ %div
+ You're receiving this email because of your account on
+ = succeed "." do
+ %a{href: root_url, style: "color:#3084bb;text-decoration:none;"}= Gitlab.config.gitlab.host
diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb
new file mode 100644
index 0000000000000000000000000000000000000000..8f8084b58e1ce3cb56248fe462206a48ff195537
--- /dev/null
+++ b/app/views/notify/pipeline_failed_email.text.erb
@@ -0,0 +1,31 @@
+Your pipeline has failed.
+
+Project: <%= @project.name %> ( <%= project_url(@project) %> )
+Branch: <%= @pipeline.ref %> ( <%= commits_url(@pipeline) %> )
+<% if @merge_request -%>
+Merge Request: <%= @merge_request.to_reference %> ( <%= merge_request_url(@merge_request) %> )
+<% end -%>
+
+Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> )
+Commit Message: <%= @pipeline.git_commit_message.truncate(50) %>
+<% commit = @pipeline.commit -%>
+<% if commit.author -%>
+Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> )
+<% else -%>
+Commit Author: <%= commit.author_name %>
+<% end -%>
+
+<% failed = @pipeline.statuses.latest.failed -%>
+Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
+
+<% failed.each do |build| -%>
+Build #<%= build.id %> ( <%= pipeline_build_url(@pipeline, build) %> )
+Stage: <%= build.stage %>
+Name: <%= build.name %>
+Trace: <%= build.trace_with_state(last_lines: 10)[:text] %>
+
+<% end -%>
+
+You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
+Manage all notifications: <%= profile_notifications_url %>
+Help: <%= help_url %>
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..0fdf118c9bcf86bbcd48c5fe7fd6bc293a2872a6
--- /dev/null
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -0,0 +1,154 @@
+
+%html{lang: "en"}
+ %head
+ %meta{content: "text/html; charset=UTF-8", "http-equiv": "Content-Type"}/
+ %meta{content: "width=device-width, initial-scale=1", name: "viewport"}/
+ %meta{content: "IE=edge", "http-equiv": "X-UA-Compatible"}/
+ %title= message.subject
+ :css
+ /* CLIENT-SPECIFIC STYLES */
+ body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
+ table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
+ img { -ms-interpolation-mode: bicubic; }
+
+ /* iOS BLUE LINKS */
+ a[x-apple-data-detectors] {
+ color: inherit !important;
+ text-decoration: none !important;
+ font-size: inherit !important;
+ font-family: inherit !important;
+ font-weight: inherit !important;
+ line-height: inherit !important;
+ }
+
+ /* ANDROID MARGIN HACK */
+ body { margin:0 !important; }
+ div[style*="margin: 16px 0"] { margin:0 !important; }
+
+ @media only screen and (max-width: 639px) {
+ body, #body {
+ min-width: 320px !important;
+ }
+ table.wrapper {
+ width: 100% !important;
+ min-width: 320px !important;
+ }
+ table.wrapper > tbody > tr > td {
+ border-left: 0 !important;
+ border-right: 0 !important;
+ border-radius: 0 !important;
+ padding-left: 10px !important;
+ padding-right: 10px !important;
+ }
+ }
+ %body{style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"}
+ %table#body{border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;"}
+ %tbody
+ %tr.line
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;"}
+ %tr.header
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
+ %img{alt: "GitLab", height: "50", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo.gif'), width: "55"}/
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"}
+ %table.wrapper{border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"}
+ %table.content{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;"}
+ %tbody
+ %tr.success
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;"}
+ %img{alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;"}
+ Your pipeline has passed.
+ %tr.spacer
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"}
+
+ %tr.section
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;"}
+ %table.info{border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"} Project
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;"}
+ - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
+ - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
+ %a.muted{href: namespace_url, style: "color:#333333;text-decoration:none;"}
+ = namespace_name
+ \/
+ %a.muted{href: project_url(@project), style: "color:#333333;text-decoration:none;"}
+ = @project.name
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Branch
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
+ %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ %a.muted{href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;"}
+ = @pipeline.ref
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Commit
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
+ %img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ %a{href: commit_url(@pipeline), style: "color:#3084bb;text-decoration:none;"}
+ = @pipeline.short_sha
+ - if @merge_request
+ in
+ %a{href: merge_request_url(@merge_request), style: "color:#3084bb;text-decoration:none;"}
+ = @merge_request.to_reference
+ .commit{style: "color:#5c5c5c;font-weight:300;"}
+ = @pipeline.git_commit_message.truncate(50)
+ %tr
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;"} Author
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;"}
+ %table.img{border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;"}
+ %tbody
+ %tr
+ - commit = @pipeline.commit
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
+ %img.avatar{height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24"}/
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
+ - if commit.author
+ %a.muted{href: user_url(commit.author), style: "color:#333333;text-decoration:none;"}
+ = commit.author.name
+ - else
+ %span
+ = commit.author_name
+ %tr.spacer
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;"}
+
+ %tr.success-message
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;"}
+ - build_count = @pipeline.statuses.latest.size
+ - stage_count = @pipeline.stages.size
+ Pipeline
+ %a{href: pipeline_url(@pipeline), style: "color:#3084bb;text-decoration:none;"}
+ = "\##{@pipeline.id}"
+ successfully completed
+ = "#{build_count} #{'build'.pluralize(build_count)}"
+ in
+ = "#{stage_count} #{'stage'.pluralize(stage_count)}."
+ %tr.footer
+ %td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
+ %img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/
+ %div
+ %a{href: profile_notifications_url, style: "color:#3084bb;text-decoration:none;"} Manage all notifications
+ ·
+ %a{href: help_url, style: "color:#3084bb;text-decoration:none;"} Help
+ %div
+ You're receiving this email because of your account on
+ = succeed "." do
+ %a{href: root_url, style: "color:#3084bb;text-decoration:none;"}= Gitlab.config.gitlab.host
diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb
new file mode 100644
index 0000000000000000000000000000000000000000..ae22d474f2ca32fefb182670617f8ee8731233b1
--- /dev/null
+++ b/app/views/notify/pipeline_success_email.text.erb
@@ -0,0 +1,24 @@
+Your pipeline has passed.
+
+Project: <%= @project.name %> ( <%= project_url(@project) %> )
+Branch: <%= @pipeline.ref %> ( <%= commits_url(@pipeline) %> )
+<% if @merge_request -%>
+Merge Request: <%= @merge_request.to_reference %> ( <%= merge_request_url(@merge_request) %> )
+<% end -%>
+
+Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> )
+Commit Message: <%= @pipeline.git_commit_message.truncate(50) %>
+<% commit = @pipeline.commit -%>
+<% if commit.author -%>
+Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> )
+<% else -%>
+Commit Author: <%= commit.author_name %>
+<% end -%>
+
+<% build_count = @pipeline.statuses.latest.size -%>
+<% stage_count = @pipeline.stages.size -%>
+Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
+
+You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
+Manage all notifications: <%= profile_notifications_url %>
+Help: <%= help_url %>
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
index d8f16022407217ea793e71be35d549b371431f1f..c6d718a1cd1f250c145dac59a31fad50c2f4c3fb 100644
--- a/app/views/projects/boards/components/_card.html.haml
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -26,7 +26,7 @@
":title" => "label.description",
data: { container: 'body' } }
{{ label.title }}
- %a.has-tooltip{ ":href" => "'/' + issue.assignee.username",
+ %a.has-tooltip{ ":href" => "'#{root_path}' + issue.assignee.username",
":title" => "'Assigned to ' + issue.assignee.name",
"v-if" => "issue.assignee",
data: { container: 'body' } }
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index 966633f1f8958352345e286c6d684992cf9a48ff..b1053028279c95ef7b57feccfcd681f026703e7c 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -1,5 +1,4 @@
-- builds = @build.pipeline.builds.latest.to_a
-- statuses = ["failed", "pending", "running", "canceled", "success", "skipped"]
+- builds = @build.pipeline.builds.to_a
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar
.block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
@@ -124,9 +123,9 @@
%a.stage-item= stage
.builds-container
- - statuses.each do |build_status|
+ - HasStatus::ORDERED_STATUSES.each do |build_status|
- builds.select{|build| build.status == build_status}.each do |build|
- .build-job{class: ('active' if build == @build), data: {stage: build.stage}}
+ .build-job{class: sidebar_build_class(build, @build), data: {stage: build.stage}}
= link_to namespace_project_build_path(@project.namespace, @project, build) do
= icon('arrow-right')
= ci_icon_for_status(build.status)
@@ -135,11 +134,5 @@
= build.name
- else
= build.id
-
- - if @build.retried?
- %li.active
- %a
- Build ##{@build.id}
- ·
- %i.fa.fa-warning
- This build was retried.
+ - if build.retried?
+ %i.fa.fa-refresh.has-tooltip{data: { container: 'body', placement: 'bottom' }, title: 'Build was retried'}
diff --git a/app/views/projects/builds/_user.html.haml b/app/views/projects/builds/_user.html.haml
index 2642de8021df5c5f59ccea327d5923889d4a1984..83f299da651f82fb6460974526e572a6b3397c7a 100644
--- a/app/views/projects/builds/_user.html.haml
+++ b/app/views/projects/builds/_user.html.haml
@@ -1,4 +1,7 @@
by
%a{ href: user_path(@build.user) }
- = image_tag avatar_icon(@build.user, 24), class: "avatar s24"
- %strong= @build.user.to_reference
+ %span.hidden-xs
+ = image_tag avatar_icon(@build.user, 24), class: "avatar s24"
+ %strong{ data: { toggle: 'tooltip', placement: 'top', title: @build.user.to_reference } }
+ = @build.user.name
+ %strong.visible-xs-inline= @build.user.to_reference
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 9248adfde800cde134d25a60d2daee9aaf762387..94632056b154fb1524df200a9ac894c7ae6ddcf6 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -6,7 +6,7 @@
- coverage = local_assigns.fetch(:coverage, false)
- allow_retry = local_assigns.fetch(:allow_retry, false)
-%tr.build.commit
+%tr.build.commit{class: ('retried' if retried)}
%td.status
- if can?(current_user, :read_build, build)
= ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build))
@@ -27,7 +27,7 @@
= link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name"
- else
.light none
- .icon-container
+ .icon-container.commit-icon
= custom_icon("icon_commit")
- if commit_sha
@@ -35,8 +35,9 @@
- if build.stuck?
= icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.')
+
- if retried
- = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.')
+ = icon('refresh', class: 'text-warning has-tooltip', title: 'Build was retried')
.label-container
- if build.tags.any?
@@ -47,8 +48,6 @@
%span.label.label-info triggered
- if build.try(:allow_failure)
%span.label.label-danger allowed to fail
- - if retried
- %span.label.label-warning retried
- if build.manual?
%span.label.label-info manual
diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml
index 017d3ff6af2470c44396117641299153a695cbb2..55965172d3fcb34925f27809a9d07645f0f314be 100644
--- a/app/views/projects/ci/builds/_build_pipeline.html.haml
+++ b/app/views/projects/ci/builds/_build_pipeline.html.haml
@@ -1,10 +1,10 @@
- is_playable = subject.playable? && can?(current_user, :update_build, @project)
- if is_playable
- = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do
+ = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.pipeline-graph', placement: 'bottom' } do
= render_status_with_link('build', 'play')
.ci-status-text= subject.name
- elsif can?(current_user, :read_build, @project)
- = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do
+ = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do
%span.ci-status-icon
= render_status_with_link('build', subject.status)
.ci-status-text= subject.name
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index 36eadbd2bf1246bb367364eed037b5e938ac882d..d69235620809c7bacea755e38021c7864508f684 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -9,17 +9,15 @@
= ci_icon_for_status(status)
- else
= ci_status_with_icon(status)
- %td.branch-commit
+
+ %td
= link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
- %span ##{pipeline.id}
- - if pipeline.ref && show_branch
- .icon-container
- = pipeline.tag? ? icon('tag') : icon('code-fork')
- = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name"
- - if show_commit
- .icon-container
- = custom_icon("icon_commit")
- = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace"
+ %span.pipeline-id ##{pipeline.id}
+ %span by
+ - if pipeline.user
+ = user_avatar(user: pipeline.user, size: 20)
+ - else
+ %span.api.monospace API
- if pipeline.latest?
%span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest
- if pipeline.triggered?
@@ -29,23 +27,32 @@
- if pipeline.builds.any?(&:stuck?)
%span.label.label-warning stuck
+ %td.branch-commit
+ - if pipeline.ref && show_branch
+ .icon-container
+ = pipeline.tag? ? icon('tag') : icon('code-fork')
+ = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name"
+ - if show_commit
+ .icon-container.commit-icon
+ = custom_icon("icon_commit")
+ = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace"
+
%p.commit-title
- if commit = pipeline.commit
= author_avatar(commit, size: 20)
- = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message"
+ = link_to_gfm truncate(commit.title, length: 60, escape: false), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message"
- else
Cant find HEAD commit for this branch
-
- - stages_status = pipeline.statuses.relevant.latest.stages_status
- %td.stage-cell
- - stages.each do |stage|
- - status = stages_status[stage]
- - tooltip = "#{stage.titleize}: #{status || 'not found'}"
- - if status
- .stage-container
- = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do
- = ci_icon_for_status(status)
+ - stages_status = pipeline.statuses.relevant.latest.stages_status
+ %td.stage-cell
+ - stages.each do |stage|
+ - status = stages_status[stage]
+ - tooltip = "#{stage.titleize}: #{status || 'not found'}"
+ - if status
+ .stage-container
+ = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do
+ = ci_icon_for_status(status)
%td
- if pipeline.duration
diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml
index 5d0d5ba026289a15852d70cc6cb40b9009202bcc..f2d71fa698958b4458a412106fdced6a596a0ab6 100644
--- a/app/views/projects/commit/_pipeline_status_group.html.haml
+++ b/app/views/projects/commit/_pipeline_status_group.html.haml
@@ -1,5 +1,5 @@
- group_status = CommitStatus.where(id: subject).status
-%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
+%button.dropdown-menu-toggle.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } }
%span.ci-status-icon
= render_status_with_link('build', group_status)
%span.ci-status-text
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 640651e93f512681400a9a877f6710b8ebb73253..ac451441eeca1eb9b26f5092369e4ae69f4a5bf8 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -8,6 +8,7 @@
%tbody
%th Status
%th Pipeline
+ %th Commit
%th Stages
%th
%th
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index 76b68c544aa40ec91e59960e6cd6b0eb598c1e1d..7bde20c32868b2401c9d39eb1a4cdb6050c5f47e 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -10,7 +10,7 @@
= button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
.dropdown-toggle-text= params[:from] || 'Select branch/tag'
= render "ref_dropdown"
- .compare-ellipsis ...
+ .compare-ellipsis.inline ...
.form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
.input-group.inline-input-group
%span.input-group-addon to
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index 7f346df8797527fecb5b31760a8907866d693d4e..b647882efa09763627274223d7bb0d8e83512957 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -2,10 +2,10 @@
- page_title "Cycle Analytics"
= render "projects/pipelines/head"
-#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project)}}
+#cycle-analytics{class: container_class, "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()")
+ = icon('times', class: 'dismiss-icon', "@click" => "dismissLanding()")
.row
.col-sm-3.col-xs-12.svg-container
= custom_icon('icon_cycle_analytics_splash')
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
index 22c4a75d21372e8801750650ea6db182c518bb68..58a214bdbd15a74d55729e6a42c5bc0a13110b93 100644
--- a/app/views/projects/deployments/_actions.haml
+++ b/app/views/projects/deployments/_actions.haml
@@ -1,28 +1,15 @@
-- if can?(current_user, :create_deployment, deployment) && deployment.deployable
- .pull-right
-
- - external_url = deployment.environment.external_url
- - if external_url
- = link_to external_url, target: '_blank', class: 'btn external-url' do
- = icon('external-link')
-
- - actions = deployment.manual_actions
- - if actions.present?
- .inline
- .dropdown
- %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
- = custom_icon('icon_play')
- = icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- - actions.each do |action|
- %li
- = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
- = custom_icon('icon_play')
- %span= action.name.humanize
+- if can?(current_user, :create_deployment, deployment)
+ - actions = deployment.manual_actions
+ - if actions.present?
+ .inline
+ .dropdown
+ %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
+ = custom_icon('icon_play')
+ = icon('caret-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
+ - actions.each do |action|
+ %li
+ = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
+ = custom_icon('icon_play')
+ %span= action.name.humanize
- - if local_assigns.fetch(:allow_rollback, false)
- = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do
- - if deployment.last?
- Re-deploy
- - else
- Rollback
diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml
index 28813babd7be6b54c0245e5cea545cd2fff47073..ff250eeca50711175b0009e3683423fcc6e70cac 100644
--- a/app/views/projects/deployments/_commit.html.haml
+++ b/app/views/projects/deployments/_commit.html.haml
@@ -3,7 +3,7 @@
.icon-container
= deployment.tag? ? icon('tag') : icon('code-fork')
= link_to deployment.ref, namespace_project_commits_path(@project.namespace, @project, deployment.ref), class: "monospace branch-name"
- .icon-container
+ .icon-container.commit-icon
= custom_icon("icon_commit")
= link_to deployment.short_sha, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-id monospace"
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index ca0005abd0ced81298519a85cc317e1163c8d288..9238f232c7eab3d03a3bf395611a23f60b7374d7 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -17,4 +17,6 @@
#{time_ago_with_tooltip(deployment.created_at)}
%td.hidden-xs
- = render 'projects/deployments/actions', deployment: deployment, allow_rollback: true
+ .pull-right
+ = render 'projects/deployments/actions', deployment: deployment
+ = render 'projects/deployments/rollback', deployment: deployment
diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml
new file mode 100644
index 0000000000000000000000000000000000000000..5941e01c6f1edac7f5cda0fb90d2fb33187e7f69
--- /dev/null
+++ b/app/views/projects/deployments/_rollback.haml
@@ -0,0 +1,6 @@
+- if can?(current_user, :create_deployment, deployment) && deployment.deployable
+ = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do
+ - if deployment.last?
+ Re-deploy
+ - else
+ Rollback
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 28aad3f472554467ef8696d8dd31aaa6cb27d054..78aa9fb73919f23973aad956b43d2d6881b26473 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -1,5 +1,5 @@
/ Side-by-side diff view
-%div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight{ data: diff_view_data }
+%div.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data }
%table
- last_line = 0
- diff_file.parallel_diff_lines.each do |line|
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index fb776e3a3e706b42f3e97690e31f6db47261cb89..c36a3f007287a12937c738f52f66701359c00ae0 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -101,7 +101,7 @@
Git Large File Storage
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
- - if Gitlab.config.lfs.enabled && current_user.admin?
+ - if Gitlab.config.registry.enabled
.form-group
.checkbox
= f.label :container_registry_enabled do
diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml
index 251694e897cdceb7ffb9ddf2004bb93f1e2d8dd3..b75d5df4150995dc49a9ad0d5cf77b1584236f11 100644
--- a/app/views/projects/environments/_environment.html.haml
+++ b/app/views/projects/environments/_environment.html.haml
@@ -28,4 +28,8 @@
#{time_ago_with_tooltip(last_deployment.created_at)}
%td.hidden-xs
- = render 'projects/deployments/actions', deployment: last_deployment
+ .pull-right
+ = render 'projects/environments/external_url', environment: environment
+ = render 'projects/deployments/actions', deployment: last_deployment
+ = render 'projects/environments/stop', environment: environment
+ = render 'projects/deployments/rollback', deployment: last_deployment
diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..4c8fe1c271b6d6cf7dec025673647d10cda0b46c
--- /dev/null
+++ b/app/views/projects/environments/_external_url.html.haml
@@ -0,0 +1,3 @@
+- if environment.external_url && can?(current_user, :read_environment, environment)
+ = link_to environment.external_url, target: '_blank', class: 'btn external-url' do
+ = icon('external-link')
diff --git a/app/views/projects/environments/_stop.html.haml b/app/views/projects/environments/_stop.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..69848123c170e950bdb5bb7e48f45b70df913688
--- /dev/null
+++ b/app/views/projects/environments/_stop.html.haml
@@ -0,0 +1,5 @@
+- if can?(current_user, :create_deployment, environment) && environment.stoppable?
+ .inline
+ = link_to stop_namespace_project_environment_path(@project.namespace, @project, environment), method: :post,
+ class: 'btn stop-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to stop this environment?' } do
+ = icon('stop', class: 'stop-env-icon')
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 721ba156334e578aab01b47d022fe130ffece50a..8f555afcf11d975bb4fd978493e86a4ffc705a98 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -3,14 +3,27 @@
= render "projects/pipelines/head"
%div{ class: container_class }
- - if can?(current_user, :create_environment, @project) && !@environments.blank?
- .top-area
+ .top-area
+ %ul.nav-links
+ %li{class: ('active' if @scope.nil?)}
+ = link_to project_environments_path(@project) do
+ Available
+ %span.badge.js-available-environments-count
+ = number_with_delimiter(@all_environments.available.count)
+
+ %li{class: ('active' if @scope == 'stopped')}
+ = link_to project_environments_path(@project, scope: :stopped) do
+ Stopped
+ %span.badge.js-stopped-environments-count
+ = number_with_delimiter(@all_environments.stopped.count)
+
+ - if can?(current_user, :create_environment, @project) && !@all_environments.blank?
.nav-controls
= link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
New environment
.environments-container
- - if @environments.blank?
+ - if @all_environments.blank?
.blank-state.blank-state-no-icon
%h2.blank-state-title
You don't have any environments right now.
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 90c59223a35728dcdf561ea05d3bf27016771339..bcac73d3698985b012c64f4c97da3839a76824eb 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -3,14 +3,16 @@
= render "projects/pipelines/head"
%div{ class: container_class }
- .top-area
+ .top-area.adjust
.col-md-9
%h3.page-title= @environment.name.capitalize
.col-md-3
.nav-controls
+ = render 'projects/environments/external_url', environment: @environment
- if can?(current_user, :update_environment, @environment)
= link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn'
- = link_to 'Destroy', namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to delete this environment?' }, class: 'btn btn-danger', method: :delete
+ - if can?(current_user, :create_deployment, @environment) && @environment.stoppable?
+ = link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post
.deployments-container
- if @deployments.blank?
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
index 0a66d60accc76d24387867885ad40eb146753f00..c45b73e4225e1618ed17a86c2dec93a66e2c7fd0 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
@@ -1,9 +1,10 @@
-- if subject.target_url
- = link_to subject.target_url do
+%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } }
+ - if subject.target_url
+ = link_to subject.target_url do
+ %span.ci-status-icon
+ = render_status_with_link('commit status', subject.status)
+ %span.ci-status-text= subject.name
+ - else
%span.ci-status-icon
= render_status_with_link('commit status', subject.status)
%span.ci-status-text= subject.name
-- else
- %span.ci-status-icon
- = render_status_with_link('commit status', subject.status)
- %span.ci-status-text= subject.name
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 8b1a8a8a2d948da9e47dc0cf6e9989da011322c7..c80210d6ff46b47fa36a02b310557f5211aa278c 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -50,7 +50,7 @@
- if issue.labels.any?
- issue.labels.each do |label|
- = link_to_label(label, project: issue.project)
+ = link_to_label(label, subject: issue.project)
- if issue.tasks?
%span.task-status
diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml
index 3a6fbbc7fbc5ad5255b89c1a2393995703f328eb..1b7d878c38c0db14eb88a6146d7f048798a3d426 100644
--- a/app/views/projects/issues/edit.html.haml
+++ b/app/views/projects/issues/edit.html.haml
@@ -1,4 +1,4 @@
-- page_title "Edit", "#{@issue.to_reference} #{@issue.title}", "Issues"
+- page_title "Edit", "#{@issue.title} (#{@issue.to_reference})", "Issues"
%h3.page-title
Edit Issue ##{@issue.iid}
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 09347ad5fffc6098c1a4c5602e74831b9857419e..bd629b5c5197e1fd7e4c404fe3ce128f166c5560 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,4 +1,4 @@
-- page_title "#{@issue.to_reference} #{@issue.title}", "Issues"
+- page_title "#{@issue.title} (#{@issue.to_reference})", "Issues"
- page_description @issue.description
- page_card_attributes @issue.card_attributes
@@ -53,7 +53,7 @@
.issue-details.issuable-details
- .detail-page-description.content-block
+ .detail-page-description.content-block{ class: ('hide-bottom-border' unless @issue.description.present? ) }
%h2.title
= markdown_field(@issue, :title)
- if @issue.description.present?
diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml
deleted file mode 100644
index 71f7f354d72dc42fbc7b8787618fd2a4930c0226..0000000000000000000000000000000000000000
--- a/app/views/projects/labels/_label.html.haml
+++ /dev/null
@@ -1,50 +0,0 @@
-- label_css_id = dom_id(label)
-%li{id: label_css_id, data: { id: label.id } }
- = render "shared/label_row", label: label
-
- .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown
- %button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } }
- Options
- = icon('caret-down')
- .dropdown-menu.dropdown-menu-align-right
- %ul
- %li
- = link_to_label(label, type: :merge_request) do
- = pluralize label.open_merge_requests_count, 'merge request'
- %li
- = link_to_label(label) do
- = pluralize label.open_issues_count(current_user), 'open issue'
- - if current_user
- %li.label-subscription{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
- %a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } }
- %span= label_subscription_toggle_button_text(label)
- - if can? current_user, :admin_label, @project
- %li
- = link_to "Edit", edit_namespace_project_label_path(@project.namespace, @project, label)
- %li
- = link_to "Delete", namespace_project_label_path(@project.namespace, @project, label), title: "Delete", method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"}
-
- .pull-right.hidden-xs.hidden-sm.hidden-md
- = link_to_label(label, type: :merge_request, css_class: 'btn btn-transparent btn-action') do
- = pluralize label.open_merge_requests_count, 'merge request'
- = link_to_label(label, css_class: 'btn btn-transparent btn-action') do
- = pluralize label.open_issues_count(current_user), 'open issue'
-
- - if current_user
- .label-subscription.inline{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
- %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } }
- %span.sr-only= label_subscription_toggle_button_text(label)
- = icon('eye', class: 'label-subscribe-button-icon')
- = icon('spinner spin', class: 'label-subscribe-button-loading')
-
- - if can? current_user, :admin_label, @project
- = link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
- %span.sr-only Edit
- = icon('pencil-square-o')
- = link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?", toggle: "tooltip"} do
- %span.sr-only Delete
- = icon('trash-o')
-
- - if current_user
- :javascript
- new Subscription('##{dom_id(label)} .label-subscription');
diff --git a/app/views/projects/labels/destroy.js.haml b/app/views/projects/labels/destroy.js.haml
index d59563b122aefb78357d98ec2cb153f0ef91b5ca..8d09e2bda114a2780f6a6c7f50a336bd2afbfdb6 100644
--- a/app/views/projects/labels/destroy.js.haml
+++ b/app/views/projects/labels/destroy.js.haml
@@ -1,2 +1,2 @@
-- if @project.labels.size == 0
+- if @labels.empty?
$('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml
index 52b187e7e58e866777a863da86894a3a6e0507be..a80a07b52e61d0dcec5c7af5632ecfa4c64d780e 100644
--- a/app/views/projects/labels/edit.html.haml
+++ b/app/views/projects/labels/edit.html.haml
@@ -6,4 +6,4 @@
%h3.page-title
Edit Label
%hr
- = render 'form'
+ = render 'shared/labels/form', url: namespace_project_label_path(@project.namespace.becomes(Namespace), @project, @label), back_path: namespace_project_labels_path(@project.namespace, @project)
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index db66a0edbd8f399469c2464e40cfd375a7c3c7ee..f135bf6f6b45636066383331b9e1cd01563870db 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -16,21 +16,22 @@
.labels
- if can?(current_user, :admin_label, @project)
-# Only show it in the first page
- - hide = @project.labels.empty? || (params[:page].present? && params[:page] != '1')
+ - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
.prioritized-labels{ class: ('hide' if hide) }
%h5 Prioritized Labels
%ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) }
%p.empty-message{ class: ('hidden' unless @prioritized_labels.empty?) } No prioritized labels yet
- if @prioritized_labels.present?
- = render @prioritized_labels
+ = render partial: 'shared/label', collection: @prioritized_labels, as: :label
+
.other-labels
- if can?(current_user, :admin_label, @project)
%h5{ class: ('hide' if hide) } Other Labels
- - if @labels.present?
- %ul.content-list.manage-labels-list.js-other-labels
- = render @labels
+ %ul.content-list.manage-labels-list.js-other-labels
+ - if @labels.present?
+ = render partial: 'shared/label', collection: @labels, as: :label
= paginate @labels, theme: 'gitlab'
- - else
+ - if @labels.blank?
.nothing-here-block
- if can?(current_user, :admin_label, @project)
Create a label or #{link_to 'generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post}.
diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml
index a1bb66cfb6c15e24444b7f754a9f17ed2f9b1c49..f0d9be744d1438952059a0d00198dbe604f335eb 100644
--- a/app/views/projects/labels/new.html.haml
+++ b/app/views/projects/labels/new.html.haml
@@ -6,4 +6,4 @@
%h3.page-title
New Label
%hr
- = render 'form'
+ = render 'shared/labels/form', url: namespace_project_labels_path(@project.namespace.becomes(Namespace), @project), back_path: namespace_project_labels_path(@project.namespace, @project)
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 68fb7d5a4145af5c7ec4e44da56ddc3be8b9fdae..12408068834deb62622037caa48fa91dbaacb2db 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -62,7 +62,7 @@
- if merge_request.labels.any?
- merge_request.labels.each do |label|
- = link_to_label(label, project: merge_request.project, type: 'merge_request')
+ = link_to_label(label, subject: merge_request.project, type: :merge_request)
- if merge_request.tasks?
%span.task-status
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index da6927879a4300717f37582be9eb237041f3be08..9c6f562f7db2f65a70503251bd97dd10cbe5be38 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -29,7 +29,11 @@
= link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
Commits
%span.badge= @commits.size
- - if @pipeline
+ - if @pipelines.any?
+ %li.builds-tab
+ = link_to url_for(params), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
+ Pipelines
+ %span.badge= @pipelines.size
%li.builds-tab
= link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do
Builds
@@ -44,9 +48,11 @@
= render "projects/merge_requests/show/commits"
#diffs.diffs.tab-pane
- # This tab is always loaded via AJAX
- - if @pipeline
+ - if @pipelines.any?
#builds.builds.tab-pane
= render "projects/merge_requests/show/builds"
+ #pipelines.pipelines.tab-pane
+ = render "projects/merge_requests/show/pipelines"
.mr-loading-status
= spinner
@@ -59,5 +65,5 @@
:javascript
var merge_request = new MergeRequest({
action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}",
- buildsLoaded: "#{@pipeline ? 'true' : 'false'}"
+ buildsLoaded: "#{@pipelines.any? ? 'true' : 'false'}"
});
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 662463bc72bcbb388933b3e381a1c80b0f9b9c1e..f57abe73977105be9ac6d8fe0eec6dd4b16f4aa3 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,4 +1,4 @@
-- page_title "#{@merge_request.to_reference} #{@merge_request.title}", "Merge Requests"
+- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
- content_for :page_specific_javascripts do
@@ -26,19 +26,19 @@
%ul.dropdown-menu.dropdown-menu-align-right
%li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch)
%li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff)
- - unless @merge_request.closed_without_fork?
- .normal
- %span Request to merge
- %span.label-branch= source_branch_with_namespace(@merge_request)
- %span into
- %span.label-branch
- = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch)
- - if @merge_request.open? && @merge_request.diverged_from_target_branch?
- %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind)
+ .normal
+ %span Request to merge
+ %span.label-branch= source_branch_with_namespace(@merge_request)
+ %span into
+ %span.label-branch
+ = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch)
+ - if @merge_request.open? && @merge_request.diverged_from_target_branch?
+ %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind)
- - unless @merge_request.closed_without_source_project?
+ - if @merge_request.source_branch_exists?
= render "projects/merge_requests/show/how_to_merge"
- = render "projects/merge_requests/widget/show.html.haml"
+
+ = render "projects/merge_requests/widget/show.html.haml"
- if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user)
.light.prepend-top-default.append-bottom-default
@@ -47,39 +47,41 @@
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
- if @commits_count.nonzero?
- %ul.merge-request-tabs.nav-links.no-top.no-bottom{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
- %li.notes-tab
- = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
- Discussion
- %span.badge= @merge_request.mr_and_commit_notes.user.count
- - unless @merge_request.closed_without_source_project?
- %li.commits-tab
- = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
- Commits
- %span.badge= @commits_count
- - if @pipeline
- %li.pipelines-tab
- = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do
- Pipelines
- %span.badge= @merge_request.all_pipelines.size
- %li.builds-tab
- = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do
- Builds
- %span.badge= @statuses.size
- %li.diffs-tab
- = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
- Changes
- %span.badge= @merge_request.diff_size
- %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true }
- %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
- .line-resolve-all{ "v-show" => "discussionCount > 0",
- ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
- %span.line-resolve-btn.is-disabled{ type: "button",
- ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
- = render "shared/icons/icon_status_success.svg"
- %span.line-resolve-text
- {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ discussionCount | pluralize 'discussion' }} resolved
- = render "discussions/jump_to_next"
+ .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
+ %div{ class: container_class }
+ %ul.merge-request-tabs.nav-links.no-top.no-bottom
+ %li.notes-tab
+ = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
+ Discussion
+ %span.badge= @merge_request.mr_and_commit_notes.user.count
+ - if @merge_request.source_project
+ %li.commits-tab
+ = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
+ Commits
+ %span.badge= @commits_count
+ - if @pipeline
+ %li.pipelines-tab
+ = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do
+ Pipelines
+ %span.badge= @pipelines.size
+ %li.builds-tab
+ = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do
+ Builds
+ %span.badge= @statuses.size
+ %li.diffs-tab
+ = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
+ Changes
+ %span.badge= @merge_request.diff_size
+ %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true }
+ %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
+ .line-resolve-all{ "v-show" => "discussionCount > 0",
+ ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
+ %span.line-resolve-btn.is-disabled{ type: "button",
+ ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
+ = render "shared/icons/icon_status_success.svg"
+ %span.line-resolve-text
+ {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ discussionCount | pluralize 'discussion' }} resolved
+ = render "discussions/jump_to_next"
.tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes
diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml
index 7c3ac6652ee2c0b7e3ebff6ab6bf86bda2241ba2..03159f123f3764b5413392e3442d012e70952844 100644
--- a/app/views/projects/merge_requests/edit.html.haml
+++ b/app/views/projects/merge_requests/edit.html.haml
@@ -1,4 +1,4 @@
-- page_title "Edit", "#{@merge_request.to_reference} #{@merge_request.title}", "Merge Requests"
+- page_title "Edit", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
%h3.page-title
Edit Merge Request #{@merge_request.to_reference}
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index c83818e9199662cb71b2fb3bd859387171c7ca48..f9ba77e87b579f9500c31cd3ef1952c07133a517 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -31,7 +31,7 @@
= link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do
Delete
- .detail-page-description.milestone-detail
+ .detail-page-description.milestone-detail{ class: ('hide-bottom-border' unless @milestone.description.present? ) }
%h2.title
= markdown_field(@milestone, :title)
%div
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index cc8cb134fb83f2206d667cf05d017ff503dcc4e0..399ccf15b7fa658049e117cb1116c7bcaae6b48d 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -27,6 +27,7 @@
- else
.input-group-addon.static-namespace
#{root_url}#{current_user.username}/
+ = f.hidden_field :namespace_id, value: current_user.namespace_id
.form-group.col-xs-12.col-sm-6.project-path
= f.label :namespace_id, class: 'label-light' do
%span
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 9eeef5f57b49d812a55b23da5ce6f5f285d12fe8..4bc49072f3577073b606605bd6f5476ad3ac73a1 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -45,11 +45,12 @@
.table-holder
%table.table.ci-table
%thead
- %th.col-xs-1.col-sm-1 Status
- %th.col-xs-2.col-sm-4 Pipeline
- %th.col-xs-2.col-sm-2 Stages
- %th.col-xs-2.col-sm-2
- %th.hidden-xs.col-sm-3
+ %th Status
+ %th Pipeline
+ %th Commit
+ %th Stages
+ %th
+ %th.hidden-xs
= render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages
= paginate @pipelines, theme: 'gitlab'
diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml
index 0740e9b56ab532edcda6a07a7f421bdadebde102..bebf0ccd54d9cb21365dffade95b19f022996f4f 100644
--- a/app/views/projects/pipelines_settings/show.html.haml
+++ b/app/views/projects/pipelines_settings/show.html.haml
@@ -64,8 +64,8 @@
.checkbox
= f.label :public_builds do
= f.check_box :public_builds
- %strong Public pipelines
- .help-block Allow everyone to access pipelines for Public and Internal projects
+ %strong Public builds
+ .help-block Allow everyone to access builds traces for Public and Internal projects
.form-group.append-bottom-default
= f.label :runners_token, "Runners token", class: 'label-light'
diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml
index 49dcc9a6ba4ea1df381a37a8504882996329b872..42e9bdbd30eb6cd4511fa9acf9d13e8ed8908c1c 100644
--- a/app/views/projects/protected_branches/index.html.haml
+++ b/app/views/projects/protected_branches/index.html.haml
@@ -1,4 +1,6 @@
- page_title "Protected branches"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('protected_branches/protected_branches_bundle.js')
.row.prepend-top-default.append-bottom-default
.col-lg-3
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index 858af78f7bf888855c1b3069aad557806e471eb9..51b0939564ebc25a0736e1db9d0a08dfddf5e9d1 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -26,4 +26,4 @@
%h4.underlined-title Available specific runners
%ul.bordered-list.available-specific-runners
= render partial: 'runner', collection: @assignable_runners, as: :runner
- = paginate @assignable_runners
+ = paginate @assignable_runners, theme: "gitlab"
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..40c8d2af226050cbc01b46ca1f465fc52440b610
--- /dev/null
+++ b/app/views/shared/_label.html.haml
@@ -0,0 +1,53 @@
+- label_css_id = dom_id(label)
+- open_issues_count = label.open_issues_count(current_user, @project)
+- open_merge_requests_count = label.open_merge_requests_count(current_user, @project)
+
+%li{id: label_css_id, data: { id: label.id } }
+ = render "shared/label_row", label: label
+
+ .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown
+ %button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } }
+ Options
+ = icon('caret-down')
+ .dropdown-menu.dropdown-menu-align-right
+ %ul
+ %li
+ = link_to_label(label, subject: @project, type: :merge_request) do
+ = pluralize open_merge_requests_count, 'merge request'
+ %li
+ = link_to_label(label, subject: @project) do
+ = pluralize open_issues_count, 'open issue'
+ - if current_user
+ %li.label-subscription{ data: toggle_subscription_data(label) }
+ %a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } }
+ %span= label_subscription_toggle_button_text(label)
+ - if can?(current_user, :admin_label, label)
+ %li
+ = link_to 'Edit', edit_label_path(label)
+ %li
+ = link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, remote: true, data: {confirm: 'Remove this label? Are you sure?'}
+
+ .pull-right.hidden-xs.hidden-sm.hidden-md
+ = link_to_label(label, subject: @project, type: :merge_request, css_class: 'btn btn-transparent btn-action') do
+ = pluralize open_merge_requests_count, 'merge request'
+ = link_to_label(label, subject: @project, css_class: 'btn btn-transparent btn-action') do
+ = pluralize open_issues_count, 'open issue'
+
+ - if current_user
+ .label-subscription.inline{ data: toggle_subscription_data(label) }
+ %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } }
+ %span.sr-only= label_subscription_toggle_button_text(label)
+ = icon('eye', class: 'label-subscribe-button-icon', disabled: label.is_a?(GroupLabel))
+ = icon('spinner spin', class: 'label-subscribe-button-loading')
+
+ - if can?(current_user, :admin_label, label)
+ = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
+ %span.sr-only Edit
+ = icon('pencil-square-o')
+ = link_to destroy_label_path(label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: label_deletion_confirm_text(label), toggle: "tooltip"} do
+ %span.sr-only Delete
+ = icon('trash-o')
+
+ - if current_user && label.is_a?(ProjectLabel)
+ :javascript
+ new Subscription('##{dom_id(label)} .label-subscription');
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index 6f593e8dff9cd1760d06486c6a2e51259c42981f..d28f9421ecf6c2bfc77271ef8af21b8eafd06404 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -3,13 +3,16 @@
.draggable-handler
= icon('bars')
.js-toggle-priority.toggle-priority{ data: { url: remove_priority_namespace_project_label_path(@project.namespace, @project, label),
- dom_id: dom_id(label) } }
+ dom_id: dom_id(label), type: label.type } }
%button.add-priority.btn.has-tooltip{ title: 'Prioritize', :'data-placement' => 'top' }
= icon('star-o')
%button.remove-priority.btn.has-tooltip{ title: 'Remove priority', :'data-placement' => 'top' }
= icon('star')
%span.label-name
- = link_to_label(label, tooltip: false)
+ = link_to_label(label, subject: @project, tooltip: false)
+ - if defined?(@project) && @project.group.present?
+ %span.label-type
+ = label.model_name.human.titleize
- if label.description
%span.label-description
= markdown_field(label, :description)
diff --git a/app/views/shared/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml
index e324d0e5203e4a1dbe51d6438b97fc46f9cad6e3..21b37a7c9ae0a0fbce6c2dfccd1375132c65e5b3 100644
--- a/app/views/shared/_labels_row.html.haml
+++ b/app/views/shared/_labels_row.html.haml
@@ -1,5 +1,5 @@
- labels.each do |label|
%span.label-row.btn-group{ role: "group", aria: { label: label.name }, style: "color: #{text_color_for_bg(label.color)}" }
- = link_to_label(label, css_class: 'btn btn-transparent')
+ = link_to_label(label, subject: @project, css_class: 'btn btn-transparent')
%button.btn.btn-transparent.label-remove.js-label-filter-remove{ type: "button", style: "background-color: #{label.color};", data: { label: label.title } }
= icon("times")
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 31620297be08ccc6346a1883ecb57aa561738272..ed93857e6d4bf7112b9411cf45e30566edb10915 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -29,8 +29,9 @@
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
- .filter-item.inline.reset-filters
- %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search])} Reset filters
+ - if issuable_filters_present
+ .filter-item.inline.reset-filters
+ %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search])} Reset filters
.pull-right
- if boards_page
@@ -77,11 +78,10 @@
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline
= button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
-
- - if !@labels.nil?
- .row-content-block.second-block.filtered-labels{ class: ("hidden" if !@labels.any?) }
- - if @labels.any?
- = render "shared/labels_row", labels: @labels
+ - has_labels = @labels && @labels.any?
+ .row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) }
+ - if has_labels
+ = render 'shared/labels_row', labels: @labels
:javascript
new UsersSelect();
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index a7944a6013083741c836f4fed01f1461e16c752a..d410755cad11e950fa3b81bf3781700c2bfc786d 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -88,19 +88,19 @@
- if issuable.assignee_id
= f.hidden_field :assignee_id
= dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
- placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee", show_menu_above: true } })
+ placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
.form-group.issue-milestone
= f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder
- = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_menu_above: true, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
+ = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
.form-group
- - has_labels = issuable.project.labels.any?
+ - has_labels = @labels && @labels.any?
= f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
= f.hidden_field :label_ids, multiple: true, value: ''
.col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
.issuable-form-select-holder
- = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false, show_menu_above: 'true' }, dropdown_title: "Select label"
+ = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label"
- if has_due_date
.col-lg-6
.form-group
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index ba9f0c276612eb03d94962d629c68c5ec9e4ee8d..7363ead09ff775f92eb8b0864fff7557932aaacb 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -107,7 +107,7 @@
= dropdown_content do
.js-due-date-calendar
- - if issuable.project.labels.any?
+ - if @labels && @labels.any?
- selected_labels = issuable.labels
.block.labels
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
diff --git a/app/views/projects/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml
similarity index 79%
rename from app/views/projects/labels/_form.html.haml
rename to app/views/shared/labels/_form.html.haml
index 6ab6ae5038935086ff5dd70abb202d63b53e6988..647e05e5ff76ee2c4c5636380126d705184caf94 100644
--- a/app/views/projects/labels/_form.html.haml
+++ b/app/views/shared/labels/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f|
+= form_for @label, as: :label, url: url, html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f|
= form_errors(@label)
.form-group
@@ -30,4 +30,4 @@
= f.submit 'Save changes', class: 'btn btn-save js-save-button'
- else
= f.submit 'Create Label', class: 'btn btn-create js-save-button'
- = link_to "Cancel", namespace_project_labels_path(@project.namespace, @project), class: 'btn btn-cancel'
+ = link_to 'Cancel', back_path, class: 'btn btn-cancel'
diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb
index 667fff031dd34f788ddbdc5983c178e63acdcd13..c2dc955b27c1eedb4612807ab52db79cd7fa7d98 100644
--- a/app/workers/admin_email_worker.rb
+++ b/app/workers/admin_email_worker.rb
@@ -1,7 +1,6 @@
class AdminEmailWorker
include Sidekiq::Worker
-
- sidekiq_options retry: false # this job auto-repeats via sidekiq-cron
+ include CronjobQueue
def perform
repository_check_failed_count = Project.where(last_repository_check_failed: true).count
diff --git a/app/workers/build_coverage_worker.rb b/app/workers/build_coverage_worker.rb
index 0680645a8dbda62773d87407281f134f5a6c97ff..def0ab1dde12a9be9b95be014bbabb27558880c3 100644
--- a/app/workers/build_coverage_worker.rb
+++ b/app/workers/build_coverage_worker.rb
@@ -1,6 +1,6 @@
class BuildCoverageWorker
include Sidekiq::Worker
- sidekiq_options queue: :default
+ include BuildQueue
def perform(build_id)
Ci::Build.find_by(id: build_id)
diff --git a/app/workers/build_email_worker.rb b/app/workers/build_email_worker.rb
index 1c7a04a66a819e59c350cba67eb35ac6b19549af..5fdb1f2baa05fce65de38baf8707fb11099d87e9 100644
--- a/app/workers/build_email_worker.rb
+++ b/app/workers/build_email_worker.rb
@@ -1,5 +1,6 @@
class BuildEmailWorker
include Sidekiq::Worker
+ include BuildQueue
def perform(build_id, recipients, push_data)
recipients.each do |recipient|
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
index e7286b77ac5caf4a510e5e28635698de95b7dc1c..466410bf08ce512a304d37f380fbfa617354d1e5 100644
--- a/app/workers/build_finished_worker.rb
+++ b/app/workers/build_finished_worker.rb
@@ -1,5 +1,6 @@
class BuildFinishedWorker
include Sidekiq::Worker
+ include BuildQueue
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb
index e22ececb3fd4af4cf8b88b8a0ccb1801382566ee..9965af935d4b2a63fe12ff2ef815b0544a445890 100644
--- a/app/workers/build_hooks_worker.rb
+++ b/app/workers/build_hooks_worker.rb
@@ -1,6 +1,6 @@
class BuildHooksWorker
include Sidekiq::Worker
- sidekiq_options queue: :default
+ include BuildQueue
def perform(build_id)
Ci::Build.find_by(id: build_id)
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index 500d357ce31c6e23c43b1e172d7a4376a0d89752..e0ad52686649d31a9c47dcae7458cb83de45c241 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -1,6 +1,6 @@
class BuildSuccessWorker
include Sidekiq::Worker
- sidekiq_options queue: :default
+ include BuildQueue
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
diff --git a/app/workers/clear_database_cache_worker.rb b/app/workers/clear_database_cache_worker.rb
index c541daba50e02ff79df6ec825c3aa62b54dc3386..c4cb4733482cdf00e9a422e5eb9a3b2d2c195702 100644
--- a/app/workers/clear_database_cache_worker.rb
+++ b/app/workers/clear_database_cache_worker.rb
@@ -1,6 +1,7 @@
# This worker clears all cache fields in the database, working in batches.
class ClearDatabaseCacheWorker
include Sidekiq::Worker
+ include DedicatedSidekiqQueue
BATCH_SIZE = 1000
diff --git a/app/workers/concerns/build_queue.rb b/app/workers/concerns/build_queue.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cf0ead40a8beab13e47b471fdb6ba9819e46d737
--- /dev/null
+++ b/app/workers/concerns/build_queue.rb
@@ -0,0 +1,8 @@
+# Concern for setting Sidekiq settings for the various CI build workers.
+module BuildQueue
+ extend ActiveSupport::Concern
+
+ included do
+ sidekiq_options queue: :build
+ end
+end
diff --git a/app/workers/concerns/cronjob_queue.rb b/app/workers/concerns/cronjob_queue.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e918bb011e01370ea4717de621c8e10d11b2e9f6
--- /dev/null
+++ b/app/workers/concerns/cronjob_queue.rb
@@ -0,0 +1,9 @@
+# Concern that sets various Sidekiq settings for workers executed using a
+# cronjob.
+module CronjobQueue
+ extend ActiveSupport::Concern
+
+ included do
+ sidekiq_options queue: :cronjob, retry: false
+ end
+end
diff --git a/app/workers/concerns/dedicated_sidekiq_queue.rb b/app/workers/concerns/dedicated_sidekiq_queue.rb
new file mode 100644
index 0000000000000000000000000000000000000000..132bae6022b4c1da9bb22ba986ab139049c5d00a
--- /dev/null
+++ b/app/workers/concerns/dedicated_sidekiq_queue.rb
@@ -0,0 +1,9 @@
+# Concern that sets the queue of a Sidekiq worker based on the worker's class
+# name/namespace.
+module DedicatedSidekiqQueue
+ extend ActiveSupport::Concern
+
+ included do
+ sidekiq_options queue: name.sub(/Worker\z/, '').underscore.tr('/', '_')
+ end
+end
diff --git a/app/workers/concerns/pipeline_queue.rb b/app/workers/concerns/pipeline_queue.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ca3860e1d389d2a77dac12adb345e19b5bfe4990
--- /dev/null
+++ b/app/workers/concerns/pipeline_queue.rb
@@ -0,0 +1,8 @@
+# Concern for setting Sidekiq settings for the various CI pipeline workers.
+module PipelineQueue
+ extend ActiveSupport::Concern
+
+ included do
+ sidekiq_options queue: :pipeline
+ end
+end
diff --git a/app/workers/concerns/repository_check_queue.rb b/app/workers/concerns/repository_check_queue.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a597321ccf45a217ee29d5a5a386c936eba684a1
--- /dev/null
+++ b/app/workers/concerns/repository_check_queue.rb
@@ -0,0 +1,8 @@
+# Concern for setting Sidekiq settings for the various repository check workers.
+module RepositoryCheckQueue
+ extend ActiveSupport::Concern
+
+ included do
+ sidekiq_options queue: :repository_check, retry: false
+ end
+end
diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb
index 6ff361e4d8009f392f901465f14bae774aa01a09..3194c389b3d40bcee73d4285d1eaf25c9d5ecc31 100644
--- a/app/workers/delete_user_worker.rb
+++ b/app/workers/delete_user_worker.rb
@@ -1,5 +1,6 @@
class DeleteUserWorker
include Sidekiq::Worker
+ include DedicatedSidekiqQueue
def perform(current_user_id, delete_user_id, options = {})
delete_user = User.find(delete_user_id)
diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index 842eebdea9e2e63ec826daa1487357f517c6cfe0..d3f7e479a8d7ec1268b0952949d5c59af84ff6f7 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -1,7 +1,6 @@
class EmailReceiverWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :incoming_email
+ include DedicatedSidekiqQueue
def perform(raw)
return unless Gitlab::IncomingEmail.enabled?
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index 1dc7e0adef7bd41773d7e72ebeace9c91fc99424..b9cd49985dcd650f98b273b351fd3c88b69e9dfe 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -1,7 +1,7 @@
class EmailsOnPushWorker
include Sidekiq::Worker
+ include DedicatedSidekiqQueue
- sidekiq_options queue: :mailers
attr_reader :email, :skip_premailer
def perform(project_id, recipients, push_data, options = {})
diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb
index 174eabff9fdcb9e13fe5e297019a50053bb6e412..a27585fd3897aa1fe9fb9ce9885bfc77b77e4d99 100644
--- a/app/workers/expire_build_artifacts_worker.rb
+++ b/app/workers/expire_build_artifacts_worker.rb
@@ -1,5 +1,6 @@
class ExpireBuildArtifactsWorker
include Sidekiq::Worker
+ include CronjobQueue
def perform
Rails.logger.info 'Scheduling removal of build artifacts'
diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb
index d9e2cc37bb3150dbe9af8060c8f3fbaf577b8e0b..eb403c134d138d36d63bd3aaaadf6d6860efdd13 100644
--- a/app/workers/expire_build_instance_artifacts_worker.rb
+++ b/app/workers/expire_build_instance_artifacts_worker.rb
@@ -1,5 +1,6 @@
class ExpireBuildInstanceArtifactsWorker
include Sidekiq::Worker
+ include DedicatedSidekiqQueue
def perform(build_id)
build = Ci::Build
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index a6cefd4d60175afa3231dd340090627766df2567..65f8093b5b092d1c0df7243ecbba0ab2cef16c5b 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -1,8 +1,9 @@
class GitGarbageCollectWorker
include Sidekiq::Worker
include Gitlab::ShellAdapter
+ include DedicatedSidekiqQueue
- sidekiq_options queue: :gitlab_shell, retry: false
+ sidekiq_options retry: false
def perform(project_id)
project = Project.find(project_id)
diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb
index cfeda88bbc5c6ceaef5d19215cf32b1dc77aa131..964287a1793a03d7f27271aa2f3019329ae6e6e0 100644
--- a/app/workers/gitlab_shell_worker.rb
+++ b/app/workers/gitlab_shell_worker.rb
@@ -1,8 +1,7 @@
class GitlabShellWorker
include Sidekiq::Worker
include Gitlab::ShellAdapter
-
- sidekiq_options queue: :gitlab_shell
+ include DedicatedSidekiqQueue
def perform(action, *arg)
gitlab_shell.send(action, *arg)
diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb
index 5048746f09ba5590f9a8b1a10318726345b112dc..a49a5fd08557ff877a1fa34051d040ad19d0d711 100644
--- a/app/workers/group_destroy_worker.rb
+++ b/app/workers/group_destroy_worker.rb
@@ -1,7 +1,6 @@
class GroupDestroyWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :default
+ include DedicatedSidekiqQueue
def perform(group_id, user_id)
begin
diff --git a/app/workers/import_export_project_cleanup_worker.rb b/app/workers/import_export_project_cleanup_worker.rb
index 72e3a9ae734686fe50e65a541b66d9a2bc7dfecb..7957ed807ab07696915e3716b632c2c0c7863d33 100644
--- a/app/workers/import_export_project_cleanup_worker.rb
+++ b/app/workers/import_export_project_cleanup_worker.rb
@@ -1,7 +1,6 @@
class ImportExportProjectCleanupWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :default
+ include CronjobQueue
def perform
ImportExportCleanUpService.new.execute
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index 19f38358eb51171951cfde71f4b4e494cd9511ad..7e44b24174358f269bc40e48b44950560a9bb9c8 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -3,6 +3,7 @@ require 'socket'
class IrkerWorker
include Sidekiq::Worker
+ include DedicatedSidekiqQueue
def perform(project_id, chans, colors, push_data, settings)
project = Project.find(project_id)
diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb
index c87c0a252b1f56f6489c4cf505ccd5e91f86e2c2..79efca4f2f9443f18e4eb45aa0236f1143bc9358 100644
--- a/app/workers/merge_worker.rb
+++ b/app/workers/merge_worker.rb
@@ -1,7 +1,6 @@
class MergeWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :default
+ include DedicatedSidekiqQueue
def perform(merge_request_id, current_user_id, params)
params = params.with_indifferent_access
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index 1b3232cd36521d62fbe3d9dcc726b62d23d221a2..c3e62bb88c0191a1714bd375f0c195d0463f3002 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -1,7 +1,6 @@
class NewNoteWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :default
+ include DedicatedSidekiqQueue
def perform(note_id, note_params)
note = Note.find(note_id)
diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb
index ab5e9f6daad11a605a920f6a29c8723a57f5e489..7e36eacebf88cd5d231b13311ed1991e3e695e3c 100644
--- a/app/workers/pipeline_hooks_worker.rb
+++ b/app/workers/pipeline_hooks_worker.rb
@@ -1,6 +1,6 @@
class PipelineHooksWorker
include Sidekiq::Worker
- sidekiq_options queue: :default
+ include PipelineQueue
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id)
diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb
index 7bb92df3bbd49738891ea3f27e5693cb7040e1a6..34f6ef161fb3e2e63c01f954b162e642ce2e41f7 100644
--- a/app/workers/pipeline_metrics_worker.rb
+++ b/app/workers/pipeline_metrics_worker.rb
@@ -1,7 +1,6 @@
class PipelineMetricsWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :default
+ include PipelineQueue
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb
index f44227d7086b61093440befba344d4f31bfac302..357e4a9a1c3aedada45058581af723f0bf5b7e9c 100644
--- a/app/workers/pipeline_process_worker.rb
+++ b/app/workers/pipeline_process_worker.rb
@@ -1,7 +1,6 @@
class PipelineProcessWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :default
+ include PipelineQueue
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id)
diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb
index 5dd443fea59f5ad6a7d9146259e171786d9379c6..2aa6fff24da1878615bad011dfcc9e4a2f9942e1 100644
--- a/app/workers/pipeline_success_worker.rb
+++ b/app/workers/pipeline_success_worker.rb
@@ -1,6 +1,6 @@
class PipelineSuccessWorker
include Sidekiq::Worker
- sidekiq_options queue: :default
+ include PipelineQueue
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
diff --git a/app/workers/pipeline_update_worker.rb b/app/workers/pipeline_update_worker.rb
index 44a7f24e40137a79b11f85681925f588ce681d5b..96c4152c674c50395240cbd55ea34cce9ce6c31c 100644
--- a/app/workers/pipeline_update_worker.rb
+++ b/app/workers/pipeline_update_worker.rb
@@ -1,7 +1,6 @@
class PipelineUpdateWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :default
+ include PipelineQueue
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id)
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index a9a2b7160059dcda26948dc1ce9201abfc657053..eee0ca12af9e87a112545c1ded7f2d2460c09397 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -1,7 +1,6 @@
class PostReceive
include Sidekiq::Worker
-
- sidekiq_options queue: :post_receive
+ include DedicatedSidekiqQueue
def perform(repo_path, identifier, changes)
if path = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1].to_s) }
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index ccefd0f71a0dfd762831b4719e1ce0a588dc0330..4dfa745fb509303562a7bc6f7ddbceef90357b7d 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -1,9 +1,41 @@
+# Worker for updating any project specific caches.
+#
+# This worker runs at most once every 15 minutes per project. This is to ensure
+# that multiple instances of jobs for this worker don't hammer the underlying
+# storage engine as much.
class ProjectCacheWorker
include Sidekiq::Worker
+ include DedicatedSidekiqQueue
- sidekiq_options queue: :default
+ LEASE_TIMEOUT = 15.minutes.to_i
+
+ def self.lease_for(project_id)
+ Gitlab::ExclusiveLease.
+ new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT)
+ end
+
+ # Overwrite Sidekiq's implementation so we only schedule when actually needed.
+ def self.perform_async(project_id)
+ # If a lease for this project is still being held there's no point in
+ # scheduling a new job.
+ super unless lease_for(project_id).exists?
+ end
def perform(project_id)
+ if try_obtain_lease_for(project_id)
+ Rails.logger.
+ info("Obtained ProjectCacheWorker lease for project #{project_id}")
+ else
+ Rails.logger.
+ info("Could not obtain ProjectCacheWorker lease for project #{project_id}")
+
+ return
+ end
+
+ update_caches(project_id)
+ end
+
+ def update_caches(project_id)
project = Project.find(project_id)
return unless project.repository.exists?
@@ -15,4 +47,8 @@ class ProjectCacheWorker
project.repository.build_cache
end
end
+
+ def try_obtain_lease_for(project_id)
+ self.class.lease_for(project_id).try_obtain
+ end
end
diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb
index 3062301a9b1fcf63cb91f7969fe8fd2d105f53db..b462327490ef4715741876ad3106a28cd718c996 100644
--- a/app/workers/project_destroy_worker.rb
+++ b/app/workers/project_destroy_worker.rb
@@ -1,7 +1,6 @@
class ProjectDestroyWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :default
+ include DedicatedSidekiqQueue
def perform(project_id, user_id, params)
begin
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
index 615311e63f50efaef365bbf8e028b49a669b34ab..6009aa1b191bf079bc89bf91c978e484a4dc0075 100644
--- a/app/workers/project_export_worker.rb
+++ b/app/workers/project_export_worker.rb
@@ -1,7 +1,8 @@
class ProjectExportWorker
include Sidekiq::Worker
+ include DedicatedSidekiqQueue
- sidekiq_options queue: :gitlab_shell, retry: 3
+ sidekiq_options retry: 3
def perform(current_user_id, project_id)
current_user = User.find(current_user_id)
diff --git a/app/workers/project_service_worker.rb b/app/workers/project_service_worker.rb
index 64d39c4d3f7ef436cb4ee49e2a2d4d9a5a5ee72a..fdfdeab7b4159ca5dc3ac0042e928ad6aab975a9 100644
--- a/app/workers/project_service_worker.rb
+++ b/app/workers/project_service_worker.rb
@@ -1,7 +1,6 @@
class ProjectServiceWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :project_web_hook
+ include DedicatedSidekiqQueue
def perform(hook_id, data)
data = data.with_indifferent_access
diff --git a/app/workers/project_web_hook_worker.rb b/app/workers/project_web_hook_worker.rb
index fb87896528889c245185fb95adb1761612bea26a..efb85eafd156d0352068a476c478de264c06f8ca 100644
--- a/app/workers/project_web_hook_worker.rb
+++ b/app/workers/project_web_hook_worker.rb
@@ -1,7 +1,6 @@
class ProjectWebHookWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :project_web_hook
+ include DedicatedSidekiqQueue
def perform(hook_id, data, hook_name)
data = data.with_indifferent_access
diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb
index 5883cafe1d16db9fdd29b4cd80e7c6dcc7868511..392abb9c21b43e2476ee37362b3ae7ebc5a09659 100644
--- a/app/workers/prune_old_events_worker.rb
+++ b/app/workers/prune_old_events_worker.rb
@@ -1,5 +1,6 @@
class PruneOldEventsWorker
include Sidekiq::Worker
+ include CronjobQueue
def perform
# Contribution calendar shows maximum 12 months of events.
diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb
index 246c8b6650a07a79111703b463466544f23757f8..2a619f834100cf278d74dab2a81482673a425069 100644
--- a/app/workers/remove_expired_group_links_worker.rb
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -1,5 +1,6 @@
class RemoveExpiredGroupLinksWorker
include Sidekiq::Worker
+ include CronjobQueue
def perform
ProjectGroupLink.expired.destroy_all
diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb
index cf765af97ce39e33a0df92f9d9d69a113f33b386..31f652e5f9bc11f177393ff2c9f3761adfb17d20 100644
--- a/app/workers/remove_expired_members_worker.rb
+++ b/app/workers/remove_expired_members_worker.rb
@@ -1,5 +1,6 @@
class RemoveExpiredMembersWorker
include Sidekiq::Worker
+ include CronjobQueue
def perform
Member.expired.find_each do |member|
diff --git a/app/workers/repository_archive_cache_worker.rb b/app/workers/repository_archive_cache_worker.rb
index a2e49c61f59543d917dbdfff4311ca9c445124d8..e47069df189069efb8ddf2f20c718242d30c9c62 100644
--- a/app/workers/repository_archive_cache_worker.rb
+++ b/app/workers/repository_archive_cache_worker.rb
@@ -1,7 +1,6 @@
class RepositoryArchiveCacheWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :default
+ include CronjobQueue
def perform
RepositoryArchiveCleanUpService.new.execute
diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb
index a3e16fa5212dff6f273638ebcf0e9ff85dd57852..c3e7491ec4ec9f1f937c9c7c9ee1dc97d16f9bf5 100644
--- a/app/workers/repository_check/batch_worker.rb
+++ b/app/workers/repository_check/batch_worker.rb
@@ -1,14 +1,13 @@
module RepositoryCheck
class BatchWorker
include Sidekiq::Worker
-
+ include CronjobQueue
+
RUN_TIME = 3600
-
- sidekiq_options retry: false
-
+
def perform
start = Time.now
-
+
# This loop will break after a little more than one hour ('a little
# more' because `git fsck` may take a few minutes), or if it runs out of
# projects to check. By default sidekiq-cron will start a new
@@ -17,15 +16,15 @@ module RepositoryCheck
project_ids.each do |project_id|
break if Time.now - start >= RUN_TIME
break unless current_settings.repository_checks_enabled
-
+
next unless try_obtain_lease(project_id)
-
+
SingleRepositoryWorker.new.perform(project_id)
end
end
-
+
private
-
+
# Project.find_each does not support WHERE clauses and
# Project.find_in_batches does not support ordering. So we just build an
# array of ID's. This is OK because we do it only once an hour, because
@@ -39,7 +38,7 @@ module RepositoryCheck
reorder('last_repository_check_at ASC').limit(limit).pluck(:id)
never_checked_projects + old_check_projects
end
-
+
def try_obtain_lease(id)
# Use a 24-hour timeout because on servers/projects where 'git fsck' is
# super slow we definitely do not want to run it twice in parallel.
@@ -48,7 +47,7 @@ module RepositoryCheck
timeout: 24.hours
).try_obtain
end
-
+
def current_settings
# No caching of the settings! If we cache them and an admin disables
# this feature, an active RepositoryCheckWorker would keep going for up
diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb
index b7202ddff34473cdeb24ca753b639718da8c3540..1f1b38540eeef0d0acc5141b9b1939fb7f1bcdf4 100644
--- a/app/workers/repository_check/clear_worker.rb
+++ b/app/workers/repository_check/clear_worker.rb
@@ -1,8 +1,7 @@
module RepositoryCheck
class ClearWorker
include Sidekiq::Worker
-
- sidekiq_options retry: false
+ include RepositoryCheckQueue
def perform
# Do small batched updates because these updates will be slow and locking
diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb
index 98ddf5d06884ef6f6c15e53b092e461474342f18..3d8bfc6fc6c4d1af52191cf0eb325c29101609e0 100644
--- a/app/workers/repository_check/single_repository_worker.rb
+++ b/app/workers/repository_check/single_repository_worker.rb
@@ -1,8 +1,7 @@
module RepositoryCheck
class SingleRepositoryWorker
include Sidekiq::Worker
-
- sidekiq_options retry: false
+ include RepositoryCheckQueue
def perform(project_id)
project = Project.find(project_id)
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 61ed1c38ac427f20cbb3a71bdbe49e768c6687c8..efc99ec962a96d86960a7e4645ce1ee8a0e2b10e 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -1,8 +1,7 @@
class RepositoryForkWorker
include Sidekiq::Worker
include Gitlab::ShellAdapter
-
- sidekiq_options queue: :gitlab_shell
+ include DedicatedSidekiqQueue
def perform(project_id, forked_from_repository_storage_path, source_path, target_path)
Gitlab::Metrics.add_event(:fork_repository,
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index d2ca8813ab9db2a955b037e3af95df7449f788bf..c8a77e21c123fccbfcc5cc8b3b52be631b0249c4 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -1,8 +1,7 @@
class RepositoryImportWorker
include Sidekiq::Worker
include Gitlab::ShellAdapter
-
- sidekiq_options queue: :gitlab_shell
+ include DedicatedSidekiqQueue
attr_accessor :project, :current_user
diff --git a/app/workers/requests_profiles_worker.rb b/app/workers/requests_profiles_worker.rb
index 9dd228a248377de6d5f01e7f7b3ff0dfec6e5ad3..703b025d76e86fd3d59a7d10175ccd01c3a2b87f 100644
--- a/app/workers/requests_profiles_worker.rb
+++ b/app/workers/requests_profiles_worker.rb
@@ -1,7 +1,6 @@
class RequestsProfilesWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :default
+ include CronjobQueue
def perform
Gitlab::RequestProfiler.remove_all_profiles
diff --git a/app/workers/stuck_ci_builds_worker.rb b/app/workers/stuck_ci_builds_worker.rb
index 6828013b3772d5fdcd9b26fdcc0c727dd70dff12..b70df5a1afaf061ad3077d3a161c7f0bf75d010f 100644
--- a/app/workers/stuck_ci_builds_worker.rb
+++ b/app/workers/stuck_ci_builds_worker.rb
@@ -1,5 +1,6 @@
class StuckCiBuildsWorker
include Sidekiq::Worker
+ include CronjobQueue
BUILD_STUCK_TIMEOUT = 1.day
diff --git a/app/workers/system_hook_worker.rb b/app/workers/system_hook_worker.rb
index a122c274763ad0ce41118de0499d11595379802f..baf2f12eeacaf3b2dd1cf25e7f631460b97a5ec7 100644
--- a/app/workers/system_hook_worker.rb
+++ b/app/workers/system_hook_worker.rb
@@ -1,7 +1,6 @@
class SystemHookWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :system_hook
+ include DedicatedSidekiqQueue
def perform(hook_id, data, hook_name)
SystemHook.find(hook_id).execute(data, hook_name)
diff --git a/app/workers/trending_projects_worker.rb b/app/workers/trending_projects_worker.rb
index df4c4a6628b59efdb609f3b19e62c31b2d56e0bf..0531630d13a7b8de98387384201629e5a8840ea1 100644
--- a/app/workers/trending_projects_worker.rb
+++ b/app/workers/trending_projects_worker.rb
@@ -1,7 +1,6 @@
class TrendingProjectsWorker
include Sidekiq::Worker
-
- sidekiq_options queue: :trending_projects
+ include CronjobQueue
def perform
Rails.logger.info('Refreshing trending projects')
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
index 03f0528cdae6f180a446681580810681af770500..acc4d8581361168025ba6f4ed6d91c1ee3e69295 100644
--- a/app/workers/update_merge_requests_worker.rb
+++ b/app/workers/update_merge_requests_worker.rb
@@ -1,5 +1,6 @@
class UpdateMergeRequestsWorker
include Sidekiq::Worker
+ include DedicatedSidekiqQueue
def perform(project_id, user_id, oldrev, newrev, ref)
project = Project.find_by(id: project_id)
diff --git a/bin/background_jobs b/bin/background_jobs
index 25a578a1c491609b73832f0d9c3ee4c5a35e2081..f28e2f722dc77538eda2b7ce9a2ef3ccab1d201f 100755
--- a/bin/background_jobs
+++ b/bin/background_jobs
@@ -4,6 +4,7 @@ cd $(dirname $0)/..
app_root=$(pwd)
sidekiq_pidfile="$app_root/tmp/pids/sidekiq.pid"
sidekiq_logfile="$app_root/log/sidekiq.log"
+sidekiq_config="$app_root/config/sidekiq_queues.yml"
gitlab_user=$(ls -l config.ru | awk '{print $3}')
warn()
@@ -37,7 +38,7 @@ start_no_deamonize()
start_sidekiq()
{
- exec bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile "$@"
+ exec bundle exec sidekiq -C "${sidekiq_config}" -e $RAILS_ENV -P $sidekiq_pidfile "$@"
}
load_ok()
diff --git a/config/application.rb b/config/application.rb
index 8a9c539cb4375774057ec431908e239c0f27b041..92c8467e7f45a54994cb83c081db2fe3beb58d71 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -24,7 +24,8 @@ module Gitlab
#{config.root}/app/models/ci
#{config.root}/app/models/hooks
#{config.root}/app/models/members
- #{config.root}/app/models/project_services))
+ #{config.root}/app/models/project_services
+ #{config.root}/app/workers/concerns))
config.generators.templates.push("#{config.root}/generator_templates")
@@ -87,6 +88,7 @@ module Gitlab
config.assets.precompile << "users/users_bundle.js"
config.assets.precompile << "network/network_bundle.js"
config.assets.precompile << "profile/profile_bundle.js"
+ config.assets.precompile << "protected_branches/protected_branches_bundle.js"
config.assets.precompile << "diff_notes/diff_notes_bundle.js"
config.assets.precompile << "boards/boards_bundle.js"
config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js"
diff --git a/config/locales/en.yml b/config/locales/en.yml
index cedb5e207bd3798f0ff2a8e6651937ba368d2b8a..12a59be79f07d6257cdf6262ddcc33e84de12af6 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -5,6 +5,7 @@ en:
hello: "Hello world"
errors:
messages:
+ label_already_exists_at_group_level: "already exists at group level for %{group}. Please choose another one."
wrong_size: "is the wrong size (should be %{file_size})"
size_too_small: "is too small (should be at least %{file_size})"
size_too_big: "is too big (should be at most %{file_size})"
diff --git a/config/mail_room.yml b/config/mail_room.yml
index c639f8260aa1b2785ca0e044fe370051623f9230..68697bd1dc412a5f966bc6c84c994cdcc8b2a5c9 100644
--- a/config/mail_room.yml
+++ b/config/mail_room.yml
@@ -25,7 +25,7 @@
:delivery_options:
:redis_url: <%= config[:redis_url].to_json %>
:namespace: <%= Gitlab::Redis::SIDEKIQ_NAMESPACE %>
- :queue: incoming_email
+ :queue: email_receiver
:worker: EmailReceiverWorker
:arbitration_method: redis
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 06b464d79c8760910581d0fce14a2639b48b5ba3..826048ba196cf9290cd624c4022f034860bbce2e 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -12,21 +12,26 @@ constraints(GroupUrlConstrainer.new) do
end
end
-resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(? 'groups#show', as: :group_canonical
end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 711a59df74424d66ce37e331a1b30b8ec294d0d0..8142e231621e7e5fbdbbdb33bb6fb741e4db0784 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -319,7 +319,11 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
end
end
- resources :environments
+ resources :environments, except: [:destroy] do
+ member do
+ post :stop
+ end
+ end
resource :cycle_analytics, only: [:show]
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f36fe893fd0cf9c9bb508d6f016d7d5c768b5996
--- /dev/null
+++ b/config/sidekiq_queues.yml
@@ -0,0 +1,47 @@
+# This configuration file should be exclusively used to set queue settings for
+# Sidekiq. Any other setting should be specified using the Sidekiq CLI or the
+# Sidekiq Ruby API (see config/initializers/sidekiq.rb).
+---
+# All the queues to process and their weights. Every queue _must_ have a weight
+# defined.
+#
+# The available weights are as follows
+#
+# 1: low priority
+# 2: medium priority
+# 3: high priority
+# 5: _super_ high priority, this should only be used for _very_ important queues
+#
+# As per http://stackoverflow.com/a/21241357/290102 the formula for calculating
+# the likelihood of a job being popped off a queue (given all queues have work
+# to perform) is:
+#
+# chance = (queue weight / total weight of all queues) * 100
+:queues:
+ - [post_receive, 5]
+ - [merge, 5]
+ - [update_merge_requests, 3]
+ - [new_note, 2]
+ - [build, 2]
+ - [pipeline, 2]
+ - [gitlab_shell, 2]
+ - [email_receiver, 2]
+ - [emails_on_push, 2]
+ - [mailers, 2]
+ - [repository_fork, 1]
+ - [repository_import, 1]
+ - [project_service, 1]
+ - [clear_database_cache, 1]
+ - [delete_user, 1]
+ - [expire_build_instance_artifacts, 1]
+ - [group_destroy, 1]
+ - [irker, 1]
+ - [project_cache, 1]
+ - [project_destroy, 1]
+ - [project_export, 1]
+ - [project_web_hook, 1]
+ - [repository_check, 1]
+ - [system_hook, 1]
+ - [git_garbage_collect, 1]
+ - [cronjob, 1]
+ - [default, 1]
diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb
index 803cbca584db58e0ad7073ed881e96c679445de7..08ad3097d343a26852412ca475e1bd02f7306ae3 100644
--- a/db/fixtures/development/14_pipelines.rb
+++ b/db/fixtures/development/14_pipelines.rb
@@ -16,7 +16,8 @@ class Gitlab::Seeder::Pipelines
{ name: 'env:alpha', stage: 'deploy', environment: 'alpha', status: :pending },
{ name: 'env:beta', stage: 'deploy', environment: 'beta', status: :running },
{ name: 'env:gamma', stage: 'deploy', environment: 'gamma', status: :canceled },
- { name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success },
+ { name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success, options: { environment: { on_stop: 'stop staging' } } },
+ { name: 'stop staging', stage: 'deploy', environment: 'staging', when: 'manual', status: :skipped },
{ name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :skipped },
{ name: 'slack', stage: 'notify', when: 'manual', status: :created },
]
diff --git a/db/migrate/20160919144305_add_type_to_labels.rb b/db/migrate/20160919144305_add_type_to_labels.rb
new file mode 100644
index 0000000000000000000000000000000000000000..66172bda6ffc92f967ea0f0d52289a60916bf754
--- /dev/null
+++ b/db/migrate/20160919144305_add_type_to_labels.rb
@@ -0,0 +1,14 @@
+class AddTypeToLabels < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Labels will not work as expected until this migration is complete.'
+
+ def change
+ add_column :labels, :type, :string
+
+ update_column_in_batches(:labels, :type, 'ProjectLabel') do |table, query|
+ query.where(table[:project_id].not_eq(nil))
+ end
+ end
+end
diff --git a/db/migrate/20160919145149_add_group_id_to_labels.rb b/db/migrate/20160919145149_add_group_id_to_labels.rb
new file mode 100644
index 0000000000000000000000000000000000000000..05e21af058447a05024abb59c69390bf66ef085a
--- /dev/null
+++ b/db/migrate/20160919145149_add_group_id_to_labels.rb
@@ -0,0 +1,13 @@
+class AddGroupIdToLabels < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_column :labels, :group_id, :integer
+ add_foreign_key :labels, :namespaces, column: :group_id, on_delete: :cascade
+ add_concurrent_index :labels, :group_id
+ end
+end
diff --git a/db/migrate/20161006104309_add_state_to_environment.rb b/db/migrate/20161006104309_add_state_to_environment.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ccb546654f988220a834278e8c56784fa3dec924
--- /dev/null
+++ b/db/migrate/20161006104309_add_state_to_environment.rb
@@ -0,0 +1,15 @@
+class AddStateToEnvironment < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_column_with_default(:environments, :state, :string, default: :available)
+ end
+
+ def down
+ remove_column(:environments, :state)
+ end
+end
diff --git a/db/migrate/20161014173530_create_label_priorities.rb b/db/migrate/20161014173530_create_label_priorities.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2c22841c28a27af37d73a525033d80e5ec82aa84
--- /dev/null
+++ b/db/migrate/20161014173530_create_label_priorities.rb
@@ -0,0 +1,25 @@
+class CreateLabelPriorities < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'This migration adds foreign keys'
+
+ disable_ddl_transaction!
+
+ def up
+ create_table :label_priorities do |t|
+ t.references :project, foreign_key: { on_delete: :cascade }, null: false
+ t.references :label, foreign_key: { on_delete: :cascade }, null: false
+ t.integer :priority, null: false
+
+ t.timestamps null: false
+ end
+
+ add_concurrent_index :label_priorities, [:project_id, :label_id], unique: true
+ add_concurrent_index :label_priorities, :priority
+ end
+
+ def down
+ drop_table :label_priorities
+ end
+end
diff --git a/db/migrate/20161017095000_add_properties_to_deployment.rb b/db/migrate/20161017095000_add_properties_to_deployment.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f620ee0de1cd76c639d6289605b70c33772988ae
--- /dev/null
+++ b/db/migrate/20161017095000_add_properties_to_deployment.rb
@@ -0,0 +1,9 @@
+class AddPropertiesToDeployment < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :deployments, :on_stop, :string
+ end
+end
diff --git a/db/migrate/20161017125927_add_unique_index_to_labels.rb b/db/migrate/20161017125927_add_unique_index_to_labels.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f2b56ebfb7b5ec12ec465790aaede27c3372ab9f
--- /dev/null
+++ b/db/migrate/20161017125927_add_unique_index_to_labels.rb
@@ -0,0 +1,32 @@
+class AddUniqueIndexToLabels < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'This migration removes duplicated labels.'
+
+ disable_ddl_transaction!
+
+ def up
+ select_all('SELECT title, project_id, COUNT(id) as cnt FROM labels GROUP BY project_id, title HAVING COUNT(id) > 1').each do |label|
+ label_title = quote_string(label['title'])
+ duplicated_ids = select_all("SELECT id FROM labels WHERE project_id = #{label['project_id']} AND title = '#{label_title}' ORDER BY id ASC").map{ |label| label['id'] }
+ label_id = duplicated_ids.first
+ duplicated_ids.delete(label_id)
+
+ execute("UPDATE label_links SET label_id = #{label_id} WHERE label_id IN(#{duplicated_ids.join(",")})")
+ execute("DELETE FROM labels WHERE id IN(#{duplicated_ids.join(",")})")
+ end
+
+ remove_index :labels, column: :project_id if index_exists?(:labels, :project_id)
+ remove_index :labels, column: :title if index_exists?(:labels, :title)
+
+ add_concurrent_index :labels, [:group_id, :project_id, :title], unique: true
+ end
+
+ def down
+ remove_index :labels, column: [:group_id, :project_id, :title] if index_exists?(:labels, [:group_id, :project_id, :title], unique: true)
+
+ add_concurrent_index :labels, :project_id
+ add_concurrent_index :labels, :title
+ end
+end
diff --git a/db/migrate/20161018024215_migrate_labels_priority.rb b/db/migrate/20161018024215_migrate_labels_priority.rb
new file mode 100644
index 0000000000000000000000000000000000000000..22bec2382f46025f90f53fff50c634f8ccf33a05
--- /dev/null
+++ b/db/migrate/20161018024215_migrate_labels_priority.rb
@@ -0,0 +1,36 @@
+class MigrateLabelsPriority < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Prioritized labels will not work as expected until this migration is complete.'
+
+ disable_ddl_transaction!
+
+ def up
+ execute <<-EOF.strip_heredoc
+ INSERT INTO label_priorities (project_id, label_id, priority, created_at, updated_at)
+ SELECT labels.project_id, labels.id, labels.priority, NOW(), NOW()
+ FROM labels
+ WHERE labels.project_id IS NOT NULL
+ AND labels.priority IS NOT NULL;
+ EOF
+ end
+
+ def down
+ if Gitlab::Database.mysql?
+ execute <<-EOF.strip_heredoc
+ UPDATE labels
+ INNER JOIN label_priorities ON labels.id = label_priorities.label_id AND labels.project_id = label_priorities.project_id
+ SET labels.priority = label_priorities.priority;
+ EOF
+ else
+ execute <<-EOF.strip_heredoc
+ UPDATE labels
+ SET priority = label_priorities.priority
+ FROM label_priorities
+ WHERE labels.id = label_priorities.label_id
+ AND labels.project_id = label_priorities.project_id;
+ EOF
+ end
+ end
+end
diff --git a/db/migrate/20161018024550_remove_priority_from_labels.rb b/db/migrate/20161018024550_remove_priority_from_labels.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b7416cca6646432207bca38927097412b5136ce5
--- /dev/null
+++ b/db/migrate/20161018024550_remove_priority_from_labels.rb
@@ -0,0 +1,17 @@
+class RemovePriorityFromLabels < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'This migration removes an existing column'
+
+ disable_ddl_transaction!
+
+ def up
+ remove_column :labels, :priority, :integer, index: true
+ end
+
+ def down
+ add_column :labels, :priority, :integer
+ add_concurrent_index :labels, :priority
+ end
+end
diff --git a/db/migrate/20161018124658_make_project_owners_masters.rb b/db/migrate/20161018124658_make_project_owners_masters.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a576bb7b6222920b939b928a543835b7099a63b8
--- /dev/null
+++ b/db/migrate/20161018124658_make_project_owners_masters.rb
@@ -0,0 +1,15 @@
+class MakeProjectOwnersMasters < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ update_column_in_batches(:members, :access_level, 40) do |table, query|
+ query.where(table[:access_level].eq(50).and(table[:source_type].eq('Project')))
+ end
+ end
+
+ def down
+ # do nothing
+ end
+end
diff --git a/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb b/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e875213ab96db659ef130a0f60f4e0f03dfc8957
--- /dev/null
+++ b/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb
@@ -0,0 +1,109 @@
+require 'json'
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MigrateSidekiqQueuesFromDefault < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+
+ DOWNTIME_REASON = <<-EOF
+ Moving Sidekiq jobs from queues requires Sidekiq to be stopped. Not stopping
+ Sidekiq will result in the loss of jobs that are scheduled after this
+ migration completes.
+ EOF
+
+ disable_ddl_transaction!
+
+ # Jobs for which the queue names have been changed (e.g. multiple workers
+ # using the same non-default queue).
+ #
+ # The keys are the old queue names, the values the jobs to move and their new
+ # queue names.
+ RENAMED_QUEUES = {
+ gitlab_shell: {
+ 'GitGarbageCollectorWorker' => :git_garbage_collector,
+ 'ProjectExportWorker' => :project_export,
+ 'RepositoryForkWorker' => :repository_fork,
+ 'RepositoryImportWorker' => :repository_import
+ },
+ project_web_hook: {
+ 'ProjectServiceWorker' => :project_service
+ },
+ incoming_email: {
+ 'EmailReceiverWorker' => :email_receiver
+ },
+ mailers: {
+ 'EmailsOnPushWorker' => :emails_on_push
+ },
+ default: {
+ 'AdminEmailWorker' => :cronjob,
+ 'BuildCoverageWorker' => :build,
+ 'BuildEmailWorker' => :build,
+ 'BuildFinishedWorker' => :build,
+ 'BuildHooksWorker' => :build,
+ 'BuildSuccessWorker' => :build,
+ 'ClearDatabaseCacheWorker' => :clear_database_cache,
+ 'DeleteUserWorker' => :delete_user,
+ 'ExpireBuildArtifactsWorker' => :cronjob,
+ 'ExpireBuildInstanceArtifactsWorker' => :expire_build_instance_artifacts,
+ 'GroupDestroyWorker' => :group_destroy,
+ 'ImportExportProjectCleanupWorker' => :cronjob,
+ 'IrkerWorker' => :irker,
+ 'MergeWorker' => :merge,
+ 'NewNoteWorker' => :new_note,
+ 'PipelineHooksWorker' => :pipeline,
+ 'PipelineMetricsWorker' => :pipeline,
+ 'PipelineProcessWorker' => :pipeline,
+ 'PipelineSuccessWorker' => :pipeline,
+ 'PipelineUpdateWorker' => :pipeline,
+ 'ProjectCacheWorker' => :project_cache,
+ 'ProjectDestroyWorker' => :project_destroy,
+ 'PruneOldEventsWorker' => :cronjob,
+ 'RemoveExpiredGroupLinksWorker' => :cronjob,
+ 'RemoveExpiredMembersWorker' => :cronjob,
+ 'RepositoryArchiveCacheWorker' => :cronjob,
+ 'RepositoryCheck::BatchWorker' => :cronjob,
+ 'RepositoryCheck::ClearWorker' => :repository_check,
+ 'RepositoryCheck::SingleRepositoryWorker' => :repository_check,
+ 'RequestsProfilesWorker' => :cronjob,
+ 'StuckCiBuildsWorker' => :cronjob,
+ 'UpdateMergeRequestsWorker' => :update_merge_requests
+ }
+ }
+
+ def up
+ Sidekiq.redis do |redis|
+ RENAMED_QUEUES.each do |queue, jobs|
+ migrate_from_queue(redis, queue, jobs)
+ end
+ end
+ end
+
+ def down
+ Sidekiq.redis do |redis|
+ RENAMED_QUEUES.each do |dest_queue, jobs|
+ jobs.each do |worker, from_queue|
+ migrate_from_queue(redis, from_queue, worker => dest_queue)
+ end
+ end
+ end
+ end
+
+ def migrate_from_queue(redis, queue, job_mapping)
+ while job = redis.lpop("queue:#{queue}")
+ payload = JSON.load(job)
+ new_queue = job_mapping[payload['class']]
+
+ # If we have no target queue to migrate to we're probably dealing with
+ # some ancient job for which the worker no longer exists. In that case
+ # there's no sane option we can take, other than just dropping the job.
+ next unless new_queue
+
+ payload['queue'] = new_queue
+
+ redis.lpush("queue:#{new_queue}", JSON.dump(payload))
+ end
+ end
+end
diff --git a/db/migrate/20161019213545_generate_project_feature_for_projects.rb b/db/migrate/20161019213545_generate_project_feature_for_projects.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4554e14b0df612cc0b9e09efc642a66913fbb9be
--- /dev/null
+++ b/db/migrate/20161019213545_generate_project_feature_for_projects.rb
@@ -0,0 +1,28 @@
+class GenerateProjectFeatureForProjects < ActiveRecord::Migration
+ DOWNTIME = true
+
+ DOWNTIME_REASON = <<-HEREDOC
+ Application was eager loading project_feature for all projects generating an extra query
+ everytime a project was fetched. We removed that behavior to avoid the extra query, this migration
+ makes sure all projects have a project_feature record associated.
+ HEREDOC
+
+ def up
+ # Generate enabled values for each project feature 20, 20, 20, 20, 20
+ # All features are enabled by default
+ enabled_values = [ProjectFeature::ENABLED] * 5
+
+ execute <<-EOF.strip_heredoc
+ INSERT INTO project_features
+ (project_id, merge_requests_access_level, builds_access_level,
+ issues_access_level, snippets_access_level, wiki_access_level)
+ (SELECT projects.id, #{enabled_values.join(',')} FROM projects LEFT OUTER JOIN project_features
+ ON project_features.project_id = projects.id
+ WHERE project_features.id IS NULL)
+ EOF
+ end
+
+ def down
+ "Not needed"
+ end
+end
diff --git a/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb b/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb
new file mode 100644
index 0000000000000000000000000000000000000000..06d07bdb83516776da89b94ce2b4a012cabdc76b
--- /dev/null
+++ b/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb
@@ -0,0 +1,63 @@
+require 'json'
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MigrateMailroomQueueFromDefault < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+
+ DOWNTIME_REASON = <<-EOF
+ Moving Sidekiq jobs from queues requires Sidekiq to be stopped. Not stopping
+ Sidekiq will result in the loss of jobs that are scheduled after this
+ migration completes.
+ EOF
+
+ disable_ddl_transaction!
+
+ # Jobs for which the queue names have been changed (e.g. multiple workers
+ # using the same non-default queue).
+ #
+ # The keys are the old queue names, the values the jobs to move and their new
+ # queue names.
+ RENAMED_QUEUES = {
+ incoming_email: {
+ 'EmailReceiverWorker' => :email_receiver
+ }
+ }
+
+ def up
+ Sidekiq.redis do |redis|
+ RENAMED_QUEUES.each do |queue, jobs|
+ migrate_from_queue(redis, queue, jobs)
+ end
+ end
+ end
+
+ def down
+ Sidekiq.redis do |redis|
+ RENAMED_QUEUES.each do |dest_queue, jobs|
+ jobs.each do |worker, from_queue|
+ migrate_from_queue(redis, from_queue, worker => dest_queue)
+ end
+ end
+ end
+ end
+
+ def migrate_from_queue(redis, queue, job_mapping)
+ while job = redis.lpop("queue:#{queue}")
+ payload = JSON.load(job)
+ new_queue = job_mapping[payload['class']]
+
+ # If we have no target queue to migrate to we're probably dealing with
+ # some ancient job for which the worker no longer exists. In that case
+ # there's no sane option we can take, other than just dropping the job.
+ next unless new_queue
+
+ payload['queue'] = new_queue
+
+ redis.lpush("queue:#{new_queue}", JSON.dump(payload))
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 51ac0fbaeb519a9967ae659909b8248d67dcf32b..02282b0f6668f6d5cf05a25df88c13d15aadd0c4 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: 20161012180455) do
+ActiveRecord::Schema.define(version: 20161024042317) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -380,6 +380,7 @@ ActiveRecord::Schema.define(version: 20161012180455) do
t.string "deployable_type"
t.datetime "created_at"
t.datetime "updated_at"
+ t.string "on_stop"
end
add_index "deployments", ["project_id", "environment_id", "iid"], name: "index_deployments_on_project_id_and_environment_id_and_iid", using: :btree
@@ -404,6 +405,7 @@ ActiveRecord::Schema.define(version: 20161012180455) do
t.datetime "updated_at"
t.string "external_url"
t.string "environment_type"
+ t.string "state", default: "available", null: false
end
add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree
@@ -517,6 +519,17 @@ ActiveRecord::Schema.define(version: 20161012180455) do
add_index "label_links", ["label_id"], name: "index_label_links_on_label_id", using: :btree
add_index "label_links", ["target_id", "target_type"], name: "index_label_links_on_target_id_and_target_type", using: :btree
+ create_table "label_priorities", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.integer "label_id", null: false
+ t.integer "priority", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "label_priorities", ["priority"], name: "index_label_priorities_on_priority", using: :btree
+ add_index "label_priorities", ["project_id", "label_id"], name: "index_label_priorities_on_project_id_and_label_id", unique: true, using: :btree
+
create_table "labels", force: :cascade do |t|
t.string "title"
t.string "color"
@@ -525,13 +538,13 @@ ActiveRecord::Schema.define(version: 20161012180455) do
t.datetime "updated_at"
t.boolean "template", default: false
t.string "description"
- t.integer "priority"
t.text "description_html"
+ t.string "type"
+ t.integer "group_id"
end
- add_index "labels", ["priority"], name: "index_labels_on_priority", using: :btree
- add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
- add_index "labels", ["title"], name: "index_labels_on_title", using: :btree
+ add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree
+ add_index "labels", ["group_id"], name: "index_labels_on_group_id", using: :btree
create_table "lfs_objects", force: :cascade do |t|
t.string "oid", null: false
@@ -830,7 +843,7 @@ ActiveRecord::Schema.define(version: 20161012180455) do
t.integer "builds_access_level"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "repository_access_level", default: 20, null: false
+ t.integer "repository_access_level", default: 20, null: false
end
add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", using: :btree
@@ -1211,6 +1224,9 @@ ActiveRecord::Schema.define(version: 20161012180455) do
add_foreign_key "boards", "projects"
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
+ add_foreign_key "label_priorities", "labels", on_delete: :cascade
+ add_foreign_key "label_priorities", "projects", on_delete: :cascade
+ add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "lists", "boards"
add_foreign_key "lists", "labels"
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 6e0882a94dea7ab5793230b75cfe28d8e3d28c9b..e1ed99d98d379afd87d6f4027b6eaa2a752a0184 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -319,7 +319,7 @@ GET /projects/:id/repository/commits/:sha/statuses
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit SHA
-| `ref_name`| string | no | The name of a repository branch or tag or, if not given, the default branch
+| `ref` | string | no | The name of a repository branch or tag or, if not given, the default branch
| `stage` | string | no | Filter by [build stage](../ci/yaml/README.md#stages), e.g., `test`
| `name` | string | no | Filter by [job name](../ci/yaml/README.md#jobs), e.g., `bundler:audit`
| `all` | boolean | no | Return all statuses, not only the latest ones
diff --git a/doc/api/projects.md b/doc/api/projects.md
index b7791b4748a963282e5cab88e9d9e869178b3bbb..b69db90e70d8d8f44848ebb5d9439dd4b3911cd2 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -1333,8 +1333,6 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `query` (required) - A string contained in the project name
-| `per_page` (optional) - number of projects to return per page
-| `page` (optional) - the page to retrieve
-| `order_by` (optional) - Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields
+| `query` | string | yes | A string contained in the project name |
+| `order_by` | string | no | Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order |
diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md
index 1802fae14feb8c87782b4eaac7a2c5c6607c3d99..073e99b7147043cef150b8e921b1e6b7983d7d69 100644
--- a/doc/api/system_hooks.md
+++ b/doc/api/system_hooks.md
@@ -98,11 +98,8 @@ Example response:
## Delete system hook
-Deletes a system hook. This is an idempotent API function and returns `200 OK`
-even if the hook is not available.
-
-If the hook is deleted, a JSON object is returned. An error is raised if the
-hook is not found.
+Deletes a system hook. It returns `200 OK` if the hooks is deleted and
+`404 Not Found` if the hook is not found.
---
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 84ea59ab6870b6ecc164d8ca62f043fe23ce3ff0..5c0e1c44e3fc5970d0f40f043933e3bdc115a34a 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -146,13 +146,17 @@ variables:
```
These variables can be later used in all executed commands and scripts.
-
The YAML-defined variables are also set to all created service containers,
-thus allowing to fine tune them.
+thus allowing to fine tune them. Variables can be also defined on a
+[job level](#job-variables).
-Variables can be also defined on [job level](#job-variables).
+Except for the user defined variables, there are also the ones set up by the
+Runner itself. One example would be `CI_BUILD_REF_NAME` which has the value of
+the branch or tag name for which project is built. Apart from the variables
+you can set in `.gitlab-ci.yml`, there are also the so called secret variables
+which can be set in GitLab's UI.
-[Learn more about variables.](../variables/README.md)
+[Learn more about variables.][variables]
### cache
@@ -541,20 +545,29 @@ An example usage of manual actions is deployment to production.
> Introduced in GitLab 8.9.
-`environment` is used to define that a job deploys to a specific [environment].
-This allows easy tracking of all deployments to your environments straight from
-GitLab.
+> You can read more about environments and find more examples in the
+[documentation about environments][environment].
+`environment` is used to define that a job deploys to a specific environment.
If `environment` is specified and no environment under that name exists, a new
one will be created automatically.
-The `environment` name must contain only letters, digits, '-', '_', '/', '$', '{', '}' and spaces. Common
-names are `qa`, `staging`, and `production`, but you can use whatever name works
-with your workflow.
+The `environment` name can contain:
----
+- letters
+- digits
+- spaces
+- `-`
+- `_`
+- `/`
+- `$`
+- `{`
+- `}`
-**Example configurations**
+Common names are `qa`, `staging`, and `production`, but you can use whatever
+name works with your workflow.
+
+In its simplest form, the `environment` keyword can be defined like:
```
deploy to production:
@@ -563,39 +576,134 @@ deploy to production:
environment: production
```
-The `deploy to production` job will be marked as doing deployment to
-`production` environment.
+In the above example, the `deploy to production` job will be marked as doing a
+deployment to the `production` environment.
+
+#### environment:name
+
+> Introduced in GitLab 8.11.
+
+>**Note:**
+Before GitLab 8.11, the name of an environment could be defined as a string like
+`environment: production`. The recommended way now is to define it under the
+`name` keyword.
+
+Instead of defining the name of the environment right after the `environment`
+keyword, it is also possible to define it as a separate value. For that, use
+the `name` keyword under `environment`:
+
+```
+deploy to production:
+ stage: deploy
+ script: git push production HEAD:master
+ environment:
+ name: production
+```
+
+#### environment:url
+
+> Introduced in GitLab 8.11.
+
+>**Note:**
+Before GitLab 8.11, the URL could be added only in GitLab's UI. The
+recommended way now is to define it in `.gitlab-ci.yml`.
+
+This is an optional value that when set, it exposes buttons in various places
+in GitLab which when clicked take you to the defined URL.
+
+In the example below, if the job finishes successfully, it will create buttons
+in the merge requests and in the environments/deployments pages which will point
+to `https://prod.example.com`.
+
+```
+deploy to production:
+ stage: deploy
+ script: git push production HEAD:master
+ environment:
+ name: production
+ url: https://prod.example.com
+```
+
+#### environment:on_stop
+
+> [Introduced][ce-6669] in GitLab 8.13.
+
+Closing (stoping) environments can be achieved with the `on_stop` keyword defined under
+`environment`. It declares a different job that runs in order to close
+the environment.
+
+Read the `environment:action` section for an example.
+
+#### environment:action
+
+> [Introduced][ce-6669] in GitLab 8.13.
+
+The `action` keyword is to be used in conjunction with `on_stop` and is defined
+in the job that is called to close the environment.
+
+Take for instance:
+
+```yaml
+review_app:
+ stage: deploy
+ script: make deploy-app
+ environment:
+ name: review
+ on_stop: stop_review_app
+
+stop_review_app:
+ stage: deploy
+ script: make delete-app
+ when: manual
+ environment:
+ name: review
+ action: stop
+```
+
+In the above example we set up the `review_app` job to deploy to the `review`
+environment, and we also defined a new `stop_review_app` job under `on_stop`.
+Once the `review_app` job is successfully finished, it will trigger the
+`stop_review_app` job based on what is defined under `when`. In this case we
+set it up to `manual` so it will need a [manual action](#manual-actions) via
+GitLab's web interface in order to run.
+
+The `stop_review_app` job is **required** to have the following keywords defined:
+
+- `when` - [reference](#when)
+- `environment:name`
+- `environment:action`
#### dynamic environments
> [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6.
`environment` can also represent a configuration hash with `name` and `url`.
-These parameters can use any of the defined CI [variables](#variables)
+These parameters can use any of the defined [CI variables](#variables)
(including predefined, secure variables and `.gitlab-ci.yml` variables).
-The common use case is to create dynamic environments for branches and use them
-as review apps.
-
----
-
-**Example configurations**
+For example:
```
deploy as review app:
stage: deploy
- script: ...
+ script: make deploy
environment:
name: review-apps/$CI_BUILD_REF_NAME
url: https://$CI_BUILD_REF_NAME.review.example.com/
```
The `deploy as review app` job will be marked as deployment to dynamically
-create the `review-apps/branch-name` environment.
+create the `review-apps/$CI_BUILD_REF_NAME` environment, which `$CI_BUILD_REF_NAME`
+is an [environment variable][variables] set by the Runner. If for example the
+`deploy as review app` job was run in a branch named `pow`, this environment
+should be accessible under `https://pow.review.example.com/`.
-This environment should be accessible under `https://branch-name.review.example.com/`.
+This of course implies that the underlying server which hosts the application
+is properly configured.
-You can see a simple example at https://gitlab.com/gitlab-examples/review-apps-nginx/.
+The common use case is to create dynamic environments for branches and use them
+as Review Apps. You can see a simple example using Review Apps at
+https://gitlab.com/gitlab-examples/review-apps-nginx/.
### artifacts
@@ -1105,3 +1213,5 @@ CI with various languages.
[examples]: ../examples/README.md
[ce-6323]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6323
[environment]: ../environments.md
+[ce-6669]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6669
+[variables]: ../variables/README.md
diff --git a/doc/development/README.md b/doc/development/README.md
index 9706cb1de7fd1927205675b239a62bde714ebb15..fb6a8a5b0950a77634a99a629470f3264368874a 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -14,7 +14,8 @@
- [Testing standards and style guidelines](testing.md)
- [UI guide](ui_guide.md) for building GitLab with existing CSS styles and elements
- [Frontend guidelines](frontend.md)
-- [SQL guidelines](sql.md) for SQL guidelines
+- [SQL guidelines](sql.md) for working with SQL queries
+- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
## Process
diff --git a/doc/development/sidekiq_style_guide.md b/doc/development/sidekiq_style_guide.md
new file mode 100644
index 0000000000000000000000000000000000000000..e3a20f29a094cea7034297615b14a4f6880b9137
--- /dev/null
+++ b/doc/development/sidekiq_style_guide.md
@@ -0,0 +1,38 @@
+# Sidekiq Style Guide
+
+This document outlines various guidelines that should be followed when adding or
+modifying Sidekiq workers.
+
+## Default Queue
+
+Use of the "default" queue is not allowed. Every worker should use a queue that
+matches the worker's purpose the closest. For example, workers that are to be
+executed periodically should use the "cronjob" queue.
+
+A list of all available queues can be found in `config/sidekiq_queues.yml`.
+
+## Dedicated Queues
+
+Most workers should use their own queue. To ease this process a worker can
+include the `DedicatedSidekiqQueue` concern as follows:
+
+```ruby
+class ProcessSomethingWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+end
+```
+
+This will set the queue name based on the class' name, minus the `Worker`
+suffix. In the above example this would lead to the queue being
+`process_something`.
+
+In some cases multiple workers do use the same queue. For example, the various
+workers for updating CI pipelines all use the `pipeline` queue. Adding workers
+to existing queues should be done with care, as adding more workers can lead to
+slow jobs blocking work (even for different jobs) on the shared queue.
+
+## Tests
+
+Each Sidekiq worker must be tested using RSpec, just like any other class. These
+tests should be placed in `spec/workers`.
diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md
index 8940d14559b880f98069250c8361aa1f337c0e05..c0084d9d59c21add44506acc5c56fa000dd0e592 100644
--- a/doc/update/8.12-to-8.13.md
+++ b/doc/update/8.12-to-8.13.md
@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-13-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v3.6.3
+sudo -u git -H git checkout v3.6.6
```
### 6. Update gitlab-workhorse
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 65ed9fae4ecda0800d10f7227cd780e0c38625f5..dfc762fe1d3501874f48084ab1821093ac55aa23 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -22,7 +22,8 @@ with all their related data and be moved into a new GitLab instance.
| GitLab version | Import/Export version |
| -------- | -------- |
-| 8.12.0 to current | 0.1.4 |
+| 8.13.0 to current | 0.1.5 |
+| 8.12.0 | 0.1.4 |
| 8.10.3 | 0.1.3 |
| 8.10.0 | 0.1.2 |
| 8.9.5 | 0.1.1 |
diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb
index 2937d5d7ca8a6647a7235feaff8c11d28fba6db2..f74a9b5df47c19898eb8d93e72a5e0d53bacdbff 100644
--- a/features/steps/project/issues/labels.rb
+++ b/features/steps/project/issues/labels.rb
@@ -8,7 +8,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
end
step 'I remove label \'bug\'' do
- page.within "#label_#{bug_label.id}" do
+ page.within "#project_label_#{bug_label.id}" do
first(:link, 'Delete').click
end
end
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 44346d99f4492401f56a0a34d04834f1443e4a5a..2ccab4334eb6797e12d52dd3a733858207c9f16b 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -9,6 +9,10 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
include SharedUser
include WaitForAjax
+ after do
+ wait_for_ajax if javascript_test?
+ end
+
step 'I click link "New Merge Request"' do
click_link "New Merge Request"
end
diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb
index d3b5b0bdebeaf4fc8b08fc46b3dfccd911804ae3..9dc1fc41b3b9b00f875de462fc9a63fbc5ffc954 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -1,5 +1,3 @@
-require Rails.root.join('features/support/wait_for_ajax')
-
module SharedNote
include Spinach::DSL
include WaitForAjax
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
index fe9e39cf50946207870e775d369de4711b538c1b..dae0d0f918cd53e16a467c970969b55de22a2c99 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -20,5 +20,5 @@ unless ENV['CI'] || ENV['CI_SERVER']
end
Spinach.hooks.before_run do
- TestEnv.warm_asset_cache
+ TestEnv.warm_asset_cache unless ENV['CI'] || ENV['CI_SERVER']
end
diff --git a/features/support/env.rb b/features/support/env.rb
index 569fd444e8652858d763562332b03354a27fb10f..8dbe3624410841dfa79af5589444d19be2de1d8d 100644
--- a/features/support/env.rb
+++ b/features/support/env.rb
@@ -15,7 +15,7 @@ if ENV['CI']
Knapsack::Adapters::SpinachAdapter.bind
end
-%w(select2_helper test_env repo_helpers).each do |f|
+%w(select2_helper test_env repo_helpers wait_for_ajax).each do |f|
require Rails.root.join('spec', 'support', f)
end
diff --git a/features/support/wait_for_ajax.rb b/features/support/wait_for_ajax.rb
deleted file mode 100644
index b90fc1126716d1511e5af745ef3e28026ce3bef6..0000000000000000000000000000000000000000
--- a/features/support/wait_for_ajax.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module WaitForAjax
- def wait_for_ajax
- Timeout.timeout(Capybara.default_max_wait_time) do
- loop until finished_all_ajax_requests?
- end
- end
-
- def finished_all_ajax_requests?
- page.evaluate_script('jQuery.active').zero?
- end
-end
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
index b14dd4f6e83cf30055424f1e59dff1dec4597f57..4ac491edc1bf40cc63121dd23875a92b65d9fd50 100644
--- a/lib/api/boards.rb
+++ b/lib/api/boards.rb
@@ -65,8 +65,8 @@ module API
requires :label_id, type: Integer, desc: 'The ID of an existing label'
end
post '/lists' do
- unless user_project.labels.exists?(params[:label_id])
- render_api_error!({ error: "Label not found!" }, 400)
+ unless available_labels.exists?(params[:label_id])
+ render_api_error!({ error: 'Label not found!' }, 400)
end
authorize!(:admin_list, user_project)
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index b615703df936c11a2f757dfc2ebbd5253284c7e4..6d8274489945ec02b88cb55356a35bc468ab6781 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -54,43 +54,25 @@ module API
not_found!('Branch') unless @branch
protected_branch = user_project.protected_branches.find_by(name: @branch.name)
- developers_can_merge = to_boolean(params[:developers_can_merge])
- developers_can_push = to_boolean(params[:developers_can_push])
-
protected_branch_params = {
- name: @branch.name
+ name: @branch.name,
+ developers_can_push: to_boolean(params[:developers_can_push]),
+ developers_can_merge: to_boolean(params[:developers_can_merge])
}
- # If `developers_can_merge` is switched off, _all_ `DEVELOPER`
- # merge_access_levels need to be deleted.
- if developers_can_merge == false
- protected_branch.merge_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all
- end
+ service_args = [user_project, current_user, protected_branch_params]
- # If `developers_can_push` is switched off, _all_ `DEVELOPER`
- # push_access_levels need to be deleted.
- if developers_can_push == false
- protected_branch.push_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all
- end
+ protected_branch = if protected_branch
+ ProtectedBranches::ApiUpdateService.new(*service_args).execute(protected_branch)
+ else
+ ProtectedBranches::ApiCreateService.new(*service_args).execute
+ end
- protected_branch_params.merge!(
- merge_access_levels_attributes: [{
- access_level: developers_can_merge ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
- }],
- push_access_levels_attributes: [{
- access_level: developers_can_push ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
- }]
- )
-
- if protected_branch
- service = ProtectedBranches::UpdateService.new(user_project, current_user, protected_branch_params)
- service.execute(protected_branch)
+ if protected_branch.valid?
+ present @branch, with: Entities::RepoBranch, project: user_project
else
- service = ProtectedBranches::CreateService.new(user_project, current_user, protected_branch_params)
- service.execute
+ render_api_error!(protected_branch.errors.full_messages, 422)
end
-
- present @branch, with: Entities::RepoBranch, project: user_project
end
# Unprotect a single branch
@@ -123,7 +105,7 @@ module API
post ":id/repository/branches" do
authorize_push_project
result = CreateBranchService.new(user_project, current_user).
- execute(params[:branch_name], params[:ref])
+ execute(params[:branch_name], params[:ref])
if result[:status] == :success
present result[:branch],
@@ -142,10 +124,10 @@ module API
# Example Request:
# DELETE /projects/:id/repository/branches/:branch
delete ":id/repository/branches/:branch",
- requirements: { branch: /.+/ } do
+ requirements: { branch: /.+/ } do
authorize_push_project
result = DeleteBranchService.new(user_project, current_user).
- execute(params[:branch])
+ execute(params[:branch])
if result[:status] == :success
{
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 67473f300c97aeabf0092d0be7b6e7601cda742c..8025581d3ca3eac50a96b74a2580d0b34a464ebe 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -6,6 +6,7 @@ module API
SUDO_PARAM = :sudo
def to_boolean(value)
+ return value if [true, false].include?(value)
return true if value =~ /^(true|t|yes|y|1|on)$/i
return false if value =~ /^(false|f|no|n|0|off)$/i
@@ -71,6 +72,10 @@ module API
@project ||= find_project(params[:id])
end
+ def available_labels
+ @available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute
+ end
+
def find_project(id)
project = Project.find_with_namespace(id) || Project.find_by(id: id)
@@ -118,7 +123,7 @@ module API
end
def find_project_label(id)
- label = user_project.labels.find_by_id(id) || user_project.labels.find_by_title(id)
+ label = available_labels.find_by_id(id) || available_labels.find_by_title(id)
label || not_found!('Label')
end
@@ -197,16 +202,11 @@ module API
def validate_label_params(params)
errors = {}
- if params[:labels].present?
- params[:labels].split(',').each do |label_name|
- label = user_project.labels.create_with(
- color: Label::DEFAULT_COLOR).find_or_initialize_by(
- title: label_name.strip)
+ params[:labels].to_s.split(',').each do |label_name|
+ label = available_labels.find_or_initialize_by(title: label_name.strip)
+ next if label.valid?
- if label.invalid?
- errors[label.title] = label.errors
- end
- end
+ errors[label.title] = label.errors
end
errors
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index c806829d69e52610352fc9e4d9fa4af02c249956..642e6345b9eeb821331d58452c6dd5a5e16fbbc3 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -11,7 +11,7 @@ module API
# Example Request:
# GET /projects/:id/labels
get ':id/labels' do
- present user_project.labels, with: Entities::Label, current_user: current_user
+ present available_labels, with: Entities::Label, current_user: current_user
end
# Creates a new label
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 2b685621da9a0c4443e30962ae59a98b490f395e..bf8504e1101858d0e49f6eb951af05ea4bc821dc 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -86,14 +86,11 @@ module API
render_api_error!({ labels: errors }, 400)
end
+ attrs[:labels] = params[:labels] if params[:labels]
+
merge_request = ::MergeRequests::CreateService.new(user_project, current_user, attrs).execute
if merge_request.valid?
- # Find or create labels and attach to issue
- if params[:labels].present?
- merge_request.add_labels_by_names(params[:labels].split(","))
- end
-
present merge_request, with: Entities::MergeRequest, current_user: current_user
else
handle_merge_request_errors! merge_request.errors
@@ -195,15 +192,11 @@ module API
render_api_error!({ labels: errors }, 400)
end
+ attrs[:labels] = params[:labels] if params[:labels]
+
merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, attrs).execute(merge_request)
if merge_request.valid?
- # Find or create labels and attach to issue
- unless params[:labels].nil?
- merge_request.remove_labels
- merge_request.add_labels_by_names(params[:labels].split(","))
- end
-
present merge_request, with: Entities::MergeRequest, current_user: current_user
else
handle_merge_request_errors! merge_request.errors
diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb
index 2e76b91051ffa0f4450688c66ada4367348441e0..794e34874f4de435aa73a83d182140359c5bbd59 100644
--- a/lib/api/system_hooks.rb
+++ b/lib/api/system_hooks.rb
@@ -56,12 +56,10 @@ module API
requires :id, type: Integer, desc: 'The ID of the system hook'
end
delete ":id" do
- begin
- hook = SystemHook.find(params[:id])
- present hook.destroy, with: Entities::Hook
- rescue
- # SystemHook raises an Error if no hook with id found
- end
+ hook = SystemHook.find_by(id: params[:id])
+ not_found!('System hook') unless hook
+
+ present hook.destroy, with: Entities::Hook
end
end
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index e868f628404d147ded45db313115dfe1c36f5ac0..c28e07a76b711f2e5979c97cb7a8e1f3cd321ec6 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -333,11 +333,11 @@ module API
user = User.find_by(id: declared(params).id)
not_found!('User') unless user
- events = user.recent_events.
+ events = user.events.
merge(ProjectsFinder.new.execute(current_user)).
references(:project).
with_associations.
- page(params[:page])
+ recent
present paginate(events), with: Entities::Event
end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index affe34394c2be362dfd6f30976b35b649a68d154..cb213a76a054a1cb9bff4ecfbf260a47ea1a924d 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -208,8 +208,12 @@ module Banzai
@references_per_project ||= begin
refs = Hash.new { |hash, key| hash[key] = Set.new }
- regex = Regexp.union(object_class.reference_pattern,
- object_class.link_reference_pattern)
+ regex =
+ if uses_reference_pattern?
+ Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern)
+ else
+ object_class.link_reference_pattern
+ end
nodes.each do |node|
node.to_html.scan(regex) do
@@ -295,6 +299,14 @@ module Banzai
value
end
end
+
+ # There might be special cases like filters
+ # that should ignore reference pattern
+ # eg: IssueReferenceFilter when using a external issues tracker
+ # In those cases this method should be overridden on the filter subclass
+ def uses_reference_pattern?
+ true
+ end
end
end
end
diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb
index eaa702952ccbeca8b4fc7c76e698ce0a7b9776cf..0d20be557a04f989f2c19856a1c77e6ae9f1e027 100644
--- a/lib/banzai/filter/external_issue_reference_filter.rb
+++ b/lib/banzai/filter/external_issue_reference_filter.rb
@@ -8,7 +8,7 @@ module Banzai
# Public: Find `JIRA-123` issue references in text
#
- # ExternalIssueReferenceFilter.references_in(text) do |match, issue|
+ # ExternalIssueReferenceFilter.references_in(text, pattern) do |match, issue|
# "##{issue}"
# end
#
@@ -17,8 +17,8 @@ module Banzai
# Yields the String match and the String issue reference.
#
# Returns a String replaced with the return of the block.
- def self.references_in(text)
- text.gsub(ExternalIssue.reference_pattern) do |match|
+ def self.references_in(text, pattern)
+ text.gsub(pattern) do |match|
yield match, $~[:issue]
end
end
@@ -27,7 +27,7 @@ module Banzai
# Early return if the project isn't using an external tracker
return doc if project.nil? || default_issues_tracker?
- ref_pattern = ExternalIssue.reference_pattern
+ ref_pattern = issue_reference_pattern
ref_start_pattern = /\A#{ref_pattern}\z/
each_node do |node|
@@ -60,7 +60,7 @@ module Banzai
def issue_link_filter(text, link_text: nil)
project = context[:project]
- self.class.references_in(text) do |match, id|
+ self.class.references_in(text, issue_reference_pattern) do |match, id|
ExternalIssue.new(id, project)
url = url_for_issue(id, project, only_path: context[:only_path])
@@ -82,18 +82,21 @@ module Banzai
end
def default_issues_tracker?
- if RequestStore.active?
- default_issues_tracker_cache[project.id] ||=
- project.default_issues_tracker?
- else
- project.default_issues_tracker?
- end
+ external_issues_cached(:default_issues_tracker?)
+ end
+
+ def issue_reference_pattern
+ external_issues_cached(:issue_reference_pattern)
end
private
- def default_issues_tracker_cache
- RequestStore[:banzai_default_issues_tracker_cache] ||= {}
+ def external_issues_cached(attribute)
+ return project.public_send(attribute) unless RequestStore.active?
+
+ cached_attributes = RequestStore[:banzai_external_issues_tracker_attributes] ||= Hash.new { |h, k| h[k] = {} }
+ cached_attributes[project.id][attribute] = project.public_send(attribute) if cached_attributes[project.id][attribute].nil?
+ cached_attributes[project.id][attribute]
end
end
end
diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb
index 0a29c547a4de70ec177eed8fc1112f0882654e4a..2f19b59e7252de0701adcfec839f412eb9415d27 100644
--- a/lib/banzai/filter/external_link_filter.rb
+++ b/lib/banzai/filter/external_link_filter.rb
@@ -3,10 +3,17 @@ module Banzai
# HTML Filter to modify the attributes of external links
class ExternalLinkFilter < HTML::Pipeline::Filter
def call
- # Skip non-HTTP(S) links and internal links
- doc.xpath("descendant-or-self::a[starts-with(@href, 'http') and not(starts-with(@href, '#{internal_url}'))]").each do |node|
- node.set_attribute('rel', 'nofollow noreferrer')
- node.set_attribute('target', '_blank')
+ links.each do |node|
+ href = href_to_lowercase_scheme(node["href"].to_s)
+
+ unless node["href"].to_s == href
+ node.set_attribute('href', href)
+ end
+
+ if href =~ /\Ahttp(s)?:\/\// && external_url?(href)
+ node.set_attribute('rel', 'nofollow noreferrer')
+ node.set_attribute('target', '_blank')
+ end
end
doc
@@ -14,6 +21,25 @@ module Banzai
private
+ def links
+ query = 'descendant-or-self::a[@href and not(@href = "")]'
+ doc.xpath(query)
+ end
+
+ def href_to_lowercase_scheme(href)
+ scheme_match = href.match(/\A(\w+):\/\//)
+
+ if scheme_match
+ scheme_match.to_s.downcase + scheme_match.post_match
+ else
+ href
+ end
+ end
+
+ def external_url?(url)
+ !url.start_with?(internal_url)
+ end
+
def internal_url
@internal_url ||= Gitlab.config.gitlab.url
end
diff --git a/lib/banzai/filter/html_entity_filter.rb b/lib/banzai/filter/html_entity_filter.rb
index e008fd428b06863da3a803eb624b6790586b4cad..f3bd587c28bbf92a333d408a4b2bdf5a6ddc538e 100644
--- a/lib/banzai/filter/html_entity_filter.rb
+++ b/lib/banzai/filter/html_entity_filter.rb
@@ -5,7 +5,7 @@ module Banzai
# Text filter that escapes these HTML entities: & " < >
class HtmlEntityFilter < HTML::Pipeline::TextFilter
def call
- ERB::Util.html_escape(text)
+ ERB::Util.html_escape_once(text)
end
end
end
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index 54c5f9a71a440da1ba909682e7c8b574f759be7d..4d1bc687696f993eda7e6b77337af1cc2d447b2b 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -4,6 +4,10 @@ module Banzai
# issues that do not exist are ignored.
#
# This filter supports cross-project references.
+ #
+ # When external issues tracker like Jira is activated we should not
+ # use issue reference pattern, but we should still be able
+ # to reference issues from other GitLab projects.
class IssueReferenceFilter < AbstractReferenceFilter
self.reference_type = :issue
@@ -11,6 +15,10 @@ module Banzai
Issue
end
+ def uses_reference_pattern?
+ context[:project].default_issues_tracker?
+ end
+
def find_object(project, iid)
issues_per_project[project][iid]
end
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
index 8f262ef3d8d55e322b466c8ecba6959bd55893c8..9f9a96cdc6564a62165ca10a451793e39ce7dd68 100644
--- a/lib/banzai/filter/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -9,7 +9,7 @@ module Banzai
end
def find_object(project, id)
- project.labels.find(id)
+ find_labels(project).find(id)
end
def self.references_in(text, pattern = Label.reference_pattern)
@@ -35,7 +35,11 @@ module Banzai
return unless project
label_params = label_params(label_id, label_name)
- project.labels.find_by(label_params)
+ find_labels(project).find_by(label_params)
+ end
+
+ def find_labels(project)
+ LabelsFinder.new(nil, project_id: project.id).execute(skip_authorization: true)
end
# Parameters to pass to `Label.find_by` based on the given arguments
@@ -60,13 +64,50 @@ module Banzai
end
def object_link_text(object, matches)
- if context[:project] == object.project
- LabelsHelper.render_colored_label(object)
+ if same_group?(object) && namespace_match?(matches)
+ render_same_project_label(object)
+ elsif same_project?(object)
+ render_same_project_label(object)
else
- LabelsHelper.render_colored_cross_project_label(object)
+ render_cross_project_label(object, matches)
end
end
+ def same_group?(object)
+ object.is_a?(GroupLabel) && object.group == project.group
+ end
+
+ def namespace_match?(matches)
+ matches[:project].blank? || matches[:project] == project.path_with_namespace
+ end
+
+ def same_project?(object)
+ object.is_a?(ProjectLabel) && object.project == project
+ end
+
+ def user
+ context[:current_user] || context[:author]
+ end
+
+ def project
+ context[:project]
+ end
+
+ def render_same_project_label(object)
+ LabelsHelper.render_colored_label(object)
+ end
+
+ def render_cross_project_label(object, matches)
+ source_project =
+ if matches[:project]
+ Project.find_with_namespace(matches[:project])
+ else
+ object.project
+ end
+
+ LabelsHelper.render_colored_cross_project_label(object, source_project)
+ end
+
def unescape_html_entities(text)
CGI.unescapeHTML(text.to_s)
end
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 2fd1fced65c619d8cccaff18e4321a8a549eadb8..3e33c9399e23506d5eedb20ab8b2ccaa7aee62dc 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -109,6 +109,7 @@ module Ci
validate_job_stage!(name, job)
validate_job_dependencies!(name, job)
+ validate_job_environment!(name, job)
end
end
@@ -150,6 +151,35 @@ module Ci
end
end
+ def validate_job_environment!(name, job)
+ return unless job[:environment]
+ return unless job[:environment].is_a?(Hash)
+
+ environment = job[:environment]
+ validate_on_stop_job!(name, environment, environment[:on_stop])
+ end
+
+ def validate_on_stop_job!(name, environment, on_stop)
+ return unless on_stop
+
+ on_stop_job = @jobs[on_stop.to_sym]
+ unless on_stop_job
+ raise ValidationError, "#{name} job: on_stop job #{on_stop} is not defined"
+ end
+
+ unless on_stop_job[:environment]
+ raise ValidationError, "#{name} job: on_stop job #{on_stop} does not have environment defined"
+ end
+
+ unless on_stop_job[:environment][:name] == environment[:name]
+ raise ValidationError, "#{name} job: on_stop job #{on_stop} have different environment name"
+ end
+
+ unless on_stop_job[:environment][:action] == 'stop'
+ raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined"
+ end
+ end
+
def process?(only_params, except_params, ref, tag, trigger_request)
if only_params.present?
return false unless matching?(only_params, ref, tag, trigger_request)
diff --git a/lib/constraints/namespace_url_constrainer.rb b/lib/constraints/namespace_url_constrainer.rb
index 239201937432d31fca2c6c45594e23bfa48b5f99..91b70143f1137fae7616389d153a6b851a2068f7 100644
--- a/lib/constraints/namespace_url_constrainer.rb
+++ b/lib/constraints/namespace_url_constrainer.rb
@@ -1,6 +1,9 @@
class NamespaceUrlConstrainer
def matches?(request)
- id = request.path.sub(/\A\/+/, '').split('/').first.sub(/.atom\z/, '')
+ id = request.path
+ id = id.sub(/\A#{relative_url_root}/, '') if relative_url_root
+ id = id.sub(/\A\/+/, '').split('/').first
+ id = id.sub(/.atom\z/, '') if id
if id =~ Gitlab::Regex.namespace_regex
find_resource(id)
@@ -10,4 +13,12 @@ class NamespaceUrlConstrainer
def find_resource(id)
Namespace.find_by_path(id)
end
+
+ private
+
+ def relative_url_root
+ if defined?(Gitlab::Application.config.relative_url_root)
+ Gitlab::Application.config.relative_url_root
+ end
+ end
end
diff --git a/lib/gitlab/ci/config/node/environment.rb b/lib/gitlab/ci/config/node/environment.rb
index d388ab6b87908e52fcf5a6441c8629534cf6c08d..9a95ef43628e84081907959e7a44fcce9d36089a 100644
--- a/lib/gitlab/ci/config/node/environment.rb
+++ b/lib/gitlab/ci/config/node/environment.rb
@@ -8,7 +8,7 @@ module Gitlab
class Environment < Entry
include Validatable
- ALLOWED_KEYS = %i[name url]
+ ALLOWED_KEYS = %i[name url action on_stop]
validations do
validate do
@@ -35,6 +35,12 @@ module Gitlab
length: { maximum: 255 },
addressable_url: true,
allow_nil: true
+
+ validates :action,
+ inclusion: { in: %w[start stop], message: 'should be start or stop' },
+ allow_nil: true
+
+ validates :on_stop, type: String, allow_nil: true
end
end
@@ -54,9 +60,17 @@ module Gitlab
value[:url]
end
+ def action
+ value[:action] || 'start'
+ end
+
+ def on_stop
+ value[:on_stop]
+ end
+
def value
case @config
- when String then { name: @config }
+ when String then { name: @config, action: 'start' }
when Hash then @config
else {}
end
diff --git a/lib/gitlab/ci/trace_reader.rb b/lib/gitlab/ci/trace_reader.rb
new file mode 100644
index 0000000000000000000000000000000000000000..37e51536e8fc1b21ddd94a2f9712f801c18e3f04
--- /dev/null
+++ b/lib/gitlab/ci/trace_reader.rb
@@ -0,0 +1,49 @@
+module Gitlab
+ module Ci
+ # This was inspired from: http://stackoverflow.com/a/10219411/1520132
+ class TraceReader
+ BUFFER_SIZE = 4096
+
+ attr_accessor :path, :buffer_size
+
+ def initialize(new_path, buffer_size: BUFFER_SIZE)
+ self.path = new_path
+ self.buffer_size = Integer(buffer_size)
+ end
+
+ def read(last_lines: nil)
+ if last_lines
+ read_last_lines(last_lines)
+ else
+ File.read(path)
+ end
+ end
+
+ def read_last_lines(max_lines)
+ File.open(path) do |file|
+ chunks = []
+ pos = lines = 0
+ max = file.size
+
+ # We want an extra line to make sure fist line has full contents
+ while lines <= max_lines && pos < max
+ pos += buffer_size
+
+ buf = if pos <= max
+ file.seek(-pos, IO::SEEK_END)
+ file.read(buffer_size)
+ else # Reached the head, read only left
+ file.seek(0)
+ file.read(buffer_size - (pos - max))
+ end
+
+ lines += buf.count("\n")
+ chunks.unshift(buf)
+ end
+
+ chunks.join.lines.last(max_lines).join
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/handler/create_note_handler.rb b/lib/gitlab/email/handler/create_note_handler.rb
index 06dae31cc27e7ce139c6a7476918df83c34141a8..447c7a6a6b9465e63ea3c8ada822cef1d13b0233 100644
--- a/lib/gitlab/email/handler/create_note_handler.rb
+++ b/lib/gitlab/email/handler/create_note_handler.rb
@@ -46,7 +46,9 @@ module Gitlab
noteable_type: sent_notification.noteable_type,
noteable_id: sent_notification.noteable_id,
commit_id: sent_notification.commit_id,
- line_code: sent_notification.line_code
+ line_code: sent_notification.line_code,
+ position: sent_notification.position,
+ type: sent_notification.note_type
).execute
end
end
diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb
index ffe49364379b253df0c45d9bb061d1765401bdc0..7e8f35e9298cf18c0e6e831ed9b97b6c89127abf 100644
--- a/lib/gitlab/exclusive_lease.rb
+++ b/lib/gitlab/exclusive_lease.rb
@@ -27,7 +27,7 @@ module Gitlab
# on begin/ensure blocks to cancel a lease, because the 'ensure' does
# not always run. Think of 'kill -9' from the Unicorn master for
# instance.
- #
+ #
# If you find that leases are getting in your way, ask yourself: would
# it be enough to lower the lease timeout? Another thing that might be
# appropriate is to only use a lease for bulk/automated operations, and
@@ -48,6 +48,13 @@ module Gitlab
end
end
+ # Returns true if the key for this lease is set.
+ def exists?
+ Gitlab::Redis.with do |redis|
+ redis.exists(redis_key)
+ end
+ end
+
# No #cancel method. See comments above!
private
diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb
index 501d5a955478abb6aa4cc112e1446bfe1d313d30..222bcdcbf9c8a569bc666c20d02db599e17760ea 100644
--- a/lib/gitlab/fogbugz_import/importer.rb
+++ b/lib/gitlab/fogbugz_import/importer.rb
@@ -74,8 +74,8 @@ module Gitlab
end
def create_label(name)
- color = nice_label_color(name)
- Label.create!(project_id: project.id, title: name, color: color)
+ params = { title: name, color: nice_label_color(name) }
+ ::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true)
end
def user_info(person_id)
@@ -122,25 +122,21 @@ module Gitlab
author_id = user_info(bug['ixPersonOpenedBy'])[:gitlab_id] || project.creator_id
issue = Issue.create!(
- project_id: project.id,
- title: bug['sTitle'],
- description: body,
- author_id: author_id,
- assignee_id: assignee_id,
- state: bug['fOpen'] == 'true' ? 'opened' : 'closed'
+ iid: bug['ixBug'],
+ project_id: project.id,
+ title: bug['sTitle'],
+ description: body,
+ author_id: author_id,
+ assignee_id: assignee_id,
+ state: bug['fOpen'] == 'true' ? 'opened' : 'closed',
+ created_at: date,
+ updated_at: DateTime.parse(bug['dtLastUpdated'])
)
- issue.add_labels_by_names(labels)
- if issue.iid != bug['ixBug']
- issue.update_attribute(:iid, bug['ixBug'])
- end
+ issue_labels = ::LabelsFinder.new(nil, project_id: project.id, title: labels).execute(skip_authorization: true)
+ issue.update_attribute(:label_ids, issue_labels.pluck(:id))
import_issue_comments(issue, comments)
-
- issue.update_attribute(:created_at, date)
-
- last_update = DateTime.parse(bug['dtLastUpdated'])
- issue.update_attribute(:updated_at, last_update)
end
end
diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb
index 78d7a4f27cf4c39fedbc839bc4079d6dd92a3f2e..a7c596dced0198f00de097ffe3b3ea5f54cc6860 100644
--- a/lib/gitlab/gfm/reference_rewriter.rb
+++ b/lib/gitlab/gfm/reference_rewriter.rb
@@ -58,7 +58,7 @@ module Gitlab
referable = find_referable(reference)
return reference unless referable
- cross_reference = referable.to_reference(target_project)
+ cross_reference = build_cross_reference(referable, target_project)
return reference if reference == cross_reference
new_text = before + cross_reference + after
@@ -72,6 +72,14 @@ module Gitlab
extractor.all.first
end
+ def build_cross_reference(referable, target_project)
+ if referable.respond_to?(:project)
+ referable.to_reference(target_project)
+ else
+ referable.to_reference(@source_project, target_project)
+ end
+ end
+
def substitution_valid?(substituted)
@original_html == markdown(substituted)
end
diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb
index 8cacf4f49252999208bc76a524f26b80264c94d6..6dbae64a9fe767b21a977cb48f2abc5c7d429d8a 100644
--- a/lib/gitlab/github_import/base_formatter.rb
+++ b/lib/gitlab/github_import/base_formatter.rb
@@ -10,7 +10,9 @@ module Gitlab
end
def create!
- self.klass.create!(self.attributes)
+ project.public_send(project_association).find_or_create_by!(find_condition) do |record|
+ record.attributes = attributes
+ end
end
private
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 7f424b74efb4ac3c97a4a049bccf01d00350c5b1..85df6547a673d61b77fc5eb7af0b7f86c0f21c98 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -105,18 +105,20 @@ module Gitlab
data = api.send(method, *args)
return data unless data.is_a?(Array)
+ last_response = api.last_response
+
if block_given?
yield data
- each_response_page(&block)
+ # api.last_response could change while we're yielding (e.g. fetching labels for each PR)
+ # so we cache our own last response
+ each_response_page(last_response, &block)
else
- each_response_page { |page| data.concat(page) }
+ each_response_page(last_response) { |page| data.concat(page) }
data
end
end
- def each_response_page
- last_response = api.last_response
-
+ def each_response_page(last_response)
while last_response.rels[:next]
sleep rate_limit_sleep_time if rate_limit_exceed?
last_response = last_response.rels[:next].get
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index 4b70f33a851c22b905271d211701e5155e2c8bbb..ecc28799737b4c06f14371c8dc129225bdca5cc3 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -24,7 +24,8 @@ module Gitlab
import_milestones
import_issues
import_pull_requests
- import_comments
+ import_comments(:issues)
+ import_comments(:pull_requests)
import_wiki
import_releases
handle_errors
@@ -48,7 +49,7 @@ module Gitlab
end
def import_labels
- client.labels(repo, per_page: 100) do |labels|
+ fetch_resources(:labels, repo, per_page: 100) do |labels|
labels.each do |raw|
begin
label = LabelFormatter.new(project, raw).create!
@@ -61,7 +62,7 @@ module Gitlab
end
def import_milestones
- client.milestones(repo, state: :all, per_page: 100) do |milestones|
+ fetch_resources(:milestones, repo, state: :all, per_page: 100) do |milestones|
milestones.each do |raw|
begin
MilestoneFormatter.new(project, raw).create!
@@ -73,7 +74,7 @@ module Gitlab
end
def import_issues
- client.issues(repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues|
+ fetch_resources(:issues, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues|
issues.each do |raw|
gh_issue = IssueFormatter.new(project, raw)
@@ -90,7 +91,7 @@ module Gitlab
end
def import_pull_requests
- client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests|
+ fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests|
pull_requests.each do |raw|
pull_request = PullRequestFormatter.new(project, raw)
next unless pull_request.valid?
@@ -132,8 +133,15 @@ module Gitlab
end
def apply_labels(issuable, raw_issuable)
- if raw_issuable.labels.count > 0
- label_ids = raw_issuable.labels
+ # GH returns labels for issues but not for pull requests!
+ labels = if issuable.is_a?(MergeRequest)
+ client.labels_for_issue(repo, raw_issuable.number)
+ else
+ raw_issuable.labels
+ end
+
+ if labels.count > 0
+ label_ids = labels
.map { |attrs| @labels[attrs.name] }
.compact
@@ -141,23 +149,35 @@ module Gitlab
end
end
- def import_comments
- client.issues_comments(repo, per_page: 100) do |comments|
- create_comments(comments, :issue)
- end
+ def import_comments(issuable_type)
+ resource_type = "#{issuable_type}_comments".to_sym
+
+ # Two notes here:
+ # 1. We don't have a distinctive attribute for comments (unlike issues iid), so we fetch the last inserted note,
+ # compare it against every comment in the current imported page until we find match, and that's where start importing
+ # 2. GH returns comments for _both_ issues and PRs through issues_comments API, while pull_requests_comments returns
+ # only comments on diffs, so select last note not based on noteable_type but on line_code
+ line_code_is = issuable_type == :pull_requests ? 'NOT NULL' : 'NULL'
+ last_note = project.notes.where("line_code IS #{line_code_is}").last
+
+ fetch_resources(resource_type, repo, per_page: 100) do |comments|
+ if last_note
+ discard_inserted_comments(comments, last_note)
+ last_note = nil
+ end
- client.pull_requests_comments(repo, per_page: 100) do |comments|
- create_comments(comments, :pull_request)
+ create_comments(comments)
end
end
- def create_comments(comments, issuable_type)
+ def create_comments(comments)
ActiveRecord::Base.no_touching do
comments.each do |raw|
begin
- comment = CommentFormatter.new(project, raw)
- issuable_class = issuable_type == :issue ? Issue : MergeRequest
- iid = raw.send("#{issuable_type}_url").split('/').last # GH doesn't return parent ID directly
+ comment = CommentFormatter.new(project, raw)
+ # GH does not return info about comment's parent, so we guess it by checking its URL!
+ *_, parent, iid = URI(raw.html_url).path.split('/')
+ issuable_class = parent == 'issues' ? Issue : MergeRequest
issuable = issuable_class.find_by_iid(iid)
next unless issuable
@@ -169,6 +189,24 @@ module Gitlab
end
end
+ def discard_inserted_comments(comments, last_note)
+ last_note_attrs = nil
+
+ cut_off_index = comments.find_index do |raw|
+ comment = CommentFormatter.new(project, raw)
+ comment_attrs = comment.attributes
+ last_note_attrs ||= last_note.slice(*comment_attrs.keys)
+
+ comment_attrs.with_indifferent_access == last_note_attrs
+ end
+
+ # No matching resource in the collection, which means we got halted right on the end of the last page, so all good
+ return unless cut_off_index
+
+ # Otherwise, remove the resources we've already inserted
+ comments.shift(cut_off_index + 1)
+ end
+
def import_wiki
unless project.wiki.repository_exists?
wiki = WikiFormatter.new(project)
@@ -184,7 +222,7 @@ module Gitlab
end
def import_releases
- client.releases(repo, per_page: 100) do |releases|
+ fetch_resources(:releases, repo, per_page: 100) do |releases|
releases.each do |raw|
begin
gh_release = ReleaseFormatter.new(project, raw)
@@ -195,6 +233,47 @@ module Gitlab
end
end
end
+
+ def fetch_resources(resource_type, *opts)
+ return if imported?(resource_type)
+
+ opts.last.merge!(page: current_page(resource_type))
+
+ client.public_send(resource_type, *opts) do |resources|
+ yield resources
+ increment_page(resource_type)
+ end
+
+ imported!(resource_type)
+ end
+
+ def imported?(resource_type)
+ Rails.cache.read("#{cache_key_prefix}:#{resource_type}:imported")
+ end
+
+ def imported!(resource_type)
+ Rails.cache.write("#{cache_key_prefix}:#{resource_type}:imported", true, ex: 1.day)
+ end
+
+ def increment_page(resource_type)
+ key = "#{cache_key_prefix}:#{resource_type}:current-page"
+
+ # Rails.cache.increment calls INCRBY directly on the value stored under the key, which is
+ # a serialized ActiveSupport::Cache::Entry, so it will return an error by Redis, hence this ugly work-around
+ page = Rails.cache.read(key)
+ page += 1
+ Rails.cache.write(key, page)
+
+ page
+ end
+
+ def current_page(resource_type)
+ Rails.cache.fetch("#{cache_key_prefix}:#{resource_type}:current-page", ex: 1.day) { 1 }
+ end
+
+ def cache_key_prefix
+ @cache_key_prefix ||= "github-import:#{project.id}"
+ end
end
end
end
diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb
index 77621de9f4c895cf1ce3669026c0d87e0412dff7..8c32ac59fc5d2b84d3f7274aa598f92b9260b678 100644
--- a/lib/gitlab/github_import/issue_formatter.rb
+++ b/lib/gitlab/github_import/issue_formatter.rb
@@ -20,8 +20,12 @@ module Gitlab
raw_data.comments > 0
end
- def klass
- Issue
+ def project_association
+ :issues
+ end
+
+ def find_condition
+ { iid: number }
end
def number
diff --git a/lib/gitlab/github_import/label_formatter.rb b/lib/gitlab/github_import/label_formatter.rb
index 2cad7fca88e429117c53ee74631fc37c094c001c..211ccdc51bb09bb34c0c22b5c10f92105b05d9b9 100644
--- a/lib/gitlab/github_import/label_formatter.rb
+++ b/lib/gitlab/github_import/label_formatter.rb
@@ -9,14 +9,18 @@ module Gitlab
}
end
- def klass
- Label
+ def project_association
+ :labels
end
def create!
- project.labels.find_or_create_by!(title: title) do |label|
- label.color = color
- end
+ params = attributes.except(:project)
+ service = ::Labels::FindOrCreateService.new(nil, project, params)
+ label = service.execute(skip_authorization: true)
+
+ raise ActiveRecord::RecordInvalid.new(label) unless label.persisted?
+
+ label
end
private
diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb
index b2fa524cf5b41816b9a96583cb10f81298b842c3..401dd962521c4ee0f470d490ff448b26a5ac8dea 100644
--- a/lib/gitlab/github_import/milestone_formatter.rb
+++ b/lib/gitlab/github_import/milestone_formatter.rb
@@ -14,8 +14,12 @@ module Gitlab
}
end
- def klass
- Milestone
+ def project_association
+ :milestones
+ end
+
+ def find_condition
+ { iid: raw_data.number }
end
private
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
index 1408683100fee10fd07876ed1b804b4239b8ea32..b9a227fb11a5d13040549327da0a41933e2f0f55 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -24,8 +24,12 @@ module Gitlab
}
end
- def klass
- MergeRequest
+ def project_association
+ :merge_requests
+ end
+
+ def find_condition
+ { iid: number }
end
def number
diff --git a/lib/gitlab/github_import/release_formatter.rb b/lib/gitlab/github_import/release_formatter.rb
index 73d643b00ad67cb5f3765f184275787ee9e9cbdc..1ad702a6058701baa5d154774f315bd1268a5f6d 100644
--- a/lib/gitlab/github_import/release_formatter.rb
+++ b/lib/gitlab/github_import/release_formatter.rb
@@ -11,8 +11,12 @@ module Gitlab
}
end
- def klass
- Release
+ def project_association
+ :releases
+ end
+
+ def find_condition
+ { tag: raw_data.tag_name }
end
def valid?
diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb
index 62da327931faff96264b4152325c321bc7f16281..1f4edc369288060fb5f75231783e259417ba5bf9 100644
--- a/lib/gitlab/google_code_import/importer.rb
+++ b/lib/gitlab/google_code_import/importer.rb
@@ -92,19 +92,17 @@ module Gitlab
end
issue = Issue.create!(
- project_id: project.id,
- title: raw_issue["title"],
- description: body,
- author_id: project.creator_id,
- assignee_id: assignee_id,
- state: raw_issue["state"] == "closed" ? "closed" : "opened"
+ iid: raw_issue['id'],
+ project_id: project.id,
+ title: raw_issue['title'],
+ description: body,
+ author_id: project.creator_id,
+ assignee_id: assignee_id,
+ state: raw_issue['state'] == 'closed' ? 'closed' : 'opened'
)
- issue.add_labels_by_names(labels)
-
- if issue.iid != raw_issue["id"]
- issue.update_attribute(:iid, raw_issue["id"])
- end
+ issue_labels = ::LabelsFinder.new(nil, project_id: project.id, title: labels).execute(skip_authorization: true)
+ issue.update_attribute(:label_ids, issue_labels.pluck(:id))
import_issue_comments(issue, comments)
end
@@ -236,8 +234,8 @@ module Gitlab
end
def create_label(name)
- color = nice_label_color(name)
- Label.create!(project_id: project.id, name: name, color: color)
+ params = { name: name, color: nice_label_color(name) }
+ ::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true)
end
def format_content(raw_content)
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index 181e288a01404f2d872d3f589faa9464f928ab54..eb667a85b78fc390709d38613295515934272624 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -3,7 +3,7 @@ module Gitlab
extend self
# For every version update, the version history in import_export.md has to be kept up to date.
- VERSION = '0.1.4'
+ VERSION = '0.1.5'
FILENAME_LIMIT = 50
def export_path(relative_path:)
diff --git a/lib/gitlab/import_export/attribute_cleaner.rb b/lib/gitlab/import_export/attribute_cleaner.rb
index b9e4042220a5a680578050e2f69a89ee250a1fd4..34169319b263cf115c6116d2cd8f935985f9fcac 100644
--- a/lib/gitlab/import_export/attribute_cleaner.rb
+++ b/lib/gitlab/import_export/attribute_cleaner.rb
@@ -1,12 +1,27 @@
module Gitlab
module ImportExport
class AttributeCleaner
- ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES
+ ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id']
- def self.clean!(relation_hash:)
- relation_hash.reject! do |key, _value|
- key.end_with?('_id') && !ALLOWED_REFERENCES.include?(key)
- end
+ def self.clean(*args)
+ new(*args).clean
+ end
+
+ def initialize(relation_hash:, relation_class:)
+ @relation_hash = relation_hash
+ @relation_class = relation_class
+ end
+
+ def clean
+ @relation_hash.reject do |key, _value|
+ prohibited_key?(key) || !@relation_class.attribute_method?(key)
+ end.except('id')
+ end
+
+ private
+
+ def prohibited_key?(key)
+ key.end_with?('_id') && !ALLOWED_REFERENCES.include?(key)
end
end
end
diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb
index 113895ba22c96aad98c5a5abbf605438b28ca6c7..ffd17118c91a2112b41f011db6fc2289d6e0cb59 100644
--- a/lib/gitlab/import_export/file_importer.rb
+++ b/lib/gitlab/import_export/file_importer.rb
@@ -43,6 +43,14 @@ module Gitlab
raise Projects::ImportService::Error.new("Unable to decompress #{@archive_file} into #{@shared.export_path}") unless result
+ remove_symlinks!
+ end
+
+ def remove_symlinks!
+ Dir["#{@shared.export_path}/**/*"].each do |path|
+ FileUtils.rm(path) if File.lstat(path).symlink?
+ end
+
true
end
end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index bb9d1080330a833a1eb17382747bba39b9285323..e6ecd11860999cdef08ad2c2d9b217419fa47249 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -1,6 +1,7 @@
# Model relationships to be included in the project import/export
project_tree:
- - :labels
+ - labels:
+ :priorities
- milestones:
- :events
- issues:
@@ -9,7 +10,8 @@ project_tree:
- :author
- :events
- label_links:
- - :label
+ - label:
+ :priorities
- milestone:
- :events
- snippets:
@@ -26,7 +28,8 @@ project_tree:
- :merge_request_diff
- :events
- label_links:
- - :label
+ - label:
+ :priorities
- milestone:
- :events
- pipelines:
@@ -71,6 +74,10 @@ excluded_attributes:
- :awardable_id
methods:
+ labels:
+ - :type
+ label:
+ - :type
statuses:
- :type
services:
diff --git a/lib/gitlab/import_export/json_hash_builder.rb b/lib/gitlab/import_export/json_hash_builder.rb
index 0cc10f4008712da59483bea5ce4efba8922a8cf6..48c09dafcb6b76dc02e3c3f6dd845827d53559f2 100644
--- a/lib/gitlab/import_export/json_hash_builder.rb
+++ b/lib/gitlab/import_export/json_hash_builder.rb
@@ -65,11 +65,17 @@ module Gitlab
# +value+ existing model to be included in the hash
# +parsed_hash+ the original hash
def parse_hash(value)
+ return nil if already_contains_methods?(value)
+
@attributes_finder.parse(value) do |hash|
{ include: hash_or_merge(value, hash) }
end
end
+ def already_contains_methods?(value)
+ value.is_a?(Hash) && value.values.detect { |val| val[:methods]}
+ end
+
# Adds new model configuration to an existing hash with key +current_key+
# It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
#
diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb
index 36c4cf6efa0493deef2d098cdb3f625bf96902e1..b790733f4a75a3bba78db43504aaadd1bd1420bd 100644
--- a/lib/gitlab/import_export/members_mapper.rb
+++ b/lib/gitlab/import_export/members_mapper.rb
@@ -55,7 +55,12 @@ module Gitlab
end
def member_hash(member)
- member.except('id').merge(source_id: @project.id, importing: true)
+ parsed_hash(member).merge('source_id' => @project.id, 'importing' => true)
+ end
+
+ def parsed_hash(member)
+ Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: member.deep_stringify_keys,
+ relation_class: ProjectMember)
end
def find_project_user_query(member)
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index 5a109f24f9f38d6e2b5ddbd468418e224b6be883..c551321c18dbb3aeb879cdc37138e40941bddb70 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -9,8 +9,14 @@ module Gitlab
end
def restore
- json = IO.read(@path)
- @tree_hash = ActiveSupport::JSON.decode(json)
+ begin
+ json = IO.read(@path)
+ @tree_hash = ActiveSupport::JSON.decode(json)
+ rescue => e
+ Rails.logger.error("Import/Export error: #{e.message}")
+ raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
+ end
+
@project_members = @tree_hash.delete('project_members')
ActiveRecord::Base.no_touching do
@@ -110,7 +116,7 @@ module Gitlab
def create_relation(relation, relation_hash_list)
relation_array = [relation_hash_list].flatten.map do |relation_hash|
Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym,
- relation_hash: relation_hash,
+ relation_hash: parsed_relation_hash(relation_hash),
members_mapper: members_mapper,
user: @user,
project_id: restored_project.id)
@@ -118,6 +124,10 @@ module Gitlab
relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
end
+
+ def parsed_relation_hash(relation_hash)
+ relation_hash.merge!('group_id' => restored_project.group.try(:id), 'project_id' => restored_project.id)
+ end
end
end
end
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 9300f789e1bc42e00c84fdf81fa11f341c8df943..a0e80fccad9295c903e2fef142ebecc77251f3e7 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -9,9 +9,12 @@ module Gitlab
builds: 'Ci::Build',
hooks: 'ProjectHook',
merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
- push_access_levels: 'ProtectedBranch::PushAccessLevel' }.freeze
+ push_access_levels: 'ProtectedBranch::PushAccessLevel',
+ labels: :project_labels,
+ priorities: :label_priorities,
+ label: :project_label }.freeze
- USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze
+ USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id].freeze
PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze
@@ -19,9 +22,7 @@ module Gitlab
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
- EXISTING_OBJECT_CHECK = %i[milestone milestones label labels].freeze
-
- FINDER_ATTRIBUTES = %w[title project_id].freeze
+ EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels project_label group_label].freeze
def self.create(*args)
new(*args).create
@@ -29,7 +30,7 @@ module Gitlab
def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project_id:)
@relation_name = OVERRIDES[relation_sym] || relation_sym
- @relation_hash = relation_hash.except('id', 'noteable_id').merge('project_id' => project_id)
+ @relation_hash = relation_hash.except('noteable_id').merge('project_id' => project_id)
@members_mapper = members_mapper
@user = user
@imported_object_retries = 0
@@ -56,6 +57,8 @@ module Gitlab
update_user_references
update_project_references
+
+ handle_group_label if group_label?
reset_ci_tokens if @relation_name == 'Ci::Trigger'
@relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data']
set_st_diffs if @relation_name == :merge_request_diff
@@ -123,6 +126,20 @@ module Gitlab
@relation_hash['target_project_id'] && @relation_hash['target_project_id'] == @relation_hash['source_project_id']
end
+ def group_label?
+ @relation_hash['type'] == 'GroupLabel'
+ end
+
+ def handle_group_label
+ # If there's no group, move the label to a project label
+ if @relation_hash['group_id']
+ @relation_hash['project_id'] = nil
+ @relation_name = :group_label
+ else
+ @relation_hash['type'] = 'ProjectLabel'
+ end
+ end
+
def reset_ci_tokens
return unless Gitlab::ImportExport.reset_tokens?
@@ -155,11 +172,8 @@ module Gitlab
end
def parsed_relation_hash
- @parsed_relation_hash ||= begin
- Gitlab::ImportExport::AttributeCleaner.clean!(relation_hash: @relation_hash)
-
- @relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) }
- end
+ @parsed_relation_hash ||= Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: @relation_hash,
+ relation_class: relation_class)
end
def set_st_diffs
@@ -171,11 +185,9 @@ module Gitlab
# Otherwise always create the record, skipping the extra SELECT clause.
@existing_or_new_object ||= begin
if EXISTING_OBJECT_CHECK.include?(@relation_name)
- events = parsed_relation_hash.delete('events')
+ attribute_hash = attribute_hash_for(['events', 'priorities'])
- unless events.blank?
- existing_object.assign_attributes(events: events)
- end
+ existing_object.assign_attributes(attribute_hash) if attribute_hash.any?
existing_object
else
@@ -184,14 +196,22 @@ module Gitlab
end
end
+ def attribute_hash_for(attributes)
+ attributes.inject({}) do |hash, value|
+ hash[value] = parsed_relation_hash.delete(value) if parsed_relation_hash[value]
+ hash
+ end
+ end
+
def existing_object
@existing_object ||=
begin
- finder_hash = parsed_relation_hash.slice(*FINDER_ATTRIBUTES)
+ finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id]
+ finder_hash = parsed_relation_hash.slice(*finder_attributes)
existing_object = relation_class.find_or_create_by(finder_hash)
# Done in two steps, as MySQL behaves differently than PostgreSQL using
# the +find_or_create_by+ method and does not return the ID the second time.
- existing_object.update(parsed_relation_hash)
+ existing_object.update!(parsed_relation_hash)
existing_object
end
end
diff --git a/lib/gitlab/import_export/version_checker.rb b/lib/gitlab/import_export/version_checker.rb
index fc08082fc862cf59ce36037daec1dc3c3ea6b210..bd3c3ee3b2fa8835f2d893a941ef4febc614467e 100644
--- a/lib/gitlab/import_export/version_checker.rb
+++ b/lib/gitlab/import_export/version_checker.rb
@@ -24,12 +24,19 @@ module Gitlab
end
def verify_version!(version)
- if Gem::Version.new(version) != Gem::Version.new(Gitlab::ImportExport.version)
+ if different_version?(version)
raise Gitlab::ImportExport::Error.new("Import version mismatch: Required #{Gitlab::ImportExport.version} but was #{version}")
else
true
end
end
+
+ def different_version?(version)
+ Gem::Version.new(version) != Gem::Version.new(Gitlab::ImportExport.version)
+ rescue => e
+ Rails.logger.error("Import/Export error: #{e.message}")
+ raise Gitlab::ImportExport::Error.new('Incorrect VERSION format')
+ end
end
end
end
diff --git a/lib/gitlab/issues_labels.rb b/lib/gitlab/issues_labels.rb
index 1bec60882925b8d3a129df8676a2b0389ac08ef3..b8ca7f2f55fc010733c11dbcc71fb6e4ed71d683 100644
--- a/lib/gitlab/issues_labels.rb
+++ b/lib/gitlab/issues_labels.rb
@@ -18,8 +18,8 @@ module Gitlab
{ title: "enhancement", color: green }
]
- labels.each do |label|
- project.labels.create(label)
+ labels.each do |params|
+ ::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true)
end
end
end
diff --git a/lib/tasks/.gitkeep b/lib/tasks/.gitkeep
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake
index a95a3455a4a61d6aa79d98426a594c2c1f279596..78ae187817aa5c80b842dfdcc21f4314c084c182 100644
--- a/lib/tasks/cache.rake
+++ b/lib/tasks/cache.rake
@@ -29,5 +29,5 @@ namespace :cache do
task all: [:db, :redis]
end
- task clear: 'cache:clear:all'
+ task clear: 'cache:clear:redis'
end
diff --git a/lib/tasks/ci/.gitkeep b/lib/tasks/ci/.gitkeep
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index 3492b6ffbbb8f686eef5814608744e5ce65deb9d..8faecec006300efa6107f7e4af43f8ad0f91ddbd 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -1,51 +1,98 @@
require 'spec_helper'
describe Projects::LabelsController do
- let(:project) { create(:project) }
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, namespace: group) }
let(:user) { create(:user) }
before do
project.team << [user, :master]
+
sign_in(user)
end
describe 'GET #index' do
- def create_label(attributes)
- create(:label, attributes.merge(project: project))
- end
+ let!(:label_1) { create(:label, project: project, priority: 1, title: 'Label 1') }
+ let!(:label_2) { create(:label, project: project, priority: 3, title: 'Label 2') }
+ let!(:label_3) { create(:label, project: project, priority: 1, title: 'Label 3') }
+ let!(:label_4) { create(:label, project: project, title: 'Label 4') }
+ let!(:label_5) { create(:label, project: project, title: 'Label 5') }
- before do
- 15.times { |i| create_label(priority: (i % 3) + 1, title: "label #{15 - i}") }
- 5.times { |i| create_label(title: "label #{100 - i}") }
+ let!(:group_label_1) { create(:group_label, group: group, title: 'Group Label 1') }
+ let!(:group_label_2) { create(:group_label, group: group, title: 'Group Label 2') }
+ let!(:group_label_3) { create(:group_label, group: group, title: 'Group Label 3') }
+ let!(:group_label_4) { create(:group_label, group: group, title: 'Group Label 4') }
- get :index, namespace_id: project.namespace.to_param, project_id: project.to_param
+ before do
+ create(:label_priority, project: project, label: group_label_1, priority: 3)
+ create(:label_priority, project: project, label: group_label_2, priority: 1)
end
context '@prioritized_labels' do
- let(:prioritized_labels) { assigns(:prioritized_labels) }
+ before do
+ list_labels
+ end
- it 'contains only prioritized labels' do
- expect(prioritized_labels).to all(have_attributes(priority: a_value > 0))
+ it 'does not include labels without priority' do
+ list_labels
+
+ expect(assigns(:prioritized_labels)).not_to include(group_label_3, group_label_4, label_4, label_5)
end
it 'is sorted by priority, then label title' do
- priorities_and_titles = prioritized_labels.pluck(:priority, :title)
-
- expect(priorities_and_titles.sort).to eq(priorities_and_titles)
+ expect(assigns(:prioritized_labels)).to eq [group_label_2, label_1, label_3, group_label_1, label_2]
end
end
context '@labels' do
- let(:labels) { assigns(:labels) }
+ it 'is sorted by label title' do
+ list_labels
- it 'contains only unprioritized labels' do
- expect(labels).to all(have_attributes(priority: nil))
+ expect(assigns(:labels)).to eq [group_label_3, group_label_4, label_4, label_5]
end
- it 'is sorted by label title' do
- titles = labels.pluck(:title)
+ it 'does not include labels with priority' do
+ list_labels
+
+ expect(assigns(:labels)).not_to include(group_label_2, label_1, label_3, group_label_1, label_2)
+ end
+
+ it 'does not include group labels when project does not belong to a group' do
+ project.update(namespace: create(:namespace))
+
+ list_labels
+
+ expect(assigns(:labels)).not_to include(group_label_3, group_label_4)
+ end
+ end
+
+ def list_labels
+ get :index, namespace_id: project.namespace.to_param, project_id: project.to_param
+ end
+ end
+
+ describe 'POST #generate' do
+ let(:admin) { create(:admin) }
+
+ before do
+ sign_in(admin)
+ end
+
+ context 'personal project' do
+ let(:personal_project) { create(:empty_project) }
+
+ it 'creates labels' do
+ post :generate, namespace_id: personal_project.namespace.to_param, project_id: personal_project.to_param
+
+ expect(response).to have_http_status(302)
+ end
+ end
+
+ context 'project belonging to a group' do
+ it 'creates labels' do
+ post :generate, namespace_id: project.namespace.to_param, project_id: project.to_param
- expect(titles.sort).to eq(titles)
+ expect(response).to have_http_status(302)
end
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index d6980471ea49d852a86a32e202dedb2979835cdd..940d54f8686ea69b19d88f32643b7036d5e71a97 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -913,7 +913,7 @@ describe Projects::MergeRequestsController do
end
describe 'GET ci_environments_status' do
- context 'when the environment is from a forked project' do
+ context 'the environment is from a forked project' do
let!(:forked) { create(:project) }
let!(:environment) { create(:environment, project: forked) }
let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') }
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 074f85157dec5d807e08d7beb53ea7c76d1d362d..d56de9adbc37e0815cdb9ccf246d0a56fe5ec11d 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -271,4 +271,42 @@ describe Projects::ProjectMembersController do
end
end
end
+
+ describe 'POST create' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:stranger) { create(:user) }
+
+ context 'when creating owner' do
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ it 'does not create a member' do
+ expect do
+ post :create, user_ids: stranger.id,
+ namespace_id: project.namespace,
+ access_level: Member::OWNER,
+ project_id: project
+ end.to change { project.members.count }.by(0)
+ end
+ end
+
+ context 'when create master' do
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ it 'creates a member' do
+ expect do
+ post :create, user_ids: stranger.id,
+ namespace_id: project.namespace,
+ access_level: Member::MASTER,
+ project_id: project
+ end.to change { project.members.count }.by(1)
+ end
+ end
+ end
end
diff --git a/spec/factories/label_priorities.rb b/spec/factories/label_priorities.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f25939d2d3eebb9e482e4de3d473a29ace74df7f
--- /dev/null
+++ b/spec/factories/label_priorities.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :label_priority do
+ project factory: :empty_project
+ label
+ sequence(:priority)
+ end
+end
diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb
index eb489099854d719e3752cf6e47701e3fc66f3ce2..3e8822faf972cff162261650b4f06f51a48ddeb6 100644
--- a/spec/factories/labels.rb
+++ b/spec/factories/labels.rb
@@ -1,7 +1,23 @@
FactoryGirl.define do
- factory :label do
+ factory :label, class: ProjectLabel do
sequence(:title) { |n| "label#{n}" }
color "#990000"
project
+
+ transient do
+ priority nil
+ end
+
+ after(:create) do |label, evaluator|
+ if evaluator.priority
+ label.priorities.create(project: label.project, priority: evaluator.priority)
+ end
+ end
+ end
+
+ factory :group_label, class: GroupLabel do
+ sequence(:title) { |n| "label#{n}" }
+ color "#990000"
+ group
end
end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index c6a08d78b78f6242a27c1353cecedf8994feaafa..f780e01253c1b57e449342b1d01516ca85fae422 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -68,5 +68,15 @@ FactoryGirl.define do
factory :closed_merge_request, traits: [:closed]
factory :reopened_merge_request, traits: [:reopened]
factory :merge_request_with_diffs, traits: [:with_diffs]
+
+ factory :labeled_merge_request do
+ transient do
+ labels []
+ end
+
+ after(:create) do |merge_request, evaluator|
+ merge_request.update_attributes(labels: evaluator.labels)
+ end
+ end
end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 0fb1608a0a3862c6bed168cf5d9bd762724cb192..c533ce1d87fde6dfb7262072d55e3a6307484a2f 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -624,6 +624,10 @@ describe 'Issue Boards', feature: true, js: true do
it 'does not show create new list' do
expect(page).not_to have_selector('.js-new-board-list')
end
+
+ it 'does not allow dragging' do
+ expect(page).not_to have_selector('.user-can-drag')
+ end
end
context 'as guest user' do
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
index 68ea4eeae318ae7c210168a3aa0145161c130040..b565586ee1454c4ea9c1c980f5e1a636ef00850c 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/environments_spec.rb
@@ -19,10 +19,22 @@ feature 'Environments', feature: true do
visit namespace_project_environments_path(project.namespace, project)
end
+ context 'shows two tabs' do
+ scenario 'shows "Available" and "Stopped" tab with links' do
+ expect(page).to have_link('Available')
+ expect(page).to have_link('Stopped')
+ end
+ end
+
context 'without environments' do
scenario 'does show no environments' do
expect(page).to have_content('You don\'t have any environments right now.')
end
+
+ scenario 'does show 0 as counter for environments in both tabs' do
+ expect(page.find('.js-available-environments-count').text).to eq('0')
+ expect(page.find('.js-stopped-environments-count').text).to eq('0')
+ end
end
context 'with environments' do
@@ -32,6 +44,11 @@ feature 'Environments', feature: true do
expect(page).to have_link(environment.name)
end
+ scenario 'does show number of available and stopped environments' do
+ expect(page.find('.js-available-environments-count').text).to eq('1')
+ expect(page.find('.js-stopped-environments-count').text).to eq('0')
+ end
+
context 'without deployments' do
scenario 'does show no deployments' do
expect(page).to have_content('No deployments yet')
@@ -44,7 +61,7 @@ feature 'Environments', feature: true do
scenario 'does show deployment SHA' do
expect(page).to have_link(deployment.short_sha)
end
-
+
scenario 'does show deployment internal id' do
expect(page).to have_content(deployment.iid)
end
@@ -65,20 +82,51 @@ feature 'Environments', feature: true do
expect(page).to have_content(manual.name)
expect(manual.reload).to be_pending
end
-
+
scenario 'does show build name and id' do
expect(page).to have_link("#{build.name} (##{build.id})")
end
-
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_selector('.stop-env-link')
+ end
+
+ scenario 'does not show external link button' do
+ expect(page).not_to have_css('external-url')
+ end
+
context 'with external_url' do
given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
given(:build) { create(:ci_build, pipeline: pipeline) }
given(:deployment) { create(:deployment, environment: environment, deployable: build) }
-
+
scenario 'does show an external link button' do
expect(page).to have_link(nil, href: environment.external_url)
end
end
+
+ context 'with stop action' do
+ given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+
+ scenario 'does show stop button' do
+ expect(page).to have_selector('.stop-env-link')
+ end
+
+ scenario 'starts build when stop button clicked' do
+ first('.stop-env-link').click
+
+ expect(page).to have_content('close_app')
+ end
+
+ context 'for reporter' do
+ let(:role) { :reporter }
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_selector('.stop-env-link')
+ end
+ end
+ end
end
end
end
@@ -127,6 +175,10 @@ feature 'Environments', feature: true do
expect(page).to have_link('Re-deploy')
end
+ scenario 'does not show stop button' do
+ expect(page).not_to have_link('Stop')
+ end
+
context 'with manual action' do
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
@@ -140,16 +192,39 @@ feature 'Environments', feature: true do
expect(page).to have_content(manual.name)
expect(manual.reload).to be_pending
end
-
+
context 'with external_url' do
given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
given(:build) { create(:ci_build, pipeline: pipeline) }
given(:deployment) { create(:deployment, environment: environment, deployable: build) }
-
+
scenario 'does show an external link button' do
expect(page).to have_link(nil, href: environment.external_url)
end
end
+
+ context 'with stop action' do
+ given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+
+ scenario 'does show stop button' do
+ expect(page).to have_link('Stop')
+ end
+
+ scenario 'does allow to stop environment' do
+ click_link('Stop')
+
+ expect(page).to have_content('close_app')
+ end
+
+ context 'for reporter' do
+ let(:role) { :reporter }
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_link('Stop')
+ end
+ end
+ end
end
end
end
@@ -196,29 +271,4 @@ feature 'Environments', feature: true do
end
end
end
-
- describe 'when deleting existing environment' do
- given(:environment) { create(:environment, project: project) }
-
- before do
- visit namespace_project_environment_path(project.namespace, project, environment)
- end
-
- context 'when logged as master' do
- given(:role) { :master }
-
- scenario 'does delete environment' do
- click_link 'Destroy'
- expect(page).not_to have_link(environment.name)
- end
- end
-
- context 'when logged as developer' do
- given(:role) { :developer }
-
- scenario 'does not have a Destroy link' do
- expect(page).not_to have_link('Destroy')
- end
- end
- end
end
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index 79cc50bc18eaa4b97baebf2357bddd23c77e01b7..ef00f2099984f866bc7650ec0b537399cc30c6b3 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
describe 'Awards Emoji', feature: true do
+ include WaitForAjax
+
let!(:project) { create(:project) }
let!(:user) { create(:user) }
@@ -16,20 +18,22 @@ describe 'Awards Emoji', feature: true do
project: project)
end
+ let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") }
+
before do
visit namespace_project_issue_path(project.namespace, project, issue)
end
it 'increments the thumbsdown emoji', js: true do
find('[data-emoji="thumbsdown"]').click
- sleep 2
+ wait_for_ajax
expect(thumbsdown_emoji).to have_text("1")
end
context 'click the thumbsup emoji' do
it 'increments the thumbsup emoji', js: true do
find('[data-emoji="thumbsup"]').click
- sleep 2
+ wait_for_ajax
expect(thumbsup_emoji).to have_text("1")
end
@@ -41,7 +45,7 @@ describe 'Awards Emoji', feature: true do
context 'click the thumbsdown emoji' do
it 'increments the thumbsdown emoji', js: true do
find('[data-emoji="thumbsdown"]').click
- sleep 2
+ wait_for_ajax
expect(thumbsdown_emoji).to have_text("1")
end
@@ -49,13 +53,45 @@ describe 'Awards Emoji', feature: true do
expect(thumbsup_emoji).to have_text("0")
end
end
+
+ it 'toggles the smiley emoji on a note', js: true do
+ toggle_smiley_emoji(true)
+
+ within('.note-awards') do
+ expect(find(emoji_counter)).to have_text("1")
+ end
+
+ toggle_smiley_emoji(false)
+
+ within('.note-awards') do
+ expect(page).not_to have_selector(emoji_counter)
+ end
+ end
end
def thumbsup_emoji
- page.all('span.js-counter').first
+ page.all(emoji_counter).first
end
def thumbsdown_emoji
- page.all('span.js-counter').last
+ page.all(emoji_counter).last
+ end
+
+ def emoji_counter
+ 'span.js-counter'
+ end
+
+ def toggle_smiley_emoji(status)
+ within('.note') do
+ find('.note-emoji-button').click
+ end
+
+ unless status
+ first('[data-emoji="smiley"]').click
+ else
+ find('[data-emoji="smiley"]').click
+ end
+
+ wait_for_ajax
end
end
diff --git a/spec/features/issues/reset_filters_spec.rb b/spec/features/issues/reset_filters_spec.rb
index f4d0f13c3d5221a3ea8603edfdc2cea8a44ee74a..c9a3ecf16ea1ae528bbb456eba4a80647f6d339e 100644
--- a/spec/features/issues/reset_filters_spec.rb
+++ b/spec/features/issues/reset_filters_spec.rb
@@ -75,6 +75,14 @@ feature 'Issues filter reset button', feature: true, js: true do
end
end
+ context 'when no filters have been applied' do
+ it 'the reset link should not be visible' do
+ visit_issues(project)
+ expect(page).to have_css('.issue', count: 2)
+ expect(page).not_to have_css '.reset_filters'
+ end
+ end
+
def reset_filters
find('.reset-filters').click
end
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index 996f39ea06d1d8f646be90e72a9a2cd2114860ee..76bcfbe523a4a113a047a9908573288706703015 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -215,4 +215,69 @@ feature 'Login', feature: true do
end
end
end
+
+ describe 'UI tabs and panes' do
+ context 'when no defaults are changed' do
+ it 'correctly renders tabs and panes' do
+ ensure_tab_pane_correctness
+ end
+ end
+
+ context 'when signup is disabled' do
+ before do
+ stub_application_setting(signup_enabled: false)
+ end
+
+ it 'correctly renders tabs and panes' do
+ ensure_tab_pane_correctness
+ end
+ end
+
+ context 'when ldap is enabled' do
+ before do
+ visit new_user_session_path
+ allow(page).to receive(:form_based_providers).and_return([:ldapmain])
+ allow(page).to receive(:ldap_enabled).and_return(true)
+ end
+
+ it 'correctly renders tabs and panes' do
+ ensure_tab_pane_correctness(false)
+ end
+ end
+
+ context 'when crowd is enabled' do
+ before do
+ visit new_user_session_path
+ allow(page).to receive(:form_based_providers).and_return([:crowd])
+ allow(page).to receive(:crowd_enabled?).and_return(true)
+ end
+
+ it 'correctly renders tabs and panes' do
+ ensure_tab_pane_correctness(false)
+ end
+ end
+
+ def ensure_tab_pane_correctness(visit_path = true)
+ if visit_path
+ visit new_user_session_path
+ end
+
+ ensure_tab_pane_counts
+ ensure_one_active_tab
+ ensure_one_active_pane
+ end
+
+ def ensure_tab_pane_counts
+ tabs_count = page.all('[role="tab"]').size
+ expect(page).to have_selector('[role="tabpanel"]', count: tabs_count)
+ end
+
+ def ensure_one_active_tab
+ expect(page).to have_selector('.nav-tabs > li.active', count: 1)
+ end
+
+ def ensure_one_active_pane
+ expect(page).to have_selector('.tab-pane.active', count: 1)
+ end
+ end
end
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
index a506624b30d71f4d4066f129a6d4ee024acd1c22..cfc1244429f75e70b0675011ccab21465eb6d228 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -25,6 +25,20 @@ feature 'Merge request created from fork' do
expect(page).to have_content 'Test merge request'
end
+ context 'source project is deleted' do
+ background do
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ fork_project.destroy!
+ end
+
+ scenario 'user can access merge request' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_content 'Test merge request'
+ expect(page).to have_content "(removed):#{merge_request.source_branch}"
+ end
+ end
+
context 'pipeline present in source project' do
include WaitForAjax
diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
index bc2b0ff3e2c82ae1e03f82363f4581509d7cb3e0..c3c3ab33872abc1cd8f9c13c30a20b1206f4e93b 100644
--- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
@@ -101,7 +101,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do
expect(page).not_to have_link "Merge When Build Succeeds"
end
end
-
+
def visit_merge_request(merge_request)
visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
end
diff --git a/spec/features/merge_requests/widget_deployments_spec.rb b/spec/features/merge_requests/widget_deployments_spec.rb
index 8e23ec50d4af647c2abd2a7f99609fa063ef76ef..6676821b8077b9d7cfe9887bcf75858675cc4f66 100644
--- a/spec/features/merge_requests/widget_deployments_spec.rb
+++ b/spec/features/merge_requests/widget_deployments_spec.rb
@@ -4,23 +4,58 @@ feature 'Widget Deployments Header', feature: true, js: true do
include WaitForAjax
describe 'when deployed to an environment' do
- let(:project) { merge_request.target_project }
- let(:merge_request) { create(:merge_request, :merged) }
- let(:environment) { create(:environment, project: project) }
- let!(:deployment) do
- create(:deployment, environment: environment, sha: project.commit('master').id)
- end
+ given(:user) { create(:user) }
+ given(:project) { merge_request.target_project }
+ given(:merge_request) { create(:merge_request, :merged) }
+ given(:environment) { create(:environment, project: project) }
+ given(:role) { :developer }
+ given(:sha) { project.commit('master').id }
+ given!(:deployment) { create(:deployment, environment: environment, sha: sha) }
+ given!(:manual) { }
- before do
- login_as :admin
+ background do
+ login_as(user)
+ project.team << [user, role]
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
- it 'displays that the environment is deployed' do
+ scenario 'displays that the environment is deployed' do
wait_for_ajax
expect(page).to have_content("Deployed to #{environment.name}")
expect(find('.ci_widget > span > span')['data-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium))
end
+
+ context 'with stop action' do
+ given(:pipeline) { create(:ci_pipeline, project: project) }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+ given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
+ given(:deployment) do
+ create(:deployment, environment: environment, ref: merge_request.target_branch,
+ sha: sha, deployable: build, on_stop: 'close_app')
+ end
+
+ background do
+ wait_for_ajax
+ end
+
+ scenario 'does show stop button' do
+ expect(page).to have_link('Stop environment')
+ end
+
+ scenario 'does start build when stop button clicked' do
+ click_link('Stop environment')
+
+ expect(page).to have_content('close_app')
+ end
+
+ context 'for reporter' do
+ given(:role) { :reporter }
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_link('Stop environment')
+ end
+ end
+ 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 f32834801a02d553be87d13b231d01631b3085cd..3015576f6f8561322936f33ade8be3a77527b4d4 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -3,13 +3,8 @@ require 'spec_helper'
feature 'Import/Export - project import integration test', feature: true, js: true do
include Select2Helper
- let(:admin) { create(:admin) }
- let(:normal_user) { create(:user) }
- let!(:namespace) { create(:namespace, name: "asd", owner: admin) }
let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') }
let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
- let(:project) { Project.last }
- let(:project_hook) { Gitlab::Git::Hook.new('post-receive', project.repository.path) }
background do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
@@ -19,41 +14,43 @@ feature 'Import/Export - project import integration test', feature: true, js: tr
FileUtils.rm_rf(export_path, secure: true)
end
- context 'admin user' do
+ context 'when selecting the namespace' do
+ let(:user) { create(:admin) }
+ let!(:namespace) { create(:namespace, name: "asd", owner: user) }
+
before do
- login_as(admin)
+ login_as(user)
end
scenario 'user imports an exported project successfully' do
- expect(Project.all.count).to be_zero
-
visit new_project_path
- select2('2', from: '#project_namespace_id')
+ select2(namespace.id, from: '#project_namespace_id')
fill_in :project_path, with: 'test-project-path', visible: true
click_link 'GitLab export'
expect(page).to have_content('GitLab project export')
- expect(URI.parse(current_url).query).to eq('namespace_id=2&path=test-project-path')
+ expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=test-project-path")
attach_file('file', file)
- click_on 'Import project' # import starts
+ expect { click_on 'Import project' }.to change { Project.count }.from(0).to(1)
+ project = Project.last
expect(project).not_to be_nil
expect(project.issues).not_to be_empty
expect(project.merge_requests).not_to be_empty
- expect(project_hook).to exist
- expect(wiki_exists?).to be true
+ expect(project_hook_exists?(project)).to be true
+ expect(wiki_exists?(project)).to be true
expect(project.import_status).to eq('finished')
end
scenario 'invalid project' do
- project = create(:project, namespace_id: 2)
+ project = create(:project, namespace: namespace)
visit new_project_path
- select2('2', from: '#project_namespace_id')
+ select2(namespace.id, from: '#project_namespace_id')
fill_in :project_path, with: project.name, visible: true
click_link 'GitLab export'
@@ -66,11 +63,11 @@ feature 'Import/Export - project import integration test', feature: true, js: tr
end
scenario 'project with no name' do
- create(:project, namespace_id: 2)
+ create(:project, namespace: namespace)
visit new_project_path
- select2('2', from: '#project_namespace_id')
+ select2(namespace.id, from: '#project_namespace_id')
# click on disabled element
find(:link, 'GitLab export').trigger('click')
@@ -81,24 +78,30 @@ feature 'Import/Export - project import integration test', feature: true, js: tr
end
end
- context 'normal user' do
+ context 'when limited to the default user namespace' do
+ let(:user) { create(:user) }
before do
- login_as(normal_user)
+ login_as(user)
end
- scenario 'non-admin user is allowed to import a project' do
- expect(Project.all.count).to be_zero
-
+ scenario 'passes correct namespace ID in the URL' do
visit new_project_path
fill_in :project_path, with: 'test-project-path', visible: true
- expect(page).to have_content('GitLab export')
+ click_link 'GitLab export'
+
+ expect(page).to have_content('GitLab project export')
+ expect(URI.parse(current_url).query).to eq("namespace_id=#{user.namespace.id}&path=test-project-path")
end
end
- def wiki_exists?
+ def wiki_exists?(project)
wiki = ProjectWiki.new(project)
File.exist?(wiki.repository.path_to_repo) && !wiki.repository.empty?
end
+
+ def project_hook_exists?(project)
+ Gitlab::Git::Hook.new('post-receive', project.repository.path).exists?
+ end
end
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index d04bdea0fe4b177e54770a1c25f83fd841f5a4bc..bfe59bdb90e75cb869cab6aa86cb9dc81ac5e225 100644
Binary files a/spec/features/projects/import_export/test_project_export.tar.gz and b/spec/features/projects/import_export/test_project_export.tar.gz differ
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index d886909ce850fa3cc85ef8b130765cb7b31c1e3a..2f377312ea5aa94bc48599b6eaa9093ffc156ec3 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -77,7 +77,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "bug" template' do
select_template 'bug'
wait_for_ajax
- preview_template("#{prior_description}\n\n#{template_content}")
+ preview_template("#{template_content}")
save_changes
end
end
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index cb7495da8ebf4b278dae1dff54547be3ee461753..c9fa8315e79e9d688de3f50f8dda4e3b835ecf48 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -3,18 +3,56 @@ require 'spec_helper'
feature 'Prioritize labels', feature: true do
include WaitForAjax
- context 'when project belongs to user' do
- let(:user) { create(:user) }
- let(:project) { create(:project, name: 'test', namespace: user.namespace) }
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, :public, namespace: group) }
+ let!(:bug) { create(:label, project: project, title: 'bug') }
+ let!(:wontfix) { create(:label, project: project, title: 'wontfix') }
+ let!(:feature) { create(:group_label, group: group, title: 'feature') }
- scenario 'user can prioritize a label', js: true do
- bug = create(:label, title: 'bug')
- wontfix = create(:label, title: 'wontfix')
-
- project.labels << bug
- project.labels << wontfix
+ context 'when user belongs to project team' do
+ before do
+ project.team << [user, :developer]
login_as user
+ end
+
+ scenario 'user can prioritize a group label', js: true do
+ visit namespace_project_labels_path(project.namespace, project)
+
+ expect(page).to have_content('No prioritized labels yet')
+
+ page.within('.other-labels') do
+ all('.js-toggle-priority')[1].click
+ wait_for_ajax
+ expect(page).not_to have_content('feature')
+ end
+
+ page.within('.prioritized-labels') do
+ expect(page).not_to have_content('No prioritized labels yet')
+ expect(page).to have_content('feature')
+ end
+ end
+
+ scenario 'user can unprioritize a group label', js: true do
+ create(:label_priority, project: project, label: feature, priority: 1)
+
+ visit namespace_project_labels_path(project.namespace, project)
+
+ page.within('.prioritized-labels') do
+ expect(page).to have_content('feature')
+
+ first('.js-toggle-priority').click
+ wait_for_ajax
+ expect(page).not_to have_content('bug')
+ end
+
+ page.within('.other-labels') do
+ expect(page).to have_content('feature')
+ end
+ end
+
+ scenario 'user can prioritize a project label', js: true do
visit namespace_project_labels_path(project.namespace, project)
expect(page).to have_content('No prioritized labels yet')
@@ -31,19 +69,14 @@ feature 'Prioritize labels', feature: true do
end
end
- scenario 'user can unprioritize a label', js: true do
- bug = create(:label, title: 'bug', priority: 1)
- wontfix = create(:label, title: 'wontfix')
-
- project.labels << bug
- project.labels << wontfix
+ scenario 'user can unprioritize a project label', js: true do
+ create(:label_priority, project: project, label: bug, priority: 1)
- login_as user
visit namespace_project_labels_path(project.namespace, project)
- expect(page).to have_content('bug')
-
page.within('.prioritized-labels') do
+ expect(page).to have_content('bug')
+
first('.js-toggle-priority').click
wait_for_ajax
expect(page).not_to have_content('bug')
@@ -56,23 +89,20 @@ feature 'Prioritize labels', feature: true do
end
scenario 'user can sort prioritized labels and persist across reloads', js: true do
- bug = create(:label, title: 'bug', priority: 1)
- wontfix = create(:label, title: 'wontfix', priority: 2)
-
- project.labels << bug
- project.labels << wontfix
+ create(:label_priority, project: project, label: bug, priority: 1)
+ create(:label_priority, project: project, label: feature, priority: 2)
- login_as user
visit namespace_project_labels_path(project.namespace, project)
expect(page).to have_content 'bug'
+ expect(page).to have_content 'feature'
expect(page).to have_content 'wontfix'
# Sort labels
- find("#label_#{bug.id}").drag_to find("#label_#{wontfix.id}")
+ find("#project_label_#{bug.id}").drag_to find("#group_label_#{feature.id}")
page.within('.prioritized-labels') do
- expect(first('li')).to have_content('wontfix')
+ expect(first('li')).to have_content('feature')
expect(page.all('li').last).to have_content('bug')
end
@@ -80,7 +110,7 @@ feature 'Prioritize labels', feature: true do
wait_for_ajax
page.within('.prioritized-labels') do
- expect(first('li')).to have_content('wontfix')
+ expect(first('li')).to have_content('feature')
expect(page.all('li').last).to have_content('bug')
end
end
@@ -88,28 +118,26 @@ feature 'Prioritize labels', feature: true do
context 'as a guest' do
it 'does not prioritize labels' do
- user = create(:user)
guest = create(:user)
- project = create(:project, name: 'test', namespace: user.namespace)
-
- create(:label, title: 'bug')
login_as guest
+
visit namespace_project_labels_path(project.namespace, project)
+ expect(page).to have_content 'bug'
+ expect(page).to have_content 'wontfix'
+ expect(page).to have_content 'feature'
expect(page).not_to have_css('.prioritized-labels')
end
end
context 'as a non signed in user' do
it 'does not prioritize labels' do
- user = create(:user)
- project = create(:project, name: 'test', namespace: user.namespace)
-
- create(:label, title: 'bug')
-
visit namespace_project_labels_path(project.namespace, project)
+ expect(page).to have_content 'bug'
+ expect(page).to have_content 'wontfix'
+ expect(page).to have_content 'feature'
expect(page).not_to have_css('.prioritized-labels')
end
end
diff --git a/spec/features/projects/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb
index 47482bc3cc95e294bea9977ec788f997a0cf7125..db56a50e0584ec8690161b59694c09301c91dcae 100644
--- a/spec/features/projects/pipelines_spec.rb
+++ b/spec/features/projects/pipelines_spec.rb
@@ -177,7 +177,7 @@ describe "Pipelines" do
before { click_on 'Retry failed' }
it { expect(page).not_to have_content('Retry failed') }
- it { expect(page).to have_content('retried') }
+ it { expect(page).to have_selector('.retried') }
end
end
diff --git a/spec/features/todos/todos_sorting_spec.rb b/spec/features/todos/todos_sorting_spec.rb
index e74a51acede8b52e189aab15020a3533e27183d9..fec28c55d30e80fad4e52867f79097c6429827bb 100644
--- a/spec/features/todos/todos_sorting_spec.rb
+++ b/spec/features/todos/todos_sorting_spec.rb
@@ -8,60 +8,90 @@ describe "Dashboard > User sorts todos", feature: true do
let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) }
let(:label_3) { create(:label, title: 'label_3', project: project, priority: 3) }
- let(:issue_1) { create(:issue, title: 'issue_1', project: project) }
- let(:issue_2) { create(:issue, title: 'issue_2', project: project) }
- let(:issue_3) { create(:issue, title: 'issue_3', project: project) }
- let(:issue_4) { create(:issue, title: 'issue_4', project: project) }
-
- let!(:merge_request_1) { create(:merge_request, source_project: project, title: "merge_request_1") }
-
- before do
- create(:todo, user: user, project: project, target: issue_4, created_at: 5.hours.ago)
- create(:todo, user: user, project: project, target: issue_2, created_at: 4.hours.ago)
- create(:todo, user: user, project: project, target: issue_3, created_at: 3.hours.ago)
- create(:todo, user: user, project: project, target: issue_1, created_at: 2.hours.ago)
- create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago)
-
- merge_request_1.labels << label_1
- issue_3.labels << label_1
- issue_2.labels << label_3
- issue_1.labels << label_2
-
- project.team << [user, :developer]
- login_as(user)
- visit dashboard_todos_path
- end
+ before { project.team << [user, :developer] }
- it "sorts with oldest created todos first" do
- click_link "Last created"
+ context 'sort options' do
+ let(:issue_1) { create(:issue, title: 'issue_1', project: project) }
+ let(:issue_2) { create(:issue, title: 'issue_2', project: project) }
+ let(:issue_3) { create(:issue, title: 'issue_3', project: project) }
+ let(:issue_4) { create(:issue, title: 'issue_4', project: project) }
- results_list = page.find('.todos-list')
- expect(results_list.all('p')[0]).to have_content("merge_request_1")
- expect(results_list.all('p')[1]).to have_content("issue_1")
- expect(results_list.all('p')[2]).to have_content("issue_3")
- expect(results_list.all('p')[3]).to have_content("issue_2")
- expect(results_list.all('p')[4]).to have_content("issue_4")
- end
+ let!(:merge_request_1) { create(:merge_request, source_project: project, title: "merge_request_1") }
+
+ before do
+ create(:todo, user: user, project: project, target: issue_4, created_at: 5.hours.ago)
+ create(:todo, user: user, project: project, target: issue_2, created_at: 4.hours.ago)
+ create(:todo, user: user, project: project, target: issue_3, created_at: 3.hours.ago)
+ create(:todo, user: user, project: project, target: issue_1, created_at: 2.hours.ago)
+ create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago)
+
+ merge_request_1.labels << label_1
+ issue_3.labels << label_1
+ issue_2.labels << label_3
+ issue_1.labels << label_2
+
+ login_as(user)
+ visit dashboard_todos_path
+ end
+
+ it "sorts with oldest created todos first" do
+ click_link "Last created"
+
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("merge_request_1")
+ expect(results_list.all('p')[1]).to have_content("issue_1")
+ expect(results_list.all('p')[2]).to have_content("issue_3")
+ expect(results_list.all('p')[3]).to have_content("issue_2")
+ expect(results_list.all('p')[4]).to have_content("issue_4")
+ end
- it "sorts with newest created todos first" do
- click_link "Oldest created"
+ it "sorts with newest created todos first" do
+ click_link "Oldest created"
- results_list = page.find('.todos-list')
- expect(results_list.all('p')[0]).to have_content("issue_4")
- expect(results_list.all('p')[1]).to have_content("issue_2")
- expect(results_list.all('p')[2]).to have_content("issue_3")
- expect(results_list.all('p')[3]).to have_content("issue_1")
- expect(results_list.all('p')[4]).to have_content("merge_request_1")
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("issue_4")
+ expect(results_list.all('p')[1]).to have_content("issue_2")
+ expect(results_list.all('p')[2]).to have_content("issue_3")
+ expect(results_list.all('p')[3]).to have_content("issue_1")
+ expect(results_list.all('p')[4]).to have_content("merge_request_1")
+ end
+
+ it "sorts by priority" do
+ click_link "Priority"
+
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("issue_3")
+ expect(results_list.all('p')[1]).to have_content("merge_request_1")
+ expect(results_list.all('p')[2]).to have_content("issue_1")
+ expect(results_list.all('p')[3]).to have_content("issue_2")
+ expect(results_list.all('p')[4]).to have_content("issue_4")
+ end
end
- it "sorts by priority" do
- click_link "Priority"
+ context 'issues and merge requests' do
+ let(:issue_1) { create(:issue, id: 10000, title: 'issue_1', project: project) }
+ let(:issue_2) { create(:issue, id: 10001, title: 'issue_2', project: project) }
+ let(:merge_request_1) { create(:merge_request, id: 10000, title: 'merge_request_1', source_project: project) }
+
+ before do
+ issue_1.labels << label_1
+ issue_2.labels << label_2
+
+ create(:todo, user: user, project: project, target: issue_1)
+ create(:todo, user: user, project: project, target: issue_2)
+ create(:todo, user: user, project: project, target: merge_request_1)
+
+ login_as(user)
+ visit dashboard_todos_path
+ end
+
+ it "doesn't mix issues and merge requests priorities" do
+ click_link "Priority"
- results_list = page.find('.todos-list')
- expect(results_list.all('p')[0]).to have_content("issue_3")
- expect(results_list.all('p')[1]).to have_content("merge_request_1")
- expect(results_list.all('p')[2]).to have_content("issue_1")
- expect(results_list.all('p')[3]).to have_content("issue_2")
- expect(results_list.all('p')[4]).to have_content("issue_4")
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("issue_1")
+ expect(results_list.all('p')[1]).to have_content("issue_2")
+ expect(results_list.all('p')[2]).to have_content("merge_request_1")
+ end
end
end
diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..10cfb66ec1ca2a199ad3c2c454c0f6d33d38e941
--- /dev/null
+++ b/spec/finders/labels_finder_spec.rb
@@ -0,0 +1,101 @@
+require 'spec_helper'
+
+describe LabelsFinder do
+ describe '#execute' do
+ let(:group_1) { create(:group) }
+ let(:group_2) { create(:group) }
+ let(:group_3) { create(:group) }
+
+ let(:project_1) { create(:empty_project, namespace: group_1) }
+ let(:project_2) { create(:empty_project, namespace: group_2) }
+ let(:project_3) { create(:empty_project) }
+ let(:project_4) { create(:empty_project, :public) }
+ let(:project_5) { create(:empty_project, namespace: group_1) }
+
+ let!(:project_label_1) { create(:label, project: project_1, title: 'Label 1') }
+ let!(:project_label_2) { create(:label, project: project_2, title: 'Label 2') }
+ let!(:project_label_4) { create(:label, project: project_4, title: 'Label 4') }
+ let!(:project_label_5) { create(:label, project: project_5, title: 'Label 5') }
+
+ let!(:group_label_1) { create(:group_label, group: group_1, title: 'Label 1') }
+ let!(:group_label_2) { create(:group_label, group: group_1, title: 'Group Label 2') }
+ let!(:group_label_3) { create(:group_label, group: group_2, title: 'Group Label 3') }
+
+ let(:user) { create(:user) }
+
+ before do
+ create(:label, project: project_3, title: 'Label 3')
+ create(:group_label, group: group_3, title: 'Group Label 4')
+
+ project_1.team << [user, :developer]
+ end
+
+ context 'with no filter' do
+ it 'returns labels from projects the user have access' do
+ group_2.add_developer(user)
+
+ finder = described_class.new(user)
+
+ expect(finder.execute).to eq [group_label_2, group_label_3, project_label_1, group_label_1, project_label_2, project_label_4]
+ end
+
+ it 'returns labels available if nil title is supplied' do
+ group_2.add_developer(user)
+ # params[:title] will return `nil` regardless whether it is specified
+ finder = described_class.new(user, title: nil)
+
+ expect(finder.execute).to eq [group_label_2, group_label_3, project_label_1, group_label_1, project_label_2, project_label_4]
+ end
+ end
+
+ context 'filtering by group_id' do
+ it 'returns labels available for any project within the group' do
+ group_1.add_developer(user)
+
+ finder = described_class.new(user, group_id: group_1.id)
+
+ expect(finder.execute).to eq [group_label_2, project_label_1, group_label_1, project_label_5]
+ end
+ end
+
+ context 'filtering by project_id' do
+ it 'returns labels available for the project' do
+ finder = described_class.new(user, project_id: project_1.id)
+
+ expect(finder.execute).to eq [group_label_2, project_label_1, group_label_1]
+ end
+ end
+
+ context 'filtering by title' do
+ it 'returns label with that title' do
+ finder = described_class.new(user, title: 'Group Label 2')
+
+ expect(finder.execute).to eq [group_label_2]
+ end
+
+ it 'returns label with title alias' do
+ finder = described_class.new(user, name: 'Group Label 2')
+
+ expect(finder.execute).to eq [group_label_2]
+ end
+
+ it 'returns no labels if empty title is supplied' do
+ finder = described_class.new(user, title: [])
+
+ expect(finder.execute).to be_empty
+ end
+
+ it 'returns no labels if blank title is supplied' do
+ finder = described_class.new(user, title: '')
+
+ expect(finder.execute).to be_empty
+ end
+
+ it 'returns no labels if empty name is supplied' do
+ finder = described_class.new(user, name: [])
+
+ expect(finder.execute).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json
index f070fa3b254f150095e6e58361bf02a5c9a8ad3c..8d94cf26ecbc9b9805b055520d09702727cbc48f 100644
--- a/spec/fixtures/api/schemas/list.json
+++ b/spec/fixtures/api/schemas/list.json
@@ -13,7 +13,7 @@
"enum": ["backlog", "label", "done"]
},
"label": {
- "type": ["object"],
+ "type": ["object", "null"],
"required": [
"id",
"color",
diff --git a/spec/fixtures/emails/commands_in_reply.eml b/spec/fixtures/emails/commands_in_reply.eml
index 06bf60ab734823074dc90bd071e132fc8a5b9e5a..712f6f797b47f241138749dd9df5ca8612627998 100644
--- a/spec/fixtures/emails/commands_in_reply.eml
+++ b/spec/fixtures/emails/commands_in_reply.eml
@@ -23,8 +23,6 @@ Cool!
/close
/todo
-/due tomorrow
-
On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
wrote:
diff --git a/spec/fixtures/emails/commands_only_reply.eml b/spec/fixtures/emails/commands_only_reply.eml
index aed64224b06e61c95a88a1b57026bc1ab3e08a6b..2d2e2f94290bd7da8f9071e23668737cd3fea629 100644
--- a/spec/fixtures/emails/commands_only_reply.eml
+++ b/spec/fixtures/emails/commands_only_reply.eml
@@ -21,8 +21,6 @@ X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
/close
/todo
-/due tomorrow
-
On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
wrote:
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index 022aba0c0d079946ad05e6749cd9491370616107..594b40303bc50793ec1632de1960d4529e1a748c 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -62,4 +62,21 @@ describe EventsHelper do
expect(helper.event_note(input)).to eq(expected)
end
end
+
+ describe '#event_commit_title' do
+ let(:message) { "foo & bar " + "A" * 70 + "\n" + "B" * 80 }
+ subject { helper.event_commit_title(message) }
+
+ it "returns the first line, truncated to 70 chars" do
+ is_expected.to eq(message[0..66] + "...")
+ end
+
+ it "is not html-safe" do
+ is_expected.not_to be_a(ActiveSupport::SafeBuffer)
+ end
+
+ it "handles empty strings" do
+ expect(helper.event_commit_title("")).to eq("")
+ end
+ end
end
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index 501f150cfda8a3494212648a239e24c2a727a0e5..d30daf4754323d5427cc579b1a130775d104a39e 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -5,27 +5,26 @@ describe LabelsHelper do
let(:project) { create(:empty_project) }
let(:label) { create(:label, project: project) }
- context 'with @project set' do
- before do
- @project = project
- end
-
- it 'uses the instance variable' do
- expect(link_to_label(label)).to match %r{}
+ context 'without subject' do
+ it "uses the label's project" do
+ expect(link_to_label(label)).to match %r{.*}
end
end
- context 'without @project set' do
- it "uses the label's project" do
- expect(link_to_label(label)).to match %r{.*}
+ context 'with a project as subject' do
+ let(:namespace) { build(:namespace, name: 'foo3') }
+ let(:another_project) { build(:empty_project, namespace: namespace, name: 'bar3') }
+
+ it 'links to project issues page' do
+ expect(link_to_label(label, subject: another_project)).to match %r{.*}
end
end
- context 'with a project argument' do
- let(:another_project) { double('project', namespace: 'foo3', to_param: 'bar3') }
+ context 'with a group as subject' do
+ let(:group) { build(:group, name: 'bar') }
- it 'links to merge requests page' do
- expect(link_to_label(label, project: another_project)).to match %r{.*}
+ it 'links to group issues page' do
+ expect(link_to_label(label, subject: group)).to match %r{.*}
end
end
diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
index 7116c09fb21670a5ed590144854e81da809a824d..2f9343fadaff067ba40c74f1e6fd670863f6c988 100644
--- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
@@ -7,12 +7,7 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
IssuesHelper
end
- let(:project) { create(:jira_project) }
-
- context 'JIRA issue references' do
- let(:issue) { ExternalIssue.new('JIRA-123', project) }
- let(:reference) { issue.to_reference }
-
+ shared_examples_for "external issue tracker" do
it 'requires project context' do
expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
end
@@ -20,6 +15,7 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
%w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Issue #{reference}#{elem}>"
+
expect(filter(act).to_html).to eq exp
end
end
@@ -33,25 +29,30 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
it 'links to a valid reference' do
doc = filter("Issue #{reference}")
+ issue_id = doc.css('a').first.attr("data-external-issue")
+
expect(doc.css('a').first.attr('href'))
- .to eq helper.url_for_issue(reference, project)
+ .to eq helper.url_for_issue(issue_id, project)
end
it 'links to the external tracker' do
doc = filter("Issue #{reference}")
+
link = doc.css('a').first.attr('href')
+ issue_id = doc.css('a').first.attr("data-external-issue")
- expect(link).to eq "http://jira.example/browse/#{reference}"
+ expect(link).to eq(helper.url_for_issue(issue_id, project))
end
it 'links with adjacent text' do
doc = filter("Issue (#{reference}.)")
+
expect(doc.to_html).to match(/\(#{reference}<\/a>\.\)/)
end
it 'includes a title attribute' do
doc = filter("Issue #{reference}")
- expect(doc.css('a').first.attr('title')).to eq "Issue in JIRA tracker"
+ expect(doc.css('a').first.attr('title')).to include("Issue in #{project.issues_tracker.title}")
end
it 'escapes the title attribute' do
@@ -69,9 +70,60 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
it 'supports an :only_path context' do
doc = filter("Issue #{reference}", only_path: true)
+
link = doc.css('a').first.attr('href')
+ issue_id = doc.css('a').first["data-external-issue"]
+
+ expect(link).to eq helper.url_for_issue(issue_id, project, only_path: true)
+ end
+
+ context 'with RequestStore enabled' do
+ let(:reference_filter) { HTML::Pipeline.new([described_class]) }
+
+ before { allow(RequestStore).to receive(:active?).and_return(true) }
+
+ it 'queries the collection on the first call' do
+ expect_any_instance_of(Project).to receive(:default_issues_tracker?).once.and_call_original
+ expect_any_instance_of(Project).to receive(:issue_reference_pattern).once.and_call_original
+
+ not_cached = reference_filter.call("look for #{reference}", { project: project })
+
+ expect_any_instance_of(Project).not_to receive(:default_issues_tracker?)
+ expect_any_instance_of(Project).not_to receive(:issue_reference_pattern)
+
+ cached = reference_filter.call("look for #{reference}", { project: project })
- expect(link).to eq helper.url_for_issue("#{reference}", project, only_path: true)
+ # Links must be the same
+ expect(cached[:output].css('a').first[:href]).to eq(not_cached[:output].css('a').first[:href])
+ end
+ end
+ end
+
+ context "redmine project" do
+ let(:project) { create(:redmine_project) }
+ let(:issue) { ExternalIssue.new("#123", project) }
+ let(:reference) { issue.to_reference }
+
+ it_behaves_like "external issue tracker"
+ end
+
+ context "jira project" do
+ let(:project) { create(:jira_project) }
+ let(:reference) { issue.to_reference }
+
+ context "with right markdown" do
+ let(:issue) { ExternalIssue.new("JIRA-123", project) }
+
+ it_behaves_like "external issue tracker"
+ end
+
+ context "with wrong markdown" do
+ let(:issue) { ExternalIssue.new("#123", project) }
+
+ it "ignores reference" do
+ exp = act = "Issue #{reference}"
+ expect(filter(act).to_html).to eq exp
+ end
end
end
end
diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb
index 695a5bc6fd4418fe6d40575e4217e670f4de86ab..167397c736bdd8e6b15355411ab3057b926f083b 100644
--- a/spec/lib/banzai/filter/external_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_link_filter_spec.rb
@@ -46,4 +46,38 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do
expect(doc.at_css('a')['rel']).to include 'noreferrer'
end
end
+
+ context 'for non-lowercase scheme links' do
+ let(:doc_with_http) { filter %q(
) }
+
+ it 'adds rel="nofollow" to external links' do
+ expect(doc_with_http.at_css('a')).to have_attribute('rel')
+ expect(doc_with_https.at_css('a')).to have_attribute('rel')
+
+ expect(doc_with_http.at_css('a')['rel']).to include 'nofollow'
+ expect(doc_with_https.at_css('a')['rel']).to include 'nofollow'
+ end
+
+ it 'adds rel="noreferrer" to external links' do
+ expect(doc_with_http.at_css('a')).to have_attribute('rel')
+ expect(doc_with_https.at_css('a')).to have_attribute('rel')
+
+ expect(doc_with_http.at_css('a')['rel']).to include 'noreferrer'
+ expect(doc_with_https.at_css('a')['rel']).to include 'noreferrer'
+ end
+
+ it 'skips internal links' do
+ internal_link = Gitlab.config.gitlab.url + "/sign_in"
+ url = internal_link.gsub(/\Ahttp/, 'HtTp')
+ act = %Q(Login)
+ exp = %Q(Login)
+ expect(filter(act).to_html).to eq(exp)
+ end
+
+ it 'skips relative links' do
+ exp = act = %q(Relative URL)
+ expect(filter(act).to_html).to eq(exp)
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/html_entity_filter_spec.rb b/spec/lib/banzai/filter/html_entity_filter_spec.rb
index 4c68ce6d6e4e0c457ec4e474f18fd90a657c64a3..f9e6bd609f0a1c7f12073639ae9906125a4b045b 100644
--- a/spec/lib/banzai/filter/html_entity_filter_spec.rb
+++ b/spec/lib/banzai/filter/html_entity_filter_spec.rb
@@ -11,4 +11,9 @@ describe Banzai::Filter::HtmlEntityFilter, lib: true do
expect(output).to eq(escaped)
end
+
+ it 'does not double-escape' do
+ escaped = ERB::Util.html_escape("Merge branch 'blabla' into 'master'")
+ expect(filter(escaped)).to eq(escaped)
+ end
end
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index fce86a9b6ad0952202b05cf794ffa4ed09afb151..a2025672ad9d6d9b276a2a873bce8e875f1be4d1 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -25,9 +25,7 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
let(:reference) { issue.to_reference }
it 'ignores valid references when using non-default tracker' do
- expect_any_instance_of(described_class).to receive(:find_object).
- with(project, issue.iid).
- and_return(nil)
+ allow(project).to receive(:default_issues_tracker?).and_return(false)
exp = act = "Issue #{reference}"
expect(reference_filter(act).to_html).to eq exp
@@ -199,19 +197,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
end
end
- context 'referencing external issues' do
- let(:project) { create(:redmine_project) }
-
- it 'renders internal issue IDs as external issue links' do
- doc = reference_filter('#1')
- link = doc.css('a').first
-
- expect(link.attr('data-reference-type')).to eq('external_issue')
- expect(link.attr('title')).to eq('Issue in Redmine')
- expect(link.attr('data-external-issue')).to eq('1')
- end
- end
-
describe '#issues_per_Project' do
context 'using an internal issue tracker' do
it 'returns a Hash containing the issues per project' do
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index 908ccebbf87b0cb14e544da7473b39ba09a9aa89..9c09f00ae8aa72793d30990aa5478c8b2ac9701b 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -305,6 +305,58 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end
end
+ describe 'group label references' do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, :public, namespace: group) }
+ let(:group_label) { create(:group_label, name: 'gfm references', group: group) }
+
+ context 'without project reference' do
+ let(:reference) { group_label.to_reference(format: :name) }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}", project: project)
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_issues_url(project.namespace, project, label_name: group_label.name)
+ expect(doc.text).to eq 'See gfm references'
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Label (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(#{group_label.name}\.\)))
+ end
+
+ it 'ignores invalid label names' do
+ exp = act = %(Label #{Label.reference_prefix}"#{group_label.name.reverse}")
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'with project reference' do
+ let(:reference) { project.to_reference + group_label.to_reference(format: :name) }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}", project: project)
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_issues_url(project.namespace, project, label_name: group_label.name)
+ expect(doc.text).to eq 'See gfm references'
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Label (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(#{group_label.name}\.\)))
+ end
+
+ it 'ignores invalid label names' do
+ exp = act = %(Label #{project.to_reference}#{Label.reference_prefix}"#{group_label.name.reverse}")
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+ end
+
describe 'cross project label references' do
context 'valid project referenced' do
let(:another_project) { create(:empty_project, :public) }
@@ -339,4 +391,34 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end
end
end
+
+ describe 'cross group label references' do
+ context 'valid project referenced' do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, :public, namespace: group) }
+ let(:another_group) { create(:group) }
+ let(:another_project) { create(:empty_project, :public, namespace: another_group) }
+ let(:project_name) { another_project.name_with_namespace }
+ let(:group_label) { create(:group_label, group: another_group, color: '#00ff00') }
+ let(:reference) { another_project.to_reference + group_label.to_reference }
+
+ let!(:result) { reference_filter("See #{reference}", project: project) }
+
+ it 'points to referenced project issues page' do
+ expect(result.css('a').first.attr('href'))
+ .to eq urls.namespace_project_issues_url(another_project.namespace,
+ another_project,
+ label_name: group_label.name)
+ end
+
+ it 'has valid color' do
+ expect(result.css('a span').first.attr('style'))
+ .to match /background-color: #00ff00/
+ end
+
+ it 'contains cross project content' do
+ expect(result.css('a').first.text).to eq "#{group_label.name} in #{project_name}"
+ end
+ end
+ end
end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 6dedd25e9d321fb50c0f9365b472b1b7770c392f..84f21631719b9e7b8f581149b8ea97c1616dda49 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -754,7 +754,7 @@ module Ci
it 'does return production' do
expect(builds.size).to eq(1)
expect(builds.first[:environment]).to eq(environment)
- expect(builds.first[:options]).to include(environment: { name: environment })
+ expect(builds.first[:options]).to include(environment: { name: environment, action: "start" })
end
end
@@ -796,6 +796,52 @@ module Ci
expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}")
end
end
+
+ context 'when on_stop is specified' do
+ let(:review) { { stage: 'deploy', script: 'test', environment: { name: 'review', on_stop: 'close_review' } } }
+ let(:config) { { review: review, close_review: close_review }.compact }
+
+ context 'with matching job' do
+ let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review', action: 'stop' } } }
+
+ it 'does return a list of builds' do
+ expect(builds.size).to eq(2)
+ expect(builds.first[:environment]).to eq('review')
+ end
+ end
+
+ context 'without matching job' do
+ let(:close_review) { nil }
+
+ it 'raises error' do
+ expect { builds }.to raise_error('review job: on_stop job close_review is not defined')
+ end
+ end
+
+ context 'with close job without environment' do
+ let(:close_review) { { stage: 'deploy', script: 'test' } }
+
+ it 'raises error' do
+ expect { builds }.to raise_error('review job: on_stop job close_review does not have environment defined')
+ end
+ end
+
+ context 'with close job for different environment' do
+ let(:close_review) { { stage: 'deploy', script: 'test', environment: 'production' } }
+
+ it 'raises error' do
+ expect { builds }.to raise_error('review job: on_stop job close_review have different environment name')
+ end
+ end
+
+ context 'with close job without stop action' do
+ let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review' } } }
+
+ it 'raises error' do
+ expect { builds }.to raise_error('review job: on_stop job close_review needs to have action stop defined')
+ end
+ end
+ end
end
describe "Dependencies" do
diff --git a/spec/lib/constraints/namespace_url_constrainer_spec.rb b/spec/lib/constraints/namespace_url_constrainer_spec.rb
index a5feaacb8eea0f6285614bd107fe391a9e3b313d..7814711fe278640e2e2bafa31ccb3b13d638c9f7 100644
--- a/spec/lib/constraints/namespace_url_constrainer_spec.rb
+++ b/spec/lib/constraints/namespace_url_constrainer_spec.rb
@@ -17,6 +17,16 @@ describe NamespaceUrlConstrainer, lib: true do
it { expect(subject.matches?(request '/g/gitlab')).to be_falsey }
it { expect(subject.matches?(request '/.gitlab')).to be_falsey }
end
+
+ context 'relative url' do
+ before do
+ allow(Gitlab::Application.config).to receive(:relative_url_root) { '/gitlab' }
+ end
+
+ it { expect(subject.matches?(request '/gitlab/gitlab')).to be_truthy }
+ it { expect(subject.matches?(request '/gitlab/gitlab-ce')).to be_falsey }
+ it { expect(subject.matches?(request '/gitlab/')).to be_falsey }
+ end
end
def request(path)
diff --git a/spec/lib/gitlab/ci/config/node/environment_spec.rb b/spec/lib/gitlab/ci/config/node/environment_spec.rb
index df453223da731f7f64109393860fda9a24df5eb3..df925ff1afd089b85ad25932f46b7608532cdd23 100644
--- a/spec/lib/gitlab/ci/config/node/environment_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/environment_spec.rb
@@ -28,7 +28,7 @@ describe Gitlab::Ci::Config::Node::Environment do
describe '#value' do
it 'returns valid hash' do
- expect(entry.value).to eq(name: 'production')
+ expect(entry.value).to include(name: 'production')
end
end
@@ -87,6 +87,68 @@ describe Gitlab::Ci::Config::Node::Environment do
end
end
+ context 'when valid action is used' do
+ let(:config) do
+ { name: 'production',
+ action: 'start' }
+ end
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'when invalid action is used' do
+ let(:config) do
+ { name: 'production',
+ action: 'invalid' }
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'contains error about invalid action' do
+ expect(entry.errors)
+ .to include 'environment action should be start or stop'
+ end
+ end
+ end
+
+ context 'when on_stop is used' do
+ let(:config) do
+ { name: 'production',
+ on_stop: 'close_app' }
+ end
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'when invalid on_stop is used' do
+ let(:config) do
+ { name: 'production',
+ on_stop: false }
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'contains error about invalid action' do
+ expect(entry.errors)
+ .to include 'environment on stop should be a string'
+ end
+ end
+ end
+
context 'when variables are used for environment' do
let(:config) do
{ name: 'review/$CI_BUILD_REF_NAME',
diff --git a/spec/lib/gitlab/ci/trace_reader_spec.rb b/spec/lib/gitlab/ci/trace_reader_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f06d78694d60022d4da37695196260e2b2e5162f
--- /dev/null
+++ b/spec/lib/gitlab/ci/trace_reader_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::TraceReader do
+ let(:path) { __FILE__ }
+ let(:lines) { File.readlines(path) }
+ let(:bytesize) { lines.sum(&:bytesize) }
+
+ it 'returns last few lines' do
+ 10.times do
+ subject = build_subject
+ last_lines = random_lines
+
+ expected = lines.last(last_lines).join
+
+ expect(subject.read(last_lines: last_lines)).to eq(expected)
+ end
+ end
+
+ it 'returns everything if trying to get too many lines' do
+ expect(build_subject.read(last_lines: lines.size * 2)).to eq(lines.join)
+ end
+
+ it 'raises an error if not passing an integer for last_lines' do
+ expect do
+ build_subject.read(last_lines: lines)
+ end.to raise_error(ArgumentError)
+ end
+
+ def random_lines
+ Random.rand(lines.size) + 1
+ end
+
+ def random_buffer
+ Random.rand(bytesize) + 1
+ end
+
+ def build_subject
+ described_class.new(__FILE__, buffer_size: random_buffer)
+ end
+end
diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
index 4909fed6b774a82eea3518aad56a65a549576cd6..48660d1dd1b035c12e9a1da674e6bc9ef33a31d4 100644
--- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -12,10 +12,13 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
let(:email_raw) { fixture_file('emails/valid_reply.eml') }
let(:project) { create(:project, :public) }
- let(:noteable) { create(:issue, project: project) }
let(:user) { create(:user) }
+ let(:note) { create(:diff_note_on_merge_request, project: project) }
+ let(:noteable) { note.noteable }
- let!(:sent_notification) { SentNotification.record(noteable, user.id, mail_key) }
+ let!(:sent_notification) do
+ SentNotification.record_note(note, user.id, mail_key)
+ end
context "when the recipient address doesn't include a mail key" do
let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "") }
@@ -82,7 +85,6 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
expect(noteable.reload).to be_closed
- expect(noteable.due_date).to eq(Date.tomorrow)
expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy
end
end
@@ -100,7 +102,6 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
expect(noteable.reload).to be_open
- expect(noteable.due_date).to be_nil
expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
end
end
@@ -117,7 +118,6 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
expect { receiver.execute }.to change { noteable.notes.count }.by(2)
expect(noteable.reload).to be_closed
- expect(noteable.due_date).to eq(Date.tomorrow)
expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy
end
end
@@ -138,10 +138,11 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
it "creates a comment" do
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
- note = noteable.notes.last
+ new_note = noteable.notes.last
- expect(note.author).to eq(sent_notification.recipient)
- expect(note.note).to include("I could not disagree more.")
+ expect(new_note.author).to eq(sent_notification.recipient)
+ expect(new_note.position).to eq(note.position)
+ expect(new_note.note).to include("I could not disagree more.")
end
it "adds all attachments" do
@@ -160,10 +161,11 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
shared_examples 'an email that contains a mail key' do |header|
it "fetches the mail key from the #{header} header and creates a comment" do
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
- note = noteable.notes.last
+ new_note = noteable.notes.last
- expect(note.author).to eq(sent_notification.recipient)
- expect(note.note).to include('I could not disagree more.')
+ expect(new_note.author).to eq(sent_notification.recipient)
+ expect(new_note.position).to eq(note.position)
+ expect(new_note.note).to include('I could not disagree more.')
end
end
diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb
index fbdb7ea34ac531311240eb53ea0c7c1c833dd607..6b3bd08b9784588801f4a20a626720e0cb100cbc 100644
--- a/spec/lib/gitlab/exclusive_lease_spec.rb
+++ b/spec/lib/gitlab/exclusive_lease_spec.rb
@@ -1,21 +1,36 @@
require 'spec_helper'
-describe Gitlab::ExclusiveLease do
- it 'cannot obtain twice before the lease has expired' do
- lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600)
- expect(lease.try_obtain).to eq(true)
- expect(lease.try_obtain).to eq(false)
- end
+describe Gitlab::ExclusiveLease, type: :redis do
+ let(:unique_key) { SecureRandom.hex(10) }
+
+ describe '#try_obtain' do
+ it 'cannot obtain twice before the lease has expired' do
+ lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600)
+ expect(lease.try_obtain).to eq(true)
+ expect(lease.try_obtain).to eq(false)
+ end
- it 'can obtain after the lease has expired' do
- timeout = 1
- lease = Gitlab::ExclusiveLease.new(unique_key, timeout: timeout)
- lease.try_obtain # start the lease
- sleep(2 * timeout) # lease should have expired now
- expect(lease.try_obtain).to eq(true)
+ it 'can obtain after the lease has expired' do
+ timeout = 1
+ lease = Gitlab::ExclusiveLease.new(unique_key, timeout: timeout)
+ lease.try_obtain # start the lease
+ sleep(2 * timeout) # lease should have expired now
+ expect(lease.try_obtain).to eq(true)
+ end
end
- def unique_key
- SecureRandom.hex(10)
+ describe '#exists?' do
+ it 'returns true for an existing lease' do
+ lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600)
+ lease.try_obtain
+
+ expect(lease.exists?).to eq(true)
+ end
+
+ it 'returns false for a lease that does not exist' do
+ lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600)
+
+ expect(lease.exists?).to eq(false)
+ end
end
end
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
index 0af249d8690edb80bceaae9fb7bf6b22323cc2d5..f045463c1cbf5b191b140e8e32f5c2214163fd69 100644
--- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe Gitlab::Gfm::ReferenceRewriter do
let(:text) { 'some text' }
- let(:old_project) { create(:project) }
- let(:new_project) { create(:project) }
+ let(:old_project) { create(:project, name: 'old') }
+ let(:new_project) { create(:project, name: 'new') }
let(:user) { create(:user) }
before { old_project.team << [user, :guest] }
@@ -62,7 +62,7 @@ describe Gitlab::Gfm::ReferenceRewriter do
it { is_expected.to eq "#{ref}, `#1`, #{ref}, `#1`" }
end
- context 'description with labels' do
+ context 'description with project labels' do
let!(:label) { create(:label, id: 123, name: 'test', project: old_project) }
let(:project_ref) { old_project.to_reference }
@@ -76,6 +76,26 @@ describe Gitlab::Gfm::ReferenceRewriter do
it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} }
end
end
+
+ context 'description with group labels' do
+ let(:old_group) { create(:group) }
+ let!(:group_label) { create(:group_label, id: 321, name: 'group label', group: old_group) }
+ let(:project_ref) { old_project.to_reference }
+
+ before do
+ old_project.update(namespace: old_group)
+ end
+
+ context 'label referenced by id' do
+ let(:text) { '#1 and ~321' }
+ it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} }
+ end
+
+ context 'label referenced by text' do
+ let(:text) { '#1 and ~"group label"' }
+ it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} }
+ end
+ end
end
context 'reference contains milestone' do
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index de68e32e5b4a1c7cf4f07f40a12af5dbdc0254be..a5aa387f4f7b6b38c1190bf4daaa719230466fba 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -185,6 +185,7 @@ describe Gitlab::GitAccess, lib: true do
end
end
+ # Run permission checks for a user
def self.run_permission_checks(permissions_matrix)
permissions_matrix.keys.each do |role|
describe "#{role} access" do
@@ -194,13 +195,12 @@ describe Gitlab::GitAccess, lib: true do
else
project.team << [user, role]
end
- end
-
- permissions_matrix[role].each do |action, allowed|
- context action do
- subject { access.push_access_check(changes[action]) }
- it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey }
+ permissions_matrix[role].each do |action, allowed|
+ context action do
+ subject { access.push_access_check(changes[action]) }
+ it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey }
+ end
end
end
end
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
index 8854c8431b5bf59c62f4c935cd4355bb6bc54253..7478f86bd2831bd23b56c4484160a714bd7cc9f5 100644
--- a/spec/lib/gitlab/github_import/importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -2,6 +2,10 @@ require 'spec_helper'
describe Gitlab::GithubImport::Importer, lib: true do
describe '#execute' do
+ before do
+ allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
+ end
+
context 'when an error occurs' do
let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_access_level: ProjectFeature::DISABLED) }
let(:octocat) { double(id: 123456, login: 'octocat') }
@@ -152,12 +156,12 @@ describe Gitlab::GithubImport::Importer, lib: true do
message: 'The remote data could not be fully imported.',
errors: [
{ type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" },
- { type: :milestone, url: "https://api.github.com/repos/octocat/Hello-World/milestones/1", errors: "Validation failed: Title has already been taken" },
{ type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank, Title is too short (minimum is 0 characters)" },
- { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Validation failed: Validate branches Cannot Create: This merge request already exists: [\"New feature\"]" },
+ { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." },
+ { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." },
{ type: :wiki, errors: "Gitlab::Shell::Error" },
{ type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" }
- ]
+ ]
}
described_class.new(project).execute
diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb
index 54f85f8cffc699b19249a77696ad98e1c06a562c..097861fd34db95f76b64710e5e1acc351fb527be 100644
--- a/spec/lib/gitlab/google_code_import/importer_spec.rb
+++ b/spec/lib/gitlab/google_code_import/importer_spec.rb
@@ -15,6 +15,7 @@ describe Gitlab::GoogleCodeImport::Importer, lib: true do
subject { described_class.new(project) }
before do
+ project.team << [project.creator, :master]
project.create_import_data(data: import_data)
end
@@ -31,9 +32,9 @@ describe Gitlab::GoogleCodeImport::Importer, lib: true do
subject.execute
%w(
- Type-Defect Type-Enhancement Type-Task Type-Review Type-Other Milestone-0.12 Priority-Critical
- Priority-High Priority-Medium Priority-Low OpSys-All OpSys-Windows OpSys-Linux OpSys-OSX Security
- Performance Usability Maintainability Component-Panel Component-Taskbar Component-Battery
+ Type-Defect Type-Enhancement Type-Task Type-Review Type-Other Milestone-0.12 Priority-Critical
+ Priority-High Priority-Medium Priority-Low OpSys-All OpSys-Windows OpSys-Linux OpSys-OSX Security
+ Performance Usability Maintainability Component-Panel Component-Taskbar Component-Battery
Component-Systray Component-Clock Component-Launcher Component-Tint2conf Component-Docs Component-New
).each do |label|
label.sub!("-", ": ")
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 5d5836e9bee639e8f282001c8724b5a54612a78a..02b11bd999a695961adb7e0deb61bbc997fe9b01 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -38,6 +38,7 @@ label:
- label_links
- issues
- merge_requests
+- priorities
milestone:
- project
- issues
@@ -125,6 +126,7 @@ project:
- drone_ci_service
- emails_on_push_service
- builds_email_service
+- pipelines_email_service
- irker_service
- pivotaltracker_service
- hipchat_service
@@ -184,4 +186,6 @@ project:
- project_feature
award_emoji:
- awardable
-- user
\ No newline at end of file
+- user
+priorities:
+- label
\ No newline at end of file
diff --git a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
index b8e7932eb4aca0ed7cf501d17ebc1d973850c4e7..63bab0f0d0d261fefc8a56d9072b1814c325fdeb 100644
--- a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
@@ -1,8 +1,10 @@
require 'spec_helper'
describe Gitlab::ImportExport::AttributeCleaner, lib: true do
+ let(:relation_class){ double('relation_class').as_null_object }
let(:unsafe_hash) do
{
+ 'id' => 101,
'service_id' => 99,
'moved_to_id' => 99,
'namespace_id' => 99,
@@ -27,8 +29,9 @@ describe Gitlab::ImportExport::AttributeCleaner, lib: true do
end
it 'removes unwanted attributes from the hash' do
- described_class.clean!(relation_hash: unsafe_hash)
+ # allow(relation_class).to receive(:attribute_method?).and_return(true)
+ parsed_hash = described_class.clean(relation_hash: unsafe_hash, relation_class: relation_class)
- expect(unsafe_hash).to eq(post_safe_hash)
+ expect(parsed_hash).to eq(post_safe_hash)
end
end
diff --git a/spec/lib/gitlab/import_export/file_importer_spec.rb b/spec/lib/gitlab/import_export/file_importer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a88ddd17aca69356ca3745466c8806d5a45825b9
--- /dev/null
+++ b/spec/lib/gitlab/import_export/file_importer_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::FileImporter, lib: true do
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') }
+ let(:export_path) { "#{Dir::tmpdir}/file_importer_spec" }
+ let(:valid_file) { "#{shared.export_path}/valid.json" }
+ let(:symlink_file) { "#{shared.export_path}/invalid.json" }
+ let(:subfolder_symlink_file) { "#{shared.export_path}/subfolder/invalid.json" }
+
+ before do
+ stub_const('Gitlab::ImportExport::FileImporter::MAX_RETRIES', 0)
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ allow_any_instance_of(Gitlab::ImportExport::CommandLineUtil).to receive(:untar_zxf).and_return(true)
+
+ setup_files
+
+ described_class.import(archive_file: '', shared: shared)
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ end
+
+ it 'removes symlinks in root folder' do
+ expect(File.exist?(symlink_file)).to be false
+ end
+
+ it 'removes symlinks in subfolders' do
+ expect(File.exist?(subfolder_symlink_file)).to be false
+ end
+
+ it 'does not remove a valid file' do
+ expect(File.exist?(valid_file)).to be true
+ end
+
+ def setup_files
+ FileUtils.mkdir_p("#{shared.export_path}/subfolder/")
+ FileUtils.touch(valid_file)
+ FileUtils.ln_s(valid_file, symlink_file)
+ FileUtils.ln_s(valid_file, subfolder_symlink_file)
+ end
+end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 98323fe6be478528ed9c0ca0f77688172985bd60..ed9df468cede2a66f28fa23296655a79b553eb0c 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -2,6 +2,21 @@
"description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
"visibility_level": 10,
"archived": false,
+ "labels": [
+ {
+ "id": 2,
+ "title": "test2",
+ "color": "#428bca",
+ "project_id": 8,
+ "created_at": "2016-07-22T08:55:44.161Z",
+ "updated_at": "2016-07-22T08:55:44.161Z",
+ "template": false,
+ "description": "",
+ "type": "ProjectLabel",
+ "priorities": [
+ ]
+ }
+ ],
"issues": [
{
"id": 40,
@@ -64,7 +79,37 @@
"updated_at": "2016-07-22T08:55:44.161Z",
"template": false,
"description": "",
- "priority": null
+ "type": "ProjectLabel"
+ }
+ },
+ {
+ "id": 3,
+ "label_id": 3,
+ "target_id": 40,
+ "target_type": "Issue",
+ "created_at": "2016-07-22T08:57:02.841Z",
+ "updated_at": "2016-07-22T08:57:02.841Z",
+ "label": {
+ "id": 3,
+ "title": "test3",
+ "color": "#428bca",
+ "group_id": 8,
+ "created_at": "2016-07-22T08:55:44.161Z",
+ "updated_at": "2016-07-22T08:55:44.161Z",
+ "template": false,
+ "description": "",
+ "project_id": null,
+ "type": "GroupLabel",
+ "priorities": [
+ {
+ "id": 1,
+ "project_id": 5,
+ "label_id": 1,
+ "priority": 1,
+ "created_at": "2016-10-18T09:35:43.338Z",
+ "updated_at": "2016-10-18T09:35:43.338Z"
+ }
+ ]
}
}
],
@@ -536,7 +581,7 @@
"updated_at": "2016-07-22T08:55:44.161Z",
"template": false,
"description": "",
- "priority": null
+ "type": "ProjectLabel"
}
}
],
@@ -2226,9 +2271,6 @@
}
]
}
- ],
- "labels": [
-
],
"milestones": [
{
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index 7582a732cdfab33d4bcc483469c75034cc5bb8cf..3038ab53ad8bf945769ab7559ecafe213dd0de54 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -1,4 +1,5 @@
require 'spec_helper'
+include ImportExport::CommonUtil
describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
describe 'restore project tree' do
@@ -32,7 +33,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
it 'has the same label associated to two issues' do
restored_project_json
- expect(Label.first.issues.count).to eq(2)
+ expect(ProjectLabel.find_by_title('test2').issues.count).to eq(2)
end
it 'has milestones associated to two separate issues' do
@@ -107,6 +108,41 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(Label.first.label_links.first.target).not_to be_nil
end
+ it 'has project labels' do
+ restored_project_json
+
+ expect(ProjectLabel.count).to eq(2)
+ end
+
+ it 'has no group labels' do
+ restored_project_json
+
+ expect(GroupLabel.count).to eq(0)
+ end
+
+ context 'with group' do
+ let!(:project) do
+ create(:empty_project,
+ name: 'project',
+ path: 'project',
+ builds_access_level: ProjectFeature::DISABLED,
+ issues_access_level: ProjectFeature::DISABLED,
+ group: create(:group))
+ end
+
+ it 'has group labels' do
+ restored_project_json
+
+ expect(GroupLabel.count).to eq(1)
+ end
+
+ it 'has label priorities' do
+ restored_project_json
+
+ expect(GroupLabel.first.priorities).not_to be_empty
+ end
+ end
+
it 'has a project feature' do
restored_project_json
@@ -140,6 +176,19 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(MergeRequest.find_by_title('MR2').source_project_id).to eq(-1)
end
end
+
+ context 'project.json file access check' do
+ it 'does not read a symlink' do
+ Dir.mktmpdir do |tmpdir|
+ setup_symlink(tmpdir, 'project.json')
+ allow(shared).to receive(:export_path).and_call_original
+
+ restored_project_json
+
+ expect(shared.errors.first).not_to include('test')
+ end
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index cf8f2200c57e526f63e9db65da529ea322d26d46..c8bba553558e50d656c97770d47ab194bd850a76 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -111,6 +111,18 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty
end
+ it 'has project and group labels' do
+ label_types = saved_project_json['issues'].first['label_links'].map { |link| link['label']['type']}
+
+ expect(label_types).to match_array(['ProjectLabel', 'GroupLabel'])
+ end
+
+ it 'has priorities associated to labels' do
+ priorities = saved_project_json['issues'].first['label_links'].map { |link| link['label']['priorities']}
+
+ expect(priorities.flatten).not_to be_empty
+ end
+
it 'saves the correct service type' do
expect(saved_project_json['services'].first['type']).to eq('CustomIssueTrackerService')
end
@@ -135,15 +147,20 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
issue = create(:issue, assignee: user)
snippet = create(:project_snippet)
release = create(:release)
+ group = create(:group)
project = create(:project,
:public,
issues: [issue],
snippets: [snippet],
- releases: [release]
+ releases: [release],
+ group: group
)
- label = create(:label, project: project)
- create(:label_link, label: label, target: issue)
+ project_label = create(:label, project: project)
+ group_label = create(:group_label, group: group)
+ create(:label_link, label: project_label, target: issue)
+ create(:label_link, label: group_label, target: issue)
+ create(:label_priority, label: group_label, priority: 1)
milestone = create(:milestone, project: project)
merge_request = create(:merge_request, source_project: project, milestone: milestone)
commit_status = create(:commit_status, project: project)
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 8c8be66df9f604b5f75400b5647cc639872ac36f..feee0f025d86c7c526bba93b0171d37ad42db46d 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -60,11 +60,13 @@ LabelLink:
- target_type
- created_at
- updated_at
-Label:
+ProjectLabel:
- id
- title
- color
+- group_id
- project_id
+- type
- created_at
- updated_at
- template
@@ -329,3 +331,10 @@ AwardEmoji:
- awardable_type
- created_at
- updated_at
+LabelPriority:
+- id
+- project_id
+- label_id
+- priority
+- created_at
+- updated_at
\ No newline at end of file
diff --git a/spec/lib/gitlab/import_export/version_checker_spec.rb b/spec/lib/gitlab/import_export/version_checker_spec.rb
index c680e712b591718e3a27b7a4c68abe06b55f6535..2405ac5abfe3ea5613f6012a191436b96c2d47f3 100644
--- a/spec/lib/gitlab/import_export/version_checker_spec.rb
+++ b/spec/lib/gitlab/import_export/version_checker_spec.rb
@@ -1,8 +1,10 @@
require 'spec_helper'
+include ImportExport::CommonUtil
describe Gitlab::ImportExport::VersionChecker, services: true do
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: '') }
+
describe 'bundle a project Git repo' do
- let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: '') }
let(:version) { Gitlab::ImportExport.version }
before do
@@ -27,4 +29,16 @@ describe Gitlab::ImportExport::VersionChecker, services: true do
end
end
end
+
+ describe 'version file access check' do
+ it 'does not read a symlink' do
+ Dir.mktmpdir do |tmpdir|
+ setup_symlink(tmpdir, 'VERSION')
+
+ described_class.check!(shared: shared)
+
+ expect(shared.errors.first).not_to include('test')
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 60e4bbc85647025e44aafaf370a0f8315b8e37a3..a59d30687f6ecc915a0ac875433419d935f81687 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -298,6 +298,20 @@ describe Issue, "Issuable" do
end
end
+ describe '.order_labels_priority' do
+ let(:label_1) { create(:label, title: 'label_1', project: issue.project, priority: 1) }
+ let(:label_2) { create(:label, title: 'label_2', project: issue.project, priority: 2) }
+
+ subject { Issue.order_labels_priority(excluded_labels: ['label_1']).first.highest_priority }
+
+ before do
+ issue.labels << label_1
+ issue.labels << label_2
+ end
+
+ it { is_expected.to eq(2) }
+ end
+
describe ".with_label" do
let(:project) { create(:project, :public) }
let(:bug) { create(:label, project: project, title: 'bug') }
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 01a4a53a264d5ed33c402728c73f0b88af4a5ee2..ca594a320c0772366241085429dba70672d02308 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -48,4 +48,50 @@ describe Deployment, models: true do
end
end
end
+
+ describe '#stop_action' do
+ let(:build) { create(:ci_build) }
+
+ subject { deployment.stop_action }
+
+ context 'when no other actions' do
+ let(:deployment) { FactoryGirl.build(:deployment, deployable: build) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with other actions' do
+ let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) }
+
+ context 'when matching action is defined' do
+ let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_other_app') }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when no matching action is defined' do
+ let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_app') }
+
+ it { is_expected.to eq(close_action) }
+ end
+ end
+ end
+
+ describe '#stoppable?' do
+ subject { deployment.stoppable? }
+
+ context 'when no other actions' do
+ let(:deployment) { build(:deployment) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when matching action is defined' do
+ let(:build) { create(:ci_build) }
+ let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_app') }
+ let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index e172ee8e59075fba37a4e8330d68011fc928038d..a94e6d0165fb4557826df0b6423e0240172982c2 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -8,6 +8,8 @@ describe Environment, models: true do
it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) }
+ it { is_expected.to delegate_method(:stop_action).to(:last_deployment) }
+
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
it { is_expected.to validate_length_of(:name).is_within(0..255) }
@@ -96,4 +98,72 @@ describe Environment, models: true do
is_expected.to be_nil
end
end
+
+ describe '#stoppable?' do
+ subject { environment.stoppable? }
+
+ context 'when no other actions' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when matching action is defined' do
+ let(:build) { create(:ci_build) }
+ let!(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+ let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) }
+
+ context 'when environment is available' do
+ before do
+ environment.start
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when environment is stopped' do
+ before do
+ environment.stop
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe '#stop!' do
+ let(:user) { create(:user) }
+
+ subject { environment.stop!(user) }
+
+ before do
+ expect(environment).to receive(:stoppable?).and_call_original
+ end
+
+ context 'when no other actions' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'when matching action is defined' do
+ let(:build) { create(:ci_build) }
+ let!(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+
+ context 'when action did not yet finish' do
+ let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') }
+
+ it 'returns the same action' do
+ expect(subject).to eq(close_action)
+ expect(subject.user).to eq(user)
+ end
+ end
+
+ context 'if action did finish' do
+ let!(:close_action) { create(:ci_build, :manual, :success, pipeline: build.pipeline, name: 'close_app') }
+
+ it 'returns a new action of the same type' do
+ is_expected.to be_persisted
+ expect(subject.name).to eq(close_action.name)
+ expect(subject.user).to eq(user)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb
index 4fc3b065592cbfa7997e6ef15022e469a6bdfe94..ebba6e14578ca998b56c2c4b731e8f622a9044bf 100644
--- a/spec/models/external_issue_spec.rb
+++ b/spec/models/external_issue_spec.rb
@@ -10,21 +10,6 @@ describe ExternalIssue, models: true do
it { is_expected.to include_module(Referable) }
end
- describe '.reference_pattern' do
- it 'allows underscores in the project name' do
- expect(ExternalIssue.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
- end
-
- it 'allows numbers in the project name' do
- expect(ExternalIssue.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234'
- end
-
- it 'requires the project name to begin with A-Z' do
- expect(ExternalIssue.reference_pattern.match('3EXT_EXT-1234')).to eq nil
- expect(ExternalIssue.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
- end
- end
-
describe '#to_reference' do
it 'returns a String reference to the object' do
expect(issue.to_reference).to eq issue.id
diff --git a/spec/models/group_label_spec.rb b/spec/models/group_label_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..85eb889225b89627f494681baf40ad8ecefac25c
--- /dev/null
+++ b/spec/models/group_label_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe GroupLabel, models: true do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:group) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:group) }
+ end
+
+ describe '#subject' do
+ it 'aliases group to subject' do
+ subject = described_class.new(group: build(:group))
+
+ expect(subject.subject).to be(subject.group)
+ end
+ end
+
+ describe '#to_reference' do
+ let(:label) { create(:group_label) }
+
+ context 'using id' do
+ it 'returns a String reference to the object' do
+ expect(label.to_reference).to eq "~#{label.id}"
+ end
+ end
+
+ context 'using name' do
+ it 'returns a String reference to the object' do
+ expect(label.to_reference(format: :name)).to eq %(~"#{label.name}")
+ end
+
+ it 'uses id when name contains double quote' do
+ label = create(:label, name: %q{"irony"})
+ expect(label.to_reference(format: :name)).to eq "~#{label.id}"
+ end
+ end
+
+ context 'using invalid format' do
+ it 'raises error' do
+ expect { label.to_reference(format: :invalid) }
+ .to raise_error StandardError, /Unknown format/
+ end
+ end
+ end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 0b3ef9b98fd4ed7c29781c96ecd90b949c5ee30a..47f89f744cb74c1138ed82bf6487de865ef993bf 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -12,6 +12,7 @@ describe Group, models: true do
it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
it { is_expected.to have_many(:shared_projects).through(:project_group_links) }
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
+ it { is_expected.to have_many(:labels).class_name('GroupLabel') }
describe '#members & #requesters' do
let(:requester) { create(:user) }
@@ -264,4 +265,10 @@ describe Group, models: true do
members
end
+
+ describe '#web_url' do
+ it 'returns the canonical URL' do
+ expect(group.web_url).to include("groups/#{group.name}")
+ end
+ end
end
diff --git a/spec/models/label_priority_spec.rb b/spec/models/label_priority_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d18c2f7949a85b6e8e3b2294548b083e33f95d4d
--- /dev/null
+++ b/spec/models/label_priority_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe LabelPriority, models: true do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:label) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:label) }
+ it { is_expected.to validate_numericality_of(:priority).only_integer.is_greater_than_or_equal_to(0) }
+
+ it 'validates uniqueness of label_id scoped to project_id' do
+ create(:label_priority)
+
+ expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:project_id)
+ end
+ end
+end
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index 5a5d1a5d60c23183eb2948a03fdd5f913a635726..0c163659a717ab11279244194eea4a5bb2a4f003 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -1,46 +1,42 @@
require 'spec_helper'
describe Label, models: true do
- let(:label) { create(:label) }
+ describe 'modules' do
+ it { is_expected.to include_module(Referable) }
+ it { is_expected.to include_module(Subscribable) }
+ end
describe 'associations' do
- it { is_expected.to belong_to(:project) }
-
- it { is_expected.to have_many(:label_links).dependent(:destroy) }
it { is_expected.to have_many(:issues).through(:label_links).source(:target) }
+ it { is_expected.to have_many(:label_links).dependent(:destroy) }
it { is_expected.to have_many(:lists).dependent(:destroy) }
- end
-
- describe 'modules' do
- subject { described_class }
-
- it { is_expected.to include_module(Referable) }
+ it { is_expected.to have_many(:priorities).class_name('LabelPriority') }
end
describe 'validation' do
- it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_uniqueness_of(:title).scoped_to([:group_id, :project_id]) }
it 'validates color code' do
- expect(label).not_to allow_value('G-ITLAB').for(:color)
- expect(label).not_to allow_value('AABBCC').for(:color)
- expect(label).not_to allow_value('#AABBCCEE').for(:color)
- expect(label).not_to allow_value('GGHHII').for(:color)
- expect(label).not_to allow_value('#').for(:color)
- expect(label).not_to allow_value('').for(:color)
-
- expect(label).to allow_value('#AABBCC').for(:color)
- expect(label).to allow_value('#abcdef').for(:color)
+ is_expected.not_to allow_value('G-ITLAB').for(:color)
+ is_expected.not_to allow_value('AABBCC').for(:color)
+ is_expected.not_to allow_value('#AABBCCEE').for(:color)
+ is_expected.not_to allow_value('GGHHII').for(:color)
+ is_expected.not_to allow_value('#').for(:color)
+ is_expected.not_to allow_value('').for(:color)
+
+ is_expected.to allow_value('#AABBCC').for(:color)
+ is_expected.to allow_value('#abcdef').for(:color)
end
it 'validates title' do
- expect(label).not_to allow_value('G,ITLAB').for(:title)
- expect(label).not_to allow_value('').for(:title)
-
- expect(label).to allow_value('GITLAB').for(:title)
- expect(label).to allow_value('gitlab').for(:title)
- expect(label).to allow_value('G?ITLAB').for(:title)
- expect(label).to allow_value('G&ITLAB').for(:title)
- expect(label).to allow_value("customer's request").for(:title)
+ is_expected.not_to allow_value('G,ITLAB').for(:title)
+ is_expected.not_to allow_value('').for(:title)
+
+ is_expected.to allow_value('GITLAB').for(:title)
+ is_expected.to allow_value('gitlab').for(:title)
+ is_expected.to allow_value('G?ITLAB').for(:title)
+ is_expected.to allow_value('G&ITLAB').for(:title)
+ is_expected.to allow_value("customer's request").for(:title)
end
end
@@ -51,45 +47,59 @@ describe Label, models: true do
end
end
- describe '#to_reference' do
- context 'using id' do
- it 'returns a String reference to the object' do
- expect(label.to_reference).to eq "~#{label.id}"
- end
- end
+ describe 'priorization' do
+ subject(:label) { create(:label) }
- context 'using name' do
- it 'returns a String reference to the object' do
- expect(label.to_reference(format: :name)).to eq %(~"#{label.name}")
+ let(:project) { label.project }
+
+ describe '#prioritize!' do
+ context 'when label is not prioritized' do
+ it 'creates a label priority' do
+ expect { label.prioritize!(project, 1) }.to change(label.priorities, :count).by(1)
+ end
+
+ it 'sets label priority' do
+ label.prioritize!(project, 1)
+
+ expect(label.priorities.first.priority).to eq 1
+ end
end
- it 'uses id when name contains double quote' do
- label = create(:label, name: %q{"irony"})
- expect(label.to_reference(format: :name)).to eq "~#{label.id}"
+ context 'when label is prioritized' do
+ let!(:priority) { create(:label_priority, project: project, label: label, priority: 0) }
+
+ it 'does not create a label priority' do
+ expect { label.prioritize!(project, 1) }.not_to change(label.priorities, :count)
+ end
+
+ it 'updates label priority' do
+ label.prioritize!(project, 1)
+
+ expect(priority.reload.priority).to eq 1
+ end
end
end
- context 'using invalid format' do
- it 'raises error' do
- expect { label.to_reference(format: :invalid) }
- .to raise_error StandardError, /Unknown format/
+ describe '#unprioritize!' do
+ it 'removes label priority' do
+ create(:label_priority, project: project, label: label, priority: 0)
+
+ expect { label.unprioritize!(project) }.to change(label.priorities, :count).by(-1)
end
end
- context 'cross project reference' do
- let(:project) { create(:project) }
-
- context 'using name' do
- it 'returns cross reference with label name' do
- expect(label.to_reference(project, format: :name))
- .to eq %Q(#{label.project.to_reference}~"#{label.name}")
+ describe '#priority' do
+ context 'when label is not prioritized' do
+ it 'returns nil' do
+ expect(label.priority(project)).to be_nil
end
end
- context 'using id' do
- it 'returns cross reference with label id' do
- expect(label.to_reference(project, format: :id))
- .to eq %Q(#{label.project.to_reference}~#{label.id})
+ context 'when label is prioritized' do
+ it 'returns label priority' do
+ create(:label_priority, project: project, label: label, priority: 1)
+
+ expect(label.priority(project)).to eq 1
end
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 91a423b670c9aab3721d30a511312dfca103d787..6e5137602aaebddc2249cba835680fb5d23366c9 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -334,7 +334,7 @@ describe MergeRequest, models: true do
wip_title = "WIP: #{subject.title}"
expect(subject.wip_title).to eq wip_title
- end
+ end
it "does not add the WIP: prefix multiple times" do
wip_title = "WIP: #{subject.title}"
@@ -640,32 +640,56 @@ describe MergeRequest, models: true do
end
describe '#all_commits_sha' do
- let(:all_commits_sha) do
- subject.merge_request_diffs.flat_map(&:commits).map(&:sha).uniq
- end
+ context 'when merge request is persisted' do
+ let(:all_commits_sha) do
+ subject.merge_request_diffs.flat_map(&:commits).map(&:sha).uniq
+ end
- 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)
+ 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
- end
- context 'with a completely different branch' do
- before do
- subject.update(target_branch: 'v1.0.0')
+ 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_behaves_like 'returning all 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
- 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
+ context 'when merge request is not persisted' do
+ context 'when compare commits are set in the service' do
+ let(:commit) { spy('commit') }
+
+ subject do
+ build(:merge_request, compare_commits: [commit, commit])
+ end
+
+ it 'returns commits from compare commits temporary data' do
+ expect(subject.all_commits_sha).to eq [commit, commit]
+ end
end
- it_behaves_like 'returning all SHA'
+ context 'when compare commits are not set in the service' do
+ subject { build(:merge_request) }
+
+ it 'returns array with diff head sha element only' do
+ expect(subject.all_commits_sha).to eq [subject.diff_head_sha]
+ end
+ end
end
end
@@ -1174,7 +1198,7 @@ describe MergeRequest, models: true do
end
end
- describe "#forked_source_project_missing?" do
+ describe "#source_project_missing?" do
let(:project) { create(:project) }
let(:fork_project) { create(:project, forked_from_project: project) }
let(:user) { create(:user) }
@@ -1187,13 +1211,13 @@ describe MergeRequest, models: true do
target_project: project)
end
- it { expect(merge_request.forked_source_project_missing?).to be_falsey }
+ it { expect(merge_request.source_project_missing?).to be_falsey }
end
context "when the source project is the same as the target project" do
let(:merge_request) { create(:merge_request, source_project: project) }
- it { expect(merge_request.forked_source_project_missing?).to be_falsey }
+ it { expect(merge_request.source_project_missing?).to be_falsey }
end
context "when the fork does not exist" do
@@ -1207,7 +1231,7 @@ describe MergeRequest, models: true do
unlink_project.execute
merge_request.reload
- expect(merge_request.forked_source_project_missing?).to be_truthy
+ expect(merge_request.source_project_missing?).to be_truthy
end
end
end
@@ -1250,38 +1274,6 @@ describe MergeRequest, models: true do
end
end
- describe '#closed_without_source_project?' do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) }
- let(:destroy_service) { Projects::DestroyService.new(fork_project, user) }
-
- context 'when the merge request is closed' do
- let(:closed_merge_request) do
- create(:closed_merge_request,
- source_project: fork_project,
- target_project: project)
- end
-
- it 'returns false if the source project exists' do
- expect(closed_merge_request.closed_without_source_project?).to be_falsey
- end
-
- it 'returns true if the source project does not exist' do
- destroy_service.execute
- closed_merge_request.reload
-
- expect(closed_merge_request.closed_without_source_project?).to be_truthy
- end
- end
-
- context 'when the merge request is open' do
- it 'returns false' do
- expect(subject.closed_without_source_project?).to be_falsey
- end
- end
- end
-
describe '#reopenable?' do
context 'when the merge request is closed' do
it 'returns true' do
diff --git a/spec/models/project_label_spec.rb b/spec/models/project_label_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..18c9d449ee54ef30c870bb1992112cc993f5593b
--- /dev/null
+++ b/spec/models/project_label_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+
+describe ProjectLabel, models: true do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+
+ context 'validates if title must not exist at group level' do
+ let(:group) { create(:group, name: 'gitlab-org') }
+ let(:project) { create(:empty_project, group: group) }
+
+ before do
+ create(:group_label, group: group, title: 'Bug')
+ end
+
+ it 'returns error if title already exists at group level' do
+ label = described_class.new(project: project, title: 'Bug')
+
+ label.valid?
+
+ expect(label.errors[:title]).to include 'already exists at group level for gitlab-org. Please choose another one.'
+ end
+
+ it 'does not returns error if title does not exist at group level' do
+ label = described_class.new(project: project, title: 'Security')
+
+ label.valid?
+
+ expect(label.errors[:title]).to be_empty
+ end
+
+ it 'does not returns error if project does not belong to group' do
+ another_project = create(:empty_project)
+ label = described_class.new(project: another_project, title: 'Bug')
+
+ label.valid?
+
+ expect(label.errors[:title]).to be_empty
+ end
+
+ it 'does not returns error when title does not change' do
+ project_label = create(:label, project: project, name: 'Security')
+ create(:group_label, group: group, name: 'Security')
+ project_label.description = 'Security related stuff.'
+
+ project_label.valid?
+
+ expect(project_label.errors[:title]).to be_empty
+ end
+ end
+
+ context 'when attempting to add more than one priority to the project label' do
+ it 'returns error' do
+ subject.priorities.build
+ subject.priorities.build
+
+ subject.valid?
+
+ expect(subject.errors[:priorities]).to include 'Number of permitted priorities exceeded'
+ end
+ end
+ end
+
+ describe '#subject' do
+ it 'aliases project to subject' do
+ subject = described_class.new(project: build(:empty_project))
+
+ expect(subject.subject).to be(subject.project)
+ end
+ end
+
+ describe '#to_reference' do
+ let(:label) { create(:label) }
+
+ context 'using id' do
+ it 'returns a String reference to the object' do
+ expect(label.to_reference).to eq "~#{label.id}"
+ end
+ end
+
+ context 'using name' do
+ it 'returns a String reference to the object' do
+ expect(label.to_reference(format: :name)).to eq %(~"#{label.name}")
+ end
+
+ it 'uses id when name contains double quote' do
+ label = create(:label, name: %q{"irony"})
+ expect(label.to_reference(format: :name)).to eq "~#{label.id}"
+ end
+ end
+
+ context 'using invalid format' do
+ it 'raises error' do
+ expect { label.to_reference(format: :invalid) }
+ .to raise_error StandardError, /Unknown format/
+ end
+ end
+
+ context 'cross project reference' do
+ let(:project) { create(:project) }
+
+ context 'using name' do
+ it 'returns cross reference with label name' do
+ expect(label.to_reference(project, format: :name))
+ .to eq %Q(#{label.project.to_reference}~"#{label.name}")
+ end
+ end
+
+ context 'using id' do
+ it 'returns cross reference with label id' do
+ expect(label.to_reference(project, format: :id))
+ .to eq %Q(#{label.project.to_reference}~#{label.id})
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index b48a317600787b29b065a823e99c6e9501abfb39..6ff32aea018a9f20da749370a7b37bb1d6905061 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -30,6 +30,15 @@ describe JiraService, models: true do
end
end
+ describe '#reference_pattern' do
+ it_behaves_like 'allows project key on reference pattern'
+
+ it 'does not allow # on the code' do
+ expect(subject.reference_pattern.match('#123')).to be_nil
+ expect(subject.reference_pattern.match('1#23#12')).to be_nil
+ end
+ end
+
describe "Execute" do
let(:user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/models/project_services/pipeline_email_service_spec.rb b/spec/models/project_services/pipeline_email_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1368a2925e88d9218ba03117106e9c6a1d70122a
--- /dev/null
+++ b/spec/models/project_services/pipeline_email_service_spec.rb
@@ -0,0 +1,182 @@
+require 'spec_helper'
+
+describe PipelinesEmailService do
+ let(:pipeline) do
+ create(:ci_pipeline, project: project, sha: project.commit('master').sha)
+ end
+
+ let(:project) { create(:project) }
+ let(:recipient) { 'test@gitlab.com' }
+
+ let(:data) do
+ Gitlab::DataBuilder::Pipeline.build(pipeline)
+ end
+
+ before do
+ ActionMailer::Base.deliveries.clear
+ end
+
+ describe 'Validations' do
+ context 'when service is active' do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of(:recipients) }
+
+ context 'when pusher is added' do
+ before do
+ subject.add_pusher = true
+ end
+
+ it { is_expected.not_to validate_presence_of(:recipients) }
+ end
+ end
+
+ context 'when service is inactive' do
+ before do
+ subject.active = false
+ end
+
+ it { is_expected.not_to validate_presence_of(:recipients) }
+ end
+ end
+
+ describe '#test_data' do
+ let(:build) { create(:ci_build) }
+ let(:project) { build.project }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'builds test data' do
+ data = subject.test_data(project, user)
+
+ expect(data[:object_kind]).to eq('pipeline')
+ end
+ end
+
+ shared_examples 'sending email' do
+ before do
+ perform_enqueued_jobs do
+ run
+ end
+ end
+
+ it 'sends email' do
+ sent_to = ActionMailer::Base.deliveries.flat_map(&:to)
+ expect(sent_to).to contain_exactly(recipient)
+ end
+ end
+
+ shared_examples 'not sending email' do
+ before do
+ perform_enqueued_jobs do
+ run
+ end
+ end
+
+ it 'does not send email' do
+ expect(ActionMailer::Base.deliveries).to be_empty
+ end
+ end
+
+ describe '#test' do
+ def run
+ subject.test(data)
+ end
+
+ before do
+ subject.recipients = recipient
+ end
+
+ context 'when pipeline is failed' do
+ before do
+ data[:object_attributes][:status] = 'failed'
+ pipeline.update(status: 'failed')
+ end
+
+ it_behaves_like 'sending email'
+ end
+
+ context 'when pipeline is succeeded' do
+ before do
+ data[:object_attributes][:status] = 'success'
+ pipeline.update(status: 'success')
+ end
+
+ it_behaves_like 'sending email'
+ end
+ end
+
+ describe '#execute' do
+ def run
+ subject.execute(data)
+ end
+
+ context 'with recipients' do
+ before do
+ subject.recipients = recipient
+ end
+
+ context 'with failed pipeline' do
+ before do
+ data[:object_attributes][:status] = 'failed'
+ pipeline.update(status: 'failed')
+ end
+
+ it_behaves_like 'sending email'
+ end
+
+ context 'with succeeded pipeline' do
+ before do
+ data[:object_attributes][:status] = 'success'
+ pipeline.update(status: 'success')
+ end
+
+ it_behaves_like 'not sending email'
+ end
+
+ context 'with notify_only_broken_pipelines on' do
+ before do
+ subject.notify_only_broken_pipelines = true
+ end
+
+ context 'with failed pipeline' do
+ before do
+ data[:object_attributes][:status] = 'failed'
+ pipeline.update(status: 'failed')
+ end
+
+ it_behaves_like 'sending email'
+ end
+
+ context 'with succeeded pipeline' do
+ before do
+ data[:object_attributes][:status] = 'success'
+ pipeline.update(status: 'success')
+ end
+
+ it_behaves_like 'not sending email'
+ end
+ end
+ end
+
+ context 'with empty recipients list' do
+ before do
+ subject.recipients = ' ,, '
+ end
+
+ context 'with failed pipeline' do
+ before do
+ data[:object_attributes][:status] = 'failed'
+ pipeline.update(status: 'failed')
+ end
+
+ it_behaves_like 'not sending email'
+ end
+ end
+ end
+end
diff --git a/spec/models/project_services/redmine_service_spec.rb b/spec/models/project_services/redmine_service_spec.rb
index b8679cd25635c913e646360c18d3e74b0aa3735a..0a7b237a0512e4d7aebb3478412c7719ea268826 100644
--- a/spec/models/project_services/redmine_service_spec.rb
+++ b/spec/models/project_services/redmine_service_spec.rb
@@ -26,4 +26,12 @@ describe RedmineService, models: true do
it { is_expected.not_to validate_presence_of(:new_issue_url) }
end
end
+
+ describe '#reference_pattern' do
+ it_behaves_like 'allows project key on reference pattern'
+
+ it 'does allow # on the reference' do
+ expect(subject.reference_pattern.match('#123')[:issue]).to eq('123')
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 67dbcc362f6115b780fa76e7aeab954462dbdea2..f4dda1ee5589e4ef2436bb661d26607b9540be75 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -56,7 +56,7 @@ describe Project, models: true do
it { is_expected.to have_many(:runners) }
it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:triggers) }
- it { is_expected.to have_many(:labels).dependent(:destroy) }
+ it { is_expected.to have_many(:labels).class_name('ProjectLabel').dependent(:destroy) }
it { is_expected.to have_many(:users_star_projects).dependent(:destroy) }
it { is_expected.to have_many(:environments).dependent(:destroy) }
it { is_expected.to have_many(:deployments).dependent(:destroy) }
@@ -67,6 +67,14 @@ describe Project, models: true do
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
it { is_expected.to have_many(:forks).through(:forked_project_links) }
+ context 'after create' do
+ it "creates project feature" do
+ project = FactoryGirl.build(:project)
+
+ expect { project.save }.to change{ project.project_feature.present? }.from(false).to(true)
+ end
+ end
+
describe '#members & #requesters' do
let(:project) { create(:project, :public) }
let(:requester) { create(:user) }
@@ -531,9 +539,9 @@ describe Project, models: true do
end
describe '#has_wiki?' do
- let(:no_wiki_project) { build(:project, wiki_enabled: false, has_external_wiki: false) }
- let(:wiki_enabled_project) { build(:project) }
- let(:external_wiki_project) { build(:project, has_external_wiki: true) }
+ let(:no_wiki_project) { create(:project, wiki_access_level: ProjectFeature::DISABLED, has_external_wiki: false) }
+ let(:wiki_enabled_project) { create(:project) }
+ let(:external_wiki_project) { create(:project, has_external_wiki: true) }
it 'returns true if project is wiki enabled or has external wiki' do
expect(wiki_enabled_project).to have_wiki
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index f977cf736733b676fcb86b10a3166d524ee02553..187a1bf2d79812f1d28cc14113fc5dc0355d3449 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1146,28 +1146,17 @@ describe Repository, models: true do
end
describe '#before_import' do
- it 'flushes the emptiness cachess' do
- expect(repository).to receive(:expire_emptiness_caches)
-
- repository.before_import
- end
-
- it 'flushes the exists cache' do
- expect(repository).to receive(:expire_exists_cache)
+ it 'flushes the repository caches' do
+ expect(repository).to receive(:expire_content_cache)
repository.before_import
end
end
describe '#after_import' do
- it 'flushes the emptiness cachess' do
- expect(repository).to receive(:expire_emptiness_caches)
-
- repository.after_import
- end
-
- it 'flushes the exists cache' do
- expect(repository).to receive(:expire_exists_cache)
+ it 'flushes and builds the cache' do
+ expect(repository).to receive(:expire_content_cache)
+ expect(repository).to receive(:build_cache)
repository.after_import
end
diff --git a/spec/policies/issues_policy_spec.rb b/spec/policies/issues_policy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2b7b6cad6547c70d0cdef934679debfc9470de72
--- /dev/null
+++ b/spec/policies/issues_policy_spec.rb
@@ -0,0 +1,193 @@
+require 'spec_helper'
+
+describe IssuePolicy, models: true do
+ let(:guest) { create(:user) }
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:group) { create(:group, :public) }
+ let(:reporter_from_group_link) { create(:user) }
+
+ def permissions(user, issue)
+ IssuePolicy.abilities(user, issue).to_set
+ end
+
+ context 'a private project' do
+ let(:non_member) { create(:user) }
+ let(:project) { create(:empty_project, :private) }
+ let(:issue) { create(:issue, project: project, assignee: assignee, author: author) }
+ let(:issue_no_assignee) { create(:issue, project: project) }
+
+ before do
+ project.team << [guest, :guest]
+ project.team << [author, :guest]
+ project.team << [assignee, :guest]
+ project.team << [reporter, :reporter]
+
+ group.add_reporter(reporter_from_group_link)
+
+ create(:project_group_link, group: group, project: project)
+ end
+
+ it 'does not allow non-members to read issues' do
+ expect(permissions(non_member, issue)).not_to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(non_member, issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows guests to read issues' do
+ expect(permissions(guest, issue)).to include(:read_issue)
+ expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue)
+
+ expect(permissions(guest, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
+
+ it 'allows reporters to read, update, and admin issues' do
+ expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows reporters from group links to read, update, and admin issues' do
+ expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows issue authors to read and update their issues' do
+ expect(permissions(author, issue)).to include(:read_issue, :update_issue)
+ expect(permissions(author, issue)).not_to include(:admin_issue)
+
+ expect(permissions(author, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
+
+ it 'allows issue assignees to read and update their issues' do
+ expect(permissions(assignee, issue)).to include(:read_issue, :update_issue)
+ expect(permissions(assignee, issue)).not_to include(:admin_issue)
+
+ expect(permissions(assignee, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
+
+ context 'with confidential issues' do
+ let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) }
+ let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) }
+
+ it 'does not allow non-members to read confidential issues' do
+ expect(permissions(non_member, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(non_member, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'does not allow guests to read confidential issues' do
+ expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows reporters to read, update, and admin confidential issues' do
+ expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows reporters from group links to read, update, and admin confidential issues' do
+ expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows issue authors to read and update their confidential issues' do
+ expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue)
+ expect(permissions(author, confidential_issue)).not_to include(:admin_issue)
+
+ expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows issue assignees to read and update their confidential issues' do
+ expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue)
+ expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue)
+
+ expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
+ end
+ end
+
+ context 'a public project' do
+ let(:project) { create(:empty_project, :public) }
+ let(:issue) { create(:issue, project: project, assignee: assignee, author: author) }
+ let(:issue_no_assignee) { create(:issue, project: project) }
+
+ before do
+ project.team << [guest, :guest]
+ project.team << [reporter, :reporter]
+
+ group.add_reporter(reporter_from_group_link)
+
+ create(:project_group_link, group: group, project: project)
+ end
+
+ it 'allows guests to read issues' do
+ expect(permissions(guest, issue)).to include(:read_issue)
+ expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue)
+
+ expect(permissions(guest, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
+
+ it 'allows reporters to read, update, and admin issues' do
+ expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows reporters from group links to read, update, and admin issues' do
+ expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows issue authors to read and update their issues' do
+ expect(permissions(author, issue)).to include(:read_issue, :update_issue)
+ expect(permissions(author, issue)).not_to include(:admin_issue)
+
+ expect(permissions(author, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
+
+ it 'allows issue assignees to read and update their issues' do
+ expect(permissions(assignee, issue)).to include(:read_issue, :update_issue)
+ expect(permissions(assignee, issue)).not_to include(:admin_issue)
+
+ expect(permissions(assignee, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
+
+ context 'with confidential issues' do
+ let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) }
+ let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) }
+
+ it 'does not allow guests to read confidential issues' do
+ expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows reporters to read, update, and admin confidential issues' do
+ expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows reporter from group links to read, update, and admin confidential issues' do
+ expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows issue authors to read and update their confidential issues' do
+ expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue)
+ expect(permissions(author, confidential_issue)).not_to include(:admin_issue)
+
+ expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows issue assignees to read and update their confidential issues' do
+ expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue)
+ expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue)
+
+ expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index 0f41f8dc7f1e9419e1da7402cd65d81e00ddb0f2..f7fe4c108732ed83bd5749d3e2701bbce5fcd796 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -266,18 +266,25 @@ describe API::Helpers, api: true do
end
describe '.to_boolean' do
+ it 'accepts booleans' do
+ expect(to_boolean(true)).to be(true)
+ expect(to_boolean(false)).to be(false)
+ end
+
it 'converts a valid string to a boolean' do
- expect(to_boolean('true')).to be_truthy
- expect(to_boolean('YeS')).to be_truthy
- expect(to_boolean('t')).to be_truthy
- expect(to_boolean('1')).to be_truthy
- expect(to_boolean('ON')).to be_truthy
- expect(to_boolean('FaLse')).to be_falsy
- expect(to_boolean('F')).to be_falsy
- expect(to_boolean('NO')).to be_falsy
- expect(to_boolean('n')).to be_falsy
- expect(to_boolean('0')).to be_falsy
- expect(to_boolean('oFF')).to be_falsy
+ expect(to_boolean(true)).to be(true)
+ expect(to_boolean('true')).to be(true)
+ expect(to_boolean('YeS')).to be(true)
+ expect(to_boolean('t')).to be(true)
+ expect(to_boolean('1')).to be(true)
+ expect(to_boolean('ON')).to be(true)
+
+ expect(to_boolean('FaLse')).to be(false)
+ expect(to_boolean('F')).to be(false)
+ expect(to_boolean('NO')).to be(false)
+ expect(to_boolean('n')).to be(false)
+ expect(to_boolean('0')).to be(false)
+ expect(to_boolean('oFF')).to be(false)
end
it 'converts an invalid string to nil' do
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index f4b04445c6c47d358cbf50c067b416e474719254..4f5c09a30291a99a29fb8057b7855909194e24e4 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -106,9 +106,20 @@ describe API::API, api: true do
describe "POST /projects/:id/board/lists" do
let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
- it 'creates a new issue board list' do
- post api(base_url, user),
- label_id: ux_label.id
+ it 'creates a new issue board list for group labels' do
+ group = create(:group)
+ group_label = create(:group_label, group: group)
+ project.update(group: group)
+
+ post api(base_url, user), label_id: group_label.id
+
+ expect(response).to have_http_status(201)
+ expect(json_response['label']['name']).to eq(group_label.title)
+ expect(json_response['position']).to eq(3)
+ end
+
+ it 'creates a new issue board list for project labels' do
+ post api(base_url, user), label_id: ux_label.id
expect(response).to have_http_status(201)
expect(json_response['label']['name']).to eq(ux_label.title)
@@ -116,15 +127,13 @@ describe API::API, api: true do
end
it 'returns 400 when creating a new list if label_id is invalid' do
- post api(base_url, user),
- label_id: 23423
+ post api(base_url, user), label_id: 23423
expect(response).to have_http_status(400)
end
- it "returns 403 for project members with guest role" do
- put api("#{base_url}/#{test_list.id}", guest),
- position: 1
+ it 'returns 403 for project members with guest role' do
+ put api("#{base_url}/#{test_list.id}", guest), position: 1
expect(response).to have_http_status(403)
end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 3fd989dd7a612ecd2d9773b171f2f240ddf94805..905f762d578c85d66b7e9f9d1b9d982e0de93cae 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -48,92 +48,154 @@ describe API::API, api: true do
end
describe 'PUT /projects/:id/repository/branches/:branch/protect' do
- it 'protects a single branch' do
- put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
+ context "when a protected branch doesn't already exist" do
+ it 'protects a single branch' do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq(branch_name)
- expect(json_response['commit']['id']).to eq(branch_sha)
- expect(json_response['protected']).to eq(true)
- expect(json_response['developers_can_push']).to eq(false)
- expect(json_response['developers_can_merge']).to eq(false)
- end
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(false)
+ expect(json_response['developers_can_merge']).to eq(false)
+ end
- it 'protects a single branch and developers can push' do
- put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
- developers_can_push: true
+ it 'protects a single branch and developers can push' do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
+ developers_can_push: true
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq(branch_name)
- expect(json_response['commit']['id']).to eq(branch_sha)
- expect(json_response['protected']).to eq(true)
- expect(json_response['developers_can_push']).to eq(true)
- expect(json_response['developers_can_merge']).to eq(false)
- end
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(true)
+ expect(json_response['developers_can_merge']).to eq(false)
+ end
- it 'protects a single branch and developers can merge' do
- put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
- developers_can_merge: true
+ it 'protects a single branch and developers can merge' do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
+ developers_can_merge: true
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq(branch_name)
- expect(json_response['commit']['id']).to eq(branch_sha)
- expect(json_response['protected']).to eq(true)
- expect(json_response['developers_can_push']).to eq(false)
- expect(json_response['developers_can_merge']).to eq(true)
- end
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(false)
+ expect(json_response['developers_can_merge']).to eq(true)
+ end
- it 'protects a single branch and developers can push and merge' do
- put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
- developers_can_push: true, developers_can_merge: true
+ it 'protects a single branch and developers can push and merge' do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
+ developers_can_push: true, developers_can_merge: true
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq(branch_name)
- expect(json_response['commit']['id']).to eq(branch_sha)
- expect(json_response['protected']).to eq(true)
- expect(json_response['developers_can_push']).to eq(true)
- expect(json_response['developers_can_merge']).to eq(true)
- end
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(true)
+ expect(json_response['developers_can_merge']).to eq(true)
+ end
- it 'protects a single branch and developers cannot push and merge' do
- put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
- developers_can_push: 'tru', developers_can_merge: 'tr'
+ it 'protects a single branch and developers cannot push and merge' do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
+ developers_can_push: 'tru', developers_can_merge: 'tr'
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq(branch_name)
- expect(json_response['commit']['id']).to eq(branch_sha)
- expect(json_response['protected']).to eq(true)
- expect(json_response['developers_can_push']).to eq(false)
- expect(json_response['developers_can_merge']).to eq(false)
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(false)
+ expect(json_response['developers_can_merge']).to eq(false)
+ end
end
- context 'on a protected branch' do
- let(:protected_branch) { 'foo' }
-
+ context 'for an existing protected branch' do
before do
- project.repository.add_branch(user, protected_branch, 'master')
- create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: protected_branch)
+ project.repository.add_branch(user, protected_branch.name, 'master')
end
- it 'updates that a developer can push' do
- put api("/projects/#{project.id}/repository/branches/#{protected_branch}/protect", user),
- developers_can_push: false, developers_can_merge: false
+ context "when developers can push and merge" do
+ let(:protected_branch) { create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: 'protected_branch') }
+
+ it 'updates that a developer cannot push or merge' do
+ put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user),
+ developers_can_push: false, developers_can_merge: false
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(protected_branch.name)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(false)
+ expect(json_response['developers_can_merge']).to eq(false)
+ end
+
+ it "doesn't result in 0 access levels when 'developers_can_push' is switched off" do
+ put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user),
+ developers_can_push: false
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(protected_branch.name)
+ expect(protected_branch.reload.push_access_levels.first).to be_present
+ expect(protected_branch.reload.push_access_levels.first.access_level).to eq(Gitlab::Access::MASTER)
+ end
+
+ it "doesn't result in 0 access levels when 'developers_can_merge' is switched off" do
+ put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user),
+ developers_can_merge: false
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(protected_branch.name)
+ expect(protected_branch.reload.merge_access_levels.first).to be_present
+ expect(protected_branch.reload.merge_access_levels.first.access_level).to eq(Gitlab::Access::MASTER)
+ end
+ end
+
+ context "when developers cannot push or merge" do
+ let(:protected_branch) { create(:protected_branch, project: project, name: 'protected_branch') }
+
+ it 'updates that a developer can push and merge' do
+ put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user),
+ developers_can_push: true, developers_can_merge: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(protected_branch.name)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(true)
+ expect(json_response['developers_can_merge']).to eq(true)
+ end
+ end
+ end
+
+ context "multiple API calls" do
+ it "returns success when `protect` is called twice" do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
expect(response).to have_http_status(200)
- expect(json_response['name']).to eq(protected_branch)
+ expect(json_response['name']).to eq(branch_name)
expect(json_response['protected']).to eq(true)
expect(json_response['developers_can_push']).to eq(false)
expect(json_response['developers_can_merge']).to eq(false)
end
- it 'does not update that a developer can push' do
- put api("/projects/#{project.id}/repository/branches/#{protected_branch}/protect", user),
- developers_can_push: 'foobar', developers_can_merge: 'foo'
+ it "returns success when `protect` is called twice with `developers_can_push` turned on" do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_push: true
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_push: true
expect(response).to have_http_status(200)
- expect(json_response['name']).to eq(protected_branch)
+ expect(json_response['name']).to eq(branch_name)
expect(json_response['protected']).to eq(true)
expect(json_response['developers_can_push']).to eq(true)
+ expect(json_response['developers_can_merge']).to eq(false)
+ end
+
+ it "returns success when `protect` is called twice with `developers_can_merge` turned on" do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_merge: true
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_merge: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(false)
expect(json_response['developers_can_merge']).to eq(true)
end
end
@@ -147,12 +209,6 @@ describe API::API, api: true do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2)
expect(response).to have_http_status(403)
end
-
- it "returns success when protect branch again" do
- put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
- put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
- expect(response).to have_http_status(200)
- end
end
describe "PUT /projects/:id/repository/branches/:branch/unprotect" do
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 83789223019c76ffdbb904c22f6e918c0367f0ea..867bc615b9753ceeef90e5b5174056112285969b 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -12,12 +12,17 @@ describe API::API, api: true do
end
describe 'GET /projects/:id/labels' do
- it 'returns project labels' do
+ it 'returns all available labels to the project' do
+ group = create(:group)
+ group_label = create(:group_label, group: group)
+ project.update(group: group)
+
get api("/projects/#{project.id}/labels", user)
+
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
- expect(json_response.size).to eq(1)
- expect(json_response.first['name']).to eq(label1.name)
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, label1.name])
end
end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index d22e059578871526a4c522e47bb4855e71924b0e..493c0a893d10fc561120c4bf06d391e23bb4ebf6 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -328,4 +328,15 @@ describe API::Members, api: true do
it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'group' do
let(:source) { group }
end
+
+ context 'Adding owner to project' do
+ it 'returns 403' do
+ expect do
+ post api("/projects/#{project.id}/members", master),
+ user_id: stranger.id, access_level: Member::OWNER
+
+ expect(response).to have_http_status(422)
+ end.to change { project.members.count }.by(0)
+ end
+ end
end
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index 1ce2658569eabcf0a8b4746f747f63b531351a3f..f8a1aed54419c8bb9b42b8d602af6ef653a70ee3 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -73,9 +73,10 @@ describe API::API, api: true do
end.to change { SystemHook.count }.by(-1)
end
- it "returns success if hook id not found" do
- delete api("/hooks/12345", admin)
- expect(response).to have_http_status(200)
+ it 'returns 404 if the system hook does not exist' do
+ delete api('/hooks/12345', admin)
+
+ expect(response).to have_http_status(404)
end
end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index f83f4d2c9b1d302dfba65463d71a0b73cf6a2019..2c4e73ed578aa3c2fa93938a6e75d95c8ab42eed 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -958,6 +958,29 @@ describe API::API, api: true do
expect(joined_event['author']['name']).to eq(user.name)
end
end
+
+ context 'when there are multiple events from different projects' do
+ let(:second_note) { create(:note_on_issue, project: create(:empty_project)) }
+ let(:third_note) { create(:note_on_issue, project: project) }
+
+ before do
+ second_note.project.add_user(user, :developer)
+
+ [second_note, third_note].each do |note|
+ EventCreateService.new.leave_note(note, user)
+ end
+ end
+
+ it 'returns events in the correct order (from newest to oldest)' do
+ get api("/users/#{user.id}/events", user)
+
+ comment_events = json_response.select { |e| e['action_name'] == 'commented on' }
+
+ expect(comment_events[0]['target_id']).to eq(third_note.id)
+ expect(comment_events[1]['target_id']).to eq(second_note.id)
+ expect(comment_events[2]['target_id']).to eq(note.id)
+ end
+ end
end
it 'returns a 404 error if not found' do
diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb
index e7806add9162855c4f6545949d06291a6d0be86c..a7e9efcf93f41c2936d99b1c957708ce99d33482 100644
--- a/spec/services/boards/lists/create_service_spec.rb
+++ b/spec/services/boards/lists/create_service_spec.rb
@@ -9,6 +9,10 @@ describe Boards::Lists::CreateService, services: true do
subject(:service) { described_class.new(project, user, label_id: label.id) }
+ before do
+ project.team << [user, :developer]
+ end
+
context 'when board lists is empty' do
it 'creates a new list at beginning of the list' do
list = service.execute(board)
diff --git a/spec/services/boards/lists/generate_service_spec.rb b/spec/services/boards/lists/generate_service_spec.rb
index 8b2f5e8133896ebb78cdfa4947eedbf5d935e5ed..ed0337662af0dcf9fb80bf43442de5544e2fc7c4 100644
--- a/spec/services/boards/lists/generate_service_spec.rb
+++ b/spec/services/boards/lists/generate_service_spec.rb
@@ -8,6 +8,10 @@ describe Boards::Lists::GenerateService, services: true do
subject(:service) { described_class.new(project, user) }
+ before do
+ project.team << [user, :developer]
+ end
+
context 'when board lists is empty' do
it 'creates the default lists' do
expect { service.execute(board) }.to change(board.lists, :count).by(2)
diff --git a/spec/services/ci/send_pipeline_notification_service_spec.rb b/spec/services/ci/send_pipeline_notification_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..288302cc94f5cc6d38ae5d02ae876c470e60e811
--- /dev/null
+++ b/spec/services/ci/send_pipeline_notification_service_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Ci::SendPipelineNotificationService, services: true do
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: project.commit('master').sha,
+ user: user,
+ status: status)
+ end
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ subject{ described_class.new(pipeline) }
+
+ describe '#execute' do
+ before do
+ reset_delivered_emails!
+ end
+
+ shared_examples 'sending emails' do
+ it 'sends an email to pipeline user' do
+ perform_enqueued_jobs do
+ subject.execute([user.email])
+ end
+
+ email = ActionMailer::Base.deliveries.last
+ expect(email.subject).to include(email_subject)
+ expect(email.to).to eq([user.email])
+ end
+ end
+
+ context 'with success pipeline' do
+ let(:status) { 'success' }
+ let(:email_subject) { "Pipeline ##{pipeline.id} has succeeded" }
+
+ it_behaves_like 'sending emails'
+ end
+
+ context 'with failed pipeline' do
+ let(:status) { 'failed' }
+ let(:email_subject) { "Pipeline ##{pipeline.id} has failed" }
+
+ it_behaves_like 'sending emails'
+ end
+ end
+end
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
index 0b84c7262c3ffa1f4a4d9451cd5316ebddfdf585..cf0a18aacec705ceeae36eab5920242e770e88bf 100644
--- a/spec/services/create_deployment_service_spec.rb
+++ b/spec/services/create_deployment_service_spec.rb
@@ -7,11 +7,13 @@ describe CreateDeploymentService, services: true do
let(:service) { described_class.new(project, user, params) }
describe '#execute' do
+ let(:options) { nil }
let(:params) do
{ environment: 'production',
ref: 'master',
tag: false,
sha: '97de212e80737a608d939f648d959671fb0a0142',
+ options: options
}
end
@@ -28,7 +30,7 @@ describe CreateDeploymentService, services: true do
end
context 'when environment exist' do
- before { create(:environment, project: project, name: 'production') }
+ let!(:environment) { create(:environment, project: project, name: 'production') }
it 'does not create a new environment' do
expect { subject }.not_to change { Environment.count }
@@ -37,6 +39,46 @@ describe CreateDeploymentService, services: true do
it 'does create a deployment' do
expect(subject).to be_persisted
end
+
+ context 'and start action is defined' do
+ let(:options) { { action: 'start' } }
+
+ context 'and environment is stopped' do
+ before do
+ environment.stop
+ end
+
+ it 'makes environment available' do
+ subject
+
+ expect(environment.reload).to be_available
+ end
+
+ it 'does create a deployment' do
+ expect(subject).to be_persisted
+ end
+ end
+ end
+
+ context 'and stop action is defined' do
+ let(:options) { { action: 'stop' } }
+
+ context 'and environment is available' do
+ before do
+ environment.start
+ end
+
+ it 'makes environment stopped' do
+ subject
+
+ expect(environment.reload).to be_stopped
+ end
+
+ it 'does not create a deployment' do
+ expect(subject).to be_nil
+ end
+ end
+ end
end
context 'for environment with invalid name' do
@@ -53,7 +95,7 @@ describe CreateDeploymentService, services: true do
end
it 'does not create a deployment' do
- expect(subject).not_to be_persisted
+ expect(subject).to be_nil
end
end
@@ -83,6 +125,25 @@ describe CreateDeploymentService, services: true do
it 'does create a new deployment' do
expect(subject).to be_persisted
end
+
+ context 'and environment exist' do
+ let!(:environment) { create(:environment, project: project, name: 'review-apps/feature-review-apps') }
+
+ it 'does not create a new environment' do
+ expect { subject }.not_to change { Environment.count }
+ end
+
+ it 'updates external url' do
+ subject
+
+ expect(subject.environment.name).to eq('review-apps/feature-review-apps')
+ expect(subject.environment.external_url).to eq('http://feature-review-apps.review-apps.gitlab.com')
+ end
+
+ it 'does create a new deployment' do
+ expect(subject).to be_persisted
+ end
+ end
end
context 'when project was removed' do
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 8dda34c7a03a3cd9b6ae86a83dfa8d6c9523e31c..ad5170afc21b71135bff7147c0247d6ccd007c80 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -415,7 +415,7 @@ describe GitPushService, services: true do
it "doesn't close issues when external issue tracker is in use" do
allow_any_instance_of(Project).to receive(:default_issues_tracker?).
and_return(false)
- external_issue_tracker = double(title: 'My Tracker', issue_path: issue.iid)
+ external_issue_tracker = double(title: 'My Tracker', issue_path: issue.iid, reference_pattern: project.issue_reference_pattern)
allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(external_issue_tracker)
# The push still shouldn't create cross-reference notes.
@@ -484,30 +484,46 @@ describe GitPushService, services: true do
end
context "closing an issue" do
- let(:message) { "this is some work.\n\ncloses JIRA-1" }
-
- it "initiates one api call to jira server to close the issue" do
- transition_body = {
- transition: {
- id: '2'
- }
- }.to_json
-
- execute_service(project, commit_author, @oldrev, @newrev, @ref )
- expect(WebMock).to have_requested(:post, jira_api_transition_url).with(
- body: transition_body
- ).once
+ let(:message) { "this is some work.\n\ncloses JIRA-1" }
+ let(:transition_body) { { transition: { id: '2' } }.to_json }
+ let(:comment_body) { { body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json }
+
+ context "using right markdown" do
+ it "initiates one api call to jira server to close the issue" do
+ execute_service(project, commit_author, @oldrev, @newrev, @ref )
+
+ expect(WebMock).to have_requested(:post, jira_api_transition_url).with(
+ body: transition_body
+ ).once
+ end
+
+ it "initiates one api call to jira server to comment on the issue" do
+ execute_service(project, commit_author, @oldrev, @newrev, @ref )
+
+ expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
+ body: comment_body
+ ).once
+ end
end
- it "initiates one api call to jira server to comment on the issue" do
- comment_body = {
- body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]."
- }.to_json
+ context "using wrong markdown" do
+ let(:message) { "this is some work.\n\ncloses #1" }
- execute_service(project, commit_author, @oldrev, @newrev, @ref )
- expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
- body: comment_body
- ).once
+ it "does not initiates one api call to jira server to close the issue" do
+ execute_service(project, commit_author, @oldrev, @newrev, @ref )
+
+ expect(WebMock).not_to have_requested(:post, jira_api_transition_url).with(
+ body: transition_body
+ )
+ end
+
+ it "does not initiates one api call to jira server to comment on the issue" do
+ execute_service(project, commit_author, @oldrev, @newrev, @ref )
+
+ expect(WebMock).not_to have_requested(:post, jira_api_comment_url).with(
+ body: comment_body
+ ).once
+ end
end
end
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 1050502fa197bc7c525045735d1029abb36bc0bc..5c0331ebe66c154944a8914e0f605563321ee672 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -67,6 +67,27 @@ describe Issues::CreateService, services: true do
expect(Todo.where(attributes).count).to eq 1
end
+ context 'when label belongs to project group' do
+ let(:group) { create(:group) }
+ let(:group_labels) { create_pair(:group_label, group: group) }
+
+ let(:opts) do
+ {
+ title: 'Title',
+ description: 'Description',
+ label_ids: group_labels.map(&:id)
+ }
+ end
+
+ before do
+ project.update(group: group)
+ end
+
+ it 'assigns group labels' do
+ expect(issue.labels).to match_array group_labels
+ end
+ end
+
context 'when label belongs to different project' do
let(:label) { create(:label) }
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index 93bf0f649634e4d0cb4e338841e3626abecd4d12..f0ded06b78504428863a4c44b9794040b1ae8d2e 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -23,14 +23,15 @@ describe Issues::MoveService, services: true do
old_project.team << [user, :reporter]
new_project.team << [user, :reporter]
- ['label1', 'label2'].each do |label|
+ labels = Array.new(2) { |x| "label%d" % (x + 1) }
+
+ labels.each do |label|
old_issue.labels << create(:label,
project_id: old_project.id,
title: label)
- end
- new_project.labels << create(:label, title: 'label1')
- new_project.labels << create(:label, title: 'label2')
+ new_project.labels << create(:label, title: label)
+ end
end
end
@@ -207,10 +208,10 @@ describe Issues::MoveService, services: true do
end
end
- describe 'rewritting references' do
+ describe 'rewriting references' do
include_context 'issue move executed'
- context 'issue reference' do
+ context 'issue references' do
let(:another_issue) { create(:issue, project: old_project) }
let(:description) { "Some description #{another_issue.to_reference}" }
@@ -219,6 +220,16 @@ describe Issues::MoveService, services: true do
.to eq "Some description #{old_project.to_reference}#{another_issue.to_reference}"
end
end
+
+ context "user references" do
+ let(:another_issue) { create(:issue, project: old_project) }
+ let(:description) { "Some description #{user.to_reference}" }
+
+ it "doesn't throw any errors for issues containing user references" do
+ expect(new_issue.description)
+ .to eq "Some description #{user.to_reference}"
+ end
+ end
end
context 'moving to same project' do
@@ -277,5 +288,25 @@ describe Issues::MoveService, services: true do
it { expect { move }.to raise_error(StandardError, /permissions/) }
end
end
+
+ context 'movable issue with no assigned labels' do
+ before do
+ old_project.team << [user, :reporter]
+ new_project.team << [user, :reporter]
+
+ labels = Array.new(2) { |x| "label%d" % (x + 1) }
+
+ labels.each do |label|
+ new_project.labels << create(:label, title: label)
+ end
+ end
+
+ include_context 'issue move executed'
+
+ it 'does not assign labels to new issue' do
+ expected_label_titles = new_issue.reload.labels.map(&:title)
+ expect(expected_label_titles.size).to eq 0
+ end
+ end
end
end
diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7a9b34f9f963bb9c7329a5fbc8d67e6b7c51422a
--- /dev/null
+++ b/spec/services/labels/find_or_create_service_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe Labels::FindOrCreateService, services: true do
+ describe '#execute' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+
+ let(:params) do
+ {
+ title: 'Security',
+ description: 'Security related stuff.',
+ color: '#FF0000'
+ }
+ end
+
+ context 'when acting on behalf of a specific user' do
+ let(:user) { create(:user) }
+ subject(:service) { described_class.new(user, project, params) }
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'when label does not exist at group level' do
+ it 'creates a new label at project level' do
+ expect { service.execute }.to change(project.labels, :count).by(1)
+ end
+ end
+
+ context 'when label exists at group level' do
+ it 'returns the group label' do
+ group_label = create(:group_label, group: group, title: 'Security')
+
+ expect(service.execute).to eq group_label
+ end
+ end
+
+ context 'when label does not exist at group level' do
+ it 'creates a new label at project leve' do
+ expect { service.execute }.to change(project.labels, :count).by(1)
+ end
+ end
+
+ context 'when label exists at project level' do
+ it 'returns the project label' do
+ project_label = create(:label, project: project, title: 'Security')
+
+ expect(service.execute).to eq project_label
+ end
+ end
+ end
+
+ context 'when authorization is not required' do
+ subject(:service) { described_class.new(nil, project, params) }
+
+ it 'returns the project label' do
+ project_label = create(:label, project: project, title: 'Security')
+
+ expect(service.execute(skip_authorization: true)).to eq project_label
+ end
+ end
+ end
+end
diff --git a/spec/services/labels/transfer_service_spec.rb b/spec/services/labels/transfer_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ddf3527dc0ffee0207549a9d5e28efe4d13807ae
--- /dev/null
+++ b/spec/services/labels/transfer_service_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe Labels::TransferService, services: true do
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:group_1) { create(:group) }
+ let(:group_2) { create(:group) }
+ let(:group_3) { create(:group) }
+ let(:project_1) { create(:project, namespace: group_2) }
+ let(:project_2) { create(:project, namespace: group_3) }
+
+ let(:group_label_1) { create(:group_label, group: group_1, name: 'Group Label 1') }
+ let(:group_label_2) { create(:group_label, group: group_1, name: 'Group Label 2') }
+ let(:group_label_3) { create(:group_label, group: group_1, name: 'Group Label 3') }
+ let(:group_label_4) { create(:group_label, group: group_2, name: 'Group Label 4') }
+ let(:group_label_5) { create(:group_label, group: group_3, name: 'Group Label 5') }
+ let(:project_label_1) { create(:label, project: project_1, name: 'Project Label 1') }
+
+ subject(:service) { described_class.new(user, group_1, project_1) }
+
+ before do
+ create(:labeled_issue, project: project_1, labels: [group_label_1])
+ create(:labeled_issue, project: project_1, labels: [group_label_4])
+ create(:labeled_issue, project: project_1, labels: [project_label_1])
+ create(:labeled_issue, project: project_2, labels: [group_label_5])
+ create(:labeled_merge_request, source_project: project_1, labels: [group_label_1, group_label_2])
+ create(:labeled_merge_request, source_project: project_2, labels: [group_label_5])
+ end
+
+ it 'recreates the missing group labels at project level' do
+ expect { service.execute }.to change(project_1.labels, :count).by(2)
+ end
+
+ it 'recreates label priorities related to the missing group labels' do
+ create(:label_priority, project: project_1, label: group_label_1, priority: 1)
+
+ service.execute
+
+ new_project_label = project_1.labels.find_by(title: group_label_1.title)
+ expect(new_project_label.id).not_to eq group_label_1.id
+ expect(new_project_label.priorities).not_to be_empty
+ end
+
+ it 'does not recreate missing group labels that are not applied to issues or merge requests' do
+ service.execute
+
+ expect(project_1.labels.where(title: group_label_3.title)).to be_empty
+ end
+
+ it 'does not recreate missing group labels that already exist in the project group' do
+ service.execute
+
+ expect(project_1.labels.where(title: group_label_4.title)).to be_empty
+ end
+ end
+end
diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb
index 03e296259f90fb26d1034141fd0078226e1471da..7b090343a3e4a0b222f4470d99c93d3b9a772cfb 100644
--- a/spec/services/members/approve_access_request_service_spec.rb
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -5,36 +5,37 @@ describe Members::ApproveAccessRequestService, services: true do
let(:access_requester) { create(:user) }
let(:project) { create(:project, :public) }
let(:group) { create(:group, :public) }
+ let(:opts) { {} }
shared_examples 'a service raising ActiveRecord::RecordNotFound' do
it 'raises ActiveRecord::RecordNotFound' do
- expect { described_class.new(source, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { described_class.new(source, user, params).execute(opts) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
it 'raises Gitlab::Access::AccessDeniedError' do
- expect { described_class.new(source, user, params).execute }.to raise_error(Gitlab::Access::AccessDeniedError)
+ expect { described_class.new(source, user, params).execute(opts) }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
shared_examples 'a service approving an access request' do
it 'succeeds' do
- expect { described_class.new(source, user, params).execute }.to change { source.requesters.count }.by(-1)
+ expect { described_class.new(source, user, params).execute(opts) }.to change { source.requesters.count }.by(-1)
end
it 'returns a Member' do
- member = described_class.new(source, user, params).execute
+ member = described_class.new(source, user, params).execute(opts)
expect(member).to be_a "#{source.class}Member".constantize
expect(member.requested_at).to be_nil
end
context 'with a custom access level' do
- let(:params) { { user_id: access_requester.id, access_level: Gitlab::Access::MASTER } }
+ let(:params2) { params.merge(user_id: access_requester.id, access_level: Gitlab::Access::MASTER) }
it 'returns a ProjectMember with the custom access level' do
- member = described_class.new(source, user, params).execute
+ member = described_class.new(source, user, params2).execute(opts)
expect(member.access_level).to eq Gitlab::Access::MASTER
end
@@ -60,6 +61,56 @@ describe Members::ApproveAccessRequestService, services: true do
end
let(:params) { { user_id: access_requester.id } }
+ context 'when current user is nil' do
+ let(:user) { nil }
+
+ context 'and :force option is not given' do
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { group }
+ end
+ end
+
+ context 'and :force option is false' do
+ let(:opts) { { force: false } }
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { group }
+ end
+ end
+
+ context 'and :force option is true' do
+ let(:opts) { { force: true } }
+
+ it_behaves_like 'a service approving an access request' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a service approving an access request' do
+ let(:source) { group }
+ end
+ end
+
+ context 'and :force param is true' do
+ let(:params) { { user_id: access_requester.id, force: true } }
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { group }
+ end
+ end
+ end
+
context 'when current user cannot approve access request to the project' do
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { project }
diff --git a/spec/services/merge_requests/assign_issues_service_spec.rb b/spec/services/merge_requests/assign_issues_service_spec.rb
index 7aeb95a15ea8f98dd9f58cbc73319166f7317bec..5034b6ef33f14780ca0771686b2fd812cc1916df 100644
--- a/spec/services/merge_requests/assign_issues_service_spec.rb
+++ b/spec/services/merge_requests/assign_issues_service_spec.rb
@@ -46,4 +46,16 @@ describe MergeRequests::AssignIssuesService, services: true do
it 'assigns these to the merge request owner' do
expect { service.execute }.to change { issue.reload.assignee }.to(user)
end
+
+ it 'ignores external issues' do
+ external_issue = ExternalIssue.new('JIRA-123', project)
+ service = described_class.new(
+ project,
+ user,
+ merge_request: merge_request,
+ closes_issues: [external_issue]
+ )
+
+ expect(service.assignable_issues.count).to eq 0
+ end
end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index ee53e110aeed0236bc2b0d6a2d582156fc3f1cca..f93d7732a9a08987416b7b33a9aa754b531495a0 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -74,6 +74,18 @@ describe MergeRequests::MergeService, services: true do
service.execute(merge_request)
end
+
+ context "wrong issue markdown" do
+ it 'does not close issues on JIRA issue tracker' do
+ jira_issue = ExternalIssue.new('#123', project)
+ commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
+ allow(merge_request).to receive(:commits).and_return([commit])
+
+ expect_any_instance_of(JiraService).not_to receive(:close_issue)
+
+ service.execute(merge_request)
+ end
+ end
end
end
@@ -120,13 +132,13 @@ describe MergeRequests::MergeService, services: true do
let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') }
it 'saves error if there is an exception' do
- allow(service).to receive(:repository).and_raise("error")
+ allow(service).to receive(:repository).and_raise("error message")
allow(service).to receive(:execute_hooks)
service.execute(merge_request)
- expect(merge_request.merge_error).to eq("Something went wrong during merge")
+ expect(merge_request.merge_error).to eq("Something went wrong during merge: error message")
end
it 'saves error if there is an PreReceiveError exception' do
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index ed1384798ab22e6c061aac6b97e6af647861dc88..ab6e8f537bac91e1f6d89c55adf0c848eacae554 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -110,7 +110,7 @@ describe Projects::ImportService, services: true do
end
it 'expires existence cache after error' do
- allow_any_instance_of(Project).to receive(:repository_exists?).and_return(true)
+ allow_any_instance_of(Project).to receive(:repository_exists?).and_return(false, true)
expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.repository_storage_path, project.path_with_namespace, project.import_url).and_raise(Gitlab::Shell::Error.new('Failed to import the repository'))
expect_any_instance_of(Repository).to receive(:expire_emptiness_caches).and_call_original
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 57c71544dfffada67f92d9a80c6f47e582bc6510..1540b90163a2f98e302442c6a46af6b8a934feef 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -71,4 +71,14 @@ describe Projects::TransferService, services: true do
it { expect(private_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) }
end
end
+
+ context 'missing group labels applied to issues or merge requests' do
+ it 'delegates tranfer to Labels::TransferService' do
+ group.add_owner(user)
+
+ expect_any_instance_of(Labels::TransferService).to receive(:execute).once.and_call_original
+
+ transfer_project(project, user, group)
+ end
+ end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index b19f5824236264b9c17aaceee1d983036cba2614..06d52f0f7357989467d981ffb0716a0093288df5 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -50,6 +50,12 @@ RSpec.configure do |config|
example.run
Rails.cache = caching_store
end
+
+ config.around(:each, :redis) do |example|
+ Gitlab::Redis.with(&:flushall)
+ example.run
+ Gitlab::Redis.with(&:flushall)
+ end
end
FactoryGirl::SyntaxRunner.class_eval do
diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2542a59bb00d019f0b4502e31e0c3193d4f32393
--- /dev/null
+++ b/spec/support/import_export/common_util.rb
@@ -0,0 +1,10 @@
+module ImportExport
+ module CommonUtil
+ def setup_symlink(tmpdir, symlink_name)
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(tmpdir)
+
+ File.open("#{tmpdir}/test", 'w') { |file| file.write("test") }
+ FileUtils.ln_s("#{tmpdir}/test", "#{tmpdir}/#{symlink_name}")
+ end
+ end
+end
diff --git a/spec/support/issue_tracker_service_shared_example.rb b/spec/support/issue_tracker_service_shared_example.rb
index b6d7436c3609efa3d740c237ed4fa0704250ccd8..e70b3963d9d068dbf3598a71e054d19d24232aa3 100644
--- a/spec/support/issue_tracker_service_shared_example.rb
+++ b/spec/support/issue_tracker_service_shared_example.rb
@@ -5,3 +5,18 @@ RSpec.shared_examples 'issue tracker service URL attribute' do |url_attr|
it { is_expected.not_to allow_value('ftp://example.com').for(url_attr) }
it { is_expected.not_to allow_value('herp-and-derp').for(url_attr) }
end
+
+RSpec.shared_examples 'allows project key on reference pattern' do |url_attr|
+ it 'allows underscores in the project name' do
+ expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
+ end
+
+ it 'allows numbers in the project name' do
+ expect(subject.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234'
+ end
+
+ it 'requires the project name to begin with A-Z' do
+ expect(subject.reference_pattern.match('3EXT_EXT-1234')).to eq nil
+ expect(subject.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234'
+ end
+end
diff --git a/spec/support/wait_for_ajax.rb b/spec/support/wait_for_ajax.rb
index b90fc1126716d1511e5af745ef3e28026ce3bef6..0f9dc2dee754e5b6035187db8446e0a9a78e35d0 100644
--- a/spec/support/wait_for_ajax.rb
+++ b/spec/support/wait_for_ajax.rb
@@ -8,4 +8,8 @@ module WaitForAjax
def finished_all_ajax_requests?
page.evaluate_script('jQuery.active').zero?
end
+
+ def javascript_test?
+ [:selenium, :webkit, :chrome, :poltergeist].include?(Capybara.current_driver)
+ end
end
diff --git a/spec/workers/concerns/build_queue_spec.rb b/spec/workers/concerns/build_queue_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6bf955e0be22c9f971458ca815b8ed303ef798cc
--- /dev/null
+++ b/spec/workers/concerns/build_queue_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe BuildQueue do
+ let(:worker) do
+ Class.new do
+ include Sidekiq::Worker
+ include BuildQueue
+ end
+ end
+
+ it 'sets the queue name of a worker' do
+ expect(worker.sidekiq_options['queue'].to_s).to eq('build')
+ end
+end
diff --git a/spec/workers/concerns/cronjob_queue_spec.rb b/spec/workers/concerns/cronjob_queue_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5d1336c21a6b71c5def5d4ebab873cdddcfb05ae
--- /dev/null
+++ b/spec/workers/concerns/cronjob_queue_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe CronjobQueue do
+ let(:worker) do
+ Class.new do
+ include Sidekiq::Worker
+ include CronjobQueue
+ end
+ end
+
+ it 'sets the queue name of a worker' do
+ expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob')
+ end
+
+ it 'disables retrying of failed jobs' do
+ expect(worker.sidekiq_options['retry']).to eq(false)
+ end
+end
diff --git a/spec/workers/concerns/dedicated_sidekiq_queue_spec.rb b/spec/workers/concerns/dedicated_sidekiq_queue_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..512baec8b7e1de795f544fb75603a3b138a7e1a7
--- /dev/null
+++ b/spec/workers/concerns/dedicated_sidekiq_queue_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe DedicatedSidekiqQueue do
+ let(:worker) do
+ Class.new do
+ def self.name
+ 'Foo::Bar::DummyWorker'
+ end
+
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+ end
+ end
+
+ describe 'queue names' do
+ it 'sets the queue name based on the class name' do
+ expect(worker.sidekiq_options['queue']).to eq('foo_bar_dummy')
+ end
+ end
+end
diff --git a/spec/workers/concerns/pipeline_queue_spec.rb b/spec/workers/concerns/pipeline_queue_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..40794d0e42a8d9ea97f0bce3f92415ad7c7743b8
--- /dev/null
+++ b/spec/workers/concerns/pipeline_queue_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe PipelineQueue do
+ let(:worker) do
+ Class.new do
+ include Sidekiq::Worker
+ include PipelineQueue
+ end
+ end
+
+ it 'sets the queue name of a worker' do
+ expect(worker.sidekiq_options['queue'].to_s).to eq('pipeline')
+ end
+end
diff --git a/spec/workers/concerns/repository_check_queue_spec.rb b/spec/workers/concerns/repository_check_queue_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8868e9698293a82ed2162cf06261db4cca1189c8
--- /dev/null
+++ b/spec/workers/concerns/repository_check_queue_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe RepositoryCheckQueue do
+ let(:worker) do
+ Class.new do
+ include Sidekiq::Worker
+ include RepositoryCheckQueue
+ end
+ end
+
+ it 'sets the queue name of a worker' do
+ expect(worker.sidekiq_options['queue'].to_s).to eq('repository_check')
+ end
+
+ it 'disables retrying of failed jobs' do
+ expect(worker.sidekiq_options['retry']).to eq(false)
+ end
+end
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fc9adf47c1e833ee529bea87e3666dff8d16a9ac
--- /dev/null
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe 'Every Sidekiq worker' do
+ let(:workers) do
+ root = Rails.root.join('app', 'workers')
+ concerns = root.join('concerns').to_s
+
+ workers = Dir[root.join('**', '*.rb')].
+ reject { |path| path.start_with?(concerns) }
+
+ workers.map do |path|
+ ns = Pathname.new(path).relative_path_from(root).to_s.gsub('.rb', '')
+
+ ns.camelize.constantize
+ end
+ end
+
+ it 'does not use the default queue' do
+ workers.each do |worker|
+ expect(worker.sidekiq_options['queue'].to_s).not_to eq('default')
+ end
+ end
+
+ it 'uses the cronjob queue when the worker runs as a cronjob' do
+ cron_workers = Settings.cron_jobs.
+ map { |job_name, options| options['job_class'].constantize }.
+ to_set
+
+ workers.each do |worker|
+ next unless cron_workers.include?(worker)
+
+ expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob')
+ end
+ end
+
+ it 'defines the queue in the Sidekiq configuration file' do
+ config = YAML.load_file(Rails.root.join('config', 'sidekiq_queues.yml').to_s)
+ queue_names = config[:queues].map { |(queue, _)| queue }.to_set
+
+ workers.each do |worker|
+ expect(queue_names).to include(worker.sidekiq_options['queue'].to_s)
+ end
+ end
+end
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index 5785a6a06ff9932d5355c4ebaca372b5d9241af3..bfa8c0ff2c6b6eaed9bb37f6c42b3209ccdb45cb 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -5,22 +5,60 @@ describe ProjectCacheWorker do
subject { described_class.new }
+ describe '.perform_async' do
+ it 'schedules the job when no lease exists' do
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:exists?).
+ and_return(false)
+
+ expect_any_instance_of(described_class).to receive(:perform)
+
+ described_class.perform_async(project.id)
+ end
+
+ it 'does not schedule the job when a lease exists' do
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:exists?).
+ and_return(true)
+
+ expect_any_instance_of(described_class).not_to receive(:perform)
+
+ described_class.perform_async(project.id)
+ end
+ end
+
describe '#perform' do
- it 'updates project cache data' do
- expect_any_instance_of(Repository).to receive(:size)
- expect_any_instance_of(Repository).to receive(:commit_count)
+ context 'when an exclusive lease can be obtained' do
+ before do
+ allow(subject).to receive(:try_obtain_lease_for).with(project.id).
+ and_return(true)
+ end
+
+ it 'updates project cache data' do
+ expect_any_instance_of(Repository).to receive(:size)
+ expect_any_instance_of(Repository).to receive(:commit_count)
- expect_any_instance_of(Project).to receive(:update_repository_size)
- expect_any_instance_of(Project).to receive(:update_commit_count)
+ expect_any_instance_of(Project).to receive(:update_repository_size)
+ expect_any_instance_of(Project).to receive(:update_commit_count)
- subject.perform(project.id)
+ subject.perform(project.id)
+ end
+
+ it 'handles missing repository data' do
+ expect_any_instance_of(Repository).to receive(:exists?).and_return(false)
+ expect_any_instance_of(Repository).not_to receive(:size)
+
+ subject.perform(project.id)
+ end
end
- it 'handles missing repository data' do
- expect_any_instance_of(Repository).to receive(:exists?).and_return(false)
- expect_any_instance_of(Repository).not_to receive(:size)
+ context 'when an exclusive lease can not be obtained' do
+ it 'does nothing' do
+ allow(subject).to receive(:try_obtain_lease_for).with(project.id).
+ and_return(false)
+
+ expect(subject).not_to receive(:update_caches)
- subject.perform(project.id)
+ subject.perform(project.id)
+ end
end
end
end