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}" + 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(

    Google

    ) } + let(:doc_with_https) { filter %q(

    Google

    ) } + + 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