diff options
487 files changed, 5287 insertions, 2261 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 782a43cbe74..deb5345d3bd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -271,7 +271,7 @@ rake db:migrate:reset: <<: *use-db <<: *dedicated-runner script: - - rake db:migrate:reset + - bundle exec rake db:migrate:reset rake db:seed_fu: stage: test @@ -302,7 +302,7 @@ teaspoon: script: - npm install - npm link istanbul - - rake teaspoon + - bundle exec rake teaspoon artifacts: name: coverage-javascript expire_in: 31d @@ -353,10 +353,10 @@ migration paths: - cp config/resque.yml.example config/resque.yml - sed -i 's/localhost/redis/g' config/resque.yml - bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3 - - rake db:drop db:create db:schema:load db:seed_fu + - bundle exec rake db:drop db:create db:schema:load db:seed_fu - git checkout $CI_BUILD_REF - source scripts/prepare_build.sh - - rake db:migrate + - bundle exec rake db:migrate coverage: stage: post-test diff --git a/.rubocop.yml b/.rubocop.yml index bf2b2d8afc2..cfff42e5c99 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -17,6 +17,7 @@ AllCops: # Exclude some GitLab files Exclude: - 'vendor/**/*' + - 'node_modules/**/*' - 'db/*' - 'db/fixtures/**/*' - 'tmp/**/*' diff --git a/CHANGELOG.md b/CHANGELOG.md index 9712b32232e..25e02b1ae1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.16.3 (2017-01-27) + +- Add caching of droplab ajax requests. !8725 +- Fix access to the wiki code via HTTP when repository feature disabled. !8758 +- Revert 3f17f29a. !8785 +- Fix race conditions for AuthorizedProjectsWorker. +- Fix autocomplete initial undefined state. +- Fix Error 500 when repositories contain annotated tags pointing to blobs. +- Fix /explore sorting. +- Fixed label dropdown toggle text not correctly updating. + ## 8.16.2 (2017-01-25) - allow issue filter bar to be operated with mouse only. !8681 @@ -18,7 +29,7 @@ entry. - Prevent users from deleting system deploy keys via the project deploy key API. - Upgrade omniauth gem to 1.3.2. -## 8.16.0 (2017-02-22) +## 8.16.0 (2017-01-22) - Add LDAP Rake task to rename a provider. !2181 - Validate label's title length. !5767 (Tomáš Kukrál) @@ -36,7 +36,7 @@ gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd', '~> 2.2.0' gem 'omniauth-authentiq', '~> 0.2.0' gem 'rack-oauth2', '~> 1.2.1' -gem 'jwt' +gem 'jwt', '~> 1.5.6' # Spam and anti-bot protection gem 'recaptcha', '~> 3.0', require: 'recaptcha/rails' @@ -109,7 +109,7 @@ gem 'org-ruby', '~> 0.9.12' gem 'creole', '~> 0.5.0' gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 1.5.2' -gem 'asciidoctor-plantuml', '0.0.6' +gem 'asciidoctor-plantuml', '0.0.7' gem 'rouge', '~> 2.0' gem 'truncato', '~> 0.7.8' diff --git a/Gemfile.lock b/Gemfile.lock index 671d7788a86..f6b889dcca4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -54,7 +54,7 @@ GEM faraday_middleware-multi_json (~> 0.0) oauth2 (~> 1.0) asciidoctor (1.5.3) - asciidoctor-plantuml (0.0.6) + asciidoctor-plantuml (0.0.7) asciidoctor (~> 1.5) ast (2.3.0) attr_encrypted (3.0.3) @@ -379,7 +379,7 @@ GEM json (1.8.3) json-schema (2.6.2) addressable (~> 2.3.8) - jwt (1.5.4) + jwt (1.5.6) kaminari (0.17.0) actionpack (>= 3.0.0) activesupport (>= 3.0.0) @@ -841,7 +841,7 @@ DEPENDENCIES allocations (~> 1.0) asana (~> 0.4.0) asciidoctor (~> 1.5.2) - asciidoctor-plantuml (= 0.0.6) + asciidoctor-plantuml (= 0.0.7) attr_encrypted (~> 3.0.0) awesome_print (~> 1.2.0) babosa (~> 1.0.2) @@ -912,7 +912,7 @@ DEPENDENCIES jquery-rails (~> 4.1.0) jquery-ui-rails (~> 5.0.0) json-schema (~> 2.6.2) - jwt + jwt (~> 1.5.6) kaminari (~> 0.17.0) knapsack (~> 1.11.0) kubeclient (~> 2.2.0) @@ -1022,4 +1022,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.13.7 + 1.14.2 diff --git a/PROCESS.md b/PROCESS.md index cbeb781cd3c..993d60bbba8 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -12,106 +12,54 @@ etc.). ## Common actions -### Issue team - -- Looks for issues without [workflow labels](#how-we-handle-issues) and triages - issue -- Closes invalid issues with a comment (duplicates, - [fixed in newer version](#issue-fixed-in-newer-version), - [issue report for old version](#issue-report-for-old-version), not a problem - in GitLab, etc.) -- Asks for feedback from issue reporter - ([invalid issue reports](#improperly-formatted-issue), - [format code](#code-format), etc.) -- Monitors all issues for feedback (but especially ones commented on since - automatically watching them) -- Closes issues with no feedback from the reporter for two weeks - -### Merge marshall & merge request coach - -- Responds to merge requests the issue team mentions them in and monitors for - new merge requests -- Provides feedback to the merge request submitter to improve the merge request - (style, tests, etc.) -- Mark merge requests `Ready for Merge` when they meet the - [contribution acceptance criteria] -- Mention developer(s) based on the - [list of members and their specialities][team] -- Closes merge requests with no feedback from the reporter for two weeks - -## Priorities of the issue team - -1. Mentioning people (critical) -1. Workflow labels (normal) -1. Functional labels (minor) -1. Assigning issues (avoid if possible) - -## Mentioning people +### Issue triaging + +Our issue triage policies are [described in our handbook]. You are very welcome +to help the GitLab team triage issues. We also organize [issue bash events] once +every quarter. The most important thing is making sure valid issues receive feedback from the development team. Therefore the priority is mentioning developers that can help on those issues. Please select someone with relevant experience from -[GitLab core team][core-team]. If there is nobody mentioned with that expertise +[GitLab team][team]. If there is nobody mentioned with that expertise look in the commit history for the affected files to find someone. Avoid mentioning the lead developer, this is the person that is least likely to give a timely response. If the involvement of the lead developer is needed the other core team members will mention this person. -## Workflow labels +[described in our handbook]: https://about.gitlab.com/handbook/engineering/issues/issue-triage-policies/ +[issue bash events]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17815 -Workflow labels are purposely not very detailed since that would be hard to keep -updated as you would need to re-evaluate them after every comment. We optionally -use functional labels on demand when we want to group related issues to get an -overview (for example all issues related to RVM, to tackle them in one go) and -to add details to the issue. +### Merge request coaching -- ~"Awaiting Feedback" Feedback pending from the reporter -- ~UX needs help from a UX designer -- ~Frontend needs help from a Front-end engineer. Please follow the - ["Implement design & UI elements" guidelines]. -- ~"Accepting Merge Requests" is a low priority, well-defined issue that we - encourage people to contribute to. Not exclusive with other labels. -- ~"feature proposal" is a proposal for a new feature for GitLab. People are encouraged to vote -in support or comment for further detail. Do not use `feature request`. -- ~bug is an issue reporting undesirable or incorrect behavior. -- ~customer is an issue reported by enterprise subscribers. This label should -be accompanied by *bug* or *feature proposal* labels. +Several people from the [GitLab team][team] are helping community members to get +their contributions accepted by meeting our [Definition of done][CONTRIBUTING.md#definition-of-done]. -Example workflow: when a UX designer provided a design but it needs frontend work they remove the UX label and add the frontend label. +What you can expect from them is described at https://about.gitlab.com/jobs/merge-request-coach/. + +## Workflow labels -## Functional labels +Labelling issues is described in the [GitLab Inc engineering workflow]. -These labels describe what development specialities are involved such as: `CI`, -`Core`, `Documentation`, `Frontend`, `Issues`, `Merge Requests`, `Omnibus`, -`Release`, `Repository`, `UX`. +[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues ## Assigning issues If an issue is complex and needs the attention of a specific person, assignment is a good option but assigning issues might discourage other people from contributing to that issue. We need all the contributions we can get so this should never be discouraged. Also, an assigned person might not have time for a few weeks, so others should feel free to takeover. -## Label colors - -- Light orange `#fef2c0`: workflow labels for issue team members (awaiting - feedback, awaiting confirmation of fix) -- Bright orange `#eb6420`: workflow labels for core team members (attached MR, - awaiting developer action/feedback) -- Light blue `#82C5FF`: functional labels -- Green labels `#009800`: issues that can generally be ignored. For example, - issues given the following labels normally can be closed immediately: - - Support (see copy & paste response: - [Support requests and configuration questions](#support-requests-and-configuration-questions) - ## Be kind Be kind to people trying to contribute. Be aware that people may be a non-native English speaker, they might not understand things or they might be very sensitive as to how you word things. Use Emoji to express your feelings (heart, -star, smile, etc.). Some good tips about giving feedback to merge requests is in -the [Thoughtbot code review guide]. +star, smile, etc.). Some good tips about code reviews can be found in our +[Code Review Guidelines]. + +[Code Review Guidelines]: https://docs.gitlab.com/ce/development/code_review.html ## Feature Freeze -5 working days before the 22nd the stable branches for the upcoming release will +On the 7th of each month, the stable branches for the upcoming release will be frozen for major changes. Merge requests may still be merged into master during this period. By freezing the stable branches prior to a release there's no need to worry about last minute merge requests potentially breaking a lot of @@ -120,10 +68,9 @@ things. What is considered to be a major change is determined on a case by case basis as this definition depends very much on the context of changes. For example, a 5 line change might have a big impact on the entire application. Ultimately the -decision will be made by those reviewing a merge request and the release -manager. +decision will be made by the maintainers and the release managers. -During the feature freeze all merge requests that are meant to go into the next +During the feature freeze all merge requests that are meant to go into the upcoming release should have the correct milestone assigned _and_ have the label ~"Pick into Stable" set. Merge requests without a milestone and this label will not be merged into any stable branches. @@ -189,7 +136,6 @@ prevent duplication with the GitLab.com issue tracker. Since this is an older issue I'll be closing this for now. If you think this is still an issue I encourage you to open it on the \[GitLab.com issue tracker\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues). -[core-team]: https://about.gitlab.com/core-team/ [team]: https://about.gitlab.com/team/ [contribution acceptance criteria]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria ["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js index a6bc262b657..7e6c44fa1cd 100644 --- a/app/assets/javascripts/behaviors/autosize.js +++ b/app/assets/javascripts/behaviors/autosize.js @@ -1,7 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, max-len */ /* global autosize */ -/*= require jquery.ba-resize */ /*= require autosize */ (function() { diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 6a49715590c..a7181904ac9 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -1,6 +1,19 @@ /* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, vars-on-top, no-var, max-len */ (function(w) { $(function() { + var toggleContainer = function(container, /* optional */toggleState) { + var $container = $(container); + + $container + .find('.js-toggle-button .fa') + .toggleClass('fa-chevron-up', toggleState) + .toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined); + + $container + .find('.js-toggle-content') + .toggle(toggleState); + }; + // Toggle button. Show/hide content inside parent container. // Button does not change visibility. If button has icon - it changes chevron style. // @@ -10,14 +23,7 @@ // $('body').on('click', '.js-toggle-button', function(e) { e.preventDefault(); - $(this) - .find('.fa') - .toggleClass('fa-chevron-down fa-chevron-up') - .end() - .closest('.js-toggle-container') - .find('.js-toggle-content') - .toggle() - ; + toggleContainer($(this).closest('.js-toggle-container')); }); // If we're accessing a permalink, ensure it is not inside a @@ -26,8 +32,8 @@ var anchor = hash && document.getElementById(hash); var container = anchor && $(anchor).closest('.js-toggle-container'); - if (container && container.find('.js-toggle-content').is(':hidden')) { - container.find('.js-toggle-button').trigger('click'); + if (container) { + toggleContainer(container, true); anchor.scrollIntoView(); } }); diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6 index 02459722bbf..75dfcb66bb0 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js.es6 +++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6 @@ -29,6 +29,12 @@ watch: { detail: { handler () { + if (this.issue.id !== this.detail.issue.id) { + $('.js-issue-board-sidebar', this.$el).each((i, el) => { + $(el).data('glDropdown').clearMenu(); + }); + } + this.issue = this.detail.issue; }, deep: true diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 529d476ca4e..edec21e3b63 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -8,7 +8,6 @@ /* global ShortcutsIssuable */ /* global ZenMode */ /* global Milestone */ -/* global GLForm */ /* global IssuableForm */ /* global LabelsSelect */ /* global MilestoneSelect */ @@ -64,17 +63,6 @@ new UsernameValidator(); new ActiveTabMemoizer(); break; - case 'sessions:create': - if (!gon.u2f) break; - window.gl.u2fAuthenticate = new gl.U2FAuthenticate( - $("#js-authenticate-u2f"), - '#js-login-u2f-form', - gon.u2f, - document.querySelector('#js-login-2fa-device'), - document.querySelector('.js-2fa-form'), - ); - window.gl.u2fAuthenticate.start(); - break; case 'projects:boards:show': case 'projects:boards:index': shortcut_handler = new ShortcutsNavigation(); @@ -110,7 +98,7 @@ case 'projects:milestones:edit': new ZenMode(); new gl.DueDateSelectors(); - new GLForm($('.milestone-form')); + new gl.GLForm($('.milestone-form')); break; case 'groups:milestones:new': new ZenMode(); @@ -121,7 +109,7 @@ case 'projects:issues:new': case 'projects:issues:edit': shortcut_handler = new ShortcutsNavigation(); - new GLForm($('.issue-form')); + new gl.GLForm($('.issue-form')); new IssuableForm($('.issue-form')); new LabelsSelect(); new MilestoneSelect(); @@ -131,7 +119,7 @@ case 'projects:merge_requests:edit': new gl.Diff(); shortcut_handler = new ShortcutsNavigation(); - new GLForm($('.merge-request-form')); + new gl.GLForm($('.merge-request-form')); new IssuableForm($('.merge-request-form')); new LabelsSelect(); new MilestoneSelect(); @@ -139,11 +127,11 @@ break; case 'projects:tags:new': new ZenMode(); - new GLForm($('.tag-form')); + new gl.GLForm($('.tag-form')); break; case 'projects:releases:edit': new ZenMode(); - new GLForm($('.release-form')); + new gl.GLForm($('.release-form')); break; case 'projects:merge_requests:show': new gl.Diff(); @@ -280,6 +268,17 @@ break; } switch (path.first()) { + case 'sessions': + case 'omniauth_callbacks': + if (!gon.u2f) break; + gl.u2fAuthenticate = new gl.U2FAuthenticate( + $('#js-authenticate-u2f'), + '#js-login-u2f-form', + gon.u2f, + document.querySelector('#js-login-2fa-device'), + document.querySelector('.js-2fa-form'), + ); + gl.u2fAuthenticate.start(); case 'admin': new Admin(); switch (path[1]) { @@ -332,7 +331,7 @@ new gl.Wikis(); shortcut_handler = new ShortcutsNavigation(); new ZenMode(); - new GLForm($('.wiki-form')); + new gl.GLForm($('.wiki-form')); break; case 'snippets': shortcut_handler = new ShortcutsNavigation(); @@ -357,7 +356,7 @@ } // If we haven't installed a custom shortcut handler, install the default one if (!shortcut_handler) { - return new Shortcuts(); + new Shortcuts(); } }; diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index fea642467fa..558828e1bc9 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -180,9 +180,9 @@ <tr> <th class="environments-name">Environment</th> <th class="environments-deploy">Last deployment</th> - <th class="environments-build">Build</th> + <th class="environments-build">Job</th> <th class="environments-commit">Commit</th> - <th class="environments-date">Created</th> + <th class="environments-date">Updated</th> <th class="hidden-xs environments-actions"></th> </tr> </thead> diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 7bf199d9274..162fd6044e5 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -39,8 +39,15 @@ getSearchInput() { const query = gl.DropdownUtils.getSearchInput(this.input); const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); + let value = lastToken.value || ''; - return lastToken.value || ''; + // Removes the first character if it is a quotation so that we can search + // with multiple words + if (value[0] === '"' || value[0] === '\'') { + value = value.slice(1); + } + + return value; } init() { diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 3f23095dad9..7f1f2a5d278 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -83,12 +83,12 @@ _a = decodeURI("%C3%80"); _y = decodeURI("%C3%BF"); - regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?![" + atSymbolsWithBar + "])([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi'); + regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?![" + atSymbolsWithBar + "])(([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi'); match = regexp.exec(subtext); if (match) { - return match[2] || match[1]; + return (match[1] || match[1] === "") ? match[1] : match[2]; } else { return null; } diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index cc1c0877cdf..5c86e98567a 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -249,7 +249,7 @@ _this.fullData = data; _this.parseData(_this.fullData); _this.focusTextInput(); - if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val().trim() !== '') { + if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') { return _this.filter.input.trigger('input'); } }; @@ -512,12 +512,17 @@ // Append the menu into the dropdown GitLabDropdown.prototype.appendMenu = function(html) { + return this.clearMenu().append(html); + }; + + GitLabDropdown.prototype.clearMenu = function() { var selector; selector = '.dropdown-content'; if (this.dropdown.find(".dropdown-toggle-page").length) { selector = ".dropdown-page-one .dropdown-content"; } - return $(selector, this.dropdown).empty().append(html); + + return $(selector, this.dropdown).empty(); }; GitLabDropdown.prototype.renderItem = function(data, group, index) { diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js deleted file mode 100644 index 08b2494f3df..00000000000 --- a/app/assets/javascripts/gl_form.js +++ /dev/null @@ -1,62 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */ -/* global GitLab */ -/* global DropzoneInput */ -/* global autosize */ - -(function() { - this.GLForm = (function() { - function GLForm(form) { - this.form = form; - this.textarea = this.form.find('textarea.js-gfm-input'); - // Before we start, we should clean up any previous data for this form - this.destroy(); - // Setup the form - this.setupForm(); - this.form.data('gl-form', this); - } - - GLForm.prototype.destroy = function() { - // Clean form listeners - this.clearEventListeners(); - return this.form.data('gl-form', null); - }; - - GLForm.prototype.setupForm = function() { - var isNewForm; - isNewForm = this.form.is(':not(.gfm-form)'); - this.form.removeClass('js-new-note-form'); - if (isNewForm) { - this.form.find('.div-dropzone').remove(); - this.form.addClass('gfm-form'); - // remove notify commit author checkbox for non-commit notes - gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button')); - gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); - new DropzoneInput(this.form); - autosize(this.textarea); - // form and textarea event listeners - this.addEventListeners(); - } - gl.text.init(this.form); - // hide discard button - this.form.find('.js-note-discard').hide(); - return this.form.show(); - }; - - GLForm.prototype.clearEventListeners = function() { - this.textarea.off('focus'); - this.textarea.off('blur'); - return gl.text.removeListeners(this.form); - }; - - GLForm.prototype.addEventListeners = function() { - this.textarea.on('focus', function() { - return $(this).closest('.md-area').addClass('is-focused'); - }); - return this.textarea.on('blur', function() { - return $(this).closest('.md-area').removeClass('is-focused'); - }); - }; - - return GLForm; - })(); -}).call(this); diff --git a/app/assets/javascripts/gl_form.js.es6 b/app/assets/javascripts/gl_form.js.es6 new file mode 100644 index 00000000000..0b446ff364a --- /dev/null +++ b/app/assets/javascripts/gl_form.js.es6 @@ -0,0 +1,92 @@ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */ +/* global GitLab */ +/* global DropzoneInput */ +/* global autosize */ + +(() => { + const global = window.gl || (window.gl = {}); + + function GLForm(form) { + this.form = form; + this.textarea = this.form.find('textarea.js-gfm-input'); + // Before we start, we should clean up any previous data for this form + this.destroy(); + // Setup the form + this.setupForm(); + this.form.data('gl-form', this); + } + + GLForm.prototype.destroy = function() { + // Clean form listeners + this.clearEventListeners(); + return this.form.data('gl-form', null); + }; + + GLForm.prototype.setupForm = function() { + var isNewForm; + isNewForm = this.form.is(':not(.gfm-form)'); + this.form.removeClass('js-new-note-form'); + if (isNewForm) { + this.form.find('.div-dropzone').remove(); + this.form.addClass('gfm-form'); + // remove notify commit author checkbox for non-commit notes + gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button')); + gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); + new DropzoneInput(this.form); + autosize(this.textarea); + // form and textarea event listeners + this.addEventListeners(); + } + gl.text.init(this.form); + // hide discard button + this.form.find('.js-note-discard').hide(); + this.form.show(); + if (this.isAutosizeable) this.setupAutosize(); + }; + + GLForm.prototype.setupAutosize = function () { + this.textarea.off('autosize:resized') + .on('autosize:resized', this.setHeightData.bind(this)); + + this.textarea.off('mouseup.autosize') + .on('mouseup.autosize', this.destroyAutosize.bind(this)); + + setTimeout(() => { + autosize(this.textarea); + this.textarea.css('resize', 'vertical'); + }, 0); + }; + + GLForm.prototype.setHeightData = function () { + this.textarea.data('height', this.textarea.outerHeight()); + }; + + GLForm.prototype.destroyAutosize = function () { + const outerHeight = this.textarea.outerHeight(); + + if (this.textarea.data('height') === outerHeight) return; + + autosize.destroy(this.textarea); + + this.textarea.data('height', outerHeight); + this.textarea.outerHeight(outerHeight); + this.textarea.css('max-height', window.outerHeight); + }; + + GLForm.prototype.clearEventListeners = function() { + this.textarea.off('focus'); + this.textarea.off('blur'); + return gl.text.removeListeners(this.form); + }; + + GLForm.prototype.addEventListeners = function() { + this.textarea.on('focus', function() { + return $(this).closest('.md-area').addClass('is-focused'); + }); + return this.textarea.on('blur', function() { + return $(this).closest('.md-area').removeClass('is-focused'); + }); + }; + + global.GLForm = GLForm; +})(); diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index a50bc4a9057..bc88dc2d092 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -59,11 +59,11 @@ } else { avatar = gon.default_avatar_url; } - return "<div class='group-result'> <div class='group-name'>" + group.name + "</div> <div class='group-path'>" + group.path + "</div> </div>"; + return "<div class='group-result'> <div class='group-name'>" + group.full_name + "</div> <div class='group-path'>" + group.full_path + "</div> </div>"; }; GroupsSelect.prototype.formatSelection = function(group) { - return group.name; + return group.full_name; }; return GroupsSelect; diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6 index 8f48b1f57ce..2a50b72c8aa 100644 --- a/app/assets/javascripts/label_manager.js.es6 +++ b/app/assets/javascripts/label_manager.js.es6 @@ -8,6 +8,7 @@ this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels'); this.otherLabels = otherLabels || $('.js-other-labels'); this.errorMessage = 'Unable to update label prioritization at this time'; + this.emptyState = document.querySelector('#js-priority-labels-empty-state'); this.prioritizedLabels.sortable({ items: 'li', placeholder: 'list-placeholder', @@ -29,7 +30,12 @@ const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`); $tooltip.tooltip('destroy'); - return _this.toggleLabelPriority($label, action); + _this.toggleLabelPriority($label, action); + _this.toggleEmptyState($label, $btn, action); + } + + toggleEmptyState($label, $btn, action) { + this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li')); } toggleLabelPriority($label, action, persistState) { diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index 51993bb3420..e3bff2559fd 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -162,6 +162,7 @@ w.gl.utils.getSelectedFragment = () => { const selection = window.getSelection(); + if (selection.rangeCount === 0) return null; const documentFragment = selection.getRangeAt(0).cloneContents(); if (documentFragment.textContent.length === 0) return null; diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 09ee8dbe9d7..37af422a09e 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -110,9 +110,8 @@ }; MergeRequest.prototype.initCommitMessageListeners = function() { - var textarea = $('textarea.js-commit-message'); - - $('a.js-with-description-link').on('click', function(e) { + $(document).on('click', 'a.js-with-description-link', function(e) { + var textarea = $('textarea.js-commit-message'); e.preventDefault(); textarea.val(textarea.data('messageWithDescription')); @@ -120,7 +119,8 @@ $('p.js-without-description-hint').show(); }); - $('a.js-without-description-link').on('click', function(e) { + $(document).on('click', 'a.js-without-description-link', function(e) { + var textarea = $('textarea.js-commit-message'); e.preventDefault(); textarea.val(textarea.data('messageWithoutDescription')); diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 7cc319e2f4e..fa782ebbedf 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -154,12 +154,22 @@ return; } if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); - if (data.status !== _this.opts.ci_status && (data.status != null)) { + if (data.status !== _this.opts.ci_status || + data.sha !== _this.opts.ci_sha || + data.pipeline !== _this.opts.ci_pipeline) { _this.opts.ci_status = data.status; _this.showCIStatus(data.status); if (data.coverage) { _this.showCICoverage(data.coverage); } + if (data.pipeline) { + _this.opts.ci_pipeline = data.pipeline; + _this.updatePipelineUrls(data.pipeline); + } + if (data.sha) { + _this.opts.ci_sha = data.sha; + _this.updateCommitUrls(data.sha); + } if (showNotification) { status = _this.ciLabelForStatus(data.status); if (status === "preparing") { @@ -248,6 +258,16 @@ return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-info btn-create').addClass(css_class); }; + MergeRequestWidget.prototype.updatePipelineUrls = function(id) { + const pipelineUrl = this.opts.pipeline_path; + $('.pipeline').text(`#${id}`).attr('href', [pipelineUrl, id].join('/')); + }; + + MergeRequestWidget.prototype.updateCommitUrls = function(id) { + const commitsUrl = this.opts.commits_path; + $('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/')); + }; + return MergeRequestWidget; })(); })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 9db830a7ada..c4722be3625 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1,6 +1,5 @@ /* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape */ /* global Flash */ -/* global GLForm */ /* global Autosave */ /* global ResolveService */ /* global mrRefreshWidgetUrl */ @@ -420,7 +419,7 @@ Notes.prototype.setupNoteForm = function(form) { var textarea; - new GLForm(form); + new gl.GLForm(form); textarea = form.find(".js-note-text"); return new Autosave(textarea, ["Note", form.find("#note_noteable_type").val(), form.find("#note_noteable_id").val(), form.find("#note_commit_id").val(), form.find("#note_type").val(), form.find("#note_line_code").val(), form.find("#note_position").val()]); }; @@ -884,7 +883,7 @@ var targetId = $originalContentEl.data('target-id'); var targetType = $originalContentEl.data('target-type'); - new GLForm($editForm.find('form')); + new gl.GLForm($editForm.find('form')); $editForm.find('form') .attr('action', postUrl) diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 7cf630a1d76..67f8804666d 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -58,6 +58,11 @@ }; Project.prototype.initRefSwitcher = function() { + var refListItem = document.createElement('li'), + refLink = document.createElement('a'); + + refLink.href = '#'; + return $('.js-project-refs-dropdown').each(function() { var $dropdown, selected; $dropdown = $(this); @@ -67,7 +72,8 @@ return $.ajax({ url: $dropdown.data('refs-url'), data: { - ref: $dropdown.data('ref') + ref: $dropdown.data('ref'), + search: term }, dataType: "json" }).done(function(refs) { @@ -76,16 +82,29 @@ }, selectable: true, filterable: true, + filterRemote: true, filterByText: true, fieldName: $dropdown.data('field-name'), renderRow: function(ref) { - var link; + var li = refListItem.cloneNode(false); + if (ref.header != null) { - return $('<li />').addClass('dropdown-header').text(ref.header); + li.className = 'dropdown-header'; + li.textContent = ref.header; } else { - link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', ref); - return $('<li />').append(link); + var link = refLink.cloneNode(false); + + if (ref === selected) { + link.className = 'is-active'; + } + + link.textContent = ref; + link.dataset.ref = ref; + + li.appendChild(link); } + + return li; }, id: function(obj, $el) { return $el.attr('data-ref'); diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 index 03f4531abf5..5cf28aa7a73 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 @@ -49,7 +49,7 @@ class ProtectedBranchDropdown { onClickCreateWildcard() { // Refresh the dropdown's data, which ends up calling `getProtectedBranches` this.$dropdown.data('glDropdown').remote.execute(); - this.$dropdown.data('glDropdown').selectRowAtIndex(0); + this.$dropdown.data('glDropdown').selectRowAtIndex(); } getProtectedBranches(term, callback) { diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index 489e567259c..b1c0dc37b4d 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -13,12 +13,12 @@ filterable: true, fieldName: 'group_id', search: { - fields: ['name'] + fields: ['full_name'] }, data: function(term, callback) { return Api.groups(term, {}, function(data) { data.unshift({ - name: 'Any' + full_name: 'Any' }); data.splice(1, 0, 'divider'); return callback(data); @@ -28,10 +28,10 @@ return obj.id; }, text: function(obj) { - return obj.name; + return obj.full_name; }, toggleLabel: function(obj) { - return ($groupDropdown.data('default-label')) + " " + obj.name; + return ($groupDropdown.data('default-label')) + " " + obj.full_name; }, clicked: (function(_this) { return function() { diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 4ef516af8c8..4dcc5ebe28f 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -39,17 +39,20 @@ } ShortcutsIssuable.prototype.replyWithSelectedText = function() { - var quote, replyField, documentFragment, selected, separator; + var quote, documentFragment, selected, separator; + var replyField = $('.js-main-target-form #note_note'); documentFragment = window.gl.utils.getSelectedFragment(); - if (!documentFragment) return; + if (!documentFragment) { + replyField.focus(); + return; + } // If the documentFragment contains more than just Markdown, don't copy as GFM. if (documentFragment.querySelector('.md, .wiki')) return; selected = window.gl.CopyAsGFM.nodeToGFM(documentFragment); - replyField = $('.js-main-target-form #note_note'); if (selected.trim() === "") { return; } diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6 index ef9c0a885fb..05622916ff8 100644 --- a/app/assets/javascripts/todos.js.es6 +++ b/app/assets/javascripts/todos.js.es6 @@ -85,7 +85,7 @@ }, success: (data) => { $target.remove(); - $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>'); + $('.js-todos-all').html('<div class="nothing-here-block">You\'re all done!</div>'); return this.updateBadges(data); } }); diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js index e7280d643d3..6e40dfdf3d8 100644 --- a/app/assets/javascripts/users/calendar.js +++ b/app/assets/javascripts/users/calendar.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, vars-on-top, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, max-len */ /* global d3 */ -/* global dateFormat */ (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; @@ -33,7 +32,7 @@ date.setDate(date.getDate() + i); var day = date.getDay(); - var count = timestamps[dateFormat(date, 'yyyy-mm-dd')]; + var count = timestamps[date.format('yyyy-mm-dd')]; // Create a new group array if this is the first day of the week // or if is first object @@ -122,7 +121,7 @@ if (stamp.count > 0) { contribText = stamp.count + " contribution" + (stamp.count > 1 ? 's' : ''); } - dateText = dateFormat(date, 'mmm d, yyyy'); + dateText = date.format('mmm d, yyyy'); return contribText + "<br />" + (gl.utils.getDayName(date)) + " " + dateText; }; })(this)).attr('class', 'user-contrib-cell js-tooltip').attr('fill', (function(_this) { diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 index b195b0ef3ba..01f8b6519a4 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -26,10 +26,9 @@ v-if='actions' class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions" data-toggle="dropdown" - title="Manual build" + title="Manual job" data-placement="top" - data-toggle="dropdown" - aria-label="Manual build" + aria-label="Manual job" > <span v-html='svgs.iconPlay' aria-hidden="true"></span> <i class="fa fa-caret-down" aria-hidden="true"></i> @@ -54,7 +53,6 @@ data-toggle="dropdown" title="Artifacts" data-placement="top" - data-toggle="dropdown" aria-label="Artifacts" > <i class="fa fa-download" aria-hidden="true"></i> diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 592ef0d647f..0f9213b98e3 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -278,6 +278,10 @@ display: inline-block; } + .btn { + margin: $btn-side-margin $btn-side-margin 0 0; + } + @media(max-width: $screen-xs-max) { margin-top: 50px; text-align: center; @@ -286,6 +290,12 @@ width: 100%; } } + + @media(min-width: $screen-xs-max) { + &.labels .text-content { + margin-top: 70px; + } + } } .flex-container-block { diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index bb6129158d9..cda46223492 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -330,10 +330,6 @@ } } -.btn-file-option { - background: linear-gradient(180deg, $white-light 25%, $gray-light 100%); -} - .btn-build { margin-left: 10px; diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index 868f28cd356..db8d231a82a 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -58,3 +58,9 @@ fill: $gl-text-color; } } + +.icon-link { + &:hover { + text-decoration: none; + } +} diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 401c2d0f6ee..fd081c2d7e1 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -294,16 +294,18 @@ .container-fluid { position: relative; + + .nav-control { + @media (max-width: $screen-sm-max) { + margin-right: 75px; + } + } } .controls { float: right; padding: 7px 0 0; - @media (max-width: $screen-sm-max) { - display: none; - } - i { color: $layout-link-gray; } @@ -361,6 +363,7 @@ .fade-left { @include fade(right, $gray-light); left: -5px; + text-align: center; .fa { left: -7px; diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 12d56359d7d..ea2d26dd5a0 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -162,6 +162,10 @@ } } } + + &.panel-without-border { + border: 0; + } } .panel-succes .panel-heading, diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index ab68b360f93..0c013915a63 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -56,15 +56,24 @@ &.right { float: right; padding-right: 0; + } - a { - color: $gl-text-color; - } + .modify-merge-commit-link { + color: $gl-text-color; } - .remove_source_checkbox { + .merge-param-checkbox { margin: 0; } + + a .fa-question-circle { + color: $gl-text-color-secondary; + + &:hover, + &:focus { + color: $link-hover-color; + } + } } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index da0caa30c26..f310cc72da0 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -467,7 +467,7 @@ ul.notes { } .add-diff-note { - margin-top: -4px; + margin-top: -8px; border-radius: 40px; background: $white-light; padding: 4px; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 47dfc22d533..367a468e1ba 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -201,7 +201,8 @@ .stage-container { display: inline-block; position: relative; - margin-right: 6px; + height: 22px; + margin: 3px 6px 3px 0; .tooltip { white-space: nowrap; @@ -494,31 +495,27 @@ // Action Icons in big pipeline-graph nodes > .ci-action-icon-container .ci-action-icon-wrapper { - i { - color: $border-color; - border-radius: 100%; - border: 1px solid $border-color; - padding: 5px 6px; - font-size: 13px; - background: $white-light; - height: 30px; - width: 30px; - - &::before { - position: relative; - top: 3px; - left: 3px; - } + height: 30px; + width: 30px; + background: $white-light; + border: 1px solid $border-color; + border-radius: 100%; + display: block; - &:hover { - color: $gl-text-color; - background-color: $stage-hover-bg; - border: 1px solid $stage-hover-bg; - } + &:hover { + background-color: $stage-hover-bg; + border: 1px solid $stage-hover-bg; } - .ci-play-icon { - padding: 5px 5px 5px 7px; + svg { + fill: $border-color; + position: relative; + left: -1px; + top: -1px; + } + + &:hover svg { + fill: $gl-text-color; } } @@ -657,7 +654,7 @@ font-weight: 100; font-size: 15px; position: absolute; - right: 5px; + right: 13px; top: 8px; } @@ -825,11 +822,23 @@ &:hover, &:focus { - text-decoration: none; - color: $gl-text-color; background-color: $stage-hover-bg; border: 1px solid transparent; } + + svg { + width: 22px; + height: 22px; + left: -6px; + position: relative; + top: -3px; + fill: $action-icon-color; + } + + &:hover svg, + &:focus svg { + fill: $gl-text-color; + } } // link to the build diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 01675acc62e..0d5604aae69 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -76,6 +76,10 @@ font-size: 14px; } + .action-name { + font-weight: normal; + } + .todo-body { .todo-note { word-wrap: break-word; diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index b09ae423096..39c8c6d8a0c 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -45,7 +45,7 @@ class Admin::ProjectsController < Admin::ApplicationController protected def project - @project = Project.find_with_namespace( + @project = Project.find_by_full_path( [params[:namespace_id], '/', params[:id]].join('') ) @project || render_404 diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb index bc65dcc33d3..70ac6a75434 100644 --- a/app/controllers/admin/runner_projects_controller.rb +++ b/app/controllers/admin/runner_projects_controller.rb @@ -24,7 +24,7 @@ class Admin::RunnerProjectsController < Admin::ApplicationController private def project - @project = Project.find_with_namespace( + @project = Project.find_by_full_path( [params[:namespace_id], '/', params[:project_id]].join('') ) @project || render_404 diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 6f43ce5226d..6286d67d30c 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -4,13 +4,15 @@ module CreatesCommit def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) set_commit_variables + start_branch = @mr_target_branch unless initial_commit? commit_params = @commit_params.merge( - source_project: @project, - source_branch: @ref, - target_branch: @target_branch + start_project: @mr_target_project, + start_branch: start_branch, + target_branch: @mr_source_branch ) - result = service.new(@tree_edit_project, current_user, commit_params).execute + result = service.new( + @mr_source_project, current_user, commit_params).execute if result[:status] == :success update_flash_notice(success_notice) @@ -89,20 +91,18 @@ module CreatesCommit @mr_source_project != @mr_target_project end - def different_branch? - @mr_source_branch != @mr_target_branch || different_project? - end - def create_merge_request? - params[:create_merge_request].present? && different_branch? + # XXX: Even if the field is set, if we're checking the same branch + # as the target branch in the same project, + # we don't want to create a merge request. + params[:create_merge_request].present? && + (different_project? || @ref != @target_branch) end + # TODO: We should really clean this up def set_commit_variables - @mr_source_branch ||= @target_branch - if can?(current_user, :push_code, @project) # Edit file in this project - @tree_edit_project = @project @mr_source_project = @project if @project.forked? @@ -112,15 +112,34 @@ module CreatesCommit else # Merge request to this project @mr_target_project = @project - @mr_target_branch ||= @ref + @mr_target_branch = @ref || @target_branch end else - # Edit file in fork - @tree_edit_project = current_user.fork_of(@project) # Merge request from fork to this project - @mr_source_project = @tree_edit_project + @mr_source_project = current_user.fork_of(@project) @mr_target_project = @project - @mr_target_branch ||= @ref + @mr_target_branch = @ref || @target_branch end + + @mr_source_branch = guess_mr_source_branch + end + + def initial_commit? + @mr_target_branch.nil? || + !@mr_target_project.repository.branch_exists?(@mr_target_branch) + end + + def guess_mr_source_branch + # XXX: Happens when viewing a commit without a branch. In this case, + # @target_branch would be the default branch for @mr_source_project, + # however we want a generated new branch here. Thus we can't use + # @target_branch, but should pass nil to indicate that we want a new + # branch instead of @target_branch. + return if + create_merge_request? && + # XXX: Don't understand why rubocop prefers this indention + @mr_source_project.repository.branch_exists?(@target_branch) + + @target_branch end end diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb index 99acd98ae13..562f92bd83c 100644 --- a/app/controllers/concerns/spammable_actions.rb +++ b/app/controllers/concerns/spammable_actions.rb @@ -7,7 +7,7 @@ module SpammableActions def mark_as_spam if SpamService.new(spammable).mark_as_spam! - redirect_to spammable, notice: "#{spammable.class} was submitted to Akismet successfully." + redirect_to spammable, notice: "#{spammable.spammable_entity_type.titlecase} was submitted to Akismet successfully." else redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.' end diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index c08eb811532..3ba8c2f8bb9 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -10,10 +10,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.page(params[:page]) - @last_push = current_user.recent_push - respond_to do |format| - format.html + format.html { @last_push = current_user.recent_push } format.atom do event_filter load_events diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 4dda4e51f6a..79d420a32d3 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -4,6 +4,7 @@ class DashboardController < Dashboard::ApplicationController before_action :event_filter, only: :activity before_action :projects, only: [:issues, :merge_requests] + before_action :set_show_full_reference, only: [:issues, :merge_requests] respond_to :html @@ -34,4 +35,8 @@ class DashboardController < Dashboard::ApplicationController @events = @event_filter.apply_filter(@events).with_associations @events = @events.limit(20).offset(params[:offset] || 0) end + + def set_show_full_reference + @show_full_reference = true + end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index f81237db991..264b14713fb 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -84,7 +84,7 @@ class GroupsController < Groups::ApplicationController if Groups::UpdateService.new(@group, current_user, group_params).execute redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated." else - @group.reset_path! + @group.restore_path! render action: "edit" end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index b2ff36f6538..ba523b190bf 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -24,7 +24,7 @@ class Projects::ApplicationController < ApplicationController end project_path = "#{namespace}/#{id}" - @project = Project.find_with_namespace(project_path) + @project = Project.find_by_full_path(project_path) if can?(current_user, :read_project, @project) && !@project.pending_delete? if @project.path_with_namespace != project_path diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index bfc59bcc862..b5a7078a3a1 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -30,6 +30,17 @@ class Projects::CommitController < Projects::ApplicationController end def pipelines + @pipelines = @commit.pipelines.order(id: :desc) + + respond_to do |format| + format.html + format.json do + render json: PipelineSerializer + .new(project: @project, user: @current_user) + .with_pagination(request, response) + .represent(@pipelines) + end + end end def branches @@ -39,7 +50,7 @@ class Projects::CommitController < Projects::ApplicationController end def revert - assign_change_commit_vars(@commit.revert_branch_name) + assign_change_commit_vars return render_404 if @target_branch.blank? @@ -48,7 +59,7 @@ class Projects::CommitController < Projects::ApplicationController end def cherry_pick - assign_change_commit_vars(@commit.cherry_pick_branch_name) + assign_change_commit_vars return render_404 if @target_branch.blank? @@ -105,11 +116,9 @@ class Projects::CommitController < Projects::ApplicationController } end - def assign_change_commit_vars(mr_source_branch) + def assign_change_commit_vars @commit = project.commit(params[:id]) @target_branch = params[:target_branch] - @mr_source_branch = mr_source_branch - @mr_target_branch = @target_branch @commit_params = { commit: @commit, create_merge_request: params[:create_merge_request].present? || different_project? diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index d32966645c8..321cde255c3 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -46,7 +46,8 @@ class Projects::CompareController < Projects::ApplicationController end def define_diff_vars - @compare = CompareService.new.execute(@project, @head_ref, @project, @start_ref) + @compare = CompareService.new(@project, @head_ref) + .execute(@project, @start_ref) if @compare @commits = @compare.commits diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 70845617d3c..216c158e41e 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -79,7 +79,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController if project_id.blank? @project = nil else - @project = Project.find_with_namespace("#{params[:namespace_id]}/#{project_id}") + @project = Project.find_by_full_path("#{params[:namespace_id]}/#{project_id}") end end diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 824ed7be73e..1593b5c1afb 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -2,12 +2,13 @@ class Projects::LabelsController < Projects::ApplicationController include ToggleSubscriptionAction before_action :module_enabled - before_action :label, only: [:edit, :update, :destroy] + before_action :label, only: [:edit, :update, :destroy, :promote] before_action :find_labels, only: [:index, :set_priorities, :remove_priority, :toggle_subscription] 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_group!, only: [:promote] respond_to :js, :html @@ -71,13 +72,7 @@ class Projects::LabelsController < Projects::ApplicationController @label.destroy @labels = find_labels - respond_to do |format| - format.html do - redirect_to(namespace_project_labels_path(@project.namespace, @project), - notice: 'Label was removed') - end - format.js - end + redirect_to(namespace_project_labels_path(@project.namespace, @project), notice: 'Label was removed') end def remove_priority @@ -108,6 +103,32 @@ class Projects::LabelsController < Projects::ApplicationController end end + def promote + promote_service = Labels::PromoteService.new(@project, @current_user) + + begin + return render_404 unless promote_service.execute(@label) + respond_to do |format| + format.html do + redirect_to(namespace_project_labels_path(@project.namespace, @project), + notice: 'Label was promoted to a Group Label') + end + format.js + end + rescue ActiveRecord::RecordInvalid => e + Gitlab::AppLogger.error "Failed to promote label \"#{@label.title}\" to group label" + Gitlab::AppLogger.error e + + respond_to do |format| + format.html do + redirect_to(namespace_project_labels_path(@project.namespace, @project), + notice: 'Failed to promote label due to internal error. Please contact administrators.') + end + format.js + end + end + end + protected def module_enabled @@ -135,4 +156,8 @@ class Projects::LabelsController < Projects::ApplicationController def authorize_admin_labels! return render_404 unless can?(current_user, :admin_label, @project) end + + def authorize_admin_group! + return render_404 unless can?(current_user, :admin_group, @project.group) + end end diff --git a/app/controllers/projects/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb index 01d99c7df35..38f7e6eb5e9 100644 --- a/app/controllers/projects/mattermosts_controller.rb +++ b/app/controllers/projects/mattermosts_controller.rb @@ -34,7 +34,7 @@ class Projects::MattermostsController < Projects::ApplicationController end def teams - @teams ||= @service.list_teams(current_user) + @teams, @teams_error_message = @service.list_teams(current_user) end def service diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 9ac5bf4b9f8..6eb542e4bd8 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -214,7 +214,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController render 'show' end - format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_pipelines') } } + + format.json do + render json: { + html: view_to_html_string('projects/merge_requests/show/_pipelines'), + pipelines: PipelineSerializer + .new(project: @project, user: @current_user) + .with_pagination(request, response) + .represent(@pipelines) + } + end end end @@ -425,7 +434,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController title: merge_request.title, sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha), status: status, - coverage: coverage + coverage: coverage, + pipeline: pipeline.try(:id) } render json: response diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 02a97c1c574..5d193f26a8e 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -1,8 +1,9 @@ class Projects::SnippetsController < Projects::ApplicationController include ToggleAwardEmoji + include SpammableActions before_action :module_enabled - before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji] + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam] # Allow read any snippet before_action :authorize_read_project_snippet!, except: [:new, :create, :index] @@ -36,8 +37,8 @@ class Projects::SnippetsController < Projects::ApplicationController end def create - @snippet = CreateSnippetService.new(@project, current_user, - snippet_params).execute + create_params = snippet_params.merge(request: request) + @snippet = CreateSnippetService.new(@project, current_user, create_params).execute if @snippet.valid? respond_with(@snippet, @@ -88,6 +89,7 @@ class Projects::SnippetsController < Projects::ApplicationController @snippet ||= @project.snippets.find(params[:id]) end alias_method :awardable, :snippet + alias_method :spammable, :snippet def authorize_read_project_snippet! return render_404 unless can?(current_user, :read_project_snippet, @snippet) diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb index e617be8f9fb..50ba33ed570 100644 --- a/app/controllers/projects/uploads_controller.rb +++ b/app/controllers/projects/uploads_controller.rb @@ -36,7 +36,7 @@ class Projects::UploadsController < Projects::ApplicationController namespace = params[:namespace_id] id = params[:project_id] - file_project = Project.find_with_namespace("#{namespace}/#{id}") + file_project = Project.find_by_full_path("#{namespace}/#{id}") if file_project.nil? @uploader = nil diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 444ff837bb3..acca821782c 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -231,12 +231,16 @@ class ProjectsController < Projects::ApplicationController end def refs + branches = BranchesFinder.new(@repository, params).execute.map(&:name) + options = { - 'Branches' => @repository.branch_names, + 'Branches' => branches.take(100), } unless @repository.tag_count.zero? - options['Tags'] = VersionSorter.rsort(@repository.tag_names) + tags = TagsFinder.new(@repository, params).execute.map(&:name) + + options['Tags'] = tags.take(100) end # If reference is commit id - we should add it to branch/tag selectbox diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb index 627be74a38f..db2817fadf6 100644 --- a/app/controllers/root_controller.rb +++ b/app/controllers/root_controller.rb @@ -7,6 +7,7 @@ # For users who haven't customized the setting, we simply delegate to # `DashboardController#show`, which is the default. class RootController < Dashboard::ProjectsController + skip_before_action :authenticate_user!, only: [:index] before_action :redirect_to_custom_dashboard, only: [:index] def index @@ -16,7 +17,7 @@ class RootController < Dashboard::ProjectsController private def redirect_to_custom_dashboard - return unless current_user + return redirect_to new_user_session_path unless current_user case current_user.dashboard when 'stars' diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index dee57e4a388..b169d993688 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,5 +1,6 @@ class SnippetsController < ApplicationController include ToggleAwardEmoji + include SpammableActions before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download] @@ -40,8 +41,8 @@ class SnippetsController < ApplicationController end def create - @snippet = CreateSnippetService.new(nil, current_user, - snippet_params).execute + create_params = snippet_params.merge(request: request) + @snippet = CreateSnippetService.new(nil, current_user, create_params).execute respond_with @snippet.becomes(Snippet) end @@ -96,6 +97,7 @@ class SnippetsController < ApplicationController end end alias_method :awardable, :snippet + alias_method :spammable, :snippet def authorize_read_snippet! authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a112928c6de..bee323993a0 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -37,7 +37,7 @@ module ApplicationHelper if project_id.is_a?(Project) project_id else - Project.find_with_namespace(project_id) + Project.find_by_full_path(project_id) end if project.avatar_url diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index c3508443d8a..311a70725ab 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -21,7 +21,7 @@ module BlobHelper options[:link_opts]) if !on_top_of_branch?(project, ref) - button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' } + button_tag "Edit", class: "btn disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' } elsif can_edit_blob?(blob, project, ref) link_to "Edit", edit_path, class: 'btn btn-sm' elsif can?(current_user, :fork_project, project) @@ -32,7 +32,7 @@ module BlobHelper } fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) - link_to "Edit", fork_path, class: 'btn btn-file-option', method: :post + link_to "Edit", fork_path, class: 'btn', method: :post end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index e9461b9f859..6dcb624c4da 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -198,7 +198,7 @@ module CommitsHelper link_to( namespace_project_blob_path(project.namespace, project, tree_join(commit_sha, diff_new_path)), - class: 'btn view-file js-view-file btn-file-option' + class: 'btn view-file js-view-file' ) do raw('View file @') + content_tag(:span, commit_sha[0..6], class: 'commit-short-id') diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index e5bb8b93e76..03354c235eb 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -162,6 +162,10 @@ module IssuablesHelper ] end + def issuable_reference(issuable) + @show_full_reference ? issuable.to_reference(full: true) : issuable.to_reference(@group || @project) + end + def issuable_filter_present? issuable_filter_params.any? { |k| params.key?(k) } end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 8c2c4e8833b..83ff898e68a 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -143,4 +143,16 @@ module MergeRequestsHelper def different_base?(version1, version2) version1 && version2 && version1.base_commit_sha != version2.base_commit_sha end + + def merge_params(merge_request) + { + merge_when_build_succeeds: true, + should_remove_source_branch: true, + sha: merge_request.diff_head_sha + }.merge(merge_params_ee(merge_request)) + end + + def merge_params_ee(merge_request) + {} + end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 6654f6997ce..37b69423c97 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -89,7 +89,7 @@ module SearchHelper { category: "Groups", id: group.id, - label: "#{search_result_sanitize(group.name)}", + label: "#{search_result_sanitize(group.full_name)}", url: group_path(group) } end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index 3a83ae15dd8..fc93acfe63e 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -93,10 +93,6 @@ module VisibilityLevelHelper current_application_settings.default_project_visibility end - def default_snippet_visibility - current_application_settings.default_snippet_visibility - end - def default_group_visibility current_application_settings.default_group_visibility end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5fe8ddf69d7..b1f77bf242c 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -275,29 +275,23 @@ module Ci end def update_coverage - return unless project - coverage_regex = project.build_coverage_regex - return unless coverage_regex coverage = extract_coverage(trace, coverage_regex) - - if coverage.is_a? Numeric - update_attributes(coverage: coverage) - end + update_attributes(coverage: coverage) if coverage.present? end def extract_coverage(text, regex) - begin - matches = text.scan(Regexp.new(regex)).last - matches = matches.last if matches.kind_of?(Array) - coverage = matches.gsub(/\d+(\.\d+)?/).first + return unless regex - if coverage.present? - coverage.to_f - end - rescue - # if bad regex or something goes wrong we dont want to interrupt transition - # so we just silentrly ignore error for now + matches = text.scan(Regexp.new(regex)).last + matches = matches.last if matches.kind_of?(Array) + coverage = matches.gsub(/\d+(\.\d+)?/).first + + if coverage.present? + coverage.to_f end + rescue + # if bad regex or something goes wrong we dont want to interrupt transition + # so we just silentrly ignore error for now end def has_trace_file? @@ -522,6 +516,10 @@ module Ci self.update(artifacts_expire_at: nil) end + def coverage_regex + super || project.try(:build_coverage_regex) + end + def when read_attribute(:when) || build_attributes_from_config[:when] || 'on_success' end diff --git a/app/models/commit.rb b/app/models/commit.rb index 316bd2e512b..46f06733da1 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -100,8 +100,8 @@ class Commit commit_reference(from_project, id, full: full) end - def reference_link_text(from_project = nil) - commit_reference(from_project, short_id) + def reference_link_text(from_project = nil, full: false) + commit_reference(from_project, short_id, full: full) end def diff_line_count diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb index 1aa97debe42..1acff093aa1 100644 --- a/app/models/concerns/spammable.rb +++ b/app/models/concerns/spammable.rb @@ -34,7 +34,13 @@ module Spammable end def check_for_spam - self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam? + if spam? + self.errors.add(:base, "Your #{spammable_entity_type} has been recognized as spam and has been discarded.") + end + end + + def spammable_entity_type + self.class.name.underscore end def spam_title diff --git a/app/models/environment.rb b/app/models/environment.rb index 652abf18a8a..577367f1eed 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -1,7 +1,8 @@ class Environment < ActiveRecord::Base # Used to generate random suffixes for the slug + LETTERS = 'a'..'z' NUMBERS = '0'..'9' - SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a + SUFFIX_CHARS = LETTERS.to_a + NUMBERS.to_a belongs_to :project, required: true, validate: true @@ -148,17 +149,24 @@ class Environment < ActiveRecord::Base slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-') # Must start with a letter - slugified = "env-" + slugified if NUMBERS.cover?(slugified[0]) + slugified = 'env-' + slugified unless LETTERS.cover?(slugified[0]) + + # Repeated dashes are invalid (OpenShift limitation) + slugified.gsub!(/\-+/, '-') # Maximum length: 24 characters (OpenShift limitation) slugified = slugified[0..23] - # Cannot end with a "-" character (Kubernetes label limitation) - slugified = slugified[0..-2] if slugified[-1] == "-" + # Cannot end with a dash (Kubernetes label limitation) + slugified.chop! if slugified.end_with?('-') # Add a random suffix, shortening the current string if necessary, if it # has been slugified. This ensures uniqueness. - slugified = slugified[0..16] + "-" + random_suffix if slugified != name + if slugified != name + slugified = slugified[0..16] + slugified << '-' unless slugified.end_with?('-') + slugified << random_suffix + end self.slug = slugified end diff --git a/app/models/issue.rb b/app/models/issue.rb index 65638d9a299..d8826b65fcc 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -97,10 +97,11 @@ class Issue < ActiveRecord::Base end end - def to_reference(from_project = nil, full: false) + # `from` argument can be a Namespace or Project. + def to_reference(from = nil, full: false) reference = "#{self.class.reference_prefix}#{iid}" - "#{project.to_reference(from_project, full: full)}#{reference}" + "#{project.to_reference(from, full: full)}#{reference}" end def referenced_merge_requests(current_user = nil) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 6753504acff..082adcafcc8 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -179,10 +179,11 @@ class MergeRequest < ActiveRecord::Base work_in_progress?(title) ? title : "WIP: #{title}" end - def to_reference(from_project = nil, full: false) + # `from` argument can be a Namespace or Project. + def to_reference(from = nil, full: false) reference = "#{self.class.reference_prefix}#{iid}" - "#{project.to_reference(from_project, full: full)}#{reference}" + "#{project.to_reference(from, full: full)}#{reference}" end def first_commit diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index dadb81f9b6e..70bad2a4396 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -169,7 +169,8 @@ class MergeRequestDiff < ActiveRecord::Base # When compare merge request versions we want diff A..B instead of A...B # so we handle cases when user does squash and rebase of the commits between versions. # For this reason we set straight to true by default. - CompareService.new.execute(project, head_commit_sha, project, sha, straight: straight) + CompareService.new(project, head_commit_sha) + .execute(project, sha, straight: straight) end def commits_count diff --git a/app/models/project.rb b/app/models/project.rb index 59faf35e051..0d286bfbaa8 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -225,6 +225,7 @@ class Project < ActiveRecord::Base scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') } scope :with_statistics, -> { includes(:statistics) } scope :with_shared_runners, -> { where(shared_runners_enabled: true) } + scope :inside_path, ->(path) { joins(:route).where('routes.path LIKE ?', "#{path}/%") } # "enabled" here means "not disabled". It includes private features! scope :with_feature_enabled, ->(feature) { @@ -369,10 +370,6 @@ class Project < ActiveRecord::Base def group_ids joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id) end - - # Add alias for Routable method for compatibility with old code. - # In future all calls `find_with_namespace` should be replaced with `find_by_full_path` - alias_method :find_with_namespace, :find_by_full_path end def lfs_enabled? @@ -591,10 +588,11 @@ class Project < ActiveRecord::Base end end - def to_reference(from_project = nil, full: false) - if full || cross_namespace_reference?(from_project) + # `from` argument can be a Namespace or Project. + def to_reference(from = nil, full: false) + if full || cross_namespace_reference?(from) path_with_namespace - elsif cross_project_reference?(from_project) + elsif cross_project_reference?(from) path end end @@ -1291,21 +1289,26 @@ class Project < ActiveRecord::Base private + def cross_namespace_reference?(from) + case from + when Project + namespace != from.namespace + when Namespace + namespace != from + end + end + # Check if a reference is being done cross-project - # - # from_project - Refering Project object - def cross_project_reference?(from_project) - from_project && self != from_project + def cross_project_reference?(from) + return true if from.is_a?(Namespace) + + from && self != from end def pushes_since_gc_redis_key "projects/#{id}/pushes_since_gc" end - def cross_namespace_reference?(from_project) - from_project && namespace != from_project.namespace - 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 @@ -1345,6 +1348,6 @@ class Project < ActiveRecord::Base def pending_delete_twin return false unless path - Project.unscoped.where(pending_delete: true).find_with_namespace(path_with_namespace) + Project.unscoped.where(pending_delete: true).find_by_full_path(path_with_namespace) end end diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb index 2bcff541cc0..5eb1bd86e9d 100644 --- a/app/models/project_services/chat_slash_commands_service.rb +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -31,13 +31,13 @@ class ChatSlashCommandsService < Service return unless valid_token?(params[:token]) user = find_chat_user(params) - unless user + + if user + Gitlab::ChatCommands::Command.new(project, user, params).execute + else url = authorize_chat_name_url(params) - return presenter.authorize_chat_name(url) + Gitlab::ChatCommands::Presenters::Access.new(url).authorize end - - Gitlab::ChatCommands::Command.new(project, user, - params).execute end private @@ -49,8 +49,4 @@ class ChatSlashCommandsService < Service def authorize_chat_name_url(params) ChatNames::AuthorizeUserService.new(self, params).execute end - - def presenter - Gitlab::ChatCommands::Presenter.new - end end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 2ac76e97de0..80d002f9c32 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -60,9 +60,9 @@ class JiraService < IssueTrackerService end def help - 'You need to configure JIRA before enabling this service. For more details + "You need to configure JIRA before enabling this service. For more details read the - [JIRA service documentation](https://docs.gitlab.com/ce/project_services/jira.html).' + [JIRA service documentation](#{help_page_url('project_services/jira')})." end def title diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 50a011db74e..b0f7a42f9a3 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -28,8 +28,8 @@ class MattermostSlashCommandsService < ChatSlashCommandsService [false, e.message] end - def list_teams(user) - Mattermost::Team.new(user).all + def list_teams(current_user) + [Mattermost::Team.new(current_user).all, nil] rescue Mattermost::Error => e [[], e.message] end diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb index 25b5d777641..9bb456eee24 100644 --- a/app/models/project_snippet.rb +++ b/app/models/project_snippet.rb @@ -9,4 +9,8 @@ class ProjectSnippet < Snippet participant :author participant :notes_with_associations + + def check_for_spam? + super && project.public? + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index d77b7692d75..7cf09c52bf4 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -5,7 +5,7 @@ class Repository attr_accessor :path_with_namespace, :project - class CommitError < StandardError; end + CommitError = Class.new(StandardError) # Methods that cache data from the Git repository. # @@ -64,10 +64,6 @@ class Repository @raw_repository ||= Gitlab::Git::Repository.new(path_to_repo) end - def update_autocrlf_option - raw_repository.autocrlf = :input if raw_repository.autocrlf != :input - end - # Return absolute path to repository def path_to_repo @path_to_repo ||= File.expand_path( @@ -168,63 +164,46 @@ class Repository tags.find { |tag| tag.name == name } end - def add_branch(user, branch_name, target) - oldrev = Gitlab::Git::BLANK_SHA - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name - target = commit(target).try(:id) + def add_branch(user, branch_name, ref) + newrev = commit(ref).try(:sha) - return false unless target + return false unless newrev - GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do - update_ref!(ref, target, oldrev) - end + GitOperationService.new(user, self).add_branch(branch_name, newrev) after_create_branch find_branch(branch_name) end def add_tag(user, tag_name, target, message = nil) - oldrev = Gitlab::Git::BLANK_SHA - ref = Gitlab::Git::TAG_REF_PREFIX + tag_name - target = commit(target).try(:id) - - return false unless target - + newrev = commit(target).try(:id) options = { message: message, tagger: user_to_committer(user) } if message - GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do |service| - raw_tag = rugged.tags.create(tag_name, target, options) - service.newrev = raw_tag.target_id - end + return false unless newrev + + GitOperationService.new(user, self).add_tag(tag_name, newrev, options) find_tag(tag_name) end def rm_branch(user, branch_name) before_remove_branch - branch = find_branch(branch_name) - oldrev = branch.try(:dereferenced_target).try(:id) - newrev = Gitlab::Git::BLANK_SHA - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name - GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do - update_ref!(ref, newrev, oldrev) - end + GitOperationService.new(user, self).rm_branch(branch) after_remove_branch true end - def rm_tag(tag_name) + def rm_tag(user, tag_name) before_remove_tag + tag = find_tag(tag_name) - begin - rugged.tags.delete(tag_name) - true - rescue Rugged::ReferenceError - false - end + GitOperationService.new(user, self).rm_tag(tag) + + after_remove_tag + true end def ref_names @@ -241,21 +220,6 @@ class Repository false end - def update_ref!(name, newrev, oldrev) - # We use 'git update-ref' because libgit2/rugged currently does not - # offer 'compare and swap' ref updates. Without compare-and-swap we can - # (and have!) accidentally reset the ref to an earlier state, clobbering - # commits. See also https://github.com/libgit2/libgit2/issues/1534. - command = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z) - _, status = Gitlab::Popen.popen(command, path_to_repo) do |stdin| - stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00") - end - - return if status.zero? - - raise CommitError.new("Could not update branch #{name.sub('refs/heads/', '')}. Please refresh and try again.") - end - # Makes sure a commit is kept around when Git garbage collection runs. # Git GC will delete commits from the repository that are no longer in any # branches or tags, but we want to keep some of these commits around, for @@ -435,6 +399,11 @@ class Repository repository_event(:remove_tag) end + # Runs code after removing a tag. + def after_remove_tag + expire_tags_cache + end + def before_import expire_content_cache end @@ -779,121 +748,132 @@ class Repository @tags ||= raw_repository.tags end - def commit_dir(user, path, message, branch, author_email: nil, author_name: nil) - update_branch_with_hooks(user, branch) do |ref| - options = { - commit: { - branch: ref, - message: message, - update_ref: false - } - } - - options.merge!(get_committer_and_author(user, email: author_email, name: author_name)) - - raw_repository.mkdir(path, options) - end - end - - def commit_file(user, path, content, message, branch, update, author_email: nil, author_name: nil) - update_branch_with_hooks(user, branch) do |ref| - options = { - commit: { - branch: ref, - message: message, - update_ref: false - }, - file: { - content: content, - path: path, - update: update - } - } - - options.merge!(get_committer_and_author(user, email: author_email, name: author_name)) + # rubocop:disable Metrics/ParameterLists + def commit_dir( + user, path, + message:, branch_name:, + author_email: nil, author_name: nil, + start_branch_name: nil, start_project: project) + check_tree_entry_for_dir(branch_name, path) - Gitlab::Git::Blob.commit(raw_repository, options) + if start_branch_name + start_project.repository. + check_tree_entry_for_dir(start_branch_name, path) end - end - - def update_file(user, path, content, branch:, previous_path:, message:, author_email: nil, author_name: nil) - update_branch_with_hooks(user, branch) do |ref| - options = { - commit: { - branch: ref, - message: message, - update_ref: false - }, - file: { - content: content, - path: path, - update: true - } - } - - options.merge!(get_committer_and_author(user, email: author_email, name: author_name)) - if previous_path && previous_path != path - options[:file][:previous_path] = previous_path - Gitlab::Git::Blob.rename(raw_repository, options) - else - Gitlab::Git::Blob.commit(raw_repository, options) + commit_file( + user, + "#{path}/.gitkeep", + '', + message: message, + branch_name: branch_name, + update: false, + author_email: author_email, + author_name: author_name, + start_branch_name: start_branch_name, + start_project: start_project) + end + # rubocop:enable Metrics/ParameterLists + + # rubocop:disable Metrics/ParameterLists + def commit_file( + user, path, content, + message:, branch_name:, update: true, + author_email: nil, author_name: nil, + start_branch_name: nil, start_project: project) + unless update + error_message = "Filename already exists; update not allowed" + + if tree_entry_at(branch_name, path) + raise Gitlab::Git::Repository::InvalidBlobName.new(error_message) end - end - end - - def remove_file(user, path, message, branch, author_email: nil, author_name: nil) - update_branch_with_hooks(user, branch) do |ref| - options = { - commit: { - branch: ref, - message: message, - update_ref: false - }, - file: { - path: path - } - } - - options.merge!(get_committer_and_author(user, email: author_email, name: author_name)) - Gitlab::Git::Blob.remove(raw_repository, options) + if start_branch_name && + start_project.repository.tree_entry_at(start_branch_name, path) + raise Gitlab::Git::Repository::InvalidBlobName.new(error_message) + end end - end - def multi_action(user:, branch:, message:, actions:, author_email: nil, author_name: nil) - update_branch_with_hooks(user, branch) do |ref| + multi_action( + user: user, + message: message, + branch_name: branch_name, + author_email: author_email, + author_name: author_name, + start_branch_name: start_branch_name, + start_project: start_project, + actions: [{ action: :create, + file_path: path, + content: content }]) + end + # rubocop:enable Metrics/ParameterLists + + # rubocop:disable Metrics/ParameterLists + def update_file( + user, path, content, + message:, branch_name:, previous_path:, + author_email: nil, author_name: nil, + start_branch_name: nil, start_project: project) + action = if previous_path && previous_path != path + :move + else + :update + end + + multi_action( + user: user, + message: message, + branch_name: branch_name, + author_email: author_email, + author_name: author_name, + start_branch_name: start_branch_name, + start_project: start_project, + actions: [{ action: action, + file_path: path, + content: content, + previous_path: previous_path }]) + end + # rubocop:enable Metrics/ParameterLists + + # rubocop:disable Metrics/ParameterLists + def remove_file( + user, path, + message:, branch_name:, + author_email: nil, author_name: nil, + start_branch_name: nil, start_project: project) + multi_action( + user: user, + message: message, + branch_name: branch_name, + author_email: author_email, + author_name: author_name, + start_branch_name: start_branch_name, + start_project: start_project, + actions: [{ action: :delete, + file_path: path }]) + end + # rubocop:enable Metrics/ParameterLists + + # rubocop:disable Metrics/ParameterLists + def multi_action( + user:, branch_name:, message:, actions:, + author_email: nil, author_name: nil, + start_branch_name: nil, start_project: project) + GitOperationService.new(user, self).with_branch( + branch_name, + start_branch_name: start_branch_name, + start_project: start_project) do |start_commit| index = rugged.index - parents = [] - branch = find_branch(ref) - if branch - last_commit = branch.dereferenced_target - index.read_tree(last_commit.raw_commit.tree) - parents = [last_commit.sha] - end + parents = if start_commit + index.read_tree(start_commit.raw_commit.tree) + [start_commit.sha] + else + [] + end - actions.each do |action| - case action[:action] - when :create, :update, :move - mode = - case action[:action] - when :update - index.get(action[:file_path])[:mode] - when :move - index.get(action[:previous_path])[:mode] - end - mode ||= 0o100644 - - index.remove(action[:previous_path]) if action[:action] == :move - - content = action[:encoding] == 'base64' ? Base64.decode64(action[:content]) : action[:content] - oid = rugged.write(content, :blob) - - index.add(path: action[:file_path], oid: oid, mode: mode) - when :delete - index.remove(action[:file_path]) - end + actions.each do |act| + git_action(index, act) end options = { @@ -906,6 +886,7 @@ class Repository Rugged::Commit.create(rugged, options) end end + # rubocop:enable Metrics/ParameterLists def get_committer_and_author(user, email: nil, name: nil) committer = user_to_committer(user) @@ -918,7 +899,7 @@ class Repository end def user_to_committer(user) - Gitlab::Git::committer_hash(email: user.email, name: user.name) + Gitlab::Git.committer_hash(email: user.email, name: user.name) end def can_be_merged?(source_sha, target_branch) @@ -932,17 +913,18 @@ class Repository end end - def merge(user, merge_request, options = {}) - our_commit = rugged.branches[merge_request.target_branch].target - their_commit = rugged.lookup(merge_request.diff_head_sha) + def merge(user, source, merge_request, options = {}) + GitOperationService.new(user, self).with_branch( + merge_request.target_branch) do |start_commit| + our_commit = start_commit.sha + their_commit = source - raise "Invalid merge target" if our_commit.nil? - raise "Invalid merge source" if their_commit.nil? + raise 'Invalid merge target' unless our_commit + raise 'Invalid merge source' unless their_commit - merge_index = rugged.merge_commits(our_commit, their_commit) - return false if merge_index.conflicts? + merge_index = rugged.merge_commits(our_commit, their_commit) + break if merge_index.conflicts? - update_branch_with_hooks(user, merge_request.target_branch) do actual_options = options.merge( parents: [our_commit, their_commit], tree: merge_index.write_tree(rugged), @@ -952,34 +934,48 @@ class Repository merge_request.update(in_progress_merge_commit_sha: commit_id) commit_id end + rescue Repository::CommitError # when merge_index.conflicts? + false end - def revert(user, commit, base_branch, revert_tree_id = nil) - source_sha = find_branch(base_branch).dereferenced_target.sha - revert_tree_id ||= check_revert_content(commit, base_branch) + def revert( + user, commit, branch_name, revert_tree_id = nil, + start_branch_name: nil, start_project: project) + revert_tree_id ||= check_revert_content(commit, branch_name) return false unless revert_tree_id - update_branch_with_hooks(user, base_branch) do + GitOperationService.new(user, self).with_branch( + branch_name, + start_branch_name: start_branch_name, + start_project: start_project) do |start_commit| + committer = user_to_committer(user) - source_sha = Rugged::Commit.create(rugged, + + Rugged::Commit.create(rugged, message: commit.revert_message(user), author: committer, committer: committer, tree: revert_tree_id, - parents: [rugged.lookup(source_sha)]) + parents: [start_commit.sha]) end end - def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil) - source_sha = find_branch(base_branch).dereferenced_target.sha - cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch) + def cherry_pick( + user, commit, branch_name, cherry_pick_tree_id = nil, + start_branch_name: nil, start_project: project) + cherry_pick_tree_id ||= check_cherry_pick_content(commit, branch_name) return false unless cherry_pick_tree_id - update_branch_with_hooks(user, base_branch) do + GitOperationService.new(user, self).with_branch( + branch_name, + start_branch_name: start_branch_name, + start_project: start_project) do |start_commit| + committer = user_to_committer(user) - source_sha = Rugged::Commit.create(rugged, + + Rugged::Commit.create(rugged, message: commit.message, author: { email: commit.author_email, @@ -988,22 +984,22 @@ class Repository }, committer: committer, tree: cherry_pick_tree_id, - parents: [rugged.lookup(source_sha)]) + parents: [start_commit.sha]) end end - def resolve_conflicts(user, branch, params) - update_branch_with_hooks(user, branch) do + def resolve_conflicts(user, branch_name, params) + GitOperationService.new(user, self).with_branch(branch_name) do committer = user_to_committer(user) Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer)) end end - def check_revert_content(commit, base_branch) - source_sha = find_branch(base_branch).dereferenced_target.sha - args = [commit.id, source_sha] - args << { mainline: 1 } if commit.merge_commit? + def check_revert_content(target_commit, branch_name) + source_sha = commit(branch_name).sha + args = [target_commit.sha, source_sha] + args << { mainline: 1 } if target_commit.merge_commit? revert_index = rugged.revert_commit(*args) return false if revert_index.conflicts? @@ -1014,10 +1010,10 @@ class Repository tree_id end - def check_cherry_pick_content(commit, base_branch) - source_sha = find_branch(base_branch).dereferenced_target.sha - args = [commit.id, source_sha] - args << 1 if commit.merge_commit? + def check_cherry_pick_content(target_commit, branch_name) + source_sha = commit(branch_name).sha + args = [target_commit.sha, source_sha] + args << 1 if target_commit.merge_commit? cherry_pick_index = rugged.cherrypick_commit(*args) return false if cherry_pick_index.conflicts? @@ -1075,6 +1071,28 @@ class Repository Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:strip) end + def with_repo_branch_commit(start_repository, start_branch_name) + branch_name_or_sha = + if start_repository == self + start_branch_name + else + tmp_ref = "refs/tmp/#{SecureRandom.hex}/head" + + fetch_ref( + start_repository.path_to_repo, + "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", + tmp_ref + ) + + start_repository.commit(start_branch_name).sha + end + + yield(commit(branch_name_or_sha)) + + ensure + rugged.references.delete(tmp_ref) if tmp_ref + end + def fetch_ref(source_path, source_ref, target_ref) args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) Gitlab::Popen.popen(args, path_to_repo) @@ -1084,39 +1102,6 @@ class Repository fetch_ref(path_to_repo, ref, ref_path) end - def update_branch_with_hooks(current_user, branch) - update_autocrlf_option - - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch - target_branch = find_branch(branch) - was_empty = empty? - - # Make commit - newrev = yield(ref) - - unless newrev - raise CommitError.new('Failed to create commit') - end - - if rugged.lookup(newrev).parent_ids.empty? || target_branch.nil? - oldrev = Gitlab::Git::BLANK_SHA - else - oldrev = rugged.merge_base(newrev, target_branch.dereferenced_target.sha) - end - - GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do - update_ref!(ref, newrev, oldrev) - - if was_empty || !target_branch - # If repo was empty expire cache - after_create if was_empty - after_create_branch - end - end - - newrev - end - def ls_files(ref) actual_ref = ref || root_ref raw_repository.ls_files(actual_ref) @@ -1175,8 +1160,76 @@ class Repository end end + protected + + def tree_entry_at(branch_name, path) + branch_exists?(branch_name) && + # tree_entry is private + raw_repository.send(:tree_entry, commit(branch_name), path) + end + + def check_tree_entry_for_dir(branch_name, path) + return unless branch_exists?(branch_name) + + entry = tree_entry_at(branch_name, path) + + return unless entry + + if entry[:type] == :blob + raise Gitlab::Git::Repository::InvalidBlobName.new( + "Directory already exists as a file") + else + raise Gitlab::Git::Repository::InvalidBlobName.new( + "Directory already exists") + end + end + private + def git_action(index, action) + path = normalize_path(action[:file_path]) + + if action[:action] == :move + previous_path = normalize_path(action[:previous_path]) + end + + case action[:action] + when :create, :update, :move + mode = + case action[:action] + when :update + index.get(path)[:mode] + when :move + index.get(previous_path)[:mode] + end + mode ||= 0o100644 + + index.remove(previous_path) if action[:action] == :move + + content = if action[:encoding] == 'base64' + Base64.decode64(action[:content]) + else + action[:content] + end + + oid = rugged.write(content, :blob) + + index.add(path: path, oid: oid, mode: mode) + when :delete + index.remove(path) + end + end + + def normalize_path(path) + pathname = Gitlab::Git::PathHelper.normalize_path(path) + + if pathname.each_filename.include?('..') + raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path') + end + + pathname.to_s + end + def refs_directory_exists? return false unless path_with_namespace diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 771a7350556..2665a7249a3 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -7,6 +7,7 @@ class Snippet < ActiveRecord::Base include Sortable include Awardable include Mentionable + include Spammable cache_markdown_field :title, pipeline: :single_line cache_markdown_field :content @@ -17,7 +18,7 @@ class Snippet < ActiveRecord::Base default_content_html_invalidator || file_name_changed? end - default_value_for :visibility_level, Snippet::PRIVATE + default_value_for(:visibility_level) { current_application_settings.default_snippet_visibility } belongs_to :author, class_name: 'User' belongs_to :project @@ -46,6 +47,9 @@ class Snippet < ActiveRecord::Base participant :author participant :notes_with_associations + attr_spammable :title, spam_title: true + attr_spammable :content, spam_description: true + def self.reference_prefix '$' end @@ -127,6 +131,14 @@ class Snippet < ActiveRecord::Base notes.includes(:author) end + def check_for_spam? + public? + end + + def spammable_entity_type + 'snippet' + end + class << self # Searches for snippets with a matching title or file name. # diff --git a/app/models/todo.rb b/app/models/todo.rb index 4c99aa0d3be..2adf494ce11 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -103,9 +103,9 @@ class Todo < ActiveRecord::Base def target_reference if for_commit? - target.short_id + target.reference_link_text(full: true) else - target.to_reference + target.to_reference(full: true) end end diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 7b1752df0e1..8b25332b73c 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -1,8 +1,6 @@ module Ci class BuildPolicy < CommitStatusPolicy def rules - can! :read_build if @subject.project.public_builds? - super # If we can't read build we should also not have that diff --git a/app/serializers/base_serializer.rb b/app/serializers/base_serializer.rb index de9a181db90..311ee9c96be 100644 --- a/app/serializers/base_serializer.rb +++ b/app/serializers/base_serializer.rb @@ -6,6 +6,7 @@ class BaseSerializer def represent(resource, opts = {}) self.class.entity_class .represent(resource, opts.merge(request: @request)) + .as_json end def self.entity(entity_class) diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index cfa86cc2553..b2de6c5832e 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -1,9 +1,10 @@ class PipelineSerializer < BaseSerializer - entity PipelineEntity class InvalidResourceError < StandardError; end include API::Helpers::Pagination Struct.new('Pagination', :request, :response) + entity PipelineEntity + def represent(resource, opts = {}) if paginated? raise InvalidResourceError unless resource.respond_to?(:page) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index c00c5aebf57..5cb7a86a5ee 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -61,7 +61,7 @@ module Auth end def process_repository_access(type, name, actions) - requested_project = Project.find_with_namespace(name) + requested_project = Project.find_by_full_path(name) return unless requested_project actions = actions.select do |action| diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index 4d410f66c55..25e22f14e60 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -4,7 +4,8 @@ module Commits class ChangeError < StandardError; end def execute - @source_project = params[:source_project] || @project + @start_project = params[:start_project] || @project + @start_branch = params[:start_branch] @target_branch = params[:target_branch] @commit = params[:commit] @create_merge_request = params[:create_merge_request].present? @@ -25,13 +26,28 @@ module Commits def commit_change(action) raise NotImplementedError unless repository.respond_to?(action) - into = @create_merge_request ? @commit.public_send("#{action}_branch_name") : @target_branch - tree_id = repository.public_send("check_#{action}_content", @commit, @target_branch) + if @create_merge_request + into = @commit.public_send("#{action}_branch_name") + tree_branch = @start_branch + else + into = tree_branch = @target_branch + end + + tree_id = repository.public_send( + "check_#{action}_content", @commit, tree_branch) if tree_id - create_target_branch(into) if @create_merge_request + validate_target_branch(into) if @create_merge_request + + repository.public_send( + action, + current_user, + @commit, + into, + tree_id, + start_project: @start_project, + start_branch_name: @start_branch) - repository.public_send(action, current_user, @commit, into, tree_id) success else error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically. @@ -50,12 +66,12 @@ module Commits true end - def create_target_branch(new_branch) + def validate_target_branch(new_branch) # Temporary branch exists and contains the change commit - return success if repository.find_branch(new_branch) + return if repository.find_branch(new_branch) - result = CreateBranchService.new(@project, current_user) - .execute(new_branch, @target_branch, source_project: @source_project) + result = ValidateNewBranchService.new(@project, current_user) + .execute(new_branch) if result[:status] == :error raise ChangeError, "There was an error creating the source branch: #{result[:message]}" diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index 5e8fafca98c..ab4c02a97a0 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -3,23 +3,27 @@ require 'securerandom' # Compare 2 branches for one repo or between repositories # and return Gitlab::Git::Compare object that responds to commits and diffs class CompareService - def execute(source_project, source_branch, target_project, target_branch, straight: false) - source_commit = source_project.commit(source_branch) - return unless source_commit + attr_reader :start_project, :start_branch_name - source_sha = source_commit.sha + def initialize(new_start_project, new_start_branch_name) + @start_project = new_start_project + @start_branch_name = new_start_branch_name + end + def execute(target_project, target_branch, straight: false) # If compare with other project we need to fetch ref first - unless target_project == source_project - random_string = SecureRandom.hex + target_project.repository.with_repo_branch_commit( + start_project.repository, + start_branch_name) do |commit| + break unless commit - target_project.repository.fetch_ref( - source_project.repository.path_to_repo, - "refs/heads/#{source_branch}", - "refs/tmp/#{random_string}/head" - ) + compare(commit.sha, target_project, target_branch, straight) end + end + + private + def compare(source_sha, target_project, target_branch, straight) raw_compare = Gitlab::Git::Compare.new( target_project.repository.raw_repository, target_branch, diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index e004a303496..77459d8779d 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -1,31 +1,11 @@ class CreateBranchService < BaseService - def execute(branch_name, ref, source_project: @project) - valid_branch = Gitlab::GitRefValidator.validate(branch_name) + def execute(branch_name, ref) + result = ValidateNewBranchService.new(project, current_user) + .execute(branch_name) - unless valid_branch - return error('Branch name is invalid') - end - - repository = project.repository - existing_branch = repository.find_branch(branch_name) - - if existing_branch - return error('Branch already exists') - end - - new_branch = if source_project != @project - repository.fetch_ref( - source_project.repository.path_to_repo, - "refs/heads/#{ref}", - "refs/heads/#{branch_name}" - ) - - repository.after_create_branch + return result if result[:status] == :error - repository.find_branch(branch_name) - else - repository.add_branch(current_user, branch_name, ref) - end + new_branch = repository.add_branch(current_user, branch_name, ref) if new_branch success(new_branch) diff --git a/app/services/create_snippet_service.rb b/app/services/create_snippet_service.rb index 95cc9baf406..14f5ba064ff 100644 --- a/app/services/create_snippet_service.rb +++ b/app/services/create_snippet_service.rb @@ -1,5 +1,8 @@ class CreateSnippetService < BaseService def execute + request = params.delete(:request) + api = params.delete(:api) + snippet = if project project.snippets.build(params) else @@ -12,8 +15,12 @@ class CreateSnippetService < BaseService end snippet.author = current_user + snippet.spam = SpamService.new(snippet, request).check(api) + + if snippet.save + UserAgentDetailService.new(snippet, request).create + end - snippet.save snippet end end diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb index a44dee14a0f..9d4bffb93e9 100644 --- a/app/services/delete_tag_service.rb +++ b/app/services/delete_tag_service.rb @@ -7,7 +7,7 @@ class DeleteTagService < BaseService return error('No such tag', 404) end - if repository.rm_tag(tag_name) + if repository.rm_tag(current_user, tag_name) release = project.releases.find_by(tag: tag_name) release.destroy if release diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 9bd4bd464f7..0a25f56d24c 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -3,9 +3,9 @@ module Files class ValidationError < StandardError; end def execute - @source_project = params[:source_project] || @project - @source_branch = params[:source_branch] - @target_branch = params[:target_branch] + @start_project = params[:start_project] || @project + @start_branch = params[:start_branch] + @target_branch = params[:target_branch] @commit_message = params[:commit_message] @file_path = params[:file_path] @@ -22,10 +22,8 @@ module Files # Validate parameters validate - # Create new branch if it different from source_branch - if different_branch? - create_target_branch - end + # Create new branch if it different from start_branch + validate_target_branch if different_branch? result = commit if result @@ -40,7 +38,7 @@ module Files private def different_branch? - @source_branch != @target_branch || @source_project != @project + @start_branch != @target_branch || @start_project != @project end def file_has_changed? @@ -61,22 +59,23 @@ module Files end unless project.empty_repo? - unless @source_project.repository.branch_names.include?(@source_branch) + unless @start_project.repository.branch_exists?(@start_branch) raise_error('You can only create or edit files when you are on a branch') end if different_branch? - if repository.branch_names.include?(@target_branch) + if repository.branch_exists?(@target_branch) raise_error('Branch with such name already exists. You need to switch to this branch in order to make changes') end end end end - def create_target_branch - result = CreateBranchService.new(project, current_user).execute(@target_branch, @source_branch, source_project: @source_project) + def validate_target_branch + result = ValidateNewBranchService.new(project, current_user). + execute(@target_branch) - unless result[:status] == :success + if result[:status] == :error raise_error("Something went wrong when we tried to create #{@target_branch} for you: #{result[:message]}") end end diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb index e5b4d60e467..858de5f0538 100644 --- a/app/services/files/create_dir_service.rb +++ b/app/services/files/create_dir_service.rb @@ -1,7 +1,15 @@ module Files class CreateDirService < Files::BaseService def commit - repository.commit_dir(current_user, @file_path, @commit_message, @target_branch, author_email: @author_email, author_name: @author_name) + repository.commit_dir( + current_user, + @file_path, + message: @commit_message, + branch_name: @target_branch, + author_email: @author_email, + author_name: @author_name, + start_project: @start_project, + start_branch_name: @start_branch) end def validate diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index b23576b9a28..88dd7bbaedb 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -1,7 +1,17 @@ module Files class CreateService < Files::BaseService def commit - repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, false, author_email: @author_email, author_name: @author_name) + repository.commit_file( + current_user, + @file_path, + @file_content, + message: @commit_message, + branch_name: @target_branch, + update: false, + author_email: @author_email, + author_name: @author_name, + start_project: @start_project, + start_branch_name: @start_branch) end def validate @@ -24,7 +34,7 @@ module Files unless project.empty_repo? @file_path.slice!(0) if @file_path.start_with?('/') - blob = repository.blob_at_branch(@source_branch, @file_path) + blob = repository.blob_at_branch(@start_branch, @file_path) if blob raise_error('Your changes could not be committed because a file with the same name already exists') diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb index 4f7e7a5baaa..50f0ffcac9f 100644 --- a/app/services/files/delete_service.rb +++ b/app/services/files/delete_service.rb @@ -1,7 +1,15 @@ module Files class DeleteService < Files::BaseService def commit - repository.remove_file(current_user, @file_path, @commit_message, @target_branch, author_email: @author_email, author_name: @author_name) + repository.remove_file( + current_user, + @file_path, + message: @commit_message, + branch_name: @target_branch, + author_email: @author_email, + author_name: @author_name, + start_project: @start_project, + start_branch_name: @start_branch) end end end diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index 54446e90007..6ba868df04d 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -5,11 +5,13 @@ module Files def commit repository.multi_action( user: current_user, - branch: @target_branch, message: @commit_message, + branch_name: @target_branch, actions: params[:actions], author_email: @author_email, - author_name: @author_name + author_name: @author_name, + start_project: @start_project, + start_branch_name: @start_branch ) end @@ -61,7 +63,7 @@ module Files end def last_commit - Gitlab::Git::Commit.last_for_path(repository, @source_branch, @file_path) + Gitlab::Git::Commit.last_for_path(repository, @start_branch, @file_path) end def regex_check(file) diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index 47a18e3e132..a71fe61a4b6 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -4,11 +4,13 @@ module Files def commit repository.update_file(current_user, @file_path, @file_content, - branch: @target_branch, - previous_path: @previous_path, message: @commit_message, + branch_name: @target_branch, + previous_path: @previous_path, author_email: @author_email, - author_name: @author_name) + author_name: @author_name, + start_project: @start_project, + start_branch_name: @start_branch) end private @@ -23,7 +25,7 @@ module Files def last_commit @last_commit ||= Gitlab::Git::Commit. - last_for_path(@source_project.repository, @source_branch, @file_path) + last_for_path(@start_project.repository, @start_branch, @file_path) end end end diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb index 6cd3908d43a..d222d1e63aa 100644 --- a/app/services/git_hooks_service.rb +++ b/app/services/git_hooks_service.rb @@ -18,9 +18,9 @@ class GitHooksService end end - yield self - - run_hook('post-receive') + yield(self).tap do + run_hook('post-receive') + end end private diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb new file mode 100644 index 00000000000..27bcc047601 --- /dev/null +++ b/app/services/git_operation_service.rb @@ -0,0 +1,179 @@ +class GitOperationService + attr_reader :user, :repository + + def initialize(new_user, new_repository) + @user = new_user + @repository = new_repository + end + + def add_branch(branch_name, newrev) + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name + oldrev = Gitlab::Git::BLANK_SHA + + update_ref_in_hooks(ref, newrev, oldrev) + end + + def rm_branch(branch) + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch.name + oldrev = branch.target + newrev = Gitlab::Git::BLANK_SHA + + update_ref_in_hooks(ref, newrev, oldrev) + end + + def add_tag(tag_name, newrev, options = {}) + ref = Gitlab::Git::TAG_REF_PREFIX + tag_name + oldrev = Gitlab::Git::BLANK_SHA + + with_hooks(ref, newrev, oldrev) do |service| + # We want to pass the OID of the tag object to the hooks. For an + # annotated tag we don't know that OID until after the tag object + # (raw_tag) is created in the repository. That is why we have to + # update the value after creating the tag object. Only the + # "post-receive" hook will receive the correct value in this case. + raw_tag = repository.rugged.tags.create(tag_name, newrev, options) + service.newrev = raw_tag.target_id + end + end + + def rm_tag(tag) + ref = Gitlab::Git::TAG_REF_PREFIX + tag.name + oldrev = tag.target + newrev = Gitlab::Git::BLANK_SHA + + update_ref_in_hooks(ref, newrev, oldrev) do + repository.rugged.tags.delete(tag_name) + end + end + + # Whenever `start_branch_name` is passed, if `branch_name` doesn't exist, + # it would be created from `start_branch_name`. + # If `start_project` is passed, and the branch doesn't exist, + # it would try to find the commits from it instead of current repository. + def with_branch( + branch_name, + start_branch_name: nil, + start_project: repository.project, + &block) + + check_with_branch_arguments!( + branch_name, start_branch_name, start_project) + + update_branch_with_hooks(branch_name) do + repository.with_repo_branch_commit( + start_project.repository, + start_branch_name || branch_name, + &block) + end + end + + private + + def update_branch_with_hooks(branch_name) + update_autocrlf_option + + was_empty = repository.empty? + + # Make commit + newrev = yield + + unless newrev + raise Repository::CommitError.new('Failed to create commit') + end + + branch = repository.find_branch(branch_name) + oldrev = find_oldrev_from_branch(newrev, branch) + + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name + update_ref_in_hooks(ref, newrev, oldrev) + + # If repo was empty expire cache + repository.after_create if was_empty + repository.after_create_branch if + was_empty || Gitlab::Git.blank_ref?(oldrev) + + newrev + end + + def find_oldrev_from_branch(newrev, branch) + return Gitlab::Git::BLANK_SHA unless branch + + oldrev = branch.target + + if oldrev == repository.rugged.merge_base(newrev, branch.target) + oldrev + else + raise Repository::CommitError.new('Branch diverged') + end + end + + def update_ref_in_hooks(ref, newrev, oldrev) + with_hooks(ref, newrev, oldrev) do + update_ref(ref, newrev, oldrev) + end + end + + def with_hooks(ref, newrev, oldrev) + GitHooksService.new.execute( + user, + repository.path_to_repo, + oldrev, + newrev, + ref) do |service| + + yield(service) + end + end + + def update_ref(ref, newrev, oldrev) + # We use 'git update-ref' because libgit2/rugged currently does not + # offer 'compare and swap' ref updates. Without compare-and-swap we can + # (and have!) accidentally reset the ref to an earlier state, clobbering + # commits. See also https://github.com/libgit2/libgit2/issues/1534. + command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z] + _, status = Gitlab::Popen.popen( + command, + repository.path_to_repo) do |stdin| + stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00") + end + + unless status.zero? + raise Repository::CommitError.new( + "Could not update branch #{Gitlab::Git.branch_name(ref)}." \ + " Please refresh and try again.") + end + end + + def update_autocrlf_option + if repository.raw_repository.autocrlf != :input + repository.raw_repository.autocrlf = :input + end + end + + def check_with_branch_arguments!( + branch_name, start_branch_name, start_project) + return if repository.branch_exists?(branch_name) + + if repository.project != start_project + unless start_branch_name + raise ArgumentError, + 'Should also pass :start_branch_name if' + + ' :start_project is different from current project' + end + + unless start_project.repository.branch_exists?(start_branch_name) + raise ArgumentError, + "Cannot find branch #{branch_name} nor" \ + " #{start_branch_name} from" \ + " #{start_project.path_with_namespace}" + end + elsif start_branch_name + unless repository.branch_exists?(start_branch_name) + raise ArgumentError, + "Cannot find branch #{branch_name} nor" \ + " #{start_branch_name} from" \ + " #{repository.project.path_with_namespace}" + end + end + end +end diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb new file mode 100644 index 00000000000..76d0ba67b07 --- /dev/null +++ b/app/services/labels/promote_service.rb @@ -0,0 +1,71 @@ +module Labels + class PromoteService < BaseService + BATCH_SIZE = 1000 + + def execute(label) + return unless project.group && + label.is_a?(ProjectLabel) + + Label.transaction do + new_label = clone_label_to_group_label(label) + + label_ids_for_merge(new_label).find_in_batches(batch_size: BATCH_SIZE) do |batched_ids| + update_issuables(new_label, batched_ids) + update_issue_board_lists(new_label, batched_ids) + update_priorities(new_label, batched_ids) + # Order is important, project labels need to be last + update_project_labels(batched_ids) + end + + # We skipped validations during creation. Let's run them now, after deleting conflicting labels + raise ActiveRecord::RecordInvalid.new(new_label) unless new_label.valid? + new_label + end + end + + private + + def label_ids_for_merge(new_label) + LabelsFinder. + new(current_user, title: new_label.title, group_id: project.group.id). + execute(skip_authorization: true). + where.not(id: new_label). + select(:id) # Can't use pluck() to avoid object-creation because of the batching + end + + def update_issuables(new_label, label_ids) + LabelLink. + where(label: label_ids). + update_all(label_id: new_label) + end + + def update_issue_board_lists(new_label, label_ids) + List. + where(label: label_ids). + update_all(label_id: new_label) + end + + def update_priorities(new_label, label_ids) + LabelPriority. + where(label: label_ids). + update_all(label_id: new_label) + end + + def update_project_labels(label_ids) + Label.where(id: label_ids).delete_all + end + + def clone_label_to_group_label(label) + params = label.attributes.slice('title', 'description', 'color') + # Since the title of the new label has to be the same as the previous labels + # and we're merging old labels in batches we'll skip validation to omit 2-step + # merge process and do it in one batch + # We'll be forcing validation at the end of the transaction to ensure everything + # was merged correctly + new_label = GroupLabel.new(params.merge(group: project.group)) + new_label.save(validate: false) + + new_label + end + end +end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 6a7393a9921..f4d52e3ebbd 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -1,62 +1,89 @@ module MergeRequests class BuildService < MergeRequests::BaseService def execute - merge_request = MergeRequest.new(params) - - # Set MR attributes - merge_request.can_be_created = true + self.merge_request = MergeRequest.new(params) + merge_request.can_be_created = true merge_request.compare_commits = [] - merge_request.source_project = project unless merge_request.source_project + merge_request.source_project = find_source_project + merge_request.target_project = find_target_project + merge_request.target_branch = find_target_branch - merge_request.target_project = nil unless can?(current_user, :read_project, merge_request.target_project) + if branches_specified? && branches_valid? + compare_branches + assign_title_and_description + else + merge_request.can_be_created = false + end - merge_request.target_project ||= (project.forked_from_project || project) - merge_request.target_branch ||= merge_request.target_project.default_branch + merge_request + end - messages = validate_branches(merge_request) - return build_failed(merge_request, messages) unless messages.empty? + private - compare = CompareService.new.execute( - merge_request.source_project, - merge_request.source_branch, - merge_request.target_project, - merge_request.target_branch, - ) + attr_accessor :merge_request - merge_request.compare_commits = compare.commits - merge_request.compare = compare + delegate :target_branch, :source_branch, :source_project, :target_project, :compare_commits, :wip_title, :description, :errors, to: :merge_request - set_title_and_description(merge_request) + def find_source_project + source_project || project end - private + def find_target_project + return target_project if target_project.present? && can?(current_user, :read_project, target_project) + project.forked_from_project || project + end - def validate_branches(merge_request) - messages = [] + def find_target_branch + target_branch || target_project.default_branch + end - if merge_request.target_branch.blank? || merge_request.source_branch.blank? - messages << - if params[:source_branch] || params[:target_branch] - "You must select source and target branch" - end - end + def branches_specified? + params[:source_branch] && params[:target_branch] + end - if merge_request.source_project == merge_request.target_project && - merge_request.target_branch == merge_request.source_branch + def branches_valid? + validate_branches + errors.blank? + end - messages << 'You must select different branches' - end + def compare_branches + compare = CompareService.new( + source_project, + source_branch + ).execute( + target_project, + target_branch + ) - # See if source and target branches exist - if merge_request.source_branch.present? && !merge_request.source_project.commit(merge_request.source_branch) - messages << "Source branch \"#{merge_request.source_branch}\" does not exist" - end + merge_request.compare_commits = compare.commits + merge_request.compare = compare + end - if merge_request.target_branch.present? && !merge_request.target_project.commit(merge_request.target_branch) - messages << "Target branch \"#{merge_request.target_branch}\" does not exist" - end + def validate_branches + add_error('You must select source and target branch') unless branches_present? + add_error('You must select different branches') if same_source_and_target? + add_error("Source branch \"#{source_branch}\" does not exist") unless source_branch_exists? + add_error("Target branch \"#{target_branch}\" does not exist") unless target_branch_exists? + end + + def add_error(message) + errors.add(:base, message) + end + + def branches_present? + target_branch.present? && source_branch.present? + end + + def same_source_and_target? + source_project == target_project && target_branch == source_branch + end - messages + def source_branch_exists? + source_branch.blank? || source_project.commit(source_branch) + end + + def target_branch_exists? + target_branch.blank? || target_project.commit(target_branch) end # When your branch name starts with an iid followed by a dash this pattern will be @@ -71,17 +98,17 @@ module MergeRequests # - Setting the title as 'Resolves "Emoji don't show up in commit title"' if there is # more than one commit in the MR # - def set_title_and_description(merge_request) - if match = merge_request.source_branch.match(/\A(\d+)-/) + def assign_title_and_description + if match = source_branch.match(/\A(\d+)-/) iid = match[1] end - commits = merge_request.compare_commits + commits = compare_commits if commits && commits.count == 1 commit = commits.first merge_request.title = commit.title merge_request.description ||= commit.description.try(:strip) - elsif iid && issue = merge_request.target_project.get_issue(iid, current_user) + elsif iid && issue = target_project.get_issue(iid, current_user) case issue when Issue merge_request.title = "Resolve \"#{issue.title}\"" @@ -89,31 +116,20 @@ module MergeRequests merge_request.title = "Resolve #{issue.title}" end else - merge_request.title = merge_request.source_branch.titleize.humanize + merge_request.title = source_branch.titleize.humanize end if iid closes_issue = "Closes ##{iid}" - if merge_request.description.present? + if description.present? merge_request.description += closes_issue.prepend("\n\n") else merge_request.description = closes_issue end end - merge_request.title = merge_request.wip_title if commits.empty? - - merge_request - end - - def build_failed(merge_request, messages) - messages.compact.each do |message| - merge_request.errors.add(:base, message) - end - merge_request.compare_commits = [] - merge_request.can_be_created = false - merge_request + merge_request.title = wip_title if commits.empty? end end end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index ab9056a3250..5ca6fec962d 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -6,13 +6,17 @@ module MergeRequests # Executed when you do merge via GitLab UI # class MergeService < MergeRequests::BaseService - attr_reader :merge_request + attr_reader :merge_request, :source def execute(merge_request) @merge_request = merge_request return log_merge_error('Merge request is not mergeable', true) unless @merge_request.mergeable? + @source = find_merge_source + + return log_merge_error('No source for merge', true) unless @source + merge_request.in_locked_state do if commit after_merge @@ -34,7 +38,7 @@ module MergeRequests committer: committer } - commit_id = repository.merge(current_user, merge_request, options) + commit_id = repository.merge(current_user, source, merge_request, options) if commit_id merge_request.update(merge_commit_sha: commit_id) @@ -73,9 +77,11 @@ module MergeRequests end def merge_request_info - project = merge_request.project + merge_request.to_reference(full: true) + end - "#{project.to_reference}#{merge_request.to_reference}" + def find_merge_source + merge_request.diff_head_sha end end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index f74e6cac174..b2cc39763f3 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -365,7 +365,7 @@ class NotificationService users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq) users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users) - users_with_group_setting = select_group_member_setting(project, project_members, users_with_group_level_global, users) + users_with_group_setting = select_group_member_setting(project.group, project_members, users_with_group_level_global, users) User.where(id: users_with_project_setting.concat(users_with_group_setting).uniq).to_a end @@ -415,8 +415,8 @@ class NotificationService end # Build a list of users based on group notification settings - def select_group_member_setting(project, project_members, global_setting, users_global_level_watch) - uids = notification_settings_for(project, :watch) + def select_group_member_setting(group, project_members, global_setting, users_global_level_watch) + uids = notification_settings_for(group, :watch) # Group setting is watch, add to users list if user is not project member users = [] @@ -473,7 +473,7 @@ class NotificationService setting = user.notification_settings_for(project) - if !setting && project.group + if project.group && (setting.nil? || setting.global?) setting = user.notification_settings_for(project.group) end diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb index aa9837038a6..781cd13b44b 100644 --- a/app/services/search/global_service.rb +++ b/app/services/search/global_service.rb @@ -9,7 +9,10 @@ module Search def execute group = Group.find_by(id: params[:group_id]) if params[:group_id].present? projects = ProjectsFinder.new.execute(current_user) - projects = projects.in_namespace(group.id) if group + + if group + projects = projects.inside_path(group.full_path) + end Gitlab::SearchResults.new(current_user, projects, params[:search]) end diff --git a/app/services/validate_new_branch_service.rb b/app/services/validate_new_branch_service.rb new file mode 100644 index 00000000000..2f61be184ce --- /dev/null +++ b/app/services/validate_new_branch_service.rb @@ -0,0 +1,22 @@ +require_relative 'base_service' + +class ValidateNewBranchService < BaseService + def execute(branch_name) + valid_branch = Gitlab::GitRefValidator.validate(branch_name) + + unless valid_branch + return error('Branch name is invalid') + end + + repository = project.repository + existing_branch = repository.find_branch(branch_name) + + if existing_branch + return error('Branch already exists') + end + + success + rescue GitHooksService::PreReceiveError => ex + error(ex.message) + end +end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 558bbe07b16..e7701d75a6e 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -204,7 +204,7 @@ .col-sm-10 = f.number_field :max_artifacts_size, class: 'form-control' .help-block - Set the maximum file size each build's artifacts can have + Set the maximum file size each jobs's artifacts can have = link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "maximum-artifacts-size") - if Gitlab.config.registry.enabled diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml index 5e3f105d41f..66d633119c2 100644 --- a/app/views/admin/builds/index.html.haml +++ b/app/views/admin/builds/index.html.haml @@ -12,7 +12,7 @@ = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post .row-content-block.second-block - #{(@scope || 'all').capitalize} builds + #{(@scope || 'all').capitalize} jobs %ul.content-list.builds-content-list.admin-builds-table = render "projects/builds/table", builds: @builds, admin: true diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml index b5f96363230..7893c1dee97 100644 --- a/app/views/admin/dashboard/_head.html.haml +++ b/app/views/admin/dashboard/_head.html.haml @@ -20,9 +20,9 @@ %span Groups = nav_link path: 'builds#index' do - = link_to admin_builds_path, title: 'Builds' do + = link_to admin_builds_path, title: 'Jobs' do %span - Builds + Jobs = nav_link path: ['runners#index', 'runners#show'] do = link_to admin_runners_path, title: 'Runners' do %span diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 124f970524e..721bc77cc2f 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -26,7 +26,7 @@ .bs-callout %p - A 'Runner' is a process which runs a build. + A 'Runner' is a process which runs a job. You can setup as many Runners as you need. %br Runners can be placed on separate users, servers, even on your local machine. @@ -37,16 +37,16 @@ %ul %li %span.label.label-success shared - \- Runner runs builds from all unassigned projects + \- Runner runs jobs from all unassigned projects %li %span.label.label-info specific - \- Runner runs builds from assigned projects + \- Runner runs jobs from assigned projects %li %span.label.label-warning locked \- Runner cannot be assigned to other projects %li %span.label.label-danger paused - \- Runner will not receive any new builds + \- Runner will not receive any new jobs .append-bottom-20.clearfix .pull-left @@ -68,7 +68,7 @@ %th Runner token %th Description %th Projects - %th Builds + %th Jobs %th Tags %th Last contact %th diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 39e103e3062..dc4116e1ce0 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -11,13 +11,13 @@ - if @runner.shared? .bs-callout.bs-callout-success - %h4 This Runner will process builds from ALL UNASSIGNED projects + %h4 This Runner will process jobs from ALL UNASSIGNED projects %p If you want Runners to build only specific projects, enable them in the table below. Keep in mind that this is a one way transition. - else .bs-callout.bs-callout-info - %h4 This Runner will process builds only from ASSIGNED projects + %h4 This Runner will process jobs only from ASSIGNED projects %p You can't make this a shared Runner. %hr @@ -70,11 +70,11 @@ = paginate @projects, theme: "gitlab" .col-md-6 - %h4 Recent builds served by this Runner + %h4 Recent jobs served by this Runner %table.table.ci-table.runner-builds %thead %tr - %th Build + %th Job %th Status %th Project %th Commit diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml index 8dea3479f82..8ed23ac4919 100644 --- a/app/views/ci/status/_dropdown_graph_badge.html.haml +++ b/app/views/ci/status/_dropdown_graph_badge.html.haml @@ -16,4 +16,4 @@ - if status.has_action? = link_to status.action_path, class: 'ci-action-icon-wrapper js-ci-action-icon', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do - = icon(status.action_icon, class: status.action_class) + = custom_icon(status.action_icon) diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml index dd2f649de9a..0530d21a7e2 100644 --- a/app/views/ci/status/_graph_badge.html.haml +++ b/app/views/ci/status/_graph_badge.html.haml @@ -2,7 +2,7 @@ - subject = local_assigns.fetch(:subject) - status = subject.detailed_status(current_user) -- klass = "ci-status-icon ci-status-icon-#{status.group}" +- klass = "ci-status-icon ci-status-icon-#{status.group} js-ci-status-icon-#{status.group}" - tooltip = "#{subject.name} - #{status.label}" - if status.has_details? @@ -16,5 +16,5 @@ - if status.has_action? = link_to status.action_path, class: 'ci-action-icon-container has-tooltip', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do - %i.ci-action-icon-wrapper - = icon(status.action_icon, class: status.action_class) + %i.ci-action-icon-wrapper{ class: "js-#{status.action_icon.dasherize}" } + = custom_icon(status.action_icon) diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index 3caaf827ff5..653052f7c54 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -15,6 +15,4 @@ = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" = render 'shared/issuable/filter', type: :issues - -.prepend-top-default - = render 'shared/issues' += render 'shared/issues' diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index fb016599fef..e64c78c4cb8 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -7,6 +7,4 @@ = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request" = render 'shared/issuable/filter', type: :merge_requests - -.prepend-top-default - = render 'shared/merge_requests' += render 'shared/merge_requests' diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index 9d7bcdb9d16..605bfd0cf8d 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -11,8 +11,11 @@ = link_to_author(todo) - else (removed) - %span.todo-label + + %span.action-name = todo_action_name(todo) + + %span.todo-label - if todo.target = todo_target_link(todo) - else diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index f4efcfb27b2..c4bf2c90cc2 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -67,21 +67,17 @@ = sort_title_oldest_created -.prepend-top-default +.js-todos-all - if @todos.any? .js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} } - - @todos.group_by(&:project).each do |group| - .panel.panel-default.panel-small - - project = group[0] - .panel-heading - = link_to project.name_with_namespace, namespace_project_path(project.namespace, project) - + .panel.panel-default.panel-small.panel-without-border %ul.content-list.todos-list - = render group[1] + = render @todos = paginate @todos, theme: "gitlab" + - elsif current_user.todos.any? .todos-all-done - = render "shared/empty_states/todos_all_done.svg" + = render "shared/empty_states/icons/todos_all_done.svg" - if todos_filter_empty? %h4.text-center = Gitlab.config.gitlab.no_todos_messages.sample @@ -98,7 +94,7 @@ - else .todos-empty .todos-empty-hero - = render "shared/empty_states/todos_empty.svg" + = render "shared/empty_states/icons/todos_empty.svg" .todos-empty-content %h4 Todos let you see what you should do next. diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml index b185b81db7f..5b1a4630c56 100644 --- a/app/views/groups/group_members/_new_group_member.html.haml +++ b/app/views/groups/group_members/_new_group_member.html.haml @@ -3,7 +3,7 @@ .col-md-4.col-lg-6 = users_select_tag(:user_ids, multiple: true, class: 'input-clamp', scope: :all, email_user: true) .help-block.append-bottom-10 - Search for users by name, username, or email, or invite new ones using their email address. + Search for members by name, username, or email, or invite new ones using their email address. .col-md-3.col-lg-2 = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select" @@ -16,7 +16,7 @@ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' %i.clear-icon.js-clear-input .help-block.append-bottom-10 - On this date, the user(s) will automatically lose access to this group and all of its projects. + On this date, the member(s) will automatically lose access to this group and all of its projects. .col-md-2 = f.submit 'Add to group', class: "btn btn-create btn-block" diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index f4c432a095a..2e4e4511bb6 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -7,7 +7,7 @@ - if can?(current_user, :admin_group_member, @group) .project-members-new.append-bottom-default %p.clearfix - Add new user to + Add new member to %strong= @group.name = render "new_group_member" @@ -15,7 +15,7 @@ .append-bottom-default.clearfix %h5.member.existing-title - Existing users + Existing members = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do .form-group = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } @@ -24,7 +24,7 @@ = render 'shared/members/sort_dropdown' .panel.panel-default .panel-heading - Users with access to + Members with access to %strong= @group.name %span.badge= @members.total_count %ul.content-list diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 6ad03a60b3a..83edb719692 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -23,7 +23,6 @@ - if current_user To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page. - .prepend-top-default - = render 'shared/issues' + = render 'shared/issues' - else = render 'shared/empty_states/issues', project_select_button: true diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index af73554086b..6ad76d23df5 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -15,5 +15,4 @@ - if current_user To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page. -.prepend-top-default - = render 'shared/merge_requests' += render 'shared/merge_requests' diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index b74cc822295..da2df0d8080 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -143,7 +143,7 @@ .key g .key b %td - Go to builds + Go to jobs %tr %td.shortcut .key g diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index ac04f57e217..19a947af4ca 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -1,5 +1,5 @@ += render 'layouts/nav/admin_settings' .scrolling-tabs-container{ class: nav_control_class } - = render 'layouts/nav/admin_settings' .fade-left = icon('angle-left') .fade-right diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index a8bbd67de80..7883823b21e 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -96,8 +96,8 @@ -# Shortcut to builds page - if project_nav_tab? :builds %li.hidden - = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do - Builds + = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do + Jobs -# Shortcut to commits page - if project_nav_tab? :commits diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml index a744c4be9d6..060b50ffc69 100644 --- a/app/views/notify/build_fail_email.html.haml +++ b/app/views/notify/build_fail_email.html.haml @@ -1,6 +1,6 @@ - content_for :header do %h1{ style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" } - GitLab (build failed) + GitLab (job failed) %h3 Project: @@ -21,4 +21,4 @@ Message: #{@build.pipeline.git_commit_message} %p - Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} + Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} diff --git a/app/views/notify/build_fail_email.text.erb b/app/views/notify/build_fail_email.text.erb index 9d497983498..2a94688a6b0 100644 --- a/app/views/notify/build_fail_email.text.erb +++ b/app/views/notify/build_fail_email.text.erb @@ -1,4 +1,4 @@ -Build failed for <%= @project.name %> +Job failed for <%= @project.name %> Status: <%= @build.status %> Commit: <%= @build.pipeline.short_sha %> diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml index 8c2e6db1426..ca0eaa96a9d 100644 --- a/app/views/notify/build_success_email.html.haml +++ b/app/views/notify/build_success_email.html.haml @@ -1,6 +1,6 @@ - content_for :header do %h1{ style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" } - GitLab (build successful) + GitLab (job successful) %h3 Project: @@ -21,4 +21,4 @@ Message: #{@build.pipeline.git_commit_message} %p - Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} + Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} diff --git a/app/views/notify/build_success_email.text.erb b/app/views/notify/build_success_email.text.erb index c5ed4f84861..445cd46e64f 100644 --- a/app/views/notify/build_success_email.text.erb +++ b/app/views/notify/build_success_email.text.erb @@ -1,4 +1,4 @@ -Build successful for <%= @project.name %> +Job successful for <%= @project.name %> Status: <%= @build.status %> Commit: <%= @build.pipeline.short_sha %> diff --git a/app/views/notify/links/ci/builds/_build.text.erb b/app/views/notify/links/ci/builds/_build.text.erb index f495a2e5486..741c7f344c8 100644 --- a/app/views/notify/links/ci/builds/_build.text.erb +++ b/app/views/notify/links/ci/builds/_build.text.erb @@ -1 +1 @@ -Build #<%= build.id %> ( <%= pipeline_build_url(pipeline, build) %> ) +Job #<%= build.id %> ( <%= pipeline_build_url(pipeline, build) %> ) diff --git a/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb index 8e89c52a1f3..af8924bad57 100644 --- a/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb +++ b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb @@ -1 +1 @@ -Build #<%= build.id %> +Job #<%= build.id %> diff --git a/app/views/projects/_customize_workflow.html.haml b/app/views/projects/_customize_workflow.html.haml index e2b73cee5a9..a41791f0eca 100644 --- a/app/views/projects/_customize_workflow.html.haml +++ b/app/views/projects/_customize_workflow.html.haml @@ -3,6 +3,6 @@ %h4 Customize your workflow! %p - Get started with GitLab by enabling features that work best for your project. From issues and wikis, to merge requests and builds, GitLab can help manage your workflow from idea to production! + Get started with GitLab by enabling features that work best for your project. From issues and wikis, to merge requests and pipelines, GitLab can help manage your workflow from idea to production! - if can?(current_user, :admin_project, @project) = link_to "Get started", edit_project_path(@project), class: "btn btn-success" diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml index 1a1327fb53c..27d25a6b682 100644 --- a/app/views/projects/_merge_request_merge_settings.html.haml +++ b/app/views/projects/_merge_request_merge_settings.html.haml @@ -4,10 +4,10 @@ .checkbox.builds-feature = form.label :only_allow_merge_if_build_succeeds do = form.check_box :only_allow_merge_if_build_succeeds - %strong Only allow merge requests to be merged if the build succeeds + %strong Only allow merge requests to be merged if the pipeline succeeds %br %span.descr - Builds need to be configured to enable this feature. + Pipelines need to be configured to enable this feature. = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds') .checkbox = form.label :only_allow_merge_if_all_discussions_are_resolved do diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index d0ff14e45e6..edf55d59f28 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -1,4 +1,4 @@ -- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds' +- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs' .top-block.row-content-block.clearfix .pull-right diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index 736b485bf06..27e81c2bec3 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -1,7 +1,7 @@ .content-block.build-header .header-content = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false - Build + Job %strong.js-build-id ##{@build.id} in pipeline = link_to pipeline_path(@build.pipeline) do @@ -17,6 +17,6 @@ = render "user" = time_ago_with_tooltip(@build.created_at) - if can?(current_user, :update_build, @build) && @build.retryable? - = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post + = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index 37bf085130a..56fc5f5e68b 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -2,7 +2,7 @@ %aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default - Build + Job %strong ##{@build.id} %a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" } = icon('angle-double-right') @@ -17,7 +17,7 @@ - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?) .block{ class: ("block-first" if !@build.coverage) } .title - Build artifacts + Job artifacts - if @build.artifacts_expired? %p.build-detail-row The artifacts were removed @@ -42,9 +42,9 @@ .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) } .title - Build details + Job details - if can?(current_user, :update_build, @build) && @build.retryable? - = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post + = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post - if @build.merge_request %p.build-detail-row %span.build-light-text Merge Request: @@ -136,4 +136,4 @@ - else = build.id - if build.retried? - %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Build was retried' } + %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' } diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml index 028664f5bba..acfdb250aff 100644 --- a/app/views/projects/builds/_table.html.haml +++ b/app/views/projects/builds/_table.html.haml @@ -2,14 +2,14 @@ - if builds.blank? %div - .nothing-here-block No builds to show + .nothing-here-block No jobs to show - else .table-holder %table.table.ci-table.builds-page %thead %tr %th Status - %th Build + %th Job %th Pipeline - if admin %th Project diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index c623e39b21f..5ffc0e20d10 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -1,5 +1,5 @@ - @no_container = true -- page_title "Builds" +- page_title "Jobs" = render "projects/pipelines/head" %div{ class: container_class } @@ -14,7 +14,7 @@ data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - unless @repository.gitlab_ci_yml - = link_to 'Get started with Builds', help_page_path('ci/quick_start/README'), class: 'btn btn-info' + = link_to 'Get started with CI/CD Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' = link_to ci_lint_path, class: 'btn btn-default' do %span CI Lint diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index c613e473e4c..228dad528ab 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,5 +1,5 @@ - @no_container = true -- page_title "#{@build.name} (##{@build.id})", "Builds" +- page_title "#{@build.name} (##{@build.id})", "Jobs" - trace_with_state = @build.trace_with_state = render "projects/pipelines/head", build_subnav: true @@ -12,14 +12,14 @@ .bs-callout.bs-callout-warning %p - if no_runners_for_project?(@build.project) - This build is stuck, because the project doesn't have any runners online assigned to it. + This job is stuck, because the project doesn't have any runners online assigned to it. - elsif @build.tags.any? - This build is stuck, because you don't have any active runners online with any of these tags assigned to them: + This job is stuck, because you don't have any active runners online with any of these tags assigned to them: - @build.tags.each do |tag| %span.label.label-primary = tag - else - This build is stuck, because you don't have any active runners that can run this build. + This job is stuck, because you don't have any active runners that can run this job. %br Go to @@ -37,14 +37,14 @@ - environment = environment_for_build(@build.project, @build) - if @build.success? && @build.last_deployment.present? - if @build.last_deployment.last? - This build is the most recent deployment to #{environment_link_for_build(@build.project, @build)}. + This job is the most recent deployment to #{environment_link_for_build(@build.project, @build)}. - else - This build is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}. + This job is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}. View the most recent deployment #{deployment_link(environment.last_deployment)}. - elsif @build.complete? && !@build.success? - The deployment of this build to #{environment_link_for_build(@build.project, @build)} did not succeed. + The deployment of this job to #{environment_link_for_build(@build.project, @build)} did not succeed. - else - This build is creating a deployment to #{environment_link_for_build(@build.project, @build)} + This job is creating a deployment to #{environment_link_for_build(@build.project, @build)} - if environment.try(:last_deployment) and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')} @@ -52,9 +52,9 @@ - if @build.erased? .erased.alert.alert-warning - if @build.erased_by_user? - Build has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)} + Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)} - else - Build has been erased #{time_ago_with_tooltip(@build.erased_at)} + Job has been erased #{time_ago_with_tooltip(@build.erased_at)} - else #js-build-scroll.scroll-controls .scroll-step diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index c1e496455d1..5ea85f9fd4c 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -32,10 +32,10 @@ = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace" - if build.stuck? - = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') + = icon('warning', class: 'text-warning has-tooltip', title: 'Job is stuck. Check runners.') - if retried - = icon('refresh', class: 'text-warning has-tooltip', title: 'Build was retried') + = icon('refresh', class: 'text-warning has-tooltip', title: 'Job was retried') .label-container - if build.tags.any? diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 818a70f38f1..cdab1e1b1a6 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -15,7 +15,7 @@ - else %span.api.monospace API - if pipeline.latest? - %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest + %span.label.label-success.has-tooltip{ title: 'Latest job for this branch' } latest - if pipeline.triggered? %span.label.label-primary triggered - if pipeline.yaml_errors.present? @@ -78,7 +78,7 @@ .btn-group.inline - if actions.any? .btn-group - %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual build', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual build' } + %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual job', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual job' } = custom_icon('icon_play') = icon('caret-down', 'aria-hidden' => 'true') %ul.dropdown-menu.dropdown-menu-align-right diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 6dba42d5226..4d0b7a5ca85 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -63,9 +63,10 @@ - if @commit.status .well-segment.pipeline-info %div{ class: "icon-container ci-status-icon-#{@commit.status}" } - = ci_icon_for_status(@commit.status) + = link_to namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id) do + = ci_icon_for_status(@commit.status) Pipeline - = link_to "##{@commit.pipelines.last.id}", pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "monospace" + = link_to "##{@commit.pipelines.last.id}", namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id), class: "monospace" for = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" %span.ci-status-label diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 08d3443b3d0..6abff6aaf95 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -13,7 +13,7 @@ Pipeline = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace" with - = pluralize pipeline.statuses.count(:id), "build" + = pluralize pipeline.statuses.count(:id), "job" - if pipeline.ref for = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace" @@ -44,7 +44,7 @@ %thead %tr %th Status - %th Build ID + %th Job ID %th Name %th - if pipeline.project.build_coverage_enabled? diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml index 00e7cdd1729..89968cf4e0d 100644 --- a/app/views/projects/commit/pipelines.html.haml +++ b/app/views/projects/commit/pipelines.html.haml @@ -1,6 +1,5 @@ -- page_title "Pipelines", "#{@commit.title} (#{@commit.short_id})", "Commits" +- page_title 'Pipelines', "#{@commit.title} (#{@commit.short_id})", 'Commits' -= render "commit_box" - -= render "ci_menu" -= render "pipelines_list", pipelines: @commit.pipelines.order(id: :desc) += render 'commit_box' += render 'ci_menu' += render 'pipelines_list', pipelines: @pipelines diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index c37a33bbcd5..fc478ccc995 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -5,7 +5,7 @@ - unless diff_file.submodule? .file-actions.hidden-xs - if blob_text_viewable?(blob) - = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file", disabled: @diff_notes_disabled do + = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file", disabled: @diff_notes_disabled do = icon('comment') \ - if editable_diff?(diff_file) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 84787155168..7a2dacdb1e7 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -63,7 +63,7 @@ .row .col-md-9.project-feature.nested - = feature_fields.label :builds_access_level, "Builds", class: 'label-light' + = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light' %span.help-block Submit, test and deploy your changes before merge .col-md-3 = project_feature_access_select(:builds_access_level) @@ -180,11 +180,13 @@ %p The following items will NOT be exported: %ul - %li Build traces and artifacts + %li Job traces and artifacts %li LFS objects %li Container registry images - %hr + %li CI variables + %li Any encrypted tokens - if can? current_user, :archive_project, @project + %hr .row.prepend-top-default .col-lg-3 %h4.warning-title.prepend-top-0 diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 6e0d9456900..7800d6ac382 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -5,7 +5,7 @@ %div{ class: container_class } .top-area.adjust .col-md-9 - %h3.page-title= @environment.name.capitalize + %h3.page-title= @environment.name .col-md-3 .nav-controls = render 'projects/environments/terminal_button', environment: @environment @@ -32,8 +32,8 @@ %tr %th ID %th Commit - %th Build - %th + %th Job + %th Created %th.hidden-xs = render @deployments diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/graphs/ci/_builds.haml index 431657c4dcb..b6f453b9736 100644 --- a/app/views/projects/graphs/ci/_builds.haml +++ b/app/views/projects/graphs/ci/_builds.haml @@ -1,4 +1,4 @@ -%h4 Build charts +%h4 Pipelines charts %p %span.cgreen @@ -11,19 +11,19 @@ .prepend-top-default %p.light - Builds for last week + Jobs for last week (#{date_from_to(Date.today - 7.days, Date.today)}) %canvas#weekChart{ height: 200 } .prepend-top-default %p.light - Builds for last month + Jobs for last month (#{date_from_to(Date.today - 30.days, Date.today)}) %canvas#monthChart{ height: 200 } .prepend-top-default %p.light - Builds for last year + Jobs for last year %canvas#yearChart.padded{ height: 250 } - [:week, :month, :year].each do |scope| diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index bd46af339cf..f3be343daae 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -34,7 +34,7 @@ = note_count .issue-info - #{issue.to_reference} · + #{issuable_reference(issue)} · opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} by #{link_to_member(@project, issue.author, avatar: false)} - if issue.milestone diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 9fa00811af0..11636d7ebc7 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -75,7 +75,7 @@ // This element is filled in using JavaScript. .content-block.content-block-small - = render 'new_branch' + = render 'new_branch' unless @issue.confidential? = render 'award_emoji/awards_block', awardable: @issue, inline: true %section.issuable-discussion diff --git a/app/views/projects/labels/destroy.js.haml b/app/views/projects/labels/destroy.js.haml deleted file mode 100644 index 8d09e2bda11..00000000000 --- a/app/views/projects/labels/destroy.js.haml +++ /dev/null @@ -1,2 +0,0 @@ -- if @labels.empty? - $('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000) diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 05a8475dcd6..29f861c09c6 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -3,37 +3,35 @@ - hide_class = '' = render "projects/issues/head" -%div{ class: container_class } - .top-area.adjust - .nav-text - Labels can be applied to issues and merge requests. Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging. +- if @labels.exists? || @prioritized_labels.exists? + %div{ class: container_class } + .top-area.adjust + .nav-text + Labels can be applied to issues and merge requests. Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging. - .nav-controls - - if can?(current_user, :admin_label, @project) - = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do - New label - - .labels - - if can?(current_user, :admin_label, @project) - -# Only show it in the first page - - 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 partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label + .nav-controls + - if can?(current_user, :admin_label, @project) + = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do + New label - .other-labels + .labels - if can?(current_user, :admin_label, @project) - %h5{ class: ('hide' if hide) } Other Labels - %ul.content-list.manage-labels-list.js-other-labels - - if @labels.present? - = render partial: 'shared/label', subject: @project, collection: @labels, as: :label - = paginate @labels, theme: 'gitlab' - - if @labels.blank? - .nothing-here-block + -# Only show it in the first page + - 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) } + #js-priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty?}" } + = render 'shared/empty_states/priority_labels' + - if @prioritized_labels.present? + = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label + + - if @labels.present? + .other-labels - 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}. - - else - No labels created + %h5{ class: ('hide' if hide) } Other Labels + %ul.content-list.manage-labels-list.js-other-labels + = render partial: 'shared/label', subject: @project, collection: @labels, as: :label + = paginate @labels, theme: 'gitlab' +- else + = render 'shared/empty_states/labels' diff --git a/app/views/projects/mattermosts/_no_teams.html.haml b/app/views/projects/mattermosts/_no_teams.html.haml index 605c7f61dee..aac74a25b75 100644 --- a/app/views/projects/mattermosts/_no_teams.html.haml +++ b/app/views/projects/mattermosts/_no_teams.html.haml @@ -1,3 +1,7 @@ +- if @teams_error_message + = content_for :flash_message do + .alert.alert-danger= @teams_error_message + %p You aren’t a member of any team on the Mattermost instance at %strong= Gitlab.config.mattermost.host diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index e3b0aa7e644..513f0818169 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -46,7 +46,7 @@ = note_count .merge-request-info - #{merge_request.to_reference} · + #{issuable_reference(merge_request)} · opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} by #{link_to_member(@project, merge_request.author, avatar: false)} - if merge_request.target_project.default_branch != merge_request.target_branch diff --git a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml index 2595ce74ac0..0839880713f 100644 --- a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml +++ b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml @@ -8,5 +8,5 @@ '@click' => "onClickResolveModeButton(file, 'edit')", type: 'button' } Edit inline - %a.btn.view-file.btn-file-option{ ":href" => "file.blobPath" } + %a.btn.view-file{ ":href" => "file.blobPath" } View file @{{conflictsData.shortCommitSha}} diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 5faa6c43f9f..ae134563ead 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -2,14 +2,15 @@ .mr-widget-heading - %w[success success_with_warnings skipped canceled failed running pending].each do |status| .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) } - = ci_icon_for_status(status) + = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do + = ci_icon_for_status(status) %span Pipeline = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline' = ci_label_for_status(status) for = succeed "." do - = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" + = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link" %span.ci-coverage - elsif @merge_request.has_ci? @@ -20,7 +21,7 @@ .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: "display:none" } = ci_icon_for_status(status) %span - CI build + CI job = ci_label_for_status(status) for - commit = @merge_request.diff_head_commit diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 38328501ffd..5de59473840 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -16,14 +16,18 @@ gitlab_icon: "#{asset_path 'gitlab_logo.png'}", ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}", ci_message: { - normal: "Build {{status}} for \"{{title}}\"", - preparing: "{{status}} build for \"{{title}}\"" + normal: "Job {{status}} for \"{{title}}\"", + preparing: "{{status}} job for \"{{title}}\"" }, ci_enable: #{@project.ci_service ? "true" : "false"}, ci_title: { - preparing: "{{status}} build", - normal: "Build {{status}}" + preparing: "{{status}} job", + normal: "Job {{status}}" }, + ci_sha: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.short_sha : ''}", + ci_pipeline: #{@merge_request.head_pipeline.try(:id).to_json}, + commits_path: "#{project_commits_path(@project)}", + pipeline_path: "#{project_pipelines_path(@project)}", pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" }; diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 7809e9c8c72..39cb0ca9cdc 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -35,10 +35,10 @@ The source branch will be removed. - elsif @merge_request.can_remove_source_branch?(current_user) .accept-control.checkbox - = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do + = label_tag :should_remove_source_branch, class: "merge-param-checkbox" do = check_box_tag :should_remove_source_branch Remove source branch - .accept-control.right + .accept-control = link_to "#", class: "modify-merge-commit-link js-toggle-button" do = icon('edit') Modify commit message diff --git a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml index 14f51af5360..a18c2ad768f 100644 --- a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml +++ b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml @@ -1,6 +1,6 @@ %h4 = icon('exclamation-triangle') - The build for this merge request failed + The job for this merge request failed %p - Please retry the build or push a new commit to fix the failure. + Please retry the job or push a new commit to fix the failure. diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index f70cd09c5f4..304b0afcf93 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -19,7 +19,7 @@ - if remove_source_branch_button || user_can_cancel_automatic_merge .clearfix.prepend-top-10 - if remove_source_branch_button - = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true, sha: @merge_request.diff_head_sha), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do + = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_params(@merge_request)), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do = icon('times') Remove Source Branch When Merged diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index b10dd47709f..721a9b6beb5 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -11,9 +11,9 @@ - if project_nav_tab? :builds = nav_link(controller: %w(builds)) do - = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do + = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do %span - Builds + Jobs - if project_nav_tab? :environments = nav_link(controller: %w(environments)) do diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index ca76f13ef5e..a6cd2d83bd5 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -23,9 +23,9 @@ .info-well - if @commit.status .well-segment.pipeline-info - %div{ class: "icon-container ci-status-icon-#{@commit.status}" } - = ci_icon_for_status(@commit.status) - = pluralize @pipeline.statuses.count(:id), "build" + .icon-container + = icon('clock-o') + = pluralize @pipeline.statuses.count(:id), "job" - if @pipeline.ref from = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace" diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 88af41aa835..53067cdcba4 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -5,7 +5,7 @@ Pipeline %li.js-builds-tab-link = link_to builds_namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: {target: 'div#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do - Builds + Jobs %span.badge.js-builds-counter= pipeline.statuses.count @@ -33,7 +33,7 @@ %thead %tr %th Status - %th Build ID + %th Job ID %th Name %th - if pipeline.project.build_coverage_enabled? diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml index 1f698558bce..18328c67f02 100644 --- a/app/views/projects/pipelines_settings/show.html.haml +++ b/app/views/projects/pipelines_settings/show.html.haml @@ -66,7 +66,7 @@ %span.input-group-addon / %p.help-block A regular expression that will be used to find the test coverage - output in the build trace. Leave blank to disable + output in the job trace. Leave blank to disable = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing') .bs-callout.bs-callout-info %p Below are examples of regex for existing tools: diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml index 33a9a96183c..98e72f6c547 100644 --- a/app/views/projects/runners/_form.html.haml +++ b/app/views/projects/runners/_form.html.haml @@ -5,7 +5,7 @@ .col-sm-10 .checkbox = f.check_box :active - %span.light Paused Runners don't accept new builds + %span.light Paused Runners don't accept new jobs .form-group = label :run_untagged, 'Run untagged jobs', class: 'control-label' .col-sm-10 diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/index.html.haml index 92957470070..d6f691d9c24 100644 --- a/app/views/projects/runners/index.html.haml +++ b/app/views/projects/runners/index.html.haml @@ -2,7 +2,7 @@ .light.prepend-top-default %p - A 'Runner' is a process which runs a build. + A 'Runner' is a process which runs a job. You can setup as many Runners as you need. %br Runners can be placed on separate users, servers, and even on your local machine. @@ -12,14 +12,14 @@ %ul %li %span.label.label-success active - \- Runner is active and can process any new builds + \- Runner is active and can process any new jobs %li %span.label.label-danger paused - \- Runner is paused and will not receive any new builds + \- Runner is paused and will not receive any new jobs %hr -%p.lead To start serving your builds you can either add specific Runners to your project or use shared Runners +%p.lead To start serving your jobs you can either add specific Runners to your project or use shared Runners .row .col-sm-6 = render 'specific_runners' diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index 068a6610350..e2a5107a883 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -8,6 +8,8 @@ - if can?(current_user, :create_project_snippet, @project) = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do New snippet + - if @snippet.submittable_as_spam? && current_user.admin? + = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } @@ -27,3 +29,6 @@ %li = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do Edit + - if @snippet.submittable_as_spam? && current_user.admin? + %li + = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml index 216f70f5605..fb39028529d 100644 --- a/app/views/projects/snippets/edit.html.haml +++ b/app/views/projects/snippets/edit.html.haml @@ -3,4 +3,4 @@ %h3.page-title Edit Snippet %hr -= render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet), visibility_level: @snippet.visibility_level += render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet) diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml index 772a594269c..cfed3a79bc5 100644 --- a/app/views/projects/snippets/new.html.haml +++ b/app/views/projects/snippets/new.html.haml @@ -3,4 +3,4 @@ %h3.page-title New Snippet %hr -= render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet), visibility_level: default_snippet_visibility += render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet) diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml index 6e5dd1b196d..b9c4e323430 100644 --- a/app/views/projects/triggers/index.html.haml +++ b/app/views/projects/triggers/index.html.haml @@ -67,7 +67,7 @@ In the %code .gitlab-ci.yml of another project, include the following snippet. - The project will be rebuilt at the end of the build. + The project will be rebuilt at the end of the job. %pre :plain @@ -86,12 +86,12 @@ :plain #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN %h5.prepend-top-default - Pass build variables + Pass job variables %p.light Add %code variables[VARIABLE]=VALUE - to an API request. Variable values can be used to distinguish between triggered builds and normal builds. + to an API request. Variable values can be used to distinguish between triggered jobs and normal jobs. With cURL: diff --git a/app/views/projects/variables/_content.html.haml b/app/views/projects/variables/_content.html.haml index 0249e0c1bf1..06477aba103 100644 --- a/app/views/projects/variables/_content.html.haml +++ b/app/views/projects/variables/_content.html.haml @@ -5,4 +5,4 @@ %p So you can use them for passwords, secret keys or whatever you want. %p - The value of the variable can be visible in build log if explicitly asked to do so. + The value of the variable can be visible in job log if explicitly asked to do so. diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml index 65a3a6bddab..54b5ae2402e 100644 --- a/app/views/shared/_import_form.html.haml +++ b/app/views/shared/_import_form.html.haml @@ -2,7 +2,7 @@ = f.label :import_url, class: 'control-label' do %span Git repository URL .col-sm-10 - = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', disabled: true + = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', disabled: true .well.prepend-top-20 %ul @@ -13,4 +13,4 @@ %li The import will time out after 15 minutes. For repositories that take longer, use a clone/push combination. %li - To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}. + To migrate an SVN repository, check out #{link_to "this document", help_page_path('workflow/importing/migrating_from_svn')}. diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml index 26b349e8a62..3a49227961f 100644 --- a/app/views/shared/_issues.html.haml +++ b/app/views/shared/_issues.html.haml @@ -1,16 +1,7 @@ - if @issues.to_a.any? - - @issues.group_by(&:project).each do |group| - .panel.panel-default.panel-small - - project = group[0] - .panel-heading - = link_to project.name_with_namespace, namespace_project_issues_path(project.namespace, project) - - if can?(current_user, :create_issue, project) - .pull-right - = link_to 'New issue', new_namespace_project_issue_path(project.namespace, project) - - %ul.content-list.issues-list - - group[1].each do |issue| - = render 'projects/issues/issue', issue: issue + .panel.panel-default.panel-small.panel-without-border + %ul.content-list.issues-list + = render partial: 'projects/issues/issue', collection: @issues = paginate @issues, theme: "gitlab" - else = render 'shared/empty_states/issues' diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index f11f4471a9d..ead9b84b991 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -36,7 +36,7 @@ %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?'} + = link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, data: {confirm: 'Remove this label? Are you sure?'} .pull-right.hidden-xs.hidden-sm.hidden-md = link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action') do @@ -66,11 +66,15 @@ %a.js-subscribe-button{ data: { url: toggle_subscription_group_label_path(label.group, label) } } Group level + - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_group, label.project.group) + = link_to promote_namespace_project_label_path(label.project.namespace, label.project, label), title: "Promote to Group Label", class: 'btn btn-transparent btn-action', data: {confirm: "Promoting this label will make this label available to all projects inside this group. Existing project labels with the same name will be merged. Are you sure?", toggle: "tooltip"}, method: :post do + %span.sr-only Promote to Group + = icon('level-up') - 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 + = link_to destroy_label_path(label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, data: {confirm: label_deletion_confirm_text(label), toggle: "tooltip"} do %span.sr-only Delete = icon('trash-o') diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml index 2f3605b4d27..b7982b7fe9b 100644 --- a/app/views/shared/_merge_requests.html.haml +++ b/app/views/shared/_merge_requests.html.haml @@ -1,16 +1,8 @@ - if @merge_requests.to_a.any? - - @merge_requests.group_by(&:target_project).each do |group| - .panel.panel-default.panel-small - - project = group[0] - .panel-heading - = link_to project.name_with_namespace, namespace_project_merge_requests_path(project.namespace, project) - - if can?(current_user, :create_merge_request, project) - .pull-right - = link_to 'New merge request', new_namespace_project_merge_request_path(project.namespace, project) + .panel.panel-default.panel-small.panel-without-border + %ul.content-list.mr-list + = render partial: 'projects/merge_requests/merge_request', collection: @merge_requests - %ul.content-list.mr-list - - group[1].each do |merge_request| - = render 'projects/merge_requests/merge_request', merge_request: merge_request = paginate @merge_requests, theme: "gitlab" - else diff --git a/app/views/shared/_outdated_browser.html.haml b/app/views/shared/_outdated_browser.html.haml index 0eba1fe075f..c06d1ffa59b 100644 --- a/app/views/shared/_outdated_browser.html.haml +++ b/app/views/shared/_outdated_browser.html.haml @@ -1,8 +1,7 @@ - if outdated_browser? - - link = "https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/requirements.md#supported-web-browsers" .browser-alert GitLab may not work properly because you are using an outdated web browser. %br Please install a - = link_to 'supported web browser', link + = link_to 'supported web browser', help_page_url('install/requirements', anchor: 'supported-web-browsers') for a better experience. diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml new file mode 100644 index 00000000000..ba5c2dae09d --- /dev/null +++ b/app/views/shared/empty_states/_labels.html.haml @@ -0,0 +1,11 @@ +.row.empty-state.labels + .pull-right.col-xs-12.col-sm-6 + .svg-content + = render 'shared/empty_states/icons/labels.svg' + .col-xs-12.col-sm-6 + .text-content + %h4 Labels can be applied to issues and merge requests to categorize them. + %p You can also star label to make it a priority label. + - if can?(current_user, :admin_label, @project) + = link_to 'New label', new_namespace_project_label_path(@project.namespace, @project), class: 'btn btn-new', title: 'New label', id: 'new_label_link' + = link_to 'Generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post, class: 'btn btn-success btn-inverted', title: 'Generate a default set of labels', id: 'generate_labels_link' diff --git a/app/views/shared/empty_states/_priority_labels.html.haml b/app/views/shared/empty_states/_priority_labels.html.haml new file mode 100644 index 00000000000..bc268301a97 --- /dev/null +++ b/app/views/shared/empty_states/_priority_labels.html.haml @@ -0,0 +1,3 @@ +.text-center + = render 'shared/empty_states/icons/priority_labels.svg' + %p Star labels to start sorting by priority diff --git a/app/views/shared/empty_states/icons/_labels.svg b/app/views/shared/empty_states/icons/_labels.svg new file mode 100644 index 00000000000..dc041a4a78b --- /dev/null +++ b/app/views/shared/empty_states/icons/_labels.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="787 240 386 274" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><circle id="a" cx="37" cy="107" r="8"/><mask id="e" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><circle id="b" cx="37" cy="75" r="8"/><mask id="f" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><circle id="c" cx="42" cy="93" r="8"/><mask id="g" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><circle id="d" cx="43" cy="75" r="8"/><mask id="h" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(791 244)"><g transform="rotate(30 49.554 229.722)"><rect width="74" height="124" x="8.6" y="95.9" fill="#FAFAFA" rx="8"/><rect width="74" height="124" y="87" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><circle cx="26.5" cy="178.5" r="3.5" fill="#FC8A51"/><circle cx="47.5" cy="178.5" r="3.5" fill="#FC8A51"/><rect width="50" height="4" x="12" y="127" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="18" y="139" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#e)" stroke-linecap="round" xlink:href="#a"/><path stroke="#EEE" stroke-width="4" d="M37.3 107S10.5 18.3 81 .6" stroke-linecap="round"/><path fill="#FDE5D8" d="M31 189c0 3.3 2.7 6 6 6s6-2.7 6-6"/></g><g transform="translate(105 47)"><rect width="74" height="124" y="64" fill="#FAFAFA" rx="8"/><rect width="74" height="124" y="55" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><rect width="50" height="4" x="12" y="95" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="18" y="107" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#f)" stroke-linecap="round" xlink:href="#b"/><path fill="#B5A7DD" d="M56 149.7c-.6-1-.2-2 .7-2.7l1.8-1c1-.6 2-.2 2.7.7.5 1 .2 2.2-.7 2.8l-1.8 1c-1 .5-2 .2-2.7-.8zm-37.8 0c.5-1 .2-2-.7-2.7l-1.8-1c-1-.6-2-.2-2.7.7-.6 1-.2 2.2.7 2.8l1.8 1c1 .5 2 .2 2.7-.8zM33 151h9v4h-9v-4z"/><path fill="#6B4FBB" d="M59 153c0-5.5-4.6-10-10-10-5.7 0-10 4.5-10 10s4.3 10 10 10c5.4 0 10-4.5 10-10zm-16 0c0-3.3 2.6-6 6-6 3.2 0 6 2.7 6 6s-2.8 6-6 6c-3.4 0-6-2.7-6-6zM35 153c0-5.5-4.6-10-10-10-5.7 0-10 4.5-10 10s4.3 10 10 10c5.4 0 10-4.5 10-10zm-16 0c0-3.3 2.6-6 6-6 3.2 0 6 2.7 6 6s-2.8 6-6 6c-3.4 0-6-2.7-6-6z"/><path stroke="#EEE" stroke-width="4" d="M37 75S30 0 80 0" stroke-linecap="round"/></g><g transform="rotate(15 -82.507 752.644)"><rect width="74" height="124" x="14.6" y="81.8" fill="#FAFAFA" rx="8"/><rect width="74" height="124" x="5" y="73" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><path fill="#FDE5D8" d="M41 147c0-1 1-2 2-2s2 1 2 2v3c0 1-1 2-2 2s-2-1-2-2v-3zm16.8 6.2c.8-.7 2-.6 2.8.3.7.8.5 2-.3 2.8L58 158c-1 .8-2.2.7-3 0-.6-1-.4-2.3.4-3l2.4-1.8zm-32 3c-1-.6-1-2-.4-2.7.7-1 2-1 2.8-.3l2.4 1.8c.8.7 1 2 .3 3-.8.7-2 1-3 0l-2.3-1.7z"/><rect width="2" height="7" x="39" y="168" fill="#FC8A51" rx="1"/><rect width="2" height="7" x="45" y="168" fill="#FC8A51" rx="1"/><circle cx="40" cy="169" r="2" fill="#FC8A51"/><circle cx="46" cy="169" r="2" fill="#FC8A51"/><rect width="22" height="18" x="32" y="158" stroke="#FC8A51" stroke-width="4" rx="8"/><rect width="34" height="5" x="26" y="174" fill="#FC8A51" rx="2.5"/><rect width="50" height="4" x="17" y="113" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="23" y="125" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#g)" stroke-linecap="round" xlink:href="#c"/><path stroke="#EEE" stroke-width="4" d="M42 93S50 0 0 0" stroke-linecap="round"/></g><g transform="rotate(-15 276.18 -697.744)"><rect width="74" height="124" x="18.7" y="65.6" fill="#FAFAFA" rx="8"/><rect width="74" height="124" x="6" y="55" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><g transform="translate(25 129)"><path stroke="#B5A7DD" stroke-width="4" d="M32 14c0-7.7-6.3-14-14-14S4 6.3 4 14" stroke-linecap="round"/><path stroke="#B5A7DD" stroke-width="2" d="M33 15v13c0 4.4-3.6 8-8 8" stroke-linecap="round"/><rect width="7" height="4" x="20" y="34" fill="#6B4FBB" rx="2"/><rect width="7" height="13" y="15" fill="#FFF" stroke="#6B4FBB" stroke-width="3" stroke-linejoin="round" rx="3.5"/><rect width="7" height="13" x="29" y="15" fill="#FFF" stroke="#6B4FBB" stroke-width="3" stroke-linejoin="round" transform="matrix(-1 0 0 1 65 0)" rx="3.5"/></g><rect width="50" height="4" x="18" y="95" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="24" y="107" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#h)" stroke-linecap="round" xlink:href="#d"/><path stroke="#EEE" stroke-width="4" d="M43 75S50 0 0 0" stroke-linecap="round"/></g><circle cx="193" cy="47" r="12" fill="#FFF" stroke="#FDE5D8" stroke-width="4"/><circle cx="193" cy="47" r="5" fill="#FFF" stroke="#FDE5D8" stroke-width="4"/><g opacity=".2"><path fill="#FC8A51" d="M30.7 254.8l-2.6 1c-1 .5-1.7 0-1.7-1v-3l-1-2.7c-.4-1 .2-1.7 1.2-1.7h3l2.6-1c1.2-.4 2 .2 2 1.2l-.2 3 1 2.6c.5 1.2 0 2-1 2l-3-.2zM374.7 133.8l-2.6 1c-1 .5-1.7 0-1.7-1v-3l-1-2.7c-.4-1 .2-1.7 1.2-1.7h3l2.6-1c1.2-.4 2 .2 2 1.2l-.2 3 1 2.6c.5 1.2 0 2-1 2l-3-.2zM5.6 95H1.8c-1.3.2-2-.8-1.4-2l1.4-3.4-.2-3.8c0-1.3 1-2 2-1.4l3.6 1.4 3.7-.2c1.2 0 2 1 1.4 2L11 91.3V95c.2 1.2-.8 2-2 1.4L5.6 95z"/><path fill="#6B4FBB" d="M308.8 62l-2-2.3c-.7-.8-.5-1.7.6-2l2.8-1 2-2c1-.6 1.8-.4 2.2.7l.8 2.8 2 2c.8 1 .5 1.8-.5 2.2l-2.8.8-2.3 2c-.8.8-1.7.5-2-.5l-1-2.8zM318 226.6h-3c-1-.2-1.4-1-1-2l1.4-2.5v-3c.2-1 1-1.4 2-1l2.6 1.4h3c1 .2 1.5 1 1 2l-1.4 2.6v3c-.2 1-1 1.5-2 1l-2.5-1.4zM121.8 8l-2-2.3c-.7-.8-.5-1.7.6-2l2.8-1 2-2c1-.6 1.8-.4 2.2.7l.8 2.8 2 2c.8 1 .5 1.8-.5 2.2l-2.8.8-2.3 2c-.8.8-1.7.5-2-.5l-1-2.8z"/></g></g></svg> diff --git a/app/views/shared/empty_states/icons/_priority_labels.svg b/app/views/shared/empty_states/icons/_priority_labels.svg new file mode 100644 index 00000000000..7282c2b215e --- /dev/null +++ b/app/views/shared/empty_states/icons/_priority_labels.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="116" height="68" viewBox="181 0 116 68"><g fill="none" fill-rule="evenodd" transform="translate(182)"><rect width="78" height="34" x="37" y="34" fill="#FAFAFA" rx="3"/><rect width="78" height="34" x="31" y="28" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="3"/><path fill="#FFF" stroke="#FC6D26" stroke-width="3" d="M34 35.8c-.6 0-1.4 0-1.8.4L29 38.8c-1 .7-1.7.4-2-.7l-.6-4c0-.5-.5-1.2-1-1.5L22 30.2c-1-.6-1-1.5 0-2l3.7-2c.5-.2 1-.8 1.2-1.3l1-4.2c.3-1 1-1.3 2-.5l3 3c.3.3 1 .6 1.6.6l4.2-.3c1 0 1.5.7 1 1.7L38 29c-.3.6-.3 1.4 0 2l1.3 3.8c.4 1 0 1.8-1.2 1.6l-4-.6z" stroke-linecap="round"/><path fill="#FDE5D8" d="M51.6 14.3c-.2-.2-.8-.4-1-.3l-2.8.5c-.7 0-1-.4-.8-1l1-2.8V9.5L46.6 7c-.3-.7 0-1.2.8-1h2.7c.3 0 .8-.2 1-.5l2-2c.6-.5 1-.4 1.3.3l.7 2.8c0 .3.4.8.7 1l2.3 1.2c.7.3.7 1 0 1.3l-2.2 1.7-.6 1-.4 3c-.2.6-.7.8-1.3.4l-2-1.7zM5.4 43.2c-.2-.2-.5-.2-.7-.2l-1.8.3c-.6 0-1-.2-.7-.7l.7-1.8V40l-1-1.7c0-.4 0-.7.6-.7h1.8c.3 0 .6 0 .8-.2L6.5 36c.3-.3.7-.2.8.2l.5 2 .5.5 1.6.8c.3.2.3.7 0 1l-1.6 1c-.2 0-.4.4-.4.7l-.4 2c0 .3-.4.5-.8.2l-1.4-1.2zM10.4 9.2C10.2 9 10 9 9.7 9L8 9.3c-.6 0-1-.2-.7-.7L8 6.8V6L7 4.3c0-.4 0-.7.6-.7h1.8c.3 0 .6 0 .8-.2L11.5 2c.3-.3.7-.2.8.2l.5 2 .5.5 1.6.8c.3.2.3.7 0 1l-1.6 1c-.2 0-.4.4-.4.7l-.4 2c0 .3-.4.5-.8.2l-1.4-1.2z"/><rect width="52" height="4" x="43" y="38" fill="#EEE" rx="2"/><rect width="36" height="4" x="43" y="48" fill="#EEE" rx="2"/></g></svg> diff --git a/app/views/shared/empty_states/_todos_all_done.svg b/app/views/shared/empty_states/icons/_todos_all_done.svg index 94b5c2e0ea0..94b5c2e0ea0 100644 --- a/app/views/shared/empty_states/_todos_all_done.svg +++ b/app/views/shared/empty_states/icons/_todos_all_done.svg diff --git a/app/views/shared/empty_states/_todos_empty.svg b/app/views/shared/empty_states/icons/_todos_empty.svg index b1e661268fb..b1e661268fb 100644 --- a/app/views/shared/empty_states/_todos_empty.svg +++ b/app/views/shared/empty_states/icons/_todos_empty.svg diff --git a/app/views/shared/icons/_icon_action_cancel.svg b/app/views/shared/icons/_icon_action_cancel.svg new file mode 100644 index 00000000000..a1f700eb0ff --- /dev/null +++ b/app/views/shared/icons/_icon_action_cancel.svg @@ -0,0 +1 @@ +<svg width="30px" height="30px" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg"><path d="M19.25,14.9765625 C19.25,14.1380166 19.0234398,13.3697952 18.5703125,12.671875 L12.6796875,18.5546875 C13.3932327,19.0182315 14.1666625,19.25 15,19.25 C15.5781279,19.25 16.1289036,19.1367199 16.6523438,18.9101562 C17.1757839,18.6835926 17.6276023,18.3802102 18.0078125,18 C18.3880227,17.6197898 18.690103,17.1653672 18.9140625,16.6367188 C19.138022,16.1080703 19.25,15.5546904 19.25,14.9765625 Z M11.4453125,17.3125 L17.34375,11.421875 C16.6406215,10.9479143 15.8593793,10.7109375 15,10.7109375 C14.2291628,10.7109375 13.5182324,10.9010398 12.8671875,11.28125 C12.2161426,11.6614602 11.7005227,12.1796842 11.3203125,12.8359375 C10.9401023,13.4921908 10.75,14.2057253 10.75,14.9765625 C10.75,15.8203167 10.9817685,16.5989548 11.4453125,17.3125 Z M21,14.9765625 C21,15.7942749 20.8411474,16.5755171 20.5234375,17.3203125 C20.2057276,18.0651079 19.7799506,18.7057265 19.2460938,19.2421875 C18.7122369,19.7786485 18.0742225,20.2057276 17.3320312,20.5234375 C16.58984,20.8411474 15.8125041,21 15,21 C14.1874959,21 13.41016,20.8411474 12.6679688,20.5234375 C11.9257775,20.2057276 11.2877631,19.7786485 10.7539062,19.2421875 C10.2200494,18.7057265 9.79427242,18.0651079 9.4765625,17.3203125 C9.15885258,16.5755171 9,15.7942749 9,14.9765625 C9,14.1588501 9.15885258,13.37891 9.4765625,12.6367188 C9.79427242,11.8945275 10.2200494,11.255211 10.7539062,10.71875 C11.2877631,10.182289 11.9257775,9.75520992 12.6679688,9.4375 C13.41016,9.11979008 14.1874959,8.9609375 15,8.9609375 C15.8125041,8.9609375 16.58984,9.11979008 17.3320312,9.4375 C18.0742225,9.75520992 18.7122369,10.182289 19.2460938,10.71875 C19.7799506,11.255211 20.2057276,11.8945275 20.5234375,12.6367188 C20.8411474,13.37891 21,14.1588501 21,14.9765625 Z"></path></svg> diff --git a/app/views/shared/icons/_icon_action_play.svg b/app/views/shared/icons/_icon_action_play.svg new file mode 100644 index 00000000000..6ac192cd7e9 --- /dev/null +++ b/app/views/shared/icons/_icon_action_play.svg @@ -0,0 +1 @@ +<svg width="30px" height="30px" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg"><path d="M21.5401786,15.2320328 L11.90625,20.5858274 C11.7950143,20.6486998 11.6994982,20.6559541 11.6196987,20.6075908 C11.5398992,20.5592275 11.5,20.4721748 11.5,20.3464301 L11.5,9.66785867 C11.5,9.54211399 11.5398992,9.45506129 11.6196987,9.40669795 C11.6994982,9.35833462 11.7950143,9.36558901 11.90625,9.42846135 L21.5401786,14.782256 C21.6514142,14.8451283 21.7070312,14.9200904 21.7070312,15.0071444 C21.7070312,15.0941984 21.6514142,15.1691604 21.5401786,15.2320328 Z"></path></svg> diff --git a/app/views/shared/icons/_icon_action_retry.svg b/app/views/shared/icons/_icon_action_retry.svg new file mode 100644 index 00000000000..0fa0243f3c0 --- /dev/null +++ b/app/views/shared/icons/_icon_action_retry.svg @@ -0,0 +1 @@ +<svg width="30px" height="30px" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg"><path d="M20.6114971,16.0821413 C20.6114971,16.106323 20.6090789,16.1232499 20.6042426,16.1329226 C20.2947172,17.42906 19.6466582,18.4797378 18.6600462,19.2849873 C17.6734341,20.0902369 16.5175677,20.4928556 15.1924122,20.4928556 C14.4863075,20.4928556 13.8031856,20.3598584 13.1430261,20.0938601 C12.4828665,19.8278617 11.8940517,19.4482152 11.376564,18.9549092 L10.4407381,19.8907351 C10.3488478,19.9826254 10.2400319,20.0285699 10.1142872,20.0285699 C9.98854256,20.0285699 9.87972669,19.9826254 9.78783635,19.8907351 C9.69594601,19.7988447 9.65000153,19.6900289 9.65000153,19.5642842 L9.65000153,16.3142842 C9.65000153,16.1885395 9.69594601,16.0797236 9.78783635,15.9878333 C9.87972669,15.895943 9.98854256,15.8499985 10.1142872,15.8499985 L13.3642872,15.8499985 C13.4900319,15.8499985 13.5988478,15.895943 13.6907381,15.9878333 C13.7826285,16.0797236 13.828573,16.1885395 13.828573,16.3142842 C13.828573,16.4400289 13.7826285,16.5488447 13.6907381,16.6407351 L12.6968765,17.6345967 C13.0402562,17.9537947 13.4295752,18.200444 13.8648453,18.374552 C14.3001153,18.5486601 14.7523057,18.6357128 15.2214301,18.6357128 C15.8694988,18.6357128 16.4740315,18.4785343 17.0350462,18.1641726 C17.5960609,17.8498109 18.0458332,17.4169655 18.3843765,16.8656235 C18.4375762,16.7834058 18.5657371,16.5004845 18.7688631,16.0168512 C18.8075538,15.9056155 18.8800977,15.8499985 18.9864971,15.8499985 L20.3793542,15.8499985 C20.4422265,15.8499985 20.4966345,15.8729707 20.5425797,15.9189159 C20.5885248,15.9648611 20.6114971,16.019269 20.6114971,16.0821413 Z M20.7928587,10.2785699 L20.7928587,13.5285699 C20.7928587,13.6543146 20.7469142,13.7631305 20.6550238,13.8550208 C20.5631335,13.9469111 20.4543176,13.9928556 20.328573,13.9928556 L17.078573,13.9928556 C16.9528283,13.9928556 16.8440124,13.9469111 16.7521221,13.8550208 C16.6602317,13.7631305 16.6142872,13.6543146 16.6142872,13.5285699 C16.6142872,13.4028252 16.6602317,13.2940094 16.7521221,13.202119 L17.7532381,12.2010029 C17.0374607,11.5384252 16.1935332,11.2071413 15.2214301,11.2071413 C14.5733614,11.2071413 13.9688287,11.3643198 13.407814,11.6786815 C12.8467993,11.9930432 12.397027,12.4258886 12.0584837,12.9772306 C12.005284,13.0594483 11.8771231,13.3423696 11.6739971,13.8260029 C11.6353064,13.9372386 11.5627625,13.9928556 11.4563631,13.9928556 L10.0127247,13.9928556 C9.9498524,13.9928556 9.89544446,13.9698834 9.84949929,13.9239382 C9.80355412,13.877993 9.78058188,13.8235851 9.78058188,13.7607128 L9.78058188,13.7099315 C10.0949436,12.4137941 10.7478388,11.3631163 11.7392872,10.5578668 C12.7307356,9.75261722 13.8914383,9.34999847 15.2214301,9.34999847 C15.9275348,9.34999847 16.6142839,9.48420472 17.281698,9.75262124 C17.949112,10.0210378 18.541554,10.3994752 19.0590417,10.8879449 L20.0021221,9.95211901 C20.0940124,9.86022867 20.2028283,9.81428419 20.328573,9.81428419 C20.4543176,9.81428419 20.5631335,9.86022867 20.6550238,9.95211901 C20.7469142,10.0440094 20.7928587,10.1528252 20.7928587,10.2785699 Z"></path></svg> diff --git a/app/views/shared/icons/_icon_action_stop.svg b/app/views/shared/icons/_icon_action_stop.svg new file mode 100644 index 00000000000..1c8e2fe4156 --- /dev/null +++ b/app/views/shared/icons/_icon_action_stop.svg @@ -0,0 +1 @@ +<svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M20.1357204,10.2985704 L20.1357204,19.7271418 C20.1357204,19.8432138 20.0933101,19.9436592 20.0084882,20.0284811 C19.9236664,20.1133029 19.823221,20.1557132 19.707149,20.1557132 L10.2785775,20.1557132 C10.1625055,20.1557132 10.0620601,20.1133029 9.97723825,20.0284811 C9.89241639,19.9436592 9.8500061,19.8432138 9.8500061,19.7271418 L9.8500061,10.2985704 C9.8500061,10.1824984 9.89241639,10.0820529 9.97723825,9.99723107 C10.0620601,9.91240922 10.1625055,9.86999893 10.2785775,9.86999893 L19.707149,9.86999893 C19.823221,9.86999893 19.9236664,9.91240922 20.0084882,9.99723107 C20.0933101,10.0820529 20.1357204,10.1824984 20.1357204,10.2985704 Z"></path></svg> diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 0a4de709fcd..cb92b2e97a7 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -43,6 +43,8 @@ = render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form += render 'shared/issuable/form/merge_params', issuable: issuable + - if @merge_request_for_resolving_discussions .form-group .col-sm-10.col-sm-offset-2 diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index ec9bcaf63dd..10fa7901874 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -130,7 +130,7 @@ .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) } - if selected_labels.any? - selected_labels.each do |label| - = link_to_label(label, type: issuable.to_ability_name) + = link_to_label(label, subject: issuable.project, type: issuable.to_ability_name) - else %span.no-value None .selectbox.hide-collapsed diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml index b757893ea04..2793e7bcff4 100644 --- a/app/views/shared/issuable/form/_branch_chooser.html.haml +++ b/app/views/shared/issuable/form/_branch_chooser.html.haml @@ -19,12 +19,3 @@ - if issuable.new_record? = link_to 'Change branches', mr_change_branches_path(issuable) - -- if issuable.can_remove_source_branch?(current_user) - .form-group - .col-sm-10.col-sm-offset-2 - .checkbox - = label_tag 'merge_request[force_remove_source_branch]' do - = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil - = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch? - Remove source branch when merge request is accepted. diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml new file mode 100644 index 00000000000..03309722326 --- /dev/null +++ b/app/views/shared/issuable/form/_merge_params.html.haml @@ -0,0 +1,16 @@ +- issuable = local_assigns.fetch(:issuable) + +- return unless issuable.is_a?(MergeRequest) +- return if issuable.closed_without_fork? + +-# This check is duplicated below, to avoid conflicts with EE. +- return unless issuable.can_remove_source_branch?(current_user) + +.form-group + .col-sm-10.col-sm-offset-2 + - if issuable.can_remove_source_branch?(current_user) + .checkbox + = label_tag 'merge_request[force_remove_source_branch]' do + = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil + = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch? + Remove source branch when merge request is accepted. diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml index b5c0a7fd6d4..a736bfd91e2 100644 --- a/app/views/shared/notifications/_custom_notifications.html.haml +++ b/app/views/shared/notifications/_custom_notifications.html.haml @@ -18,7 +18,7 @@ %p Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out = succeed "." do - %a{ href: "http://docs.gitlab.com/ce/workflow/notifications.html", target: "_blank" } notification emails + %a{ href: help_page_path('workflow/notifications'), target: "_blank" } notification emails .col-lg-8 - NotificationSetting::EMAIL_EVENTS.each_with_index do |event, index| - field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]" diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 0c788032020..2d22782eb36 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -11,7 +11,7 @@ .col-sm-10 = f.text_field :title, class: 'form-control', required: true, autofocus: true - = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: true, form_model: @snippet + = render 'shared/visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet .file-editor .form-group @@ -34,4 +34,3 @@ = link_to "Cancel", namespace_project_snippets_path(@project.namespace, @project), class: "btn btn-cancel" - else = link_to "Cancel", snippets_path(@project), class: "btn btn-cancel" - diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 13586a5a12a..c212d1c86bf 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -66,9 +66,9 @@ = f.check_box :build_events, class: 'pull-left' .prepend-left-20 = f.label :build_events, class: 'list-label' do - %strong Build events + %strong Jobs events %p.light - This URL will be triggered when the build status changes + This URL will be triggered when the job status changes %li = f.check_box :pipeline_events, class: 'pull-left' .prepend-left-20 diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index 95fc7198104..9a9a3ff9220 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -8,6 +8,8 @@ - if current_user = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do New snippet + - if @snippet.submittable_as_spam? && current_user.admin? + = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' - if current_user .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } @@ -26,3 +28,6 @@ %li = link_to edit_snippet_path(@snippet) do Edit + - if @snippet.submittable_as_spam? && current_user.admin? + %li + = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml index 82f44a9a5c3..915bf98eb3e 100644 --- a/app/views/snippets/edit.html.haml +++ b/app/views/snippets/edit.html.haml @@ -2,4 +2,4 @@ %h3.page-title Edit Snippet %hr -= render 'shared/snippets/form', url: snippet_path(@snippet), visibility_level: @snippet.visibility_level += render 'shared/snippets/form', url: snippet_path(@snippet) diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml index 79e2392490d..ca8afb4bb6a 100644 --- a/app/views/snippets/new.html.haml +++ b/app/views/snippets/new.html.haml @@ -2,4 +2,4 @@ %h3.page-title New Snippet %hr -= render "shared/snippets/form", url: snippets_path(@snippet), visibility_level: default_snippet_visibility += render "shared/snippets/form", url: snippets_path(@snippet) diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index b9cd49985dc..f5ccc84c160 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -33,13 +33,15 @@ class EmailsOnPushWorker reverse_compare = false if action == :push - compare = CompareService.new.execute(project, after_sha, project, before_sha) + compare = CompareService.new(project, after_sha) + .execute(project, before_sha) diff_refs = compare.diff_refs return false if compare.same if compare.commits.empty? - compare = CompareService.new.execute(project, before_sha, project, after_sha) + compare = CompareService.new(project, before_sha) + .execute(project, after_sha) diff_refs = compare.diff_refs reverse_compare = true diff --git a/changelogs/unreleased/17662-rename-builds.yml b/changelogs/unreleased/17662-rename-builds.yml new file mode 100644 index 00000000000..12f2998d1c8 --- /dev/null +++ b/changelogs/unreleased/17662-rename-builds.yml @@ -0,0 +1,4 @@ +--- +title: Rename Builds to Pipelines, CI/CD Pipelines, or Jobs everywhere +merge_request: 8787 +author: diff --git a/changelogs/unreleased/19164-mobile-settings.yml b/changelogs/unreleased/19164-mobile-settings.yml new file mode 100644 index 00000000000..c26a20f87e2 --- /dev/null +++ b/changelogs/unreleased/19164-mobile-settings.yml @@ -0,0 +1,4 @@ +--- +title: 19164 Add settings dropdown to mobile screens +merge_request: +author: diff --git a/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml b/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml new file mode 100644 index 00000000000..965d0648adf --- /dev/null +++ b/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml @@ -0,0 +1,4 @@ +--- +title: Don't require lib/gitlab/request_profiler/middleware.rb in config/initializers/request_profiler.rb +merge_request: +author: diff --git a/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml b/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml new file mode 100644 index 00000000000..eda872049fd --- /dev/null +++ b/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml @@ -0,0 +1,4 @@ +--- +title: Added labels empty state +merge_request: 7443 +author: diff --git a/changelogs/unreleased/23634-remove-project-grouping.yml b/changelogs/unreleased/23634-remove-project-grouping.yml new file mode 100644 index 00000000000..dde8b2d1815 --- /dev/null +++ b/changelogs/unreleased/23634-remove-project-grouping.yml @@ -0,0 +1,4 @@ +--- +title: Don't group issues by project on group-level and dashboard issue indexes. +merge_request: 8111 +author: Bernardo Castro diff --git a/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml b/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml new file mode 100644 index 00000000000..587ef4f9a73 --- /dev/null +++ b/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml @@ -0,0 +1,4 @@ +--- +title: Fix disable storing of sensitive information when importing a new repo +merge_request: 8885 +author: Bernard Pietraga diff --git a/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml b/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml new file mode 100644 index 00000000000..05fbd8f0bf2 --- /dev/null +++ b/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml @@ -0,0 +1,4 @@ +--- +title: Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms +merge_request: 8752 +author: diff --git a/changelogs/unreleased/24606-force-password-reset-on-next-login.yml b/changelogs/unreleased/24606-force-password-reset-on-next-login.yml new file mode 100644 index 00000000000..fd671d04a9f --- /dev/null +++ b/changelogs/unreleased/24606-force-password-reset-on-next-login.yml @@ -0,0 +1,4 @@ +--- +title: Force new password after password reset via API +merge_request: +author: George Andrinopoulos diff --git a/changelogs/unreleased/24795_refactor_merge_request_build_service.yml b/changelogs/unreleased/24795_refactor_merge_request_build_service.yml new file mode 100644 index 00000000000..b735fb57649 --- /dev/null +++ b/changelogs/unreleased/24795_refactor_merge_request_build_service.yml @@ -0,0 +1,4 @@ +--- +title: Refactor MergeRequests::BuildService +merge_request: 8462 +author: Rydkin Maxim diff --git a/changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml b/changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml new file mode 100644 index 00000000000..50a5c879446 --- /dev/null +++ b/changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml @@ -0,0 +1,4 @@ +--- +title: Remove flash warning from login page +merge_request: 8864 +author: Gerald J. Padilla diff --git a/changelogs/unreleased/25460-replace-word-users-with-members.yml b/changelogs/unreleased/25460-replace-word-users-with-members.yml new file mode 100644 index 00000000000..dac90eaa34d --- /dev/null +++ b/changelogs/unreleased/25460-replace-word-users-with-members.yml @@ -0,0 +1,4 @@ +--- +title: Replace word user with member +merge_request: 8872 +author: diff --git a/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml b/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml new file mode 100644 index 00000000000..f74e9fa8b6d --- /dev/null +++ b/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml @@ -0,0 +1,4 @@ +--- +title: Update pipeline and commit links when CI status is updated +merge_request: 8351 +author: diff --git a/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml b/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml new file mode 100644 index 00000000000..9506692dd40 --- /dev/null +++ b/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml @@ -0,0 +1,4 @@ +--- +title: Convert pipeline action icons to svg to have them propperly positioned +merge_request: +author: diff --git a/changelogs/unreleased/26775-fix-auto-complete-initial-loading.yml b/changelogs/unreleased/26775-fix-auto-complete-initial-loading.yml deleted file mode 100644 index 2d4ec482ee0..00000000000 --- a/changelogs/unreleased/26775-fix-auto-complete-initial-loading.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix autocomplete initial undefined state -merge_request: -author: diff --git a/changelogs/unreleased/26844-new-search-bar-performs-a-new-request-for-each-tag.yml b/changelogs/unreleased/26844-new-search-bar-performs-a-new-request-for-each-tag.yml deleted file mode 100644 index 4678297cfd4..00000000000 --- a/changelogs/unreleased/26844-new-search-bar-performs-a-new-request-for-each-tag.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add caching of droplab ajax requests -merge_request: 8725 -author: diff --git a/changelogs/unreleased/26852-fix-slug-for-openshift.yml b/changelogs/unreleased/26852-fix-slug-for-openshift.yml new file mode 100644 index 00000000000..fb65b068b23 --- /dev/null +++ b/changelogs/unreleased/26852-fix-slug-for-openshift.yml @@ -0,0 +1,4 @@ +--- +title: Avoid repeated dashes in $CI_ENVIRONMENT_SLUG +merge_request: 8638 +author: diff --git a/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml b/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml new file mode 100644 index 00000000000..c5c57af5aaf --- /dev/null +++ b/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml @@ -0,0 +1,4 @@ +--- +title: Improve pipeline status icon linking in widgets +merge_request: +author: diff --git a/changelogs/unreleased/27044-fix-explore-sorting-on-trending.yml b/changelogs/unreleased/27044-fix-explore-sorting-on-trending.yml deleted file mode 100644 index 0f0a8940f72..00000000000 --- a/changelogs/unreleased/27044-fix-explore-sorting-on-trending.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix /explore sorting -merge_request: -author: diff --git a/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml b/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml new file mode 100644 index 00000000000..1758ed9e9ea --- /dev/null +++ b/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml @@ -0,0 +1,4 @@ +--- +title: Support non-ASCII characters in GFM autocomplete +merge_request: 8729 +author: diff --git a/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml b/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml new file mode 100644 index 00000000000..ddd454da376 --- /dev/null +++ b/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml @@ -0,0 +1,4 @@ +--- +title: Fix permalink discussion note being collapsed +merge_request: +author: diff --git a/changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml b/changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml new file mode 100644 index 00000000000..7b307b501f4 --- /dev/null +++ b/changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml @@ -0,0 +1,4 @@ +--- +title: Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index +merge_request: 8956 +author: diff --git a/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml b/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml new file mode 100644 index 00000000000..293aab67d39 --- /dev/null +++ b/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml @@ -0,0 +1,4 @@ +--- +title: Unify MR diff file button style +merge_request: 8874 +author: diff --git a/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml b/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml new file mode 100644 index 00000000000..502927cd160 --- /dev/null +++ b/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml @@ -0,0 +1,4 @@ +--- +title: Only render hr when user can't archive project. +merge_request: !8917 +author: diff --git a/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml b/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml new file mode 100644 index 00000000000..79316abbaf7 --- /dev/null +++ b/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml @@ -0,0 +1,4 @@ +--- +title: Fix pipeline graph vertical spacing in Firefox and Safari +merge_request: 8886 +author: diff --git a/changelogs/unreleased/27484-environment-show-name.yml b/changelogs/unreleased/27484-environment-show-name.yml new file mode 100644 index 00000000000..dc400d65006 --- /dev/null +++ b/changelogs/unreleased/27484-environment-show-name.yml @@ -0,0 +1,4 @@ +--- +title: Don't capitalize environment name in show page +merge_request: +author: diff --git a/changelogs/unreleased/27488-fix-jwt-version.yml b/changelogs/unreleased/27488-fix-jwt-version.yml new file mode 100644 index 00000000000..5135ff0fd60 --- /dev/null +++ b/changelogs/unreleased/27488-fix-jwt-version.yml @@ -0,0 +1,4 @@ +--- +title: Update and pin the `jwt` gem to ~> 1.5.6 +merge_request: +author: diff --git a/changelogs/unreleased/27494-environment-list-column-headers.yml b/changelogs/unreleased/27494-environment-list-column-headers.yml new file mode 100644 index 00000000000..798c01f3238 --- /dev/null +++ b/changelogs/unreleased/27494-environment-list-column-headers.yml @@ -0,0 +1,4 @@ +--- +title: Edited the column header for the environments list from created to updated and added created to environments detail page colum header titles +merge_request: +author: diff --git a/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml b/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml new file mode 100644 index 00000000000..bc990c66866 --- /dev/null +++ b/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml @@ -0,0 +1,4 @@ +--- +title: Fix wrong call to ProjectCacheWorker.perform +merge_request: 8910 +author: diff --git a/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml b/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml new file mode 100644 index 00000000000..11d1f55172b --- /dev/null +++ b/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml @@ -0,0 +1,4 @@ +--- +title: Fix notifications when set at group level +merge_request: 6813 +author: Alexandre Maia diff --git a/changelogs/unreleased/cop-gem-fetcher.yml b/changelogs/unreleased/cop-gem-fetcher.yml new file mode 100644 index 00000000000..506815a5b54 --- /dev/null +++ b/changelogs/unreleased/cop-gem-fetcher.yml @@ -0,0 +1,4 @@ +--- +title: Cop for gem fetched from a git source +merge_request: 8856 +author: Adam Pahlevi diff --git a/changelogs/unreleased/document-how-to-vue.yml b/changelogs/unreleased/document-how-to-vue.yml new file mode 100644 index 00000000000..863e41b6413 --- /dev/null +++ b/changelogs/unreleased/document-how-to-vue.yml @@ -0,0 +1,4 @@ +--- +title: Adds documentation for how to use Vue.js +merge_request: 8866 +author: diff --git a/changelogs/unreleased/dz-nested-groups-improvements-2.yml b/changelogs/unreleased/dz-nested-groups-improvements-2.yml new file mode 100644 index 00000000000..8e4eb7f1fff --- /dev/null +++ b/changelogs/unreleased/dz-nested-groups-improvements-2.yml @@ -0,0 +1,4 @@ +--- +title: Add read-only full_path and full_name attributes to Group API +merge_request: 8827 +author: diff --git a/changelogs/unreleased/empty-selection-reply-shortcut.yml b/changelogs/unreleased/empty-selection-reply-shortcut.yml new file mode 100644 index 00000000000..5a42c98a800 --- /dev/null +++ b/changelogs/unreleased/empty-selection-reply-shortcut.yml @@ -0,0 +1,4 @@ +--- +title: Change the reply shortcut to focus the field even without a selection. +merge_request: 8873 +author: Brian Hall diff --git a/changelogs/unreleased/fix-26518.yml b/changelogs/unreleased/fix-26518.yml deleted file mode 100644 index 961ac2642fb..00000000000 --- a/changelogs/unreleased/fix-26518.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix access to the wiki code via HTTP when repository feature disabled -merge_request: 8758 -author: diff --git a/changelogs/unreleased/fix-27479.yml b/changelogs/unreleased/fix-27479.yml new file mode 100644 index 00000000000..cc72a830695 --- /dev/null +++ b/changelogs/unreleased/fix-27479.yml @@ -0,0 +1,4 @@ +--- +title: Remove new branch button for confidential issues +merge_request: +author: diff --git a/changelogs/unreleased/fix-ci-build-policy.yml b/changelogs/unreleased/fix-ci-build-policy.yml new file mode 100644 index 00000000000..26003713ed4 --- /dev/null +++ b/changelogs/unreleased/fix-ci-build-policy.yml @@ -0,0 +1,4 @@ +--- +title: Improve build policy and access abilities +merge_request: 8711 +author: diff --git a/changelogs/unreleased/fix-depr-warn.yml b/changelogs/unreleased/fix-depr-warn.yml new file mode 100644 index 00000000000..61817027720 --- /dev/null +++ b/changelogs/unreleased/fix-depr-warn.yml @@ -0,0 +1,4 @@ +--- +title: resolve deprecation warnings +merge_request: 8855 +author: Adam Pahlevi diff --git a/changelogs/unreleased/fix-filtering-username-with-multiple-words.yml b/changelogs/unreleased/fix-filtering-username-with-multiple-words.yml new file mode 100644 index 00000000000..3513f5afdfb --- /dev/null +++ b/changelogs/unreleased/fix-filtering-username-with-multiple-words.yml @@ -0,0 +1,4 @@ +--- +title: Fix filtering usernames with multiple words +merge_request: 8851 +author: diff --git a/changelogs/unreleased/fix-import-encrypt-atts.yml b/changelogs/unreleased/fix-import-encrypt-atts.yml new file mode 100644 index 00000000000..e34d895570b --- /dev/null +++ b/changelogs/unreleased/fix-import-encrypt-atts.yml @@ -0,0 +1,4 @@ +--- +title: Ignore encrypted attributes in Import/Export +merge_request: +author: diff --git a/changelogs/unreleased/fix-import-user-validation-error.yml b/changelogs/unreleased/fix-import-user-validation-error.yml new file mode 100644 index 00000000000..985a3b0b26f --- /dev/null +++ b/changelogs/unreleased/fix-import-user-validation-error.yml @@ -0,0 +1,4 @@ +--- +title: Remove old project members when retrying an export +merge_request: +author: diff --git a/changelogs/unreleased/fwn-to-find-by-full-path.yml b/changelogs/unreleased/fwn-to-find-by-full-path.yml new file mode 100644 index 00000000000..1427e4e7624 --- /dev/null +++ b/changelogs/unreleased/fwn-to-find-by-full-path.yml @@ -0,0 +1,4 @@ +--- +title: replace `find_with_namespace` with `find_by_full_path` +merge_request: 8949 +author: Adam Pahlevi diff --git a/changelogs/unreleased/group-label-sidebar-link.yml b/changelogs/unreleased/group-label-sidebar-link.yml new file mode 100644 index 00000000000..c11c2d4ede1 --- /dev/null +++ b/changelogs/unreleased/group-label-sidebar-link.yml @@ -0,0 +1,4 @@ +--- +title: Fixed group label links in issue/merge request sidebar +merge_request: +author: diff --git a/changelogs/unreleased/hardcode-title-system-note.yml b/changelogs/unreleased/hardcode-title-system-note.yml new file mode 100644 index 00000000000..1b0a63efa51 --- /dev/null +++ b/changelogs/unreleased/hardcode-title-system-note.yml @@ -0,0 +1,4 @@ +--- +title: Ensure autogenerated title does not cause failing spec +merge_request: 8963 +author: brian m. carlson diff --git a/changelogs/unreleased/improve-ci-example-php-doc.yml b/changelogs/unreleased/improve-ci-example-php-doc.yml new file mode 100644 index 00000000000..39a85e3d261 --- /dev/null +++ b/changelogs/unreleased/improve-ci-example-php-doc.yml @@ -0,0 +1,4 @@ +--- +title: Changed composer installer script in the CI PHP example doc +merge_request: 4342 +author: Jeffrey Cafferata diff --git a/changelogs/unreleased/issue-20428.yml b/changelogs/unreleased/issue-20428.yml new file mode 100644 index 00000000000..60da1c14702 --- /dev/null +++ b/changelogs/unreleased/issue-20428.yml @@ -0,0 +1,4 @@ +--- +title: Add ability to define a coverage regex in the .gitlab-ci.yml +merge_request: 7447 +author: Leandro Camargo diff --git a/changelogs/unreleased/issue-sidebar-empty-assignee.yml b/changelogs/unreleased/issue-sidebar-empty-assignee.yml new file mode 100644 index 00000000000..263af75b9e9 --- /dev/null +++ b/changelogs/unreleased/issue-sidebar-empty-assignee.yml @@ -0,0 +1,4 @@ +--- +title: Resets assignee dropdown when sidebar is open +merge_request: +author: diff --git a/changelogs/unreleased/label-promotion.yml b/changelogs/unreleased/label-promotion.yml new file mode 100644 index 00000000000..2ab997bf420 --- /dev/null +++ b/changelogs/unreleased/label-promotion.yml @@ -0,0 +1,4 @@ +--- +title: "Project labels can now be promoted to group labels" +merge_request: 7242 +author: Olaf Tomalka diff --git a/changelogs/unreleased/label-select-toggle.yml b/changelogs/unreleased/label-select-toggle.yml deleted file mode 100644 index af5b4246521..00000000000 --- a/changelogs/unreleased/label-select-toggle.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed label dropdown toggle text not correctly updating -merge_request: -author: diff --git a/changelogs/unreleased/markdown-plantuml.yml b/changelogs/unreleased/markdown-plantuml.yml new file mode 100644 index 00000000000..c855f0cbcf7 --- /dev/null +++ b/changelogs/unreleased/markdown-plantuml.yml @@ -0,0 +1,4 @@ +--- +title: PlantUML support for Markdown +merge_request: 8588 +author: Horacio Sanson diff --git a/changelogs/unreleased/refresh-authorizations-fork-join.yml b/changelogs/unreleased/refresh-authorizations-fork-join.yml deleted file mode 100644 index b1349b9447d..00000000000 --- a/changelogs/unreleased/refresh-authorizations-fork-join.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix race conditions for AuthorizedProjectsWorker -merge_request: -author: diff --git a/changelogs/unreleased/revert-filter-assigned-to-me.yml b/changelogs/unreleased/revert-filter-assigned-to-me.yml deleted file mode 100644 index 37f9d2f5fc4..00000000000 --- a/changelogs/unreleased/revert-filter-assigned-to-me.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Revert 3f17f29a -merge_request: 8785 -author: diff --git a/changelogs/unreleased/sh-add-project-id-index-project-authorizations.yml b/changelogs/unreleased/sh-add-project-id-index-project-authorizations.yml new file mode 100644 index 00000000000..e69fcd2aa63 --- /dev/null +++ b/changelogs/unreleased/sh-add-project-id-index-project-authorizations.yml @@ -0,0 +1,4 @@ +--- +title: Add project ID index to `project_authorizations` table to optimize queries +merge_request: +author: diff --git a/changelogs/unreleased/sh-fix-annotated-tags-pointing-to-blob.yml b/changelogs/unreleased/sh-fix-annotated-tags-pointing-to-blob.yml deleted file mode 100644 index ff2b38f21f2..00000000000 --- a/changelogs/unreleased/sh-fix-annotated-tags-pointing-to-blob.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Error 500 when repositories contain annotated tags pointing to blobs -merge_request: -author: diff --git a/changelogs/unreleased/snippet-spam.yml b/changelogs/unreleased/snippet-spam.yml new file mode 100644 index 00000000000..4867f088953 --- /dev/null +++ b/changelogs/unreleased/snippet-spam.yml @@ -0,0 +1,4 @@ +--- +title: Check public snippets for spam +merge_request: +author: diff --git a/changelogs/unreleased/zj-format-chat-messages.yml b/changelogs/unreleased/zj-format-chat-messages.yml new file mode 100644 index 00000000000..2494884f5c9 --- /dev/null +++ b/changelogs/unreleased/zj-format-chat-messages.yml @@ -0,0 +1,4 @@ +--- +title: Reformat messages ChatOps +merge_request: 8528 +author: diff --git a/changelogs/unreleased/zj-slow-service-fetch.yml b/changelogs/unreleased/zj-slow-service-fetch.yml new file mode 100644 index 00000000000..8037361d2fc --- /dev/null +++ b/changelogs/unreleased/zj-slow-service-fetch.yml @@ -0,0 +1,4 @@ +--- +title: Improve performance of slash commands +merge_request: 8876 +author: diff --git a/config/initializers/plantuml_lexer.rb b/config/initializers/plantuml_lexer.rb new file mode 100644 index 00000000000..e8a77b146fa --- /dev/null +++ b/config/initializers/plantuml_lexer.rb @@ -0,0 +1,2 @@ +# Touch the lexers so it is registered with Rouge +Rouge::Lexers::Plantuml diff --git a/config/initializers/request_profiler.rb b/config/initializers/request_profiler.rb index a9aa802681a..fb5a7b8372e 100644 --- a/config/initializers/request_profiler.rb +++ b/config/initializers/request_profiler.rb @@ -1,5 +1,3 @@ -require 'gitlab/request_profiler/middleware' - Rails.application.configure do |config| config.middleware.use(Gitlab::RequestProfiler::Middleware) end diff --git a/config/routes/project.rb b/config/routes/project.rb index 6620b765e02..efe2fbc521d 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -64,6 +64,7 @@ constraints(ProjectUrlConstrainer.new) do resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do member do get 'raw' + post :mark_as_spam end end @@ -220,6 +221,7 @@ constraints(ProjectUrlConstrainer.new) do end member do + post :promote post :toggle_subscription delete :remove_priority end diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb index 3ca096f31ba..ce0d1314292 100644 --- a/config/routes/snippets.rb +++ b/config/routes/snippets.rb @@ -2,6 +2,7 @@ resources :snippets, concerns: :awardable do member do get 'raw' get 'download' + post :mark_as_spam end end diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb index c04afe97277..c304e0706dc 100644 --- a/db/fixtures/development/10_merge_requests.rb +++ b/db/fixtures/development/10_merge_requests.rb @@ -26,7 +26,7 @@ Gitlab::Seeder.quiet do end end - project = Project.find_with_namespace('gitlab-org/gitlab-test') + project = Project.find_by_full_path('gitlab-org/gitlab-test') params = { source_branch: 'feature', diff --git a/db/migrate/20161114024742_add_coverage_regex_to_builds.rb b/db/migrate/20161114024742_add_coverage_regex_to_builds.rb new file mode 100644 index 00000000000..88aa5d52b39 --- /dev/null +++ b/db/migrate/20161114024742_add_coverage_regex_to_builds.rb @@ -0,0 +1,13 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddCoverageRegexToBuilds < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def change + add_column :ci_builds, :coverage_regex, :string + end +end diff --git a/db/migrate/20161207231626_add_environment_slug.rb b/db/migrate/20161207231626_add_environment_slug.rb index 7153e6a32b1..8e98ee5b9ba 100644 --- a/db/migrate/20161207231626_add_environment_slug.rb +++ b/db/migrate/20161207231626_add_environment_slug.rb @@ -8,8 +8,9 @@ class AddEnvironmentSlug < ActiveRecord::Migration DOWNTIME_REASON = 'Adding NOT NULL column environments.slug with dependent data' # Used to generate random suffixes for the slug + LETTERS = 'a'..'z' NUMBERS = '0'..'9' - SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a + SUFFIX_CHARS = LETTERS.to_a + NUMBERS.to_a def up environments = Arel::Table.new(:environments) @@ -39,17 +40,24 @@ class AddEnvironmentSlug < ActiveRecord::Migration slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-') # Must start with a letter - slugified = "env-" + slugified if NUMBERS.cover?(slugified[0]) + slugified = 'env-' + slugified unless LETTERS.cover?(slugified[0]) + + # Repeated dashes are invalid (OpenShift limitation) + slugified.gsub!(/\-+/, '-') # Maximum length: 24 characters (OpenShift limitation) slugified = slugified[0..23] - # Cannot end with a "-" character (Kubernetes label limitation) - slugified = slugified[0..-2] if slugified[-1] == "-" + # Cannot end with a dash (Kubernetes label limitation) + slugified.chop! if slugified.end_with?('-') # Add a random suffix, shortening the current string if necessary, if it # has been slugified. This ensures uniqueness. - slugified = slugified[0..16] + "-" + random_suffix if slugified != name + if slugified != name + slugified = slugified[0..16] + slugified << '-' unless slugified.end_with?('-') + slugified << random_suffix + end slugified end diff --git a/db/migrate/20170130204620_add_index_to_project_authorizations.rb b/db/migrate/20170130204620_add_index_to_project_authorizations.rb new file mode 100644 index 00000000000..e9a0aee4d6a --- /dev/null +++ b/db/migrate/20170130204620_add_index_to_project_authorizations.rb @@ -0,0 +1,11 @@ +class AddIndexToProjectAuthorizations < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index(:project_authorizations, :project_id) + end +end diff --git a/db/schema.rb b/db/schema.rb index 3c836db27fc..c73c311ccb2 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: 20170121130655) do +ActiveRecord::Schema.define(version: 20170130204620) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -215,6 +215,7 @@ ActiveRecord::Schema.define(version: 20170121130655) do t.datetime "queued_at" t.string "token" t.integer "lock_version" + t.string "coverage_regex" end add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree @@ -874,6 +875,7 @@ ActiveRecord::Schema.define(version: 20170121130655) do t.integer "access_level" end + add_index "project_authorizations", ["project_id"], name: "index_project_authorizations_on_project_id", using: :btree add_index "project_authorizations", ["user_id", "project_id", "access_level"], name: "index_project_authorizations_on_user_id_project_id_access_level", unique: true, using: :btree create_table "project_features", force: :cascade do |t| diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index d7cfb464f74..a6300e18dc0 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -379,6 +379,10 @@ Read more about the individual driver's config options in the filesystem. Remember to enable backups with your object storage provider if desired. +> **Important** Enabling storage driver other than `filesystem` would mean +that your Docker client needs to be able to access the storage backend directly. +So you must use an address that resolves and is accessible outside GitLab server. + --- **Omnibus GitLab installations** diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md index e5cf592e0a6..6515b1a264a 100644 --- a/doc/administration/integration/plantuml.md +++ b/doc/administration/integration/plantuml.md @@ -3,8 +3,8 @@ > [Introduced][ce-7810] in GitLab 8.16. When [PlantUML](http://plantuml.com) integration is enabled and configured in -GitLab we are able to create simple diagrams in AsciiDoc documents created in -snippets, wikis, and repos. +GitLab we are able to create simple diagrams in AsciiDoc and Markdown documents +created in snippets, wikis, and repos. ## PlantUML Server @@ -54,7 +54,7 @@ that, login with an Admin account and do following: ## Creating Diagrams With PlantUML integration enabled and configured, we can start adding diagrams to -our AsciiDoc snippets, wikis and repos using blocks: +our AsciiDoc snippets, wikis and repos using delimited blocks: ``` [plantuml, format="png", id="myDiagram", width="200px"] @@ -64,7 +64,14 @@ Alice -> Bob : Go Away -- ``` -The above block will be converted to an HTML img tag with source pointing to the +And in Markdown using fenced code blocks: + + ```plantuml + Bob -> Alice : hello + Alice -> Bob : Go Away + ``` + +The above blocks will be converted to an HTML img tag with source pointing to the PlantUML instance. If the PlantUML server is correctly configured, this should render a nice diagram instead of the block: @@ -77,7 +84,7 @@ Inside the block you can add any of the supported diagrams by PlantUML such as and [Object](http://plantuml.com/object-diagram) diagrams. You do not need to use the PlantUML diagram delimiters `@startuml`/`@enduml` as these are replaced by the AsciiDoc `plantuml` block. -Some parameters can be added to the block definition: +Some parameters can be added to the AsciiDoc block definition: - *format*: Can be either `png` or `svg`. Note that `svg` is not supported by all browsers so use with care. The default is `png`. @@ -85,3 +92,4 @@ Some parameters can be added to the block definition: - *width*: Width attribute added to the img tag. - *height*: Height attribute added to the img tag. +Markdown does not support any parameters and will always use PNG format. diff --git a/doc/api/groups.md b/doc/api/groups.md index f7807390e68..3b38e3e1bee 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -25,7 +25,14 @@ GET /groups "id": 1, "name": "Foobar Group", "path": "foo-bar", - "description": "An interesting group" + "description": "An interesting group", + "visibility_level": 20, + "lfs_enabled": true, + "avatar_url": "http://localhost:3000/uploads/group/avatar/1/foo.jpg", + "web_url": "http://localhost:3000/groups/foo-bar", + "request_access_enabled": false, + "full_name": "Foobar Group", + "full_path": "foo-bar" } ] ``` @@ -149,6 +156,8 @@ Example response: "avatar_url": null, "web_url": "https://gitlab.example.com/groups/twitter", "request_access_enabled": false, + "full_name": "Foobar Group", + "full_path": "foo-bar", "projects": [ { "id": 7, @@ -372,6 +381,8 @@ Example response: "avatar_url": null, "web_url": "http://gitlab.example.com/groups/h5bp", "request_access_enabled": false, + "full_name": "Foobar Group", + "full_path": "foo-bar", "projects": [ { "id": 9, diff --git a/doc/api/users.md b/doc/api/users.md index 28b6c7bd491..fea9bdf9639 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -271,6 +271,7 @@ Parameters: - `can_create_group` (optional) - User can create groups - true or false - `external` (optional) - Flags the user as external - true or false(default) +On password update, user will be forced to change it upon next login. Note, at the moment this method does only return a `404` error, even in cases where a `409` (Conflict) would be more appropriate, e.g. when renaming the email address to some existing one. diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md index 630207ffa09..c4c4d95b68a 100644 --- a/doc/ci/autodeploy/index.md +++ b/doc/ci/autodeploy/index.md @@ -1,6 +1,6 @@ # Auto deploy -> [Introduced][mr-8135] in GitLab 8.15. +> [Introduced][mr-8135] in GitLab 8.15. Currently requires a [Public project][project-settings]. Auto deploy is an easy way to configure GitLab CI for the deployment of your application. GitLab Community maintains a list of `.gitlab-ci.yml` @@ -33,6 +33,7 @@ enable [Kubernetes service][kubernetes-service]. created automatically for you. [mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135 +[project-settings]: https://docs.gitlab.com/ce/public_access/public_access.html [project-services]: ../../project_services/project_services.md [auto-deploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy [kubernetes-service]: ../../project_services/kubernetes.md diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 98cd29c9567..ef04c537367 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -297,7 +297,7 @@ deploy_review: - echo "Deploy a review app" environment: name: review/$CI_BUILD_REF_NAME - url: https://$CI_BUILD_REF_SLUG.review.example.com + url: https://$CI_ENVIRONMENT_SLUG.example.com only: - branches except: @@ -318,15 +318,15 @@ also contain `/`, or other characters that would be invalid in a domain name or URL, we use `$CI_ENVIRONMENT_SLUG` in the `environment:url` so that the environment can get a specific and distinct URL for each branch. In this case, given a `$CI_BUILD_REF_NAME` of `100-Do-The-Thing`, the URL will be something -like `https://review-100-do-the-4f99a2.example.com`. Again, the way you set up +like `https://100-do-the-4f99a2.example.com`. Again, the way you set up the web server to serve these requests is based on your setup. You could also use `$CI_BUILD_REF_SLUG` in `environment:url`, e.g.: -`https://$CI_BUILD_REF_SLUG.review.example.com`. We use `$CI_ENVIRONMENT_SLUG` +`https://$CI_BUILD_REF_SLUG.example.com`. We use `$CI_ENVIRONMENT_SLUG` here because it is guaranteed to be unique, but if you're using a workflow like [GitLab Flow][gitlab-flow], collisions are very unlikely, and you may prefer environment names to be more closely based on the branch name - the example -above would give you an URL like `https://100-do-the-thing.review.example.com` +above would give you an URL like `https://100-do-the-thing.example.com` Last but not least, we tell the job to run [`only`][only] on branches [`except`][only] master. diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md index 82ffb841729..5eeec92d976 100644 --- a/doc/ci/examples/php.md +++ b/doc/ci/examples/php.md @@ -235,7 +235,11 @@ cache: before_script: # Install composer dependencies -- curl --silent --show-error https://getcomposer.org/installer | php +- wget https://composer.github.io/installer.sig -O - -q | tr -d '\n' > installer.sig +- php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" +- php -r "if (hash_file('SHA384', 'composer-setup.php') === file_get_contents('installer.sig')) { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" +- php composer-setup.php +- php -r "unlink('composer-setup.php'); unlink('installer.sig');" - php composer.phar install ... diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index f11257be5c3..06810898cfe 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -76,6 +76,7 @@ There are a few reserved `keywords` that **cannot** be used as job names: | after_script | no | Define commands that run after each job's script | | variables | no | Define build variables | | cache | no | Define list of files that should be cached between subsequent runs | +| coverage | no | Define coverage settings for all jobs | ### image and services @@ -278,6 +279,23 @@ cache: untracked: true ``` +### coverage + +`coverage` allows you to configure how coverage will be filtered out from the +build outputs. Setting this up globally will make all the jobs to use this +setting for output filtering and extracting the coverage information from your +builds. + +Regular expressions are the only valid kind of value expected here. So, using +surrounding `/` is mandatory in order to consistently and explicitly represent +a regular expression string. You must escape special characters if you want to +match them literally. + +A simple example: +```yaml +coverage: /\(\d+\.\d+\) covered\./ +``` + ## Jobs `.gitlab-ci.yml` allows you to specify an unlimited number of jobs. Each job @@ -319,6 +337,7 @@ job_name: | before_script | no | Override a set of commands that are executed before build | | after_script | no | Override a set of commands that are executed after build | | environment | no | Defines a name of environment to which deployment is done by this build | +| coverage | no | Define coverage settings for a given job | ### script @@ -993,6 +1012,25 @@ job: - execute this after my script ``` +### job coverage + +This entry is pretty much the same as described in the global context in +[`coverage`](#coverage). The only difference is that, by setting it inside +the job level, whatever is set in there will take precedence over what has +been defined in the global level. A quick example of one overriding the +other would be: + +```yaml +coverage: /\(\d+\.\d+\) covered\./ + +job1: + coverage: /Code coverage: \d+\.\d+/ +``` + +In the example above, considering the context of the job `job1`, the coverage +regex that would be used is `/Code coverage: \d+\.\d+/` instead of +`/\(\d+\.\d+\) covered\./`. + ## Git Strategy > Introduced in GitLab 8.9 as an experimental feature. May change or be removed diff --git a/doc/development/frontend.md b/doc/development/frontend.md index f79bd23dc90..75fdf3d8e63 100644 --- a/doc/development/frontend.md +++ b/doc/development/frontend.md @@ -23,6 +23,69 @@ some ideas with React.js as well as Angular. To get started with Vue, read through [their documentation][vue-docs]. +#### How to build a new feature with Vue.js +**Components, Stores and Services** + +In some features implemented with Vue.js, like the [issue board][issue-boards] +or [environments table][environments-table] +you can find a clear separation of concerns: + +``` +new_feature +├── components +│ └── component.js.es6 +│ └── ... +├── store +│ └── new_feature_store.js.es6 +├── service +│ └── new_feature_service.js.es6 +├── new_feature_bundle.js.es6 +``` +_For consistency purposes, we recommend you to follow the same structure._ + +Let's look into each of them: + +**A `*_bundle.js` file** + +This is the index file of your new feature. This is where the root Vue instance +of the new feature should be. + +Don't forget to follow [these steps.][page-specific-javascript] + +**A folder for Components** + +This folder holds all components that are specific of this new feature. +If you need to use or create a component that will probably be used somewhere +else, please refer to `vue_shared/components`. + +A good thumb rule to know when you should create a component is to think if +it will be reusable elsewhere. + +For example, tables are used in a quite amount of places across GitLab, a table +would be a good fit for a component. +On the other hand, a table cell used only in on table, would not be a good use +of this pattern. + +You can read more about components in Vue.js site, [Component System][component-system] + +**A folder for the Store** + +The Store is a simple object that allows us to manage the state in a single +source of truth. + +The concept we are trying to follow is better explained by Vue documentation +itself, please read this guide: [State Management][state-management] + +**A folder for the Service** + +The Service is used only to communicate with the server. +It does not store or manipulate any data. +We use [vue-resource][vue-resource-repo] to +communicate with the server. + +The [issue boards service][issue-boards-service] +is a good example of this pattern. + ## Performance ### Resources @@ -198,8 +261,8 @@ As long as the fixtures don't change, `rake teaspoon:tests` is sufficient If you need to debug your tests and/or application code while they're running, navigate to [localhost:3000/teaspoon](http://localhost:3000/teaspoon) -in your browser, open DevTools, and run tests for individual files by clicking -on them. This is also much faster than setting up and running tests from the +in your browser, open DevTools, and run tests for individual files by clicking +on them. This is also much faster than setting up and running tests from the command line. Please note: Not all of the frontend fixtures are generated. Some are still static @@ -294,20 +357,27 @@ For our currently-supported browsers, see our [requirements][requirements]. [xss]: https://en.wikipedia.org/wiki/Cross-site_scripting [scss-style-guide]: scss_styleguide.md [requirements]: ../install/requirements.md#supported-web-browsers +[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards +[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments +[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript +[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components +[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch +[vue-resource-repo]: https://github.com/pagekit/vue-resource +[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6 ## Gotchas ### Spec errors due to use of ES6 features in `.js` files -If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being -thrown in Teaspoon, Spinach, or Rspec tests but can't reproduce them manually, -you may have included `ES6`-style JavaScript in files that don't have the -`.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file -you're working in (`git mv <file.js> <file.js.es6>`). +If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being +thrown in Teaspoon, Spinach, or Rspec tests but can't reproduce them manually, +you may have included `ES6`-style JavaScript in files that don't have the +`.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file +you're working in (`git mv <file.js> <file.js.es6>`). ### Spec errors due to use of unsupported JavaScript -Similar errors will be thrown if you're using JavaScript features not yet +Similar errors will be thrown if you're using JavaScript features not yet supported by our test runner's version of webkit, whether or not you've updated the file extension. Examples of unsupported JavaScript features are: @@ -322,20 +392,20 @@ the file extension. Examples of unsupported JavaScript features are: - Symbol/Symbol.iterator - Spread -Until these are polyfilled or transpiled appropriately, they should not be used. -Please update this list with additional unsupported features or when any of +Until these are polyfilled or transpiled appropriately, they should not be used. +Please update this list with additional unsupported features or when any of these are made usable. ### Spec errors due to JavaScript not enabled -If, as a result of a change you've made, a feature now depends on JavaScript to +If, as a result of a change you've made, a feature now depends on JavaScript to run correctly, you need to make sure a JavaScript web driver is enabled when -specs are run. If you don't you'll see vague error messages from the spec -runner, and an explosion of vague console errors in the HTML snapshot. +specs are run. If you don't you'll see vague error messages from the spec +runner, and an explosion of vague console errors in the HTML snapshot. -To enable a JavaScript driver in an `rspec` test, add `js: true` to the -individual spec or the context block containing multiple specs that need -JavaScript enabled: +To enable a JavaScript driver in an `rspec` test, add `js: true` to the +individual spec or the context block containing multiple specs that need +JavaScript enabled: ```ruby @@ -354,8 +424,8 @@ describe "Admin::AbuseReports", js: true do end ``` -In Spinach, the JavaScript driver is enabled differently. In the `*.feature` -file for the failing spec, add the `@javascript` flag above the Scenario: +In Spinach, the JavaScript driver is enabled differently. In the `*.feature` +file for the failing spec, add the `@javascript` flag above the Scenario: ``` @javascript diff --git a/doc/development/ux_guide/animation.md b/doc/development/ux_guide/animation.md index 903e54bf9dc..5dae4bcc905 100644 --- a/doc/development/ux_guide/animation.md +++ b/doc/development/ux_guide/animation.md @@ -19,7 +19,7 @@ Easing specifies the rate of change of a parameter over time (see [easings.net]( ### Hover -Interactive elements (links, buttons, etc.) should have a hover state. A subtle animation for this transition adds a polished feel. We should target a `200ms linear` transition for a color hover effect. +Interactive elements (links, buttons, etc.) should have a hover state. A subtle animation for this transition adds a polished feel. We should target a `100ms - 150ms linear` transition for a color hover effect. View the [interactive example](http://codepen.io/awhildy/full/GNyEvM/) here. diff --git a/doc/install/README.md b/doc/install/README.md index 239f5f301ec..2d2fd8cb380 100644 --- a/doc/install/README.md +++ b/doc/install/README.md @@ -4,3 +4,6 @@ - [Requirements](requirements.md) - [Structure](structure.md) - [Database MySQL](database_mysql.md) +- [Digital Ocean and Docker](digitaloceandocker.md) +- [Docker](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/docker) +- [All installation methods](https://about.gitlab.com/installation/) diff --git a/doc/install/digitaloceandocker.md b/doc/install/digitaloceandocker.md new file mode 100644 index 00000000000..820060a489b --- /dev/null +++ b/doc/install/digitaloceandocker.md @@ -0,0 +1,136 @@ +# Digital Ocean and Docker + +## Initial setup + +In this guide you'll configure a Digital Ocean droplet and set up Docker +locally on either macOS or Linux. + +### On macOS + +#### Install Docker Toolbox + +1. [https://www.docker.com/products/docker-toolbox](https://www.docker.com/products/docker-toolbox) + +### On Linux + +#### Install Docker Engine + +1. [https://docs.docker.com/engine/installation/linux](https://docs.docker.com/engine/installation/linux/) + +#### Install Docker Machine + +1. [https://docs.docker.com/machine/install-machine](https://docs.docker.com/machine/install-machine/) + +_The rest of the steps are identical for macOS and Linux_ + +### Create new docker host + +1. Login to Digital Ocean +1. Generate a new API token at https://cloud.digitalocean.com/settings/api/tokens + + +This command will create a new DO droplet called `gitlab-test-env-do` that will act as a docker host. + +**Note: 4GB is the minimum requirement for a Docker host that will run more then one GitLab instance** + ++ RAM: 4GB ++ Name: `gitlab-test-env-do` ++ Driver: `digitalocean` + + +**Set the DO token** - Replace the string below with your generated token + +``` +export DOTOKEN=cf3dfd0662933203005c4a73396214b7879d70aabc6352573fe178d340a80248 +``` + +**Create the machine** + +``` +docker-machine create \ + --driver digitalocean \ + --digitalocean-access-token=$DOTOKEN \ + --digitalocean-size "4gb" \ + gitlab-test-env-do +``` + ++ Resource: https://docs.docker.com/machine/drivers/digital-ocean/ + + +### Creating GitLab test instance + + +#### Connect your shell to the new machine + + +In this example we'll create a GitLab EE 8.10.8 instance. + + +First connect the docker client to the docker host you created previously. + +``` +eval "$(docker-machine env gitlab-test-env-do)" +``` + +You can add this to your `~/.bash_profile` file to ensure the `docker` client uses the `gitlab-test-env-do` docker host + + +#### Create new GitLab container + ++ HTTP port: `8888` ++ SSH port: `2222` + + Set `gitlab_shell_ssh_port` using `--env GITLAB_OMNIBUS_CONFIG ` ++ Hostname: IP of docker host ++ Container name: `gitlab-test-8.10` ++ GitLab version: **EE** `8.10.8-ee.0` + +##### Setup container settings + +``` +export SSH_PORT=2222 +export HTTP_PORT=8888 +export VERSION=8.10.8-ee.0 +export NAME=gitlab-test-8.10 +``` + +##### Create container +``` +docker run --detach \ +--env GITLAB_OMNIBUS_CONFIG="external_url 'http://$(docker-machine ip gitlab-test-env-do):$HTTP_PORT'; gitlab_rails['gitlab_shell_ssh_port'] = $SSH_PORT;" \ +--hostname $(docker-machine ip gitlab-test-env-do) \ +-p $HTTP_PORT:$HTTP_PORT -p $SSH_PORT:22 \ +--name $NAME \ +gitlab/gitlab-ee:$VERSION +``` + +#### Connect to the GitLab container + +##### Retrieve the docker host IP + +``` +docker-machine ip gitlab-test-env-do +# example output: 192.168.151.134 +``` + + ++ Browse to: http://192.168.151.134:8888/ + + +##### Execute interactive shell/edit configuration + + +``` +docker exec -it $NAME /bin/bash +``` + +``` +# example commands +root@192:/# vi /etc/gitlab/gitlab.rb +root@192:/# gitlab-ctl reconfigure +``` + +#### Resources + ++ [https://docs.gitlab.com/omnibus/docker/](https://docs.gitlab.com/omnibus/docker/) ++ [https://docs.docker.com/machine/get-started/](https://docs.docker.com/machine/get-started/) ++ [https://docs.docker.com/machine/reference/ip/](https://docs.docker.com/machine/reference/ip/)+ diff --git a/doc/install/installation.md b/doc/install/installation.md index 425c5d93efb..b2d5d51d37d 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -124,7 +124,7 @@ Download Ruby and compile it: mkdir /tmp/ruby && cd /tmp/ruby curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz - echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz + echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz cd ruby-2.3.3 ./configure --disable-install-rdoc make diff --git a/doc/project_services/slack.md b/doc/project_services/slack.md index 0b682b43810..eaceb2be137 100644 --- a/doc/project_services/slack.md +++ b/doc/project_services/slack.md @@ -15,7 +15,7 @@ Slack: After you set up Slack, it's time to set up GitLab. -Go to your project's **Settings > Services > Slack Notifications** and you will see a +Go to your project's **Settings > Integrations > Slack Notifications** and you will see a checkbox with the following events that can be triggered: - Push diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 9803937fcf9..9e391d647a8 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -4,10 +4,12 @@ Git is a distributed version control system, which means you can work locally but you can also share or "push" your changes to other servers. Before you can push your changes to a GitLab server you need a secure communication channel for sharing information. -GitLab uses Public-key or asymmetric cryptography -which encrypts a communication channel by locking it with your "private key" -and allows trusted parties to unlock it with your "public key". -If someone does not have your public key they cannot access the unencrypted message. + +The SSH protocol provides this security and allows you to authenticate to the +GitLab remote server without supplying your username or password each time. + +For a more detailed explanation of how the SSH protocol works, we advise you to +read [this nice tutorial by DigitalOcean](https://www.digitalocean.com/community/tutorials/understanding-the-ssh-encryption-and-connection-process). ## Locating an existing SSH key pair diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index dfc762fe1d3..cb1c1a84f8c 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.13.0 to current | 0.1.5 | +| 8.16.2 to current | 0.1.6 | +| 8.13.0 | 0.1.5 | | 8.12.0 | 0.1.4 | | 8.10.3 | 0.1.3 | | 8.10.0 | 0.1.2 | @@ -47,6 +48,9 @@ The following items will NOT be exported: - Build traces and artifacts - LFS objects +- Container registry images +- CI variables +- Any encrypted tokens ## Exporting a project and its data diff --git a/features/dashboard/shortcuts.feature b/features/dashboard/shortcuts.feature deleted file mode 100644 index 41d79aa6ec8..00000000000 --- a/features/dashboard/shortcuts.feature +++ /dev/null @@ -1,21 +0,0 @@ -@dashboard -Feature: Dashboard Shortcuts - Background: - Given I sign in as a user - And I visit dashboard page - - @javascript - Scenario: Navigate to projects tab - Given I press "g" and "p" - Then the active main tab should be Projects - - @javascript - Scenario: Navigate to issue tab - Given I press "g" and "i" - Then the active main tab should be Issues - - @javascript - Scenario: Navigate to merge requests tab - Given I press "g" and "m" - Then the active main tab should be Merge Requests - diff --git a/features/steps/dashboard/shortcuts.rb b/features/steps/dashboard/shortcuts.rb deleted file mode 100644 index 118d27888df..00000000000 --- a/features/steps/dashboard/shortcuts.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Spinach::Features::DashboardShortcuts < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedProject - include SharedSidebarActiveTab - include SharedShortcuts -end diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb index 344b6fda9a6..2bbc43b491f 100644 --- a/features/steps/dashboard/todos.rb +++ b/features/steps/dashboard/todos.rb @@ -25,15 +25,18 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps end step 'I should see todos assigned to me' do + merge_request_reference = merge_request.to_reference(full: true) + issue_reference = issue.to_reference(full: true) + page.within('.todos-pending-count') { expect(page).to have_content '4' } expect(page).to have_content 'To do 4' expect(page).to have_content 'Done 0' expect(page).to have_link project.name_with_namespace - should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title) - should_see_todo(2, "John Doe mentioned you on issue #{issue.to_reference}", "#{current_user.to_reference} Wdyt?") - should_see_todo(3, "John Doe assigned you issue #{issue.to_reference}", issue.title) - should_see_todo(4, "Mary Jane mentioned you on issue #{issue.to_reference}", issue.title) + should_see_todo(1, "John Doe assigned you merge request #{merge_request_reference}", merge_request.title) + should_see_todo(2, "John Doe mentioned you on issue #{issue_reference}", "#{current_user.to_reference} Wdyt?") + should_see_todo(3, "John Doe assigned you issue #{issue_reference}", issue.title) + should_see_todo(4, "Mary Jane mentioned you on issue #{issue_reference}", issue.title) end step 'I mark the todo as done' do @@ -44,10 +47,13 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps page.within('.todos-pending-count') { expect(page).to have_content '3' } expect(page).to have_content 'To do 3' expect(page).to have_content 'Done 1' - should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}" + should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference(full: true)}" end step 'I mark all todos as done' do + merge_request_reference = merge_request.to_reference(full: true) + issue_reference = issue.to_reference(full: true) + click_link 'Mark all as done' page.within('.todos-pending-count') { expect(page).to have_content '0' } @@ -55,27 +61,30 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps expect(page).to have_content 'Done 4' expect(page).to have_content "You're all done!" expect('.prepend-top-default').not_to have_link project.name_with_namespace - should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}" - should_not_see_todo "John Doe mentioned you on issue #{issue.to_reference}" - should_not_see_todo "John Doe assigned you issue #{issue.to_reference}" - should_not_see_todo "Mary Jane mentioned you on issue #{issue.to_reference}" + should_not_see_todo "John Doe assigned you merge request #{merge_request_reference}" + should_not_see_todo "John Doe mentioned you on issue #{issue_reference}" + should_not_see_todo "John Doe assigned you issue #{issue_reference}" + should_not_see_todo "Mary Jane mentioned you on issue #{issue_reference}" end step 'I should see the todo marked as done' do click_link 'Done 1' expect(page).to have_link project.name_with_namespace - should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title, false) + should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference(full: true)}", merge_request.title, false) end step 'I should see all todos marked as done' do + merge_request_reference = merge_request.to_reference(full: true) + issue_reference = issue.to_reference(full: true) + click_link 'Done 4' expect(page).to have_link project.name_with_namespace - should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title, false) - should_see_todo(2, "John Doe mentioned you on issue #{issue.to_reference}", "#{current_user.to_reference} Wdyt?", false) - should_see_todo(3, "John Doe assigned you issue #{issue.to_reference}", issue.title, false) - should_see_todo(4, "Mary Jane mentioned you on issue #{issue.to_reference}", issue.title, false) + should_see_todo(1, "John Doe assigned you merge request #{merge_request_reference}", merge_request.title, false) + should_see_todo(2, "John Doe mentioned you on issue #{issue_reference}", "#{current_user.to_reference} Wdyt?", false) + should_see_todo(3, "John Doe assigned you issue #{issue_reference}", issue.title, false) + should_see_todo(4, "Mary Jane mentioned you on issue #{issue_reference}", issue.title, false) end step 'I filter by "Enterprise"' do @@ -111,16 +120,16 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps end step 'I should not see todos related to "Mary Jane" in the list' do - should_not_see_todo "Mary Jane mentioned you on issue #{issue.to_reference}" + should_not_see_todo "Mary Jane mentioned you on issue #{issue.to_reference(full: true)}" end step 'I should not see todos related to "Merge Requests" in the list' do - should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}" + should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference(full: true)}" end step 'I should not see todos related to "Assignments" in the list' do - should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}" - should_not_see_todo "John Doe assigned you issue #{issue.to_reference}" + should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference(full: true)}" + should_not_see_todo "John Doe assigned you issue #{issue.to_reference(full: true)}" end step 'I click on the todo' do diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb index 374eb0b0e07..19ff92f6dc6 100644 --- a/features/steps/project/builds/summary.rb +++ b/features/steps/project/builds/summary.rb @@ -33,7 +33,7 @@ class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps step 'recent build summary contains information saying that build has been erased' do page.within('.erased') do - expect(page).to have_content 'Build has been erased' + expect(page).to have_content 'Job has been erased' end end diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb index 7490d2bc6e7..48ac7a98f0d 100644 --- a/features/steps/project/graph.rb +++ b/features/steps/project/graph.rb @@ -34,9 +34,9 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps step 'page should have CI graphs' do expect(page).to have_content 'Overall' - expect(page).to have_content 'Builds for last week' - expect(page).to have_content 'Builds for last month' - expect(page).to have_content 'Builds for last year' + expect(page).to have_content 'Jobs for last week' + expect(page).to have_content 'Jobs for last month' + expect(page).to have_content 'Jobs for last year' expect(page).to have_content 'Commit duration in minutes for last 30 commits' end diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb index f74a9b5df47..4a35b71af2f 100644 --- a/features/steps/project/issues/labels.rb +++ b/features/steps/project/issues/labels.rb @@ -15,17 +15,16 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps step 'I delete all labels' do page.within '.labels' do - page.all('.remove-row').each do |remove| - remove.click - sleep 0.05 + page.all('.remove-row').each do + first('.remove-row').click end end end step 'I should see labels help message' do page.within '.labels' do - expect(page).to have_content 'Create a label or generate a default set '\ - 'of labels' + expect(page).to have_content 'Generate a default set of labels' + expect(page).to have_content 'New label' end end diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb index 70e6d4836b2..d008a8a26af 100644 --- a/features/steps/shared/builds.rb +++ b/features/steps/shared/builds.rb @@ -47,7 +47,7 @@ module SharedBuilds end step 'recent build has a build trace' do - @build.trace = 'build trace' + @build.trace = 'job trace' end step 'download of build artifacts archive starts' do @@ -60,7 +60,7 @@ module SharedBuilds end step 'I see details of a build' do - expect(page).to have_content "Build ##{@build.id}" + expect(page).to have_content "Job ##{@build.id}" end step 'I see build trace' do diff --git a/lib/api/builds.rb b/lib/api/builds.rb index af61be343be..44fe0fc4a95 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -209,7 +209,7 @@ module API build = get_build!(params[:build_id]) - bad_request!("Unplayable Build") unless build.playable? + bad_request!("Unplayable Job") unless build.playable? build.play(current_user) diff --git a/lib/api/commits.rb b/lib/api/commits.rb index e6d707f3c3d..2fefe760d24 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -54,7 +54,7 @@ module API authorize! :push_code, user_project attrs = declared_params - attrs[:source_branch] = attrs[:branch_name] + attrs[:start_branch] = attrs[:branch_name] attrs[:target_branch] = attrs[:branch_name] attrs[:actions].map! do |action| action[:action] = action[:action].to_sym @@ -139,8 +139,6 @@ module API commit_params = { commit: commit, create_merge_request: false, - source_project: user_project, - source_branch: commit.cherry_pick_branch_name, target_branch: params[:branch] } diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 9f59939e9ae..a07b2a9ca0f 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -137,6 +137,7 @@ module API expose :avatar_url expose :web_url expose :request_access_enabled + expose :full_name, :full_path expose :statistics, if: :statistics do with_options format_with: -> (value) { value.to_i } do diff --git a/lib/api/files.rb b/lib/api/files.rb index 2e79e22e649..c58472de578 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -5,7 +5,7 @@ module API def commit_params(attrs) { file_path: attrs[:file_path], - source_branch: attrs[:branch_name], + start_branch: attrs[:branch_name], target_branch: attrs[:branch_name], commit_message: attrs[:commit_message], file_content: attrs[:content], diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index a1d7b323f4f..eb5b947172a 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -45,7 +45,7 @@ module API if id =~ /^\d+$/ Project.find_by(id: id) else - Project.find_with_namespace(id) + Project.find_by_full_path(id) end end diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index e8975eb57e0..080a6274957 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -30,7 +30,7 @@ module API def wiki? @wiki ||= project_path.end_with?('.wiki') && - !Project.find_with_namespace(project_path) + !Project.find_by_full_path(project_path) end def project @@ -41,7 +41,7 @@ module API # the wiki repository as well. project_path.chomp!('.wiki') if wiki? - Project.find_with_namespace(project_path) + Project.find_by_full_path(project_path) end end diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index 9d8c5b63685..dcc0c82ee27 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -58,7 +58,7 @@ module API end post ":id/snippets" do authorize! :create_project_snippet, user_project - snippet_params = declared_params + snippet_params = declared_params.merge(request: request, api: true) snippet_params[:content] = snippet_params.delete(:code) snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute diff --git a/lib/api/services.rb b/lib/api/services.rb index a0abec49438..1456fe4688b 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -661,6 +661,14 @@ module API end trigger_services.each do |service_slug, settings| + helpers do + def chat_command_service(project, service_slug, params) + project.services.active.where(template: false).find do |service| + service.try(:token) == params[:token] && service.to_param == service_slug.underscore + end + end + end + params do requires :id, type: String, desc: 'The ID of a project' end @@ -679,9 +687,8 @@ module API # This is not accurate, but done to prevent leakage of the project names not_found!('Service') unless project - service = project.find_or_initialize_service(service_slug.underscore) - - result = service.try(:active?) && service.try(:trigger, params) + service = chat_command_service(project, service_slug, params) + result = service.try(:trigger, params) if result status result[:status] || 200 diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index e096e636806..eb9ece49e7f 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -64,7 +64,7 @@ module API desc: 'The visibility level of the snippet' end post do - attrs = declared_params(include_missing: false) + attrs = declared_params(include_missing: false).merge(request: request, api: true) snippet = CreateSnippetService.new(nil, current_user, attrs).execute if snippet.persisted? diff --git a/lib/api/users.rb b/lib/api/users.rb index 11a7368b4c0..0ed468626b7 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -160,6 +160,8 @@ module API end end + user_params.merge!(password_expires_at: Time.now) if user_params[:password].present? + if user.update_attributes(user_params.except(:extern_uid, :provider)) present user, with: Entities::UserPublic else diff --git a/lib/banzai/cross_project_reference.rb b/lib/banzai/cross_project_reference.rb index 0257848b6bc..e2b57adf611 100644 --- a/lib/banzai/cross_project_reference.rb +++ b/lib/banzai/cross_project_reference.rb @@ -14,7 +14,7 @@ module Banzai def project_from_ref(ref) return context[:project] unless ref - Project.find_with_namespace(ref) + Project.find_by_full_path(ref) end end end diff --git a/lib/banzai/filter/plantuml_filter.rb b/lib/banzai/filter/plantuml_filter.rb new file mode 100644 index 00000000000..e194cf59275 --- /dev/null +++ b/lib/banzai/filter/plantuml_filter.rb @@ -0,0 +1,39 @@ +require "nokogiri" +require "asciidoctor-plantuml/plantuml" + +module Banzai + module Filter + # HTML that replaces all `code plantuml` tags with PlantUML img tags. + # + class PlantumlFilter < HTML::Pipeline::Filter + def call + return doc unless doc.at('pre.plantuml') and settings.plantuml_enabled + + plantuml_setup + + doc.css('pre.plantuml').each do |el| + img_tag = Nokogiri::HTML::DocumentFragment.parse( + Asciidoctor::PlantUml::Processor.plantuml_content(el.content, {})) + el.replace img_tag + end + + doc + end + + private + + def settings + ApplicationSetting.current || ApplicationSetting.create_from_defaults + end + + def plantuml_setup + Asciidoctor::PlantUml.configure do |conf| + conf.url = settings.plantuml_url + conf.png_enable = settings.plantuml_enabled + conf.svg_enable = false + conf.txt_enable = false + end + end + end + end +end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index ac95a79009b..b25d6f18d59 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -10,6 +10,7 @@ module Banzai def self.filters @filters ||= FilterArray[ Filter::SyntaxHighlightFilter, + Filter::PlantumlFilter, Filter::SanitizationFilter, Filter::MathFilter, diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 7463bd719d5..649ee4d018b 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -61,6 +61,7 @@ module Ci allow_failure: job[:allow_failure] || false, when: job[:when] || 'on_success', environment: job[:environment_name], + coverage_regex: job[:coverage], yaml_variables: yaml_variables(name), options: { image: job[:image], diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb index 730b05bed97..a10b4657d7d 100644 --- a/lib/constraints/project_url_constrainer.rb +++ b/lib/constraints/project_url_constrainer.rb @@ -8,6 +8,6 @@ class ProjectUrlConstrainer return false end - Project.find_with_namespace(full_path).present? + Project.find_by_full_path(full_path).present? end end diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 8dda65c71ef..f638905a1e0 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -10,13 +10,16 @@ module Gitlab def find_for_git_client(login, password, project:, ip:) raise "Must provide an IP for rate limiting" if ip.nil? + # `user_with_password_for_git` should be the last check + # because it's the most expensive, especially when LDAP + # is enabled. result = service_request_check(login, password, project) || build_access_token_check(login, password) || - user_with_password_for_git(login, password) || - oauth_access_token_check(login, password) || lfs_token_check(login, password) || + oauth_access_token_check(login, password) || personal_access_token_check(login, password) || + user_with_password_for_git(login, password) || Gitlab::Auth::Result.new rate_limit!(ip, success: result.success?, login: login) @@ -143,7 +146,9 @@ module Gitlab read_authentication_abilities end - Result.new(actor, nil, token_handler.type, authentication_abilities) if Devise.secure_compare(token_handler.token, password) + if Devise.secure_compare(token_handler.token, password) + Gitlab::Auth::Result.new(actor, nil, token_handler.type, authentication_abilities) + end end def build_access_token_check(login, password) diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb index 4fe53ce93a9..25da8474e95 100644 --- a/lib/gitlab/chat_commands/base_command.rb +++ b/lib/gitlab/chat_commands/base_command.rb @@ -42,10 +42,6 @@ module Gitlab def find_by_iid(iid) collection.find_by(iid: iid) end - - def presenter - Gitlab::ChatCommands::Presenter.new - end end end end diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index 145086755e4..f34ed0f4cf2 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -3,7 +3,7 @@ module Gitlab class Command < BaseCommand COMMANDS = [ Gitlab::ChatCommands::IssueShow, - Gitlab::ChatCommands::IssueCreate, + Gitlab::ChatCommands::IssueNew, Gitlab::ChatCommands::IssueSearch, Gitlab::ChatCommands::Deploy, ].freeze @@ -13,51 +13,32 @@ module Gitlab if command if command.allowed?(project, current_user) - present command.new(project, current_user, params).execute(match) + command.new(project, current_user, params).execute(match) else - access_denied + Gitlab::ChatCommands::Presenters::Access.new.access_denied end else - help(help_messages) + Gitlab::ChatCommands::Help.new(project, current_user, params).execute(available_commands, params[:text]) end end def match_command match = nil - service = available_commands.find do |klass| - match = klass.match(command) - end + service = + available_commands.find do |klass| + match = klass.match(params[:text]) + end [service, match] end private - def help_messages - available_commands.map(&:help_message) - end - def available_commands COMMANDS.select do |klass| klass.available?(project) end end - - def command - params[:text] - end - - def help(messages) - presenter.help(messages, params[:command]) - end - - def access_denied - presenter.access_denied - end - - def present(resource) - presenter.present(resource) - end end end end diff --git a/lib/gitlab/chat_commands/deploy.rb b/lib/gitlab/chat_commands/deploy.rb index 7127d2f6d04..458d90f84e8 100644 --- a/lib/gitlab/chat_commands/deploy.rb +++ b/lib/gitlab/chat_commands/deploy.rb @@ -1,8 +1,6 @@ module Gitlab module ChatCommands class Deploy < BaseCommand - include Gitlab::Routing.url_helpers - def self.match(text) /\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text) end @@ -24,35 +22,29 @@ module Gitlab to = match[:to] actions = find_actions(from, to) - return unless actions.present? - if actions.one? - play!(from, to, actions.first) + if actions.none? + Gitlab::ChatCommands::Presenters::Deploy.new(nil).no_actions + elsif actions.one? + action = play!(from, to, actions.first) + Gitlab::ChatCommands::Presenters::Deploy.new(action).present(from, to) else - Result.new(:error, 'Too many actions defined') + Gitlab::ChatCommands::Presenters::Deploy.new(actions).too_many_actions end end private def play!(from, to, action) - new_action = action.play(current_user) - - Result.new(:success, "Deployment from #{from} to #{to} started. Follow the progress: #{url(new_action)}.") + action.play(current_user) end def find_actions(from, to) environment = project.environments.find_by(name: from) - return unless environment + return [] unless environment environment.actions_for(to).select(&:starts_environment?) end - - def url(subject) - polymorphic_url( - [subject.project.namespace.becomes(Namespace), subject.project, subject] - ) - end end end end diff --git a/lib/gitlab/chat_commands/help.rb b/lib/gitlab/chat_commands/help.rb new file mode 100644 index 00000000000..6c0e4d304a4 --- /dev/null +++ b/lib/gitlab/chat_commands/help.rb @@ -0,0 +1,28 @@ +module Gitlab + module ChatCommands + class Help < BaseCommand + # This class has to be used last, as it always matches. It has to match + # because other commands were not triggered and we want to show the help + # command + def self.match(_text) + true + end + + def self.help_message + 'help' + end + + def self.allowed?(_project, _user) + true + end + + def execute(commands, text) + Gitlab::ChatCommands::Presenters::Help.new(commands).present(trigger, text) + end + + def trigger + params[:command] + end + end + end +end diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_new.rb index cefb6775db8..016054ecd46 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_new.rb @@ -1,8 +1,8 @@ module Gitlab module ChatCommands - class IssueCreate < IssueCommand + class IssueNew < IssueCommand def self.match(text) - # we can not match \n with the dot by passing the m modifier as than + # we can not match \n with the dot by passing the m modifier as than # the title and description are not seperated /\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text) end @@ -19,8 +19,24 @@ module Gitlab title = match[:title] description = match[:description].to_s.rstrip + issue = create_issue(title: title, description: description) + + if issue.persisted? + presenter(issue).present + else + presenter(issue).display_errors + end + end + + private + + def create_issue(title:, description:) Issues::CreateService.new(project, current_user, title: title, description: description).execute end + + def presenter(issue) + Gitlab::ChatCommands::Presenters::IssueNew.new(issue) + end end end end diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/chat_commands/issue_search.rb index 51bf80c800b..3491b53093e 100644 --- a/lib/gitlab/chat_commands/issue_search.rb +++ b/lib/gitlab/chat_commands/issue_search.rb @@ -10,7 +10,13 @@ module Gitlab end def execute(match) - collection.search(match[:query]).limit(QUERY_LIMIT) + issues = collection.search(match[:query]).limit(QUERY_LIMIT) + + if issues.present? + Presenters::IssueSearch.new(issues).present + else + Presenters::Access.new(issues).not_found + end end end end diff --git a/lib/gitlab/chat_commands/issue_show.rb b/lib/gitlab/chat_commands/issue_show.rb index 2a45d49cf6b..d6013f4d10c 100644 --- a/lib/gitlab/chat_commands/issue_show.rb +++ b/lib/gitlab/chat_commands/issue_show.rb @@ -10,7 +10,13 @@ module Gitlab end def execute(match) - find_by_iid(match[:iid]) + issue = find_by_iid(match[:iid]) + + if issue + Gitlab::ChatCommands::Presenters::IssueShow.new(issue).present + else + Gitlab::ChatCommands::Presenters::Access.new.not_found + end end end end diff --git a/lib/gitlab/chat_commands/presenter.rb b/lib/gitlab/chat_commands/presenter.rb deleted file mode 100644 index 8930a21f406..00000000000 --- a/lib/gitlab/chat_commands/presenter.rb +++ /dev/null @@ -1,131 +0,0 @@ -module Gitlab - module ChatCommands - class Presenter - include Gitlab::Routing - - def authorize_chat_name(url) - message = if url - ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})." - else - ":sweat_smile: Couldn't identify you, nor can I autorize you!" - end - - ephemeral_response(message) - end - - def help(commands, trigger) - if commands.none? - ephemeral_response("No commands configured") - else - commands.map! { |command| "#{trigger} #{command}" } - message = header_with_list("Available commands", commands) - - ephemeral_response(message) - end - end - - def present(subject) - return not_found unless subject - - if subject.is_a?(Gitlab::ChatCommands::Result) - show_result(subject) - elsif subject.respond_to?(:count) - if subject.none? - not_found - elsif subject.one? - single_resource(subject.first) - else - multiple_resources(subject) - end - else - single_resource(subject) - end - end - - def access_denied - ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).") - end - - private - - def show_result(result) - case result.type - when :success - in_channel_response(result.message) - else - ephemeral_response(result.message) - end - end - - def not_found - ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:") - end - - def single_resource(resource) - return error(resource) if resource.errors.any? || !resource.persisted? - - message = "#{title(resource)}:" - message << "\n\n#{resource.description}" if resource.try(:description) - - in_channel_response(message) - end - - def multiple_resources(resources) - titles = resources.map { |resource| title(resource) } - - message = header_with_list("Multiple results were found:", titles) - - ephemeral_response(message) - end - - def error(resource) - message = header_with_list("The action was not successful, because:", resource.errors.messages) - - ephemeral_response(message) - end - - def title(resource) - reference = resource.try(:to_reference) || resource.try(:id) - title = resource.try(:title) || resource.try(:name) - - "[#{reference} #{title}](#{url(resource)})" - end - - def header_with_list(header, items) - message = [header] - - items.each do |item| - message << "- #{item}" - end - - message.join("\n") - end - - def url(resource) - url_for( - [ - resource.project.namespace.becomes(Namespace), - resource.project, - resource - ] - ) - end - - def ephemeral_response(message) - { - response_type: :ephemeral, - text: message, - status: 200 - } - end - - def in_channel_response(message) - { - response_type: :in_channel, - text: message, - status: 200 - } - end - end - end -end diff --git a/lib/gitlab/chat_commands/presenters/access.rb b/lib/gitlab/chat_commands/presenters/access.rb new file mode 100644 index 00000000000..92f4fa17f78 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/access.rb @@ -0,0 +1,40 @@ +module Gitlab + module ChatCommands + module Presenters + class Access < Presenters::Base + def access_denied + ephemeral_response(text: "Whoops! This action is not allowed. This incident will be [reported](https://xkcd.com/838/).") + end + + def not_found + ephemeral_response(text: "404 not found! GitLab couldn't find what you were looking for! :boom:") + end + + def authorize + message = + if @resource + ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{@resource})." + else + ":sweat_smile: Couldn't identify you, nor can I autorize you!" + end + + ephemeral_response(text: message) + end + + def unknown_command(commands) + ephemeral_response(text: help_message(trigger)) + end + + private + + def help_message(trigger) + header_with_list("Command not found, these are the commands you can use", full_commands(trigger)) + end + + def full_commands(trigger) + @resource.map { |command| "#{trigger} #{command.help_message}" } + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/base.rb b/lib/gitlab/chat_commands/presenters/base.rb new file mode 100644 index 00000000000..2700a5a2ad5 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/base.rb @@ -0,0 +1,77 @@ +module Gitlab + module ChatCommands + module Presenters + class Base + include Gitlab::Routing.url_helpers + + def initialize(resource = nil) + @resource = resource + end + + def display_errors + message = header_with_list("The action was not successful, because:", @resource.errors.full_messages) + + ephemeral_response(text: message) + end + + private + + def header_with_list(header, items) + message = [header] + + items.each do |item| + message << "- #{item}" + end + + message.join("\n") + end + + def ephemeral_response(message) + response = { + response_type: :ephemeral, + status: 200 + }.merge(message) + + format_response(response) + end + + def in_channel_response(message) + response = { + response_type: :in_channel, + status: 200 + }.merge(message) + + format_response(response) + end + + def format_response(response) + response[:text] = format(response[:text]) if response.has_key?(:text) + + if response.has_key?(:attachments) + response[:attachments].each do |attachment| + attachment[:pretext] = format(attachment[:pretext]) if attachment[:pretext] + attachment[:text] = format(attachment[:text]) if attachment[:text] + end + end + + response + end + + # Convert Markdown to slacks format + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def resource_url + url_for( + [ + @resource.project.namespace.becomes(Namespace), + @resource.project, + @resource + ] + ) + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/deploy.rb b/lib/gitlab/chat_commands/presenters/deploy.rb new file mode 100644 index 00000000000..863d0bf99ca --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/deploy.rb @@ -0,0 +1,21 @@ +module Gitlab + module ChatCommands + module Presenters + class Deploy < Presenters::Base + def present(from, to) + message = "Deployment started from #{from} to #{to}. [Follow its progress](#{resource_url})." + + in_channel_response(text: message) + end + + def no_actions + ephemeral_response(text: "No action found to be executed") + end + + def too_many_actions + ephemeral_response(text: "Too many actions defined") + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/chat_commands/presenters/help.rb new file mode 100644 index 00000000000..cd47b7f4c6a --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/help.rb @@ -0,0 +1,27 @@ +module Gitlab + module ChatCommands + module Presenters + class Help < Presenters::Base + def present(trigger, text) + ephemeral_response(text: help_message(trigger, text)) + end + + private + + def help_message(trigger, text) + return "No commands available :thinking_face:" unless @resource.present? + + if text.start_with?('help') + header_with_list("Available commands", full_commands(trigger)) + else + header_with_list("Unknown command, these commands are available", full_commands(trigger)) + end + end + + def full_commands(trigger) + @resource.map { |command| "#{trigger} #{command.help_message}" } + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/issuable.rb b/lib/gitlab/chat_commands/presenters/issuable.rb new file mode 100644 index 00000000000..dfb1c8f6616 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/issuable.rb @@ -0,0 +1,43 @@ +module Gitlab + module ChatCommands + module Presenters + module Issuable + def color(issuable) + issuable.open? ? '#38ae67' : '#d22852' + end + + def status_text(issuable) + issuable.open? ? 'Open' : 'Closed' + end + + def project + @resource.project + end + + def author + @resource.author + end + + def fields + [ + { + title: "Assignee", + value: @resource.assignee ? @resource.assignee.name : "_None_", + short: true + }, + { + title: "Milestone", + value: @resource.milestone ? @resource.milestone.title : "_None_", + short: true + }, + { + title: "Labels", + value: @resource.labels.any? ? @resource.label_names : "_None_", + short: true + } + ] + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/issue_new.rb b/lib/gitlab/chat_commands/presenters/issue_new.rb new file mode 100644 index 00000000000..a1a3add56c9 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/issue_new.rb @@ -0,0 +1,50 @@ +module Gitlab + module ChatCommands + module Presenters + class IssueNew < Presenters::Base + include Presenters::Issuable + + def present + in_channel_response(new_issue) + end + + private + + def new_issue + { + attachments: [ + { + title: "#{@resource.title} · #{@resource.to_reference}", + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "New issue #{@resource.to_reference}: #{@resource.title}", + pretext: pretext, + color: color(@resource), + fields: fields, + mrkdwn_in: [ + :title, + :pretext, + :text, + :fields + ] + } + ] + } + end + + def pretext + "I created an issue on #{author_profile_link}'s behalf: **#{@resource.to_reference}** in #{project_link}" + end + + def project_link + "[#{project.name_with_namespace}](#{projects_url(project)})" + end + + def author_profile_link + "[#{author.to_reference}](#{url_for(author)})" + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/issue_search.rb b/lib/gitlab/chat_commands/presenters/issue_search.rb new file mode 100644 index 00000000000..3478359b91d --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/issue_search.rb @@ -0,0 +1,47 @@ +module Gitlab + module ChatCommands + module Presenters + class IssueSearch < Presenters::Base + include Presenters::Issuable + + def present + text = if @resource.count >= 5 + "Here are the first 5 issues I found:" + elsif @resource.one? + "Here is the only issue I found:" + else + "Here are the #{@resource.count} issues I found:" + end + + ephemeral_response(text: text, attachments: attachments) + end + + private + + def attachments + @resource.map do |issue| + url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})" + + { + color: color(issue), + fallback: "#{issue.to_reference} #{issue.title}", + text: "#{url} · #{issue.title} (#{status_text(issue)})", + + mrkdwn_in: [ + :text + ] + } + end + end + + def project + @project ||= @resource.first.project + end + + def namespace + @namespace ||= project.namespace.becomes(Namespace) + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/issue_show.rb b/lib/gitlab/chat_commands/presenters/issue_show.rb new file mode 100644 index 00000000000..fe5847ccd15 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/issue_show.rb @@ -0,0 +1,61 @@ +module Gitlab + module ChatCommands + module Presenters + class IssueShow < Presenters::Base + include Presenters::Issuable + + def present + if @resource.confidential? + ephemeral_response(show_issue) + else + in_channel_response(show_issue) + end + end + + private + + def show_issue + { + attachments: [ + { + title: "#{@resource.title} · #{@resource.to_reference}", + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "Issue #{@resource.to_reference}: #{@resource.title}", + pretext: pretext, + text: text, + color: color(@resource), + fields: fields, + mrkdwn_in: [ + :pretext, + :text, + :fields + ] + } + ] + } + end + + def text + message = "**#{status_text(@resource)}**" + + if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero? + return message + end + + message << " · " + message << ":+1: #{@resource.upvotes} " unless @resource.upvotes.zero? + message << ":-1: #{@resource.downvotes} " unless @resource.downvotes.zero? + message << ":speech_balloon: #{@resource.user_notes_count}" unless @resource.user_notes_count.zero? + + message + end + + def pretext + "Issue *#{@resource.to_reference}* from #{project.name_with_namespace}" + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb new file mode 100644 index 00000000000..12a063059cb --- /dev/null +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -0,0 +1,22 @@ +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents Coverage settings. + # + class Coverage < Node + include Validatable + + validations do + validates :config, regexp: true + end + + def value + @config[1...-1] + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/global.rb b/lib/gitlab/ci/config/entry/global.rb index a4ec8f0ff2f..ede97cc0504 100644 --- a/lib/gitlab/ci/config/entry/global.rb +++ b/lib/gitlab/ci/config/entry/global.rb @@ -33,8 +33,11 @@ module Gitlab entry :cache, Entry::Cache, description: 'Configure caching between build jobs.' + entry :coverage, Entry::Coverage, + description: 'Coverage configuration for this pipeline.' + helpers :before_script, :image, :services, :after_script, - :variables, :stages, :types, :cache, :jobs + :variables, :stages, :types, :cache, :coverage, :jobs def compose!(_deps = nil) super(self) do diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index a55362f0b6b..69a5e6f433d 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -11,7 +11,7 @@ module Gitlab ALLOWED_KEYS = %i[tags script only except type image services allow_failure type stage when artifacts cache dependencies before_script - after_script variables environment] + after_script variables environment coverage] validations do validates :config, allowed_keys: ALLOWED_KEYS @@ -71,9 +71,12 @@ module Gitlab entry :environment, Entry::Environment, description: 'Environment configuration for this job.' + entry :coverage, Entry::Coverage, + description: 'Coverage configuration for this job.' + helpers :before_script, :script, :stage, :type, :after_script, :cache, :image, :services, :only, :except, :variables, - :artifacts, :commands, :environment + :artifacts, :commands, :environment, :coverage attributes :script, :tags, :allow_failure, :when, :dependencies @@ -130,6 +133,7 @@ module Gitlab variables: variables_defined? ? variables_value : nil, environment: environment_defined? ? environment_value : nil, environment_name: environment_defined? ? environment_value[:name] : nil, + coverage: coverage_defined? ? coverage_value : nil, artifacts: artifacts_value, after_script: after_script_value } end diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb index f01975aab5c..9b9a0a8125a 100644 --- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb +++ b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb @@ -28,17 +28,21 @@ module Gitlab value.is_a?(String) || value.is_a?(Symbol) end + def validate_regexp(value) + !value.nil? && Regexp.new(value.to_s) && true + rescue RegexpError, TypeError + false + end + def validate_string_or_regexp(value) return true if value.is_a?(Symbol) return false unless value.is_a?(String) if value.first == '/' && value.last == '/' - Regexp.new(value[1...-1]) + validate_regexp(value[1...-1]) else true end - rescue RegexpError - false end def validate_boolean(value) diff --git a/lib/gitlab/ci/config/entry/trigger.rb b/lib/gitlab/ci/config/entry/trigger.rb index 28b0a9ffe01..16b234e6c59 100644 --- a/lib/gitlab/ci/config/entry/trigger.rb +++ b/lib/gitlab/ci/config/entry/trigger.rb @@ -9,15 +9,7 @@ module Gitlab include Validatable validations do - include LegacyValidationHelpers - - validate :array_of_strings_or_regexps - - def array_of_strings_or_regexps - unless validate_array_of_strings_or_regexps(config) - errors.add(:config, 'should be an array of strings or regexps') - end - end + validates :config, array_of_strings_or_regexps: true end end end diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb index 8632dd0e233..bd7428b1272 100644 --- a/lib/gitlab/ci/config/entry/validators.rb +++ b/lib/gitlab/ci/config/entry/validators.rb @@ -54,6 +54,51 @@ module Gitlab end end + class RegexpValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless validate_regexp(value) + record.errors.add(attribute, 'must be a regular expression') + end + end + + private + + def look_like_regexp?(value) + value.is_a?(String) && value.start_with?('/') && + value.end_with?('/') + end + + def validate_regexp(value) + look_like_regexp?(value) && + Regexp.new(value.to_s[1...-1]) && + true + rescue RegexpError + false + end + end + + class ArrayOfStringsOrRegexpsValidator < RegexpValidator + def validate_each(record, attribute, value) + unless validate_array_of_strings_or_regexps(value) + record.errors.add(attribute, 'should be an array of strings or regexps') + end + end + + private + + def validate_array_of_strings_or_regexps(values) + values.is_a?(Array) && values.all?(&method(:validate_string_or_regexp)) + end + + def validate_string_or_regexp(value) + return false unless value.is_a?(String) + return validate_regexp(value) if look_like_regexp?(value) + true + end + end + class TypeValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) type = options[:with] diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb index a979fe7d573..67bbc3c4849 100644 --- a/lib/gitlab/ci/status/build/cancelable.rb +++ b/lib/gitlab/ci/status/build/cancelable.rb @@ -10,7 +10,7 @@ module Gitlab end def action_icon - 'ban' + 'icon_action_cancel' end def action_path diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb index 1bf949c96dd..0f4b7b24cef 100644 --- a/lib/gitlab/ci/status/build/play.rb +++ b/lib/gitlab/ci/status/build/play.rb @@ -26,17 +26,13 @@ module Gitlab end def action_icon - 'play' + 'icon_action_play' end def action_title 'Play' end - def action_class - 'ci-play-icon' - end - def action_path play_namespace_project_build_path(subject.project.namespace, subject.project, diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb index 8e38d6a8523..6b362af7634 100644 --- a/lib/gitlab/ci/status/build/retryable.rb +++ b/lib/gitlab/ci/status/build/retryable.rb @@ -10,7 +10,7 @@ module Gitlab end def action_icon - 'refresh' + 'icon_action_retry' end def action_title diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb index e1dfdb76d41..90401cad0d2 100644 --- a/lib/gitlab/ci/status/build/stop.rb +++ b/lib/gitlab/ci/status/build/stop.rb @@ -26,7 +26,7 @@ module Gitlab end def action_icon - 'stop' + 'icon_action_stop' end def action_title diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb index 73b6ab5a635..3dd2b9e01f6 100644 --- a/lib/gitlab/ci/status/core.rb +++ b/lib/gitlab/ci/status/core.rb @@ -42,9 +42,6 @@ module Gitlab raise NotImplementedError end - def action_class - end - def action_path raise NotImplementedError end diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb index 74bbcdcb3dd..559e3939da6 100644 --- a/lib/gitlab/cycle_analytics/base_stage.rb +++ b/lib/gitlab/cycle_analytics/base_stage.rb @@ -13,7 +13,7 @@ module Gitlab end def as_json - AnalyticsStageSerializer.new.represent(self).as_json + AnalyticsStageSerializer.new.represent(self) end def title diff --git a/lib/gitlab/cycle_analytics/code_event_fetcher.rb b/lib/gitlab/cycle_analytics/code_event_fetcher.rb index 5245b9ca8fc..d5bf6149749 100644 --- a/lib/gitlab/cycle_analytics/code_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/code_event_fetcher.rb @@ -18,7 +18,7 @@ module Gitlab private def serialize(event) - AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json + AnalyticsMergeRequestSerializer.new(project: @project).represent(event) end end end diff --git a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb index 0d8da99455e..3df9cbdcfce 100644 --- a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb @@ -16,7 +16,7 @@ module Gitlab private def serialize(event) - AnalyticsIssueSerializer.new(project: @project).represent(event).as_json + AnalyticsIssueSerializer.new(project: @project).represent(event) end end end diff --git a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb index 88a8710dbe6..7d342a2d2cb 100644 --- a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb @@ -37,7 +37,7 @@ module Gitlab def serialize_commit(event, st_commit, query) commit = Commit.new(Gitlab::Git::Commit.new(st_commit), @project) - AnalyticsCommitSerializer.new(project: @project, total_time: event['total_time']).represent(commit).as_json + AnalyticsCommitSerializer.new(project: @project, total_time: event['total_time']).represent(commit) end end end diff --git a/lib/gitlab/cycle_analytics/review_event_fetcher.rb b/lib/gitlab/cycle_analytics/review_event_fetcher.rb index 4df0bd06393..4c7b3f4467f 100644 --- a/lib/gitlab/cycle_analytics/review_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/review_event_fetcher.rb @@ -15,7 +15,7 @@ module Gitlab end def serialize(event) - AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json + AnalyticsMergeRequestSerializer.new(project: @project).represent(event) end end end diff --git a/lib/gitlab/cycle_analytics/stage_summary.rb b/lib/gitlab/cycle_analytics/stage_summary.rb index b34baf5b081..fc77bd86097 100644 --- a/lib/gitlab/cycle_analytics/stage_summary.rb +++ b/lib/gitlab/cycle_analytics/stage_summary.rb @@ -16,7 +16,7 @@ module Gitlab private def serialize(summary_object) - AnalyticsSummarySerializer.new.represent(summary_object).as_json + AnalyticsSummarySerializer.new.represent(summary_object) end end end diff --git a/lib/gitlab/cycle_analytics/staging_event_fetcher.rb b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb index a34731a5fcd..36c0260dbfe 100644 --- a/lib/gitlab/cycle_analytics/staging_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb @@ -23,7 +23,7 @@ module Gitlab private def serialize(event) - AnalyticsBuildSerializer.new.represent(event['build']).as_json + AnalyticsBuildSerializer.new.represent(event['build']) end end end diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb index 127fae159d5..b8ec9138c10 100644 --- a/lib/gitlab/email/handler/create_issue_handler.rb +++ b/lib/gitlab/email/handler/create_issue_handler.rb @@ -34,7 +34,7 @@ module Gitlab end def project - @project ||= Project.find_with_namespace(project_path) + @project ||= Project.find_by_full_path(project_path) end private diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index 3cd515e4a3a..d3df3f1bca1 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -6,7 +6,7 @@ module Gitlab class << self def ref_name(ref) - ref.gsub(/\Arefs\/(tags|heads)\//, '') + ref.sub(/\Arefs\/(tags|heads)\//, '') end def branch_name(ref) diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb index d32bdd86427..6babea144c7 100644 --- a/lib/gitlab/git_post_receive.rb +++ b/lib/gitlab/git_post_receive.rb @@ -30,11 +30,11 @@ module Gitlab def retrieve_project_and_type @type = :project - @project = Project.find_with_namespace(@repo_path) + @project = Project.find_by_full_path(@repo_path) if @repo_path.end_with?('.wiki') && !@project @type = :wiki - @project = Project.find_with_namespace(@repo_path.gsub(/\.wiki\z/, '')) + @project = Project.find_by_full_path(@repo_path.gsub(/\.wiki\z/, '')) end end diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index eb667a85b78..d679edec36b 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.5' + VERSION = '0.1.6' FILENAME_LIMIT = 50 def export_path(relative_path:) diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 08ad3274b38..416194e57d7 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -39,7 +39,6 @@ project_tree: - :author - :events - :statuses - - :variables - :triggers - :deploy_keys - :services diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb index 2405b94db50..a09577ae48d 100644 --- a/lib/gitlab/import_export/members_mapper.rb +++ b/lib/gitlab/import_export/members_mapper.rb @@ -41,6 +41,8 @@ module Gitlab end def ensure_default_member! + @project.project_members.destroy_all + ProjectMember.create!(user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true) end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 19e43cce768..0319d7707a8 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -4,7 +4,6 @@ module Gitlab OVERRIDES = { snippets: :project_snippets, pipelines: 'Ci::Pipeline', statuses: 'commit_status', - variables: 'Ci::Variable', triggers: 'Ci::Trigger', builds: 'Ci::Build', hooks: 'ProjectHook', @@ -24,6 +23,8 @@ module Gitlab EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels].freeze + TOKEN_RESET_MODELS = %w[Ci::Trigger Ci::Build ProjectHook].freeze + def self.create(*args) new(*args).create end @@ -61,7 +62,9 @@ module Gitlab update_project_references handle_group_label if group_label? - reset_ci_tokens if @relation_name == 'Ci::Trigger' + reset_tokens! + remove_encrypted_attributes! + @relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data'] set_st_diffs if @relation_name == :merge_request_diff end @@ -140,11 +143,22 @@ module Gitlab end end - def reset_ci_tokens - return unless Gitlab::ImportExport.reset_tokens? + def reset_tokens! + return unless Gitlab::ImportExport.reset_tokens? && TOKEN_RESET_MODELS.include?(@relation_name.to_s) # If we import/export a project to the same instance, tokens will have to be reset. - @relation_hash['token'] = nil + # We also have to reset them to avoid issues when the gitlab secrets file cannot be copied across. + relation_class.attribute_names.select { |name| name.include?('token') }.each do |token| + @relation_hash[token] = nil + end + end + + def remove_encrypted_attributes! + return unless relation_class.respond_to?(:encrypted_attributes) && relation_class.encrypted_attributes.any? + + relation_class.encrypted_attributes.each_key do |key| + @relation_hash[key.to_s] = nil + end end def relation_class diff --git a/lib/gitlab/request_profiler/middleware.rb b/lib/gitlab/request_profiler/middleware.rb index 786e1d49f5e..ef42b0557e0 100644 --- a/lib/gitlab/request_profiler/middleware.rb +++ b/lib/gitlab/request_profiler/middleware.rb @@ -1,5 +1,4 @@ require 'ruby-prof' -require_dependency 'gitlab/request_profiler' module Gitlab module RequestProfiler @@ -20,7 +19,7 @@ module Gitlab header_token = env['HTTP_X_PROFILE_TOKEN'] return unless header_token.present? - profile_token = RequestProfiler.profile_token + profile_token = Gitlab::RequestProfiler.profile_token return unless profile_token.present? header_token == profile_token diff --git a/lib/rouge/lexers/plantuml.rb b/lib/rouge/lexers/plantuml.rb new file mode 100644 index 00000000000..7d5700b7f6d --- /dev/null +++ b/lib/rouge/lexers/plantuml.rb @@ -0,0 +1,21 @@ +module Rouge + module Lexers + class Plantuml < Lexer + title "A passthrough lexer used for PlantUML input" + desc "A boring lexer that doesn't highlight anything" + + tag 'plantuml' + mimetypes 'text/plain' + + default_options token: 'Text' + + def token + @token ||= Token[option :token] + end + + def stream_tokens(string, &b) + yield self.token, string + end + end + end +end diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index 4a696a52b4d..967f630ef20 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -58,7 +58,7 @@ namespace :gitlab do sub(%r{^/*}, ''). chomp('.git'). chomp('.wiki') - next if Project.find_with_namespace(repo_with_namespace) + next if Project.find_by_full_path(repo_with_namespace) new_path = path + move_suffix puts path.inspect + ' -> ' + new_path.inspect File.rename(path, new_path) diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index a2eca74a3c8..b4015f5238e 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -29,7 +29,7 @@ namespace :gitlab do next end - project = Project.find_with_namespace(path) + project = Project.find_by_full_path(path) if project puts " * #{project.name} (#{repo_path}) exists" @@ -63,7 +63,7 @@ namespace :gitlab do if project.persisted? puts " * Created #{project.name} (#{repo_path})".color(:green) - ProjectCacheWorker.perform(project.id) + ProjectCacheWorker.perform_async(project.id) else puts " * Failed trying to create #{project.name} (#{repo_path})".color(:red) puts " Errors: #{project.errors.messages}".color(:red) diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake index 7e2a6668e59..f2e12d85045 100644 --- a/lib/tasks/gitlab/sidekiq.rake +++ b/lib/tasks/gitlab/sidekiq.rake @@ -7,7 +7,7 @@ namespace :gitlab do unless args.project.present? abort "Please specify the project you want to drop PostReceive jobs for:\n rake gitlab:sidekiq:drop_post_receive[group/project]" end - project_path = Project.find_with_namespace(args.project).repository.path_to_repo + project_path = Project.find_by_full_path(args.project).repository.path_to_repo Sidekiq.redis do |redis| unless redis.exists(QUEUE) diff --git a/rubocop/cop/gem_fetcher.rb b/rubocop/cop/gem_fetcher.rb new file mode 100644 index 00000000000..4a63c760744 --- /dev/null +++ b/rubocop/cop/gem_fetcher.rb @@ -0,0 +1,28 @@ +module RuboCop + module Cop + # Cop that checks for all gems specified in the Gemfile, and will + # alert if any gem is to be fetched not from the RubyGems index. + # This enforcement is done so as to minimize external build + # dependencies and build times. + class GemFetcher < RuboCop::Cop::Cop + MSG = 'Do not use gems from git repositories, only use gems from RubyGems.' + + GIT_KEYS = [:git, :github] + + def on_send(node) + file_path = node.location.expression.source_buffer.name + return unless file_path.end_with?("Gemfile") + + func_name = node.children[1] + return unless func_name == :gem + + node.children.last.each_node(:pair) do |pair| + key_name = pair.children[0].children[0].to_sym + if GIT_KEYS.include?(key_name) + add_offense(node, :selector) + end + end + end + end + end +end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index 7922e19768b..7f20754ee51 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -1,3 +1,4 @@ require_relative 'migration_helpers' require_relative 'cop/migration/add_index' require_relative 'cop/migration/column_with_default' +require_relative 'cop/gem_fetcher' diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index a95cfc5c6be..ebd2d0e092b 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -4,7 +4,6 @@ describe Projects::CommitController do let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:commit) { project.commit("master") } - let(:pipeline) { create(:ci_pipeline, project: project, commit: commit) } let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' } let(:master_pickable_commit) { project.commit(master_pickable_sha) } @@ -322,11 +321,26 @@ describe Projects::CommitController do end context 'when the commit exists' do - context 'when the commit has one or more pipelines' do - it 'shows pipelines' do - get_pipelines(id: commit.id) + context 'when the commit has pipelines' do + before do + create(:ci_pipeline, project: project, sha: commit.id) + end + + context 'when rendering a HTML format' do + it 'shows pipelines' do + get_pipelines(id: commit.id) + + expect(response).to be_ok + end + end - expect(response).to be_ok + context 'when rendering a JSON format' do + it 'responds with serialized pipelines' do + get_pipelines(id: commit.id, format: :json) + + expect(response).to be_ok + expect(JSON.parse(response.body)).not_to be_empty + end end end end diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb index ec6cea5c0f4..3e0326dd47d 100644 --- a/spec/controllers/projects/labels_controller_spec.rb +++ b/spec/controllers/projects/labels_controller_spec.rb @@ -112,4 +112,49 @@ describe Projects::LabelsController do post :toggle_subscription, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label.to_param end end + + describe 'POST #promote' do + let!(:promoted_label_name) { "Promoted Label" } + let!(:label_1) { create(:label, title: promoted_label_name, project: project) } + + context 'not group owner' do + it 'denies access' do + post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param + + expect(response).to have_http_status(404) + end + end + + context 'group owner' do + before do + GroupMember.add_users_to_group(group, [user], :owner) + end + + it 'gives access' do + post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param + + expect(response).to redirect_to(namespace_project_labels_path) + end + + it 'promotes the label' do + post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param + + expect(Label.where(id: label_1.id)).to be_empty + expect(GroupLabel.find_by(title: promoted_label_name)).not_to be_nil + end + + context 'service raising InvalidRecord' do + before do + expect_any_instance_of(Labels::PromoteService).to receive(:execute) do |label| + raise ActiveRecord::RecordInvalid.new(label_1) + end + end + + it 'returns to label list' do + post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param + expect(response).to redirect_to(namespace_project_labels_path) + end + end + end + end end diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb index 2ae635a1244..cae733f0cfb 100644 --- a/spec/controllers/projects/mattermosts_controller_spec.rb +++ b/spec/controllers/projects/mattermosts_controller_spec.rb @@ -13,13 +13,13 @@ describe Projects::MattermostsController do before do allow_any_instance_of(MattermostSlashCommandsService). to receive(:list_teams).and_return([]) + end + it 'accepts the request' do get(:new, namespace_id: project.namespace.to_param, project_id: project.to_param) - end - it 'accepts the request' do expect(response).to have_http_status(200) end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 7ea3ea4f376..e019541e74f 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Projects::MergeRequestsController do + include ApiHelpers + let(:project) { create(:project) } let(:user) { create(:user) } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } @@ -455,7 +457,7 @@ describe Projects::MergeRequestsController do it 'renders the diffs template to a string' do expect(response).to render_template('projects/merge_requests/show/_diffs') - expect(JSON.parse(response.body)).to have_key('html') + expect(json_response).to have_key('html') end end @@ -494,7 +496,7 @@ describe Projects::MergeRequestsController do it 'renders the diffs template to a string' do expect(response).to render_template('projects/merge_requests/show/_diffs') - expect(JSON.parse(response.body)).to have_key('html') + expect(json_response).to have_key('html') end end end @@ -662,18 +664,45 @@ describe Projects::MergeRequestsController do go format: 'json' expect(response).to render_template('projects/merge_requests/show/_commits') - expect(JSON.parse(response.body)).to have_key('html') + expect(json_response).to have_key('html') end end end describe 'GET pipelines' do - it_behaves_like "loads labels", :pipelines + before do + create(:ci_pipeline, project: merge_request.source_project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha) + end + + context 'when using HTML format' do + it_behaves_like "loads labels", :pipelines + end + + context 'when using JSON format' do + before do + get :pipelines, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: merge_request.iid, + format: :json + end + + it 'responds with a rendered HTML partial' do + expect(response) + .to render_template('projects/merge_requests/show/_pipelines') + expect(json_response).to have_key 'html' + end + + it 'responds with serialized pipelines' do + expect(json_response).to have_key 'pipelines' + expect(json_response['pipelines']).not_to be_empty + end + end end describe 'GET conflicts' do - let(:json_response) { JSON.parse(response.body) } - context 'when the conflicts cannot be resolved in the UI' do before do allow_any_instance_of(Gitlab::Conflict::Parser). @@ -770,8 +799,6 @@ describe Projects::MergeRequestsController do end describe 'GET conflict_for_path' do - let(:json_response) { JSON.parse(response.body) } - def conflict_for_path(path) get :conflict_for_path, namespace_id: merge_request_with_conflicts.project.namespace.to_param, @@ -826,7 +853,6 @@ describe Projects::MergeRequestsController do end context 'POST resolve_conflicts' do - let(:json_response) { JSON.parse(response.body) } let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha } def resolve_conflicts(files) @@ -1024,7 +1050,6 @@ describe Projects::MergeRequestsController do let!(:forked) { create(:project) } let!(:environment) { create(:environment, project: forked) } let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') } - let(:json_response) { JSON.parse(response.body) } let(:admin) { create(:admin) } let(:merge_request) do diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index 32b0e42c3cd..19e948d8fb8 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -6,8 +6,8 @@ describe Projects::SnippetsController do let(:user2) { create(:user) } before do - project.team << [user, :master] - project.team << [user2, :master] + project.add_master(user) + project.add_master(user2) end describe 'GET #index' do @@ -69,6 +69,86 @@ describe Projects::SnippetsController do end end + describe 'POST #create' do + def create_snippet(project, snippet_params = {}) + sign_in(user) + + project.add_developer(user) + + post :create, { + namespace_id: project.namespace.to_param, + project_id: project.to_param, + project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params) + } + end + + context 'when the snippet is spam' do + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the project is private' do + let(:private_project) { create(:project_empty_repo, :private) } + + context 'when the snippet is public' do + it 'creates the snippet' do + expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }. + to change { Snippet.count }.by(1) + end + end + end + + context 'when the project is public' do + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }. + to change { Snippet.count }.by(1) + end + end + + context 'when the snippet is public' do + it 'rejects the shippet' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + not_to change { Snippet.count } + expect(response).to render_template(:new) + end + + it 'creates a spam log' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + end + end + end + end + + describe 'POST #mark_as_spam' do + let(:snippet) { create(:project_snippet, :private, project: project, author: user) } + + before do + allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true) + stub_application_setting(akismet_enabled: true) + end + + def mark_as_spam + admin = create(:admin) + create(:user_agent_detail, subject: snippet) + project.add_master(admin) + sign_in(admin) + + post :mark_as_spam, + namespace_id: project.namespace.path, + project_id: project.path, + id: snippet.id + end + + it 'updates the snippet' do + mark_as_spam + + expect(snippet.reload).not_to be_submittable_as_spam + end + end + %w[show raw].each do |action| describe "GET ##{action}" do context 'when the project snippet is private' do diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb index 99d0bcfa8d1..80f84a388ce 100644 --- a/spec/controllers/projects/templates_controller_spec.rb +++ b/spec/controllers/projects/templates_controller_spec.rb @@ -14,7 +14,8 @@ describe Projects::TemplatesController do before do project.add_user(user, Gitlab::Access::MASTER) - project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false) + project.repository.commit_file(user, file_path_1, 'something valid', + message: 'test 3', branch_name: 'master', update: false) end describe '#show' do diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index d76fe9f580f..dadcb90cfc2 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -138,6 +138,65 @@ describe SnippetsController do end end + describe 'POST #create' do + def create_snippet(snippet_params = {}) + sign_in(user) + + post :create, { + personal_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params) + } + end + + context 'when the snippet is spam' do + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(visibility_level: Snippet::PRIVATE) }. + to change { Snippet.count }.by(1) + end + end + + context 'when the snippet is public' do + it 'rejects the shippet' do + expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + not_to change { Snippet.count } + expect(response).to render_template(:new) + end + + it 'creates a spam log' do + expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + end + end + end + + describe 'POST #mark_as_spam' do + let(:snippet) { create(:personal_snippet, :public, author: user) } + + before do + allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true) + stub_application_setting(akismet_enabled: true) + end + + def mark_as_spam + admin = create(:admin) + create(:user_agent_detail, subject: snippet) + sign_in(admin) + + post :mark_as_spam, id: snippet.id + end + + it 'updates the snippet' do + mark_as_spam + + expect(snippet.reload).not_to be_submittable_as_spam + end + end + %w(raw download).each do |action| describe "GET #{action}" do context 'when the personal snippet is private' do diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 992580a6b34..715b2a27b30 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -106,6 +106,42 @@ FactoryGirl.define do path { 'gitlabhq' } test_repo + + transient do + create_template nil + end + + after :create do |project, evaluator| + TestEnv.copy_repo(project) + + if evaluator.create_template + args = evaluator.create_template + + project.add_user(args[:user], args[:access]) + + project.repository.commit_file( + args[:user], + ".gitlab/#{args[:path]}/bug.md", + 'something valid', + message: 'test 3', + branch_name: 'master', + update: false) + project.repository.commit_file( + args[:user], + ".gitlab/#{args[:path]}/template_test.md", + 'template_test', + message: 'test 1', + branch_name: 'master', + update: false) + project.repository.commit_file( + args[:user], + ".gitlab/#{args[:path]}/feature_proposal.md", + 'feature_proposal', + message: 'test 2', + branch_name: 'master', + update: false) + end + end end factory :forked_project_with_submodules, parent: :empty_project do diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb index e177059d959..9d5ce876c29 100644 --- a/spec/features/admin/admin_builds_spec.rb +++ b/spec/features/admin/admin_builds_spec.rb @@ -9,8 +9,8 @@ describe 'Admin Builds' do let(:pipeline) { create(:ci_pipeline) } context 'All tab' do - context 'when have builds' do - it 'shows all builds' do + context 'when have jobs' do + it 'shows all jobs' do create(:ci_build, pipeline: pipeline, status: :pending) create(:ci_build, pipeline: pipeline, status: :running) create(:ci_build, pipeline: pipeline, status: :success) @@ -19,26 +19,26 @@ describe 'Admin Builds' do visit admin_builds_path expect(page).to have_selector('.nav-links li.active', text: 'All') - expect(page).to have_selector('.row-content-block', text: 'All builds') + expect(page).to have_selector('.row-content-block', text: 'All jobs') expect(page.all('.build-link').size).to eq(4) expect(page).to have_link 'Cancel all' end end - context 'when have no builds' do + context 'when have no jobs' do it 'shows a message' do visit admin_builds_path expect(page).to have_selector('.nav-links li.active', text: 'All') - expect(page).to have_content 'No builds to show' + expect(page).to have_content 'No jobs to show' expect(page).not_to have_link 'Cancel all' end end end context 'Pending tab' do - context 'when have pending builds' do - it 'shows pending builds' do + context 'when have pending jobs' do + it 'shows pending jobs' do build1 = create(:ci_build, pipeline: pipeline, status: :pending) build2 = create(:ci_build, pipeline: pipeline, status: :running) build3 = create(:ci_build, pipeline: pipeline, status: :success) @@ -55,22 +55,22 @@ describe 'Admin Builds' do end end - context 'when have no builds pending' do + context 'when have no jobs pending' do it 'shows a message' do create(:ci_build, pipeline: pipeline, status: :success) visit admin_builds_path(scope: :pending) expect(page).to have_selector('.nav-links li.active', text: 'Pending') - expect(page).to have_content 'No builds to show' + expect(page).to have_content 'No jobs to show' expect(page).not_to have_link 'Cancel all' end end end context 'Running tab' do - context 'when have running builds' do - it 'shows running builds' do + context 'when have running jobs' do + it 'shows running jobs' do build1 = create(:ci_build, pipeline: pipeline, status: :running) build2 = create(:ci_build, pipeline: pipeline, status: :success) build3 = create(:ci_build, pipeline: pipeline, status: :failed) @@ -87,22 +87,22 @@ describe 'Admin Builds' do end end - context 'when have no builds running' do + context 'when have no jobs running' do it 'shows a message' do create(:ci_build, pipeline: pipeline, status: :success) visit admin_builds_path(scope: :running) expect(page).to have_selector('.nav-links li.active', text: 'Running') - expect(page).to have_content 'No builds to show' + expect(page).to have_content 'No jobs to show' expect(page).not_to have_link 'Cancel all' end end end context 'Finished tab' do - context 'when have finished builds' do - it 'shows finished builds' do + context 'when have finished jobs' do + it 'shows finished jobs' do build1 = create(:ci_build, pipeline: pipeline, status: :pending) build2 = create(:ci_build, pipeline: pipeline, status: :running) build3 = create(:ci_build, pipeline: pipeline, status: :success) @@ -117,14 +117,14 @@ describe 'Admin Builds' do end end - context 'when have no builds finished' do + context 'when have no jobs finished' do it 'shows a message' do create(:ci_build, pipeline: pipeline, status: :running) visit admin_builds_path(scope: :finished) expect(page).to have_selector('.nav-links li.active', text: 'Finished') - expect(page).to have_content 'No builds to show' + expect(page).to have_content 'No jobs to show' expect(page).to have_link 'Cancel all' end end diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 188d33e8ef4..c28bb0dcdae 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -141,6 +141,36 @@ describe 'Issue Boards', feature: true, js: true do end end end + + it 'resets assignee dropdown' do + page.within(first('.board')) do + first('.card').click + end + + page.within('.assignee') do + click_link 'Edit' + + wait_for_ajax + + page.within('.dropdown-menu-user') do + click_link user.name + + wait_for_vue_resource + end + + expect(page).to have_content(user.name) + end + + page.within(first('.board')) do + find('.card:nth-child(2)').click + end + + page.within('.assignee') do + click_link 'Edit' + + expect(page).not_to have_selector('.is-active') + end + end end context 'milestone' do diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb new file mode 100644 index 00000000000..d9be4e5dbdd --- /dev/null +++ b/spec/features/dashboard/shortcuts_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +feature 'Dashboard shortcuts', feature: true, js: true do + before do + login_as :user + visit dashboard_projects_path + end + + scenario 'Navigate to tabs' do + find('body').native.send_key('g') + find('body').native.send_key('p') + + ensure_active_main_tab('Projects') + + find('body').native.send_key('g') + find('body').native.send_key('i') + + ensure_active_main_tab('Issues') + + find('body').native.send_key('g') + find('body').native.send_key('m') + + ensure_active_main_tab('Merge Requests') + end + + def ensure_active_main_tab(content) + expect(find('.nav-sidebar li.active')).to have_content(content) + end +end diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb index 56f6cd2e095..511c95b758f 100644 --- a/spec/features/environment_spec.rb +++ b/spec/features/environment_spec.rb @@ -19,6 +19,10 @@ feature 'Environment', :feature do visit_environment(environment) end + scenario 'shows environment name' do + expect(page).to have_content(environment.name) + end + context 'without deployments' do scenario 'does show no deployments' do expect(page).to have_content('You don\'t have any deployments right now.') diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index 72b984cfab8..c033b693213 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -194,7 +194,7 @@ feature 'Environments page', :feature, :js do end scenario 'does create a new pipeline' do - expect(page).to have_content('Production') + expect(page).to have_content('production') end end diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 31156fcf994..93139dc9e94 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' feature 'GFM autocomplete', feature: true, js: true do include WaitForAjax - let(:user) { create(:user, username: 'someone.special') } + let(:user) { create(:user, name: '💃speciÄ…l someone💃', username: 'someone.special') } let(:project) { create(:project) } let(:label) { create(:label, project: project, title: 'special+') } let(:issue) { create(:issue, project: project) } @@ -59,6 +59,19 @@ feature 'GFM autocomplete', feature: true, js: true do expect(find('#at-view-64')).to have_selector('.cur:first-of-type') end + it 'includes items for assignee dropdowns with non-ASCII characters in name' do + page.within '.timeline-content-form' do + find('#note_note').native.send_keys('') + find('#note_note').native.send_keys("@#{user.name[0...8]}") + end + + expect(page).to have_selector('.atwho-container') + + wait_for_ajax + + expect(find('#at-view-64')).to have_content(user.name) + end + it 'selects the first item for non-assignee dropdowns if a query is entered' do page.within '.timeline-content-form' do find('#note_note').native.send_keys('') diff --git a/spec/features/issues/group_label_sidebar_spec.rb b/spec/features/issues/group_label_sidebar_spec.rb new file mode 100644 index 00000000000..fc8515cfe9b --- /dev/null +++ b/spec/features/issues/group_label_sidebar_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +describe 'Group label on issue', :feature do + it 'renders link to the project issues page' do + group = create(:group) + project = create(:empty_project, :public, namespace: group) + feature = create(:group_label, group: group, title: 'feature') + issue = create(:labeled_issue, project: project, labels: [feature]) + label_link = namespace_project_issues_path( + project.namespace, + project, + label_name: [feature.name] + ) + + visit namespace_project_issue_path(project.namespace, project, issue) + + link = find('.issuable-show-labels a') + + expect(link[:href]).to eq(label_link) + end +end diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb index a4d3053d10c..c0ab42c6822 100644 --- a/spec/features/issues/new_branch_button_spec.rb +++ b/spec/features/issues/new_branch_button_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Start new branch from an issue', feature: true do +feature 'Start new branch from an issue', feature: true, js: true do let!(:project) { create(:project) } let!(:issue) { create(:issue, project: project) } let!(:user) { create(:user)} @@ -11,7 +11,7 @@ feature 'Start new branch from an issue', feature: true do login_as(user) end - it 'shows the new branch button', js: true do + it 'shows the new branch button' do visit namespace_project_issue_path(project.namespace, project, issue) expect(page).to have_css('#new-branch .available') @@ -34,16 +34,26 @@ feature 'Start new branch from an issue', feature: true do visit namespace_project_issue_path(project.namespace, project, issue) end - it "hides the new branch button", js: true do + it "hides the new branch button" do expect(page).to have_css('#new-branch .unavailable') expect(page).not_to have_css('#new-branch .available') expect(page).to have_content /1 Related Merge Request/ end end + + context 'when issue is confidential' do + it 'hides the new branch button' do + issue = create(:issue, :confidential, project: project) + + visit namespace_project_issue_path(project.namespace, project, issue) + + expect(page).not_to have_css('#new-branch') + end + end end - context "for visiters" do - it 'shows no buttons', js: true do + context 'for visitors' do + it 'shows no buttons' do visit namespace_project_issue_path(project.namespace, project, issue) expect(page).not_to have_css('#new-branch') diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index 76bcfbe523a..ab7d89306db 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -25,6 +25,11 @@ feature 'Login', feature: true do expect(current_path).to eq root_path end + + it 'does not show flash messages when login page' do + visit root_path + expect(page).not_to have_content('You need to sign in or sign up before continuing.') + end end describe 'with two-factor authentication' do diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb index 7e2907cd26f..d2f5c4afc93 100644 --- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb +++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb @@ -50,7 +50,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: visit_merge_request(merge_request) expect(page).not_to have_button 'Accept Merge Request' - expect(page).to have_content('Please retry the build or push a new commit to fix the failure.') + expect(page).to have_content('Please retry the job or push a new commit to fix the failure.') end end @@ -61,7 +61,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: visit_merge_request(merge_request) expect(page).not_to have_button 'Accept Merge Request' - expect(page).to have_content('Please retry the build or push a new commit to fix the failure.') + expect(page).to have_content('Please retry the job or push a new commit to fix the failure.') end end diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb new file mode 100644 index 00000000000..44a9b545ff8 --- /dev/null +++ b/spec/features/merge_requests/toggler_behavior_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +feature 'toggler_behavior', js: true, feature: true do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:merge_request) { create(:merge_request, source_project: project, author: user) } + let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) } + let(:fragment_id) { "#note_#{note.id}" } + + before do + login_as :admin + project = merge_request.source_project + visit "#{namespace_project_merge_request_path(project.namespace, project, merge_request)}#{fragment_id}" + page.current_window.resize_to(1000, 300) + end + + describe 'scroll position' do + it 'should be scrolled down to fragment' do + page_height = page.current_window.size[1] + page_scroll_y = page.evaluate_script("window.scrollY") + fragment_position_top = page.evaluate_script("$('#{fragment_id}').offset().top") + expect(find('.js-toggle-content').visible?).to eq true + expect(find(fragment_id).visible?).to eq true + expect(fragment_position_top).to be >= page_scroll_y + expect(fragment_position_top).to be < (page_scroll_y + page_height) + end + end +end diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index cf56715f508..2b454b38231 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -11,7 +11,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do it_behaves_like 'issuable record that supports slash commands in its description and notes', :merge_request do let(:issuable) { create(:merge_request, source_project: project) } - let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } } + let(:new_url_opts) { { merge_request: { source_branch: 'feature', target_branch: 'master' } } } end describe 'merge-request-only commands' do diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb new file mode 100644 index 00000000000..7d1805f5001 --- /dev/null +++ b/spec/features/merge_requests/widget_spec.rb @@ -0,0 +1,34 @@ +require 'rails_helper' + +describe 'Merge request', :feature, :js do + include WaitForAjax + + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.team << [user, :master] + login_as(user) + + visit new_namespace_project_merge_request_path( + project.namespace, + project, + merge_request: { + source_project_id: project.id, + target_project_id: project.id, + source_branch: 'feature', + target_branch: 'master' + } + ) + end + + it 'shows widget status after creating new merge request' do + click_button 'Submit merge request' + + expect(find('.mr-state-widget')).to have_content('Checking ability to merge automatically') + + wait_for_ajax + + expect(page).to have_selector('.accept_merge_request') + end +end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index b785b2f7704..fab2d532e06 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -89,7 +89,7 @@ describe 'Comments', feature: true do end end - it 'should reset the edit note form textarea with the original content of the note if cancelled' do + it 'resets the edit note form textarea with the original content of the note if cancelled' do within('.current-note-edit-form') do fill_in 'note[note]', with: 'Some new content' find('.btn-cancel').click @@ -198,7 +198,7 @@ describe 'Comments', feature: true do end describe 'the note form' do - it "shouldn't add a second form for same row" do + it "does not add a second form for same row" do click_diff_line is_expected. @@ -206,7 +206,7 @@ describe 'Comments', feature: true do count: 1) end - it 'should be removed when canceled' do + it 'is removed when canceled' do is_expected.to have_css('.js-temp-notes-holder') page.within("form[data-line-code='#{line_code}']") do diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index 11d27feab0b..f7e0115643e 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -27,7 +27,7 @@ feature 'Builds', :feature do visit namespace_project_builds_path(project.namespace, project, scope: :pending) end - it "shows Pending tab builds" do + it "shows Pending tab jobs" do expect(page).to have_link 'Cancel running' expect(page).to have_selector('.nav-links li.active', text: 'Pending') expect(page).to have_content build.short_sha @@ -42,7 +42,7 @@ feature 'Builds', :feature do visit namespace_project_builds_path(project.namespace, project, scope: :running) end - it "shows Running tab builds" do + it "shows Running tab jobs" do expect(page).to have_selector('.nav-links li.active', text: 'Running') expect(page).to have_link 'Cancel running' expect(page).to have_content build.short_sha @@ -57,20 +57,20 @@ feature 'Builds', :feature do visit namespace_project_builds_path(project.namespace, project, scope: :finished) end - it "shows Finished tab builds" do + it "shows Finished tab jobs" do expect(page).to have_selector('.nav-links li.active', text: 'Finished') - expect(page).to have_content 'No builds to show' + expect(page).to have_content 'No jobs to show' expect(page).to have_link 'Cancel running' end end - context "All builds" do + context "All jobs" do before do project.builds.running_or_pending.each(&:success) visit namespace_project_builds_path(project.namespace, project) end - it "shows All tab builds" do + it "shows All tab jobs" do expect(page).to have_selector('.nav-links li.active', text: 'All') expect(page).to have_content build.short_sha expect(page).to have_content build.ref @@ -98,7 +98,7 @@ feature 'Builds', :feature do end describe "GET /:project/builds/:id" do - context "Build from project" do + context "Job from project" do before do visit namespace_project_build_path(project.namespace, project, build) end @@ -111,7 +111,7 @@ feature 'Builds', :feature do end end - context "Build from other project" do + context "Job from other project" do before do visit namespace_project_build_path(project.namespace, project, build2) end @@ -149,7 +149,7 @@ feature 'Builds', :feature do context 'when expire date is defined' do let(:expire_at) { Time.now + 7.days } - context 'when user has ability to update build' do + context 'when user has ability to update job' do it 'keeps artifacts when keep button is clicked' do expect(page).to have_content 'The artifacts will be removed' @@ -160,7 +160,7 @@ feature 'Builds', :feature do end end - context 'when user does not have ability to update build' do + context 'when user does not have ability to update job' do let(:user_access_level) { :guest } it 'does not have keep button' do @@ -197,8 +197,8 @@ feature 'Builds', :feature do visit namespace_project_build_path(project.namespace, project, build) end - context 'when build has an initial trace' do - it 'loads build trace' do + context 'when job has an initial trace' do + it 'loads job trace' do expect(page).to have_content 'BUILD TRACE' build.append_trace(' and more trace', 11) @@ -242,32 +242,32 @@ feature 'Builds', :feature do end end - context 'when build starts environment' do + context 'when job starts environment' do let(:environment) { create(:environment, project: project) } let(:pipeline) { create(:ci_pipeline, project: project) } - context 'build is successfull and has deployment' do + context 'job is successfull and has deployment' do let(:deployment) { create(:deployment) } let(:build) { create(:ci_build, :success, environment: environment.name, deployments: [deployment], pipeline: pipeline) } - it 'shows a link for the build' do + it 'shows a link for the job' do visit namespace_project_build_path(project.namespace, project, build) expect(page).to have_link environment.name end end - context 'build is complete and not successfull' do + context 'job is complete and not successfull' do let(:build) { create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) } - it 'shows a link for the build' do + it 'shows a link for the job' do visit namespace_project_build_path(project.namespace, project, build) expect(page).to have_link environment.name end end - context 'build creates a new deployment' do + context 'job creates a new deployment' do let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) } let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } @@ -281,7 +281,7 @@ feature 'Builds', :feature do end describe "POST /:project/builds/:id/cancel" do - context "Build from project" do + context "Job from project" do before do build.run! visit namespace_project_build_path(project.namespace, project, build) @@ -295,7 +295,7 @@ feature 'Builds', :feature do end end - context "Build from other project" do + context "Job from other project" do before do build.run! visit namespace_project_build_path(project.namespace, project, build) @@ -307,13 +307,13 @@ feature 'Builds', :feature do end describe "POST /:project/builds/:id/retry" do - context "Build from project" do + context "Job from project" do before do build.run! visit namespace_project_build_path(project.namespace, project, build) click_link 'Cancel' page.within('.build-header') do - click_link 'Retry build' + click_link 'Retry job' end end diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb index fe047e00409..36a80d7575d 100644 --- a/spec/features/projects/files/editing_a_file_spec.rb +++ b/spec/features/projects/files/editing_a_file_spec.rb @@ -7,7 +7,7 @@ feature 'User wants to edit a file', feature: true do let(:user) { create(:user) } let(:commit_params) do { - source_branch: project.default_branch, + start_branch: project.default_branch, target_branch: project.default_branch, commit_message: "Committing First Update", file_path: ".gitignore", diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb index a521ce50f35..64094af29c0 100644 --- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb +++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb @@ -6,7 +6,8 @@ feature 'project owner creates a license file', feature: true, js: true do let(:project_master) { create(:user) } let(:project) { create(:project) } background do - project.repository.remove_file(project_master, 'LICENSE', 'Remove LICENSE', 'master') + project.repository.remove_file(project_master, 'LICENSE', + message: 'Remove LICENSE', branch_name: 'master') project.team << [project_master, :master] login_as(project_master) visit namespace_project_path(project.namespace, project) diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb index 52d08982c7a..16dddb2a86b 100644 --- a/spec/features/projects/import_export/export_file_spec.rb +++ b/spec/features/projects/import_export/export_file_spec.rb @@ -74,6 +74,9 @@ feature 'Import/Export - project export integration test', feature: true, js: tr Otherwise, please add the exception to +safe_list+ in CURRENT_SPEC using #{sensitive_word} as the key and the correspondent hash or model as the value. + Also, if the attribute is a generated unique token, please add it to RelationFactory::TOKEN_RESET_MODELS if it needs to be + reset (to prevent duplicate column problems while importing to the same instance). + IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file} CURRENT_SPEC: #{__FILE__} MSG diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz Binary files differindex 7655c2b351f..20cdfbae24f 100644 --- a/spec/features/projects/import_export/test_project_export.tar.gz +++ b/spec/features/projects/import_export/test_project_export.tar.gz diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index 6dae5c64b30..e90a033b8c4 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -18,8 +18,20 @@ feature 'issuable templates', feature: true, js: true do let(:description_addition) { ' appending to description' } background do - project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false) - project.repository.commit_file(user, '.gitlab/issue_templates/test.md', longtemplate_content, 'added issue template', 'master', false) + project.repository.commit_file( + user, + '.gitlab/issue_templates/bug.md', + template_content, + message: 'added issue template', + branch_name: 'master', + update: false) + project.repository.commit_file( + user, + '.gitlab/issue_templates/test.md', + longtemplate_content, + message: 'added issue template', + branch_name: 'master', + update: false) visit edit_namespace_project_issue_path project.namespace, project, issue fill_in :'issue[title]', with: 'test issue title' end @@ -67,7 +79,13 @@ feature 'issuable templates', feature: true, js: true do let(:issue) { create(:issue, author: user, assignee: user, project: project) } background do - project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false) + project.repository.commit_file( + user, + '.gitlab/issue_templates/bug.md', + template_content, + message: 'added issue template', + branch_name: 'master', + update: false) visit edit_namespace_project_issue_path project.namespace, project, issue fill_in :'issue[title]', with: 'test issue title' fill_in :'issue[description]', with: prior_description @@ -86,7 +104,13 @@ feature 'issuable templates', feature: true, js: true do let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) } background do - project.repository.commit_file(user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false) + project.repository.commit_file( + user, + '.gitlab/merge_request_templates/feature-proposal.md', + template_content, + message: 'added merge request template', + branch_name: 'master', + update: false) visit edit_namespace_project_merge_request_path project.namespace, project, merge_request fill_in :'merge_request[title]', with: 'test merge request title' end @@ -111,7 +135,13 @@ feature 'issuable templates', feature: true, js: true do fork_project.team << [fork_user, :master] create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project) login_as fork_user - project.repository.commit_file(fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false) + project.repository.commit_file( + fork_user, + '.gitlab/merge_request_templates/feature-proposal.md', + template_content, + message: 'added merge request template', + branch_name: 'master', + update: false) visit edit_namespace_project_merge_request_path project.namespace, project, merge_request fill_in :'merge_request[title]', with: 'test merge request title' end diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb index c9fa8315e79..97ce9cdfd87 100644 --- a/spec/features/projects/labels/update_prioritization_spec.rb +++ b/spec/features/projects/labels/update_prioritization_spec.rb @@ -20,7 +20,7 @@ feature 'Prioritize labels', feature: true do 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') + expect(page).to have_content('Star labels to start sorting by priority') page.within('.other-labels') do all('.js-toggle-priority')[1].click @@ -29,7 +29,7 @@ feature 'Prioritize labels', feature: true do end page.within('.prioritized-labels') do - expect(page).not_to have_content('No prioritized labels yet') + expect(page).not_to have_content('Star labels to start sorting by priority') expect(page).to have_content('feature') end end @@ -55,7 +55,7 @@ feature 'Prioritize labels', feature: true do 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') + expect(page).to have_content('Star labels to start sorting by priority') page.within('.other-labels') do first('.js-toggle-priority').click @@ -64,7 +64,7 @@ feature 'Prioritize labels', feature: true do end page.within('.prioritized-labels') do - expect(page).not_to have_content('No prioritized labels yet') + expect(page).not_to have_content('Star labels to start sorting by priority') expect(page).to have_content('bug') end end diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index abfc46601fb..b56e562b2b6 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -1,11 +1,13 @@ require "spec_helper" feature "New project", feature: true do - context "Visibility level selector" do - let(:user) { create(:admin) } + let(:user) { create(:admin) } - before { login_as(user) } + before do + login_as(user) + end + context "Visibility level selector" do Gitlab::VisibilityLevel.options.each do |key, level| it "sets selector to #{key}" do stub_application_setting(default_project_visibility: level) @@ -16,4 +18,16 @@ feature "New project", feature: true do end end end + + context 'Import project options' do + before do + visit new_project_path + end + + it 'does not autocomplete sensitive git repo URL' do + autocomplete = find('#project_import_url')['autocomplete'] + + expect(autocomplete).to eq('off') + end + end end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index e673ece37c3..0b5ccc8c515 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -66,8 +66,8 @@ describe 'Pipeline', :feature, :js do context 'when pipeline has running builds' do it 'shows a running icon and a cancel action for the running build' do page.within('#ci-badge-deploy') do - expect(page).to have_selector('.ci-status-icon-running') - expect(page).to have_selector('.ci-action-icon-container .fa-ban') + expect(page).to have_selector('.js-ci-status-icon-running') + expect(page).to have_selector('.js-icon-action-cancel') expect(page).to have_content('deploy') end end @@ -82,63 +82,63 @@ describe 'Pipeline', :feature, :js do context 'when pipeline has successful builds' do it 'shows the success icon and a retry action for the successful build' do page.within('#ci-badge-build') do - expect(page).to have_selector('.ci-status-icon-success') + expect(page).to have_selector('.js-ci-status-icon-success') expect(page).to have_content('build') end page.within('#ci-badge-build .ci-action-icon-container') do - expect(page).to have_selector('.ci-action-icon-container .fa-refresh') + expect(page).to have_selector('.js-icon-action-retry') end end - it 'should be possible to retry the success build' do + it 'should be possible to retry the success job' do find('#ci-badge-build .ci-action-icon-container').trigger('click') - expect(page).not_to have_content('Retry build') + expect(page).not_to have_content('Retry job') end end context 'when pipeline has failed builds' do it 'shows the failed icon and a retry action for the failed build' do page.within('#ci-badge-test') do - expect(page).to have_selector('.ci-status-icon-failed') + expect(page).to have_selector('.js-ci-status-icon-failed') expect(page).to have_content('test') end page.within('#ci-badge-test .ci-action-icon-container') do - expect(page).to have_selector('.ci-action-icon-container .fa-refresh') + expect(page).to have_selector('.js-icon-action-retry') end end it 'should be possible to retry the failed build' do find('#ci-badge-test .ci-action-icon-container').trigger('click') - expect(page).not_to have_content('Retry build') + expect(page).not_to have_content('Retry job') end end - context 'when pipeline has manual builds' do + context 'when pipeline has manual jobs' do it 'shows the skipped icon and a play action for the manual build' do page.within('#ci-badge-manual-build') do - expect(page).to have_selector('.ci-status-icon-manual') + expect(page).to have_selector('.js-ci-status-icon-manual') expect(page).to have_content('manual') end page.within('#ci-badge-manual-build .ci-action-icon-container') do - expect(page).to have_selector('.ci-action-icon-container .fa-play') + expect(page).to have_selector('.js-icon-action-play') end end - it 'should be possible to play the manual build' do + it 'should be possible to play the manual job' do find('#ci-badge-manual-build .ci-action-icon-container').trigger('click') - expect(page).not_to have_content('Play build') + expect(page).not_to have_content('Play job') end end - context 'when pipeline has external build' do + context 'when pipeline has external job' do it 'shows the success icon and the generic comit status build' do - expect(page).to have_selector('.ci-status-icon-success') + expect(page).to have_selector('.js-ci-status-icon-success') expect(page).to have_content('jenkins') expect(page).to have_link('jenkins', href: 'http://gitlab.com/status') end @@ -146,12 +146,12 @@ describe 'Pipeline', :feature, :js do end context 'page tabs' do - it 'shows Pipeline and Builds tabs with link' do + it 'shows Pipeline and Jobs tabs with link' do expect(page).to have_link('Pipeline') - expect(page).to have_link('Builds') + expect(page).to have_link('Jobs') end - it 'shows counter in Builds tab' do + it 'shows counter in Jobs tab' do expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s) end @@ -160,7 +160,7 @@ describe 'Pipeline', :feature, :js do end end - context 'retrying builds' do + context 'retrying jobs' do it { expect(page).not_to have_content('retried') } context 'when retrying' do @@ -170,7 +170,7 @@ describe 'Pipeline', :feature, :js do end end - context 'canceling builds' do + context 'canceling jobs' do it { expect(page).not_to have_selector('.ci-canceled') } context 'when canceling' do @@ -191,7 +191,7 @@ describe 'Pipeline', :feature, :js do visit builds_namespace_project_pipeline_path(project.namespace, project, pipeline) end - it 'shows a list of builds' do + it 'shows a list of jobs' do expect(page).to have_content('Test') expect(page).to have_content(build_passed.id) expect(page).to have_content('Deploy') @@ -203,26 +203,26 @@ describe 'Pipeline', :feature, :js do expect(page).to have_link('Play') end - it 'shows Builds tab pane as active' do + it 'shows jobs tab pane as active' do expect(page).to have_css('#js-tab-builds.active') end context 'page tabs' do - it 'shows Pipeline and Builds tabs with link' do + it 'shows Pipeline and Jobs tabs with link' do expect(page).to have_link('Pipeline') - expect(page).to have_link('Builds') + expect(page).to have_link('Jobs') end - it 'shows counter in Builds tab' do + it 'shows counter in Jobs tab' do expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s) end - it 'shows Builds tab as active' do + it 'shows Jobs tab as active' do expect(page).to have_css('li.js-builds-tab-link.active') end end - context 'retrying builds' do + context 'retrying jobs' do it { expect(page).not_to have_content('retried') } context 'when retrying' do @@ -233,7 +233,7 @@ describe 'Pipeline', :feature, :js do end end - context 'canceling builds' do + context 'canceling jobs' do it { expect(page).not_to have_selector('.ci-canceled') } context 'when canceling' do @@ -244,7 +244,7 @@ describe 'Pipeline', :feature, :js do end end - context 'playing manual build' do + context 'playing manual job' do before do within '.pipeline-holder' do click_link('Play') diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb index 472491188c9..38fe2d92885 100644 --- a/spec/features/projects/ref_switcher_spec.rb +++ b/spec/features/projects/ref_switcher_spec.rb @@ -17,14 +17,15 @@ feature 'Ref switcher', feature: true, js: true do page.within '.project-refs-form' do input = find('input[type="search"]') - input.set 'expand' + input.set 'binary' + wait_for_ajax input.native.send_keys :down input.native.send_keys :down input.native.send_keys :enter end - expect(page).to have_title 'expand-collapse-files' + expect(page).to have_title 'binary-encoding' end it "user selects ref with special characters" do diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb index 86a07b2c679..042a1ccab51 100644 --- a/spec/features/projects/services/mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/mattermost_slash_command_spec.rb @@ -99,6 +99,15 @@ feature 'Setup Mattermost slash commands', feature: true do expect(select_element.all('option').count).to eq(3) end + it 'shows an error alert with the error message if there is an error requesting teams' do + allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { [[], 'test mattermost error message'] } + + click_link 'Add to Mattermost' + + expect(page).to have_selector('.alert') + expect(page).to have_content('test mattermost error message') + end + def stub_teams(count: 0) teams = create_teams(count) diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb index 4bfaa499272..034b75c2e51 100644 --- a/spec/features/projects/settings/merge_requests_settings_spec.rb +++ b/spec/features/projects/settings/merge_requests_settings_spec.rb @@ -11,41 +11,41 @@ feature 'Project settings > Merge Requests', feature: true, js: true do login_as(user) end - context 'when Merge Request and Builds are initially enabled' do + context 'when Merge Request and Pipelines are initially enabled' do before do project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::ENABLED) end - context 'when Builds are initially enabled' do + context 'when Pipelines are initially enabled' do before do project.project_feature.update_attribute('builds_access_level', ProjectFeature::ENABLED) visit edit_project_path(project) end scenario 'shows the Merge Requests settings' do - expect(page).to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') select 'Disabled', from: "project_project_feature_attributes_merge_requests_access_level" - expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved') end end - context 'when Builds are initially disabled' do + context 'when Pipelines are initially disabled' do before do project.project_feature.update_attribute('builds_access_level', ProjectFeature::DISABLED) visit edit_project_path(project) end scenario 'shows the Merge Requests settings that do not depend on Builds feature' do - expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') select 'Everyone with access', from: "project_project_feature_attributes_builds_access_level" - expect(page).to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') end end @@ -58,12 +58,12 @@ feature 'Project settings > Merge Requests', feature: true, js: true do end scenario 'does not show the Merge Requests settings' do - expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved') select 'Everyone with access', from: "project_project_feature_attributes_merge_requests_access_level" - expect(page).to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') end end diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 3850e930b6d..1b352be9331 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -171,7 +171,7 @@ describe 'Dashboard Todos', feature: true do it 'links to the pipelines for the merge request' do href = pipelines_namespace_project_merge_request_path(project.namespace, project, todo.target) - expect(page).to have_link "merge request #{todo.target.to_reference}", href + expect(page).to have_link "merge request #{todo.target.to_reference(full: true)}", href end end end diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 468bcc7badc..eae097126ce 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -134,7 +134,7 @@ describe DiffHelper do let(:new_pos) { 50 } let(:text) { 'some_text' } - it "should generate foldable top match line for inline view with empty text by default" do + it "generates foldable top match line for inline view with empty text by default" do output = diff_match_line old_pos, new_pos expect(output).to be_html_safe @@ -143,7 +143,7 @@ describe DiffHelper do expect(output).to have_css 'td:nth-child(3):not(.parallel).line_content.match', text: '' end - it "should allow to define text and bottom option" do + it "allows to define text and bottom option" do output = diff_match_line old_pos, new_pos, text: text, bottom: true expect(output).to be_html_safe @@ -152,7 +152,7 @@ describe DiffHelper do expect(output).to have_css 'td:nth-child(3):not(.parallel).line_content.match', text: text end - it "should generate match line for parallel view" do + it "generates match line for parallel view" do output = diff_match_line old_pos, new_pos, text: text, view: :parallel expect(output).to be_html_safe @@ -162,7 +162,7 @@ describe DiffHelper do expect(output).to have_css 'td:nth-child(4).line_content.match.parallel', text: text end - it "should allow to generate only left match line for parallel view" do + it "allows to generate only left match line for parallel view" do output = diff_match_line old_pos, nil, text: text, view: :parallel expect(output).to be_html_safe @@ -171,7 +171,7 @@ describe DiffHelper do expect(output).not_to have_css 'td:nth-child(3)' end - it "should allow to generate only right match line for parallel view" do + it "allows to generate only right match line for parallel view" do output = diff_match_line nil, new_pos, text: text, view: :parallel expect(output).to be_html_safe diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index a4f08dc4af0..df71680e44c 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -115,6 +115,46 @@ describe IssuablesHelper do end end + describe '#issuable_reference' do + context 'when show_full_reference truthy' do + it 'display issuable full reference' do + assign(:show_full_reference, true) + issue = build_stubbed(:issue) + + expect(helper.issuable_reference(issue)).to eql(issue.to_reference(full: true)) + end + end + + context 'when show_full_reference falsey' do + context 'when @group present' do + it 'display issuable reference to @group' do + project = build_stubbed(:project) + + assign(:show_full_reference, nil) + assign(:group, project.namespace) + + issue = build_stubbed(:issue) + + expect(helper.issuable_reference(issue)).to eql(issue.to_reference(project.namespace)) + end + end + + context 'when @project present' do + it 'display issuable reference to @project' do + project = build_stubbed(:project) + + assign(:show_full_reference, nil) + assign(:group, nil) + assign(:project, project) + + issue = build_stubbed(:issue) + + expect(helper.issuable_reference(issue)).to eql(issue.to_reference(project)) + end + end + end + end + describe '#issuable_filter_present?' do it 'returns true when any key is present' do allow(helper).to receive(:params).and_return( diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index e51720f10ed..b7e547dc1f5 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -41,6 +41,11 @@ describe SearchHelper do expect(search_autocomplete_opts("gro").size).to eq(1) end + it "includes nested group" do + create(:group, :nested, name: 'foo').add_owner(user) + expect(search_autocomplete_opts('foo').size).to eq(1) + end + it "includes the user's projects" do project = create(:empty_project, namespace: create(:namespace, owner: user)) expect(search_autocomplete_opts(project.name).size).to eq(1) diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc index 3cd419b37c9..fbd9bb9f0ff 100644 --- a/spec/javascripts/.eslintrc +++ b/spec/javascripts/.eslintrc @@ -22,9 +22,10 @@ }, "plugins": ["jasmine"], "rules": { - "prefer-arrow-callback": 0, "func-names": 0, "jasmine/no-suite-dupes": [1, "branch"], - "jasmine/no-spec-dupes": [1, "branch"] + "jasmine/no-spec-dupes": [1, "branch"], + "no-console": 0, + "prefer-arrow-callback": 0 } } diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 71446b9df61..f1bfd529983 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -113,7 +113,7 @@ }); }); describe('::getAwardUrl', function() { - return it('should return the url for request', function() { + return it('returns the url for request', function() { return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji'); }); }); diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6 index 8d3e2237fda..7a399b307ad 100644 --- a/spec/javascripts/boards/mock_data.js.es6 +++ b/spec/javascripts/boards/mock_data.js.es6 @@ -56,3 +56,8 @@ const boardsMockInterceptor = (request, next) => { status: 200 })); }; + +window.listObj = listObj; +window.listObjDuplicate = listObjDuplicate; +window.BoardsMockData = BoardsMockData; +window.boardsMockInterceptor = boardsMockInterceptor; diff --git a/spec/javascripts/environments/environment_spec.js.es6 b/spec/javascripts/environments/environment_spec.js.es6 index 20e11ca3738..239cd69dd3a 100644 --- a/spec/javascripts/environments/environment_spec.js.es6 +++ b/spec/javascripts/environments/environment_spec.js.es6 @@ -8,12 +8,12 @@ //= require ./mock_data describe('Environment', () => { - preloadFixtures('environments/environments'); + preloadFixtures('static/environments/environments.html.raw'); let component; beforeEach(() => { - loadFixtures('environments/environments'); + loadFixtures('static/environments/environments.html.raw'); }); describe('successfull request', () => { diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6 index 8ecd01f9a83..58f6fb96afb 100644 --- a/spec/javascripts/environments/mock_data.js.es6 +++ b/spec/javascripts/environments/mock_data.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable no-unused-vars */ + const environmentsList = [ { id: 31, @@ -134,6 +134,8 @@ const environmentsList = [ }, ]; +window.environmentsList = environmentsList; + const environment = { id: 4, name: 'production', @@ -147,3 +149,5 @@ const environment = { created_at: '2016-12-16T11:51:04.690Z', updated_at: '2016-12-16T12:04:51.133Z', }; + +window.environment = environment; diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 new file mode 100644 index 00000000000..5eba4343a1d --- /dev/null +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 @@ -0,0 +1,40 @@ +//= require filtered_search/dropdown_utils +//= require filtered_search/filtered_search_tokenizer +//= require filtered_search/filtered_search_dropdown +//= require filtered_search/dropdown_user + +(() => { + describe('Dropdown User', () => { + describe('getSearchInput', () => { + let dropdownUser; + + beforeEach(() => { + spyOn(gl.FilteredSearchDropdown.prototype, 'constructor').and.callFake(() => {}); + spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {}); + + dropdownUser = new gl.DropdownUser(); + }); + + it('should not return the double quote found in value', () => { + spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ + lastToken: { + value: '"johnny appleseed', + }, + }); + + expect(dropdownUser.getSearchInput()).toBe('johnny appleseed'); + }); + + it('should not return the single quote found in value', () => { + spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ + lastToken: { + value: '\'larry boy', + }, + }); + + expect(dropdownUser.getSearchInput()).toBe('larry boy'); + }); + }); + }); +})(); diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 index c8b5c2b36ad..a508dacf7f0 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 @@ -23,6 +23,7 @@ `); spyOn(gl.FilteredSearchManager.prototype, 'bindEvents').and.callFake(() => {}); + spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {}); spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); spyOn(gl.utils, 'getParameterByName').and.returnValue(null); diff --git a/spec/javascripts/fixtures/environments/table.html.haml b/spec/javascripts/fixtures/environments/table.html.haml index 1ea1725c561..59edc0396d2 100644 --- a/spec/javascripts/fixtures/environments/table.html.haml +++ b/spec/javascripts/fixtures/environments/table.html.haml @@ -3,7 +3,7 @@ %tr %th Environment %th Last deployment - %th Build + %th Job %th Commit %th %th diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 index 06fa64b1b4e..4e7eed2767c 100644 --- a/spec/javascripts/gl_dropdown_spec.js.es6 +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -44,6 +44,7 @@ describe('Dropdown', function describeDropdown() { preloadFixtures('static/gl_dropdown.html.raw'); + loadJSONFixtures('projects.json'); function initDropDown(hasRemote, isFilterable) { this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({ diff --git a/spec/javascripts/gl_form_spec.js.es6 b/spec/javascripts/gl_form_spec.js.es6 new file mode 100644 index 00000000000..b5f99483bfb --- /dev/null +++ b/spec/javascripts/gl_form_spec.js.es6 @@ -0,0 +1,122 @@ +/* global autosize */ +/*= require gl_form */ +/*= require autosize */ +/*= require lib/utils/text_utility */ +/*= require lib/utils/common_utils */ + +describe('GLForm', () => { + const global = window.gl || (window.gl = {}); + const GLForm = global.GLForm; + + it('should be defined in the global scope', () => { + expect(GLForm).toBeDefined(); + }); + + describe('when instantiated', function () { + beforeEach((done) => { + this.form = $('<form class="gfm-form"><textarea class="js-gfm-input"></form>'); + this.textarea = this.form.find('textarea'); + spyOn($.prototype, 'off').and.returnValue(this.textarea); + spyOn($.prototype, 'on').and.returnValue(this.textarea); + spyOn($.prototype, 'css'); + spyOn(window, 'autosize'); + + this.glForm = new GLForm(this.form); + setTimeout(() => { + $.prototype.off.calls.reset(); + $.prototype.on.calls.reset(); + $.prototype.css.calls.reset(); + autosize.calls.reset(); + done(); + }); + }); + + describe('.setupAutosize', () => { + beforeEach((done) => { + this.glForm.setupAutosize(); + setTimeout(() => { + done(); + }); + }); + + it('should register an autosize event handler on the textarea', () => { + expect($.prototype.off).toHaveBeenCalledWith('autosize:resized'); + expect($.prototype.on).toHaveBeenCalledWith('autosize:resized', jasmine.any(Function)); + }); + + it('should register a mouseup event handler on the textarea', () => { + expect($.prototype.off).toHaveBeenCalledWith('mouseup.autosize'); + expect($.prototype.on).toHaveBeenCalledWith('mouseup.autosize', jasmine.any(Function)); + }); + + it('should autosize the textarea', () => { + expect(autosize).toHaveBeenCalledWith(jasmine.any(Object)); + }); + + it('should set the resize css property to vertical', () => { + expect($.prototype.css).toHaveBeenCalledWith('resize', 'vertical'); + }); + }); + + describe('.setHeightData', () => { + beforeEach(() => { + spyOn($.prototype, 'data'); + spyOn($.prototype, 'outerHeight').and.returnValue(200); + this.glForm.setHeightData(); + }); + + it('should set the height data attribute', () => { + expect($.prototype.data).toHaveBeenCalledWith('height', 200); + }); + + it('should call outerHeight', () => { + expect($.prototype.outerHeight).toHaveBeenCalled(); + }); + }); + + describe('.destroyAutosize', () => { + describe('when called', () => { + beforeEach(() => { + spyOn($.prototype, 'data'); + spyOn($.prototype, 'outerHeight').and.returnValue(200); + spyOn(window, 'outerHeight').and.returnValue(400); + spyOn(autosize, 'destroy'); + + this.glForm.destroyAutosize(); + }); + + it('should call outerHeight', () => { + expect($.prototype.outerHeight).toHaveBeenCalled(); + }); + + it('should get data-height attribute', () => { + expect($.prototype.data).toHaveBeenCalledWith('height'); + }); + + it('should call autosize destroy', () => { + expect(autosize.destroy).toHaveBeenCalledWith(this.textarea); + }); + + it('should set the data-height attribute', () => { + expect($.prototype.data).toHaveBeenCalledWith('height', 200); + }); + + it('should set the outerHeight', () => { + expect($.prototype.outerHeight).toHaveBeenCalledWith(200); + }); + + it('should set the css', () => { + expect($.prototype.css).toHaveBeenCalledWith('max-height', window.outerHeight); + }); + }); + + it('should return undefined if the data-height equals the outerHeight', () => { + spyOn($.prototype, 'outerHeight').and.returnValue(200); + spyOn($.prototype, 'data').and.returnValue(200); + spyOn(autosize, 'destroy'); + expect(this.glForm.destroyAutosize()).toBeUndefined(); + expect(autosize.destroy).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/spec/javascripts/helpers/class_spec_helper.js.es6 b/spec/javascripts/helpers/class_spec_helper.js.es6 index 92a20687ec5..d3c37d39431 100644 --- a/spec/javascripts/helpers/class_spec_helper.js.es6 +++ b/spec/javascripts/helpers/class_spec_helper.js.es6 @@ -1,5 +1,3 @@ -/* eslint-disable no-unused-vars */ - class ClassSpecHelper { static itShouldBeAStaticMethod(base, method) { return it('should be a static method', () => { @@ -7,3 +5,5 @@ class ClassSpecHelper { }); } } + +window.ClassSpecHelper = ClassSpecHelper; diff --git a/spec/javascripts/issuable_time_tracker_spec.js.es6 b/spec/javascripts/issuable_time_tracker_spec.js.es6 index a1e979e8d09..c5671af235e 100644 --- a/spec/javascripts/issuable_time_tracker_spec.js.es6 +++ b/spec/javascripts/issuable_time_tracker_spec.js.es6 @@ -4,7 +4,7 @@ //= require issuable/time_tracking/components/time_tracker function initTimeTrackingComponent(opts) { - fixture.set(` + setFixtures(` <div> <div id="mock-container"></div> </div> diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index 1ce8f28e568..32c96e2a088 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -10,9 +10,9 @@ // IE11 will return a relative pathname while other browsers will return a full pathname. // parseUrl uses an anchor element for parsing an url. With relative urls, the anchor // element will create an absolute url relative to the current execution context. - // The JavaScript test suite is executed at '/teaspoon' which will lead to an absolute - // url starting with '/teaspoon'. - expect(gl.utils.parseUrl('" test="asf"').pathname).toEqual('/teaspoon/%22%20test=%22asf%22'); + // The JavaScript test suite is executed at '/' which will lead to an absolute url + // starting with '/'. + expect(gl.utils.parseUrl('" test="asf"').pathname).toContain('/%22%20test=%22asf%22'); }); }); @@ -42,9 +42,13 @@ }); describe('gl.utils.getParameterByName', () => { + beforeEach(() => { + window.history.pushState({}, null, '?scope=all&p=2'); + }); + it('should return valid parameter', () => { - const value = gl.utils.getParameterByName('reporter'); - expect(value).toBe('Console'); + const value = gl.utils.getParameterByName('scope'); + expect(value).toBe('all'); }); it('should return invalid parameter', () => { diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index bf45100af03..6f1d6406897 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -1,6 +1,7 @@ /* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, quote-props, no-var, max-len */ /*= require merge_request_widget */ +/*= require smart_interval */ /*= require lib/utils/datetime_utility */ (function() { @@ -21,7 +22,11 @@ normal: "Build {{status}}" }, gitlab_icon: "gitlab_logo.png", - builds_path: "http://sampledomain.local/sampleBuildsPath" + ci_pipeline: 80, + ci_sha: "12a34bc5", + builds_path: "http://sampledomain.local/sampleBuildsPath", + commits_path: "http://sampledomain.local/commits", + pipeline_path: "http://sampledomain.local/pipelines" }; this["class"] = new window.gl.MergeRequestWidget(this.opts); }); @@ -118,10 +123,11 @@ }); }); - return describe('getCIStatus', function() { + describe('getCIStatus', function() { beforeEach(function() { this.ciStatusData = { "title": "Sample MR title", + "pipeline": 80, "sha": "12a34bc5", "status": "success", "coverage": 98 @@ -165,6 +171,22 @@ this["class"].getCIStatus(true); return expect(spy).not.toHaveBeenCalled(); }); + it('should update the pipeline URL when the pipeline changes', function() { + var spy; + spy = spyOn(this["class"], 'updatePipelineUrls').and.stub(); + this["class"].getCIStatus(false); + this.ciStatusData.pipeline += 1; + this["class"].getCIStatus(false); + return expect(spy).toHaveBeenCalled(); + }); + it('should update the commit URL when the sha changes', function() { + var spy; + spy = spyOn(this["class"], 'updateCommitUrls').and.stub(); + this["class"].getCIStatus(false); + this.ciStatusData.sha = "9b50b99a"; + this["class"].getCIStatus(false); + return expect(spy).toHaveBeenCalled(); + }); }); }); }).call(this); diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index 0202c9ba85e..e562385a6c6 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -17,6 +17,8 @@ describe('Project Title', function() { preloadFixtures('static/project_title.html.raw'); + loadJSONFixtures('projects.json'); + beforeEach(function() { loadFixtures('static/project_title.html.raw'); return this.project = new Project(); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 942778229b5..3a01a534557 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -37,6 +37,8 @@ describe('RightSidebar', function() { var fixtureName = 'issues/open-issue.html.raw'; preloadFixtures(fixtureName); + loadJSONFixtures('todos.json'); + beforeEach(function() { loadFixtures(fixtureName); this.sidebar = new Sidebar; diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index 386fc8f514e..e0a5a7927bb 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -11,9 +11,9 @@ beforeEach(function() { loadFixtures(fixtureName); document.querySelector('.js-new-note-form').classList.add('js-main-target-form'); - return this.shortcut = new ShortcutsIssuable(); + this.shortcut = new ShortcutsIssuable(); }); - return describe('#replyWithSelectedText', function() { + describe('#replyWithSelectedText', function() { var stubSelection; // Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML. stubSelection = function(html) { @@ -24,56 +24,57 @@ }; }; beforeEach(function() { - return this.selector = 'form.js-main-target-form textarea#note_note'; + this.selector = 'form.js-main-target-form textarea#note_note'; }); describe('with empty selection', function() { - return it('does nothing', function() { - stubSelection(''); + it('does not return an error', function() { this.shortcut.replyWithSelectedText(); - return expect($(this.selector).val()).toBe(''); + expect($(this.selector).val()).toBe(''); + }); + it('triggers `input`', function() { + var focused = false; + $(this.selector).on('focus', function() { + focused = true; + }); + this.shortcut.replyWithSelectedText(); + expect(focused).toBe(true); }); }); describe('with any selection', function() { beforeEach(function() { - return stubSelection('<p>Selected text.</p>'); + stubSelection('<p>Selected text.</p>'); }); it('leaves existing input intact', function() { $(this.selector).val('This text was already here.'); expect($(this.selector).val()).toBe('This text was already here.'); this.shortcut.replyWithSelectedText(); - return expect($(this.selector).val()).toBe("This text was already here.\n\n> Selected text.\n\n"); + expect($(this.selector).val()).toBe("This text was already here.\n\n> Selected text.\n\n"); }); it('triggers `input`', function() { - var triggered; - triggered = false; + var triggered = false; $(this.selector).on('input', function() { - return triggered = true; + triggered = true; }); this.shortcut.replyWithSelectedText(); - return expect(triggered).toBe(true); + expect(triggered).toBe(true); }); - return it('triggers `focus`', function() { - var focused; - focused = false; - $(this.selector).on('focus', function() { - return focused = true; - }); + it('triggers `focus`', function() { this.shortcut.replyWithSelectedText(); - return expect(focused).toBe(true); + expect(document.activeElement).toBe(document.querySelector(this.selector)); }); }); describe('with a one-line selection', function() { - return it('quotes the selection', function() { + it('quotes the selection', function() { stubSelection('<p>This text has been selected.</p>'); this.shortcut.replyWithSelectedText(); - return expect($(this.selector).val()).toBe("> This text has been selected.\n\n"); + expect($(this.selector).val()).toBe("> This text has been selected.\n\n"); }); }); - return describe('with a multi-line selection', function() { - return it('quotes the selected lines as a group', function() { + describe('with a multi-line selection', function() { + it('quotes the selected lines as a group', function() { stubSelection("<p>Selected line one.</p>\n\n<p>Selected line two.</p>\n\n<p>Selected line three.</p>"); this.shortcut.replyWithSelectedText(); - return expect($(this.selector).val()).toBe("> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n"); + expect($(this.selector).val()).toBe("> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n"); }); }); }); diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index 80163fd72d3..0e2fb07ba7f 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -25,19 +25,20 @@ document.querySelector('#js-login-2fa-device'), document.querySelector('.js-2fa-form') ); + + // bypass automatic form submission within renderAuthenticated + spyOn(this.component, 'renderAuthenticated').and.returnValue(true); + return this.component.start(); }); it('allows authenticating via a U2F device', function() { - var authenticatedMessage, deviceResponse, inProgressMessage; + var inProgressMessage; inProgressMessage = this.container.find("p"); expect(inProgressMessage.text()).toContain("Trying to communicate with your device"); this.u2fDevice.respondToAuthenticateRequest({ deviceData: "this is data from the device" }); - authenticatedMessage = this.container.find("p"); - deviceResponse = this.container.find('#js-device-response'); - expect(authenticatedMessage.text()).toContain('We heard back from your U2F device. You have been authenticated.'); - return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}'); + expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}'); }); return describe("errors", function() { it("displays an error message", function() { @@ -51,7 +52,7 @@ return expect(errorMessage.text()).toContain("There was a problem communicating with your device"); }); return it("allows retrying authentication after an error", function() { - var authenticatedMessage, retryButton, setupButton; + var retryButton, setupButton; setupButton = this.container.find("#js-login-u2f-device"); setupButton.trigger('click'); this.u2fDevice.respondToAuthenticateRequest({ @@ -64,8 +65,7 @@ this.u2fDevice.respondToAuthenticateRequest({ deviceData: "this is data from the device" }); - authenticatedMessage = this.container.find("p"); - return expect(authenticatedMessage.text()).toContain("We heard back from your U2F device. You have been authenticated."); + expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}'); }); }); }); diff --git a/spec/javascripts/vue_pagination/pagination_spec.js.es6 b/spec/javascripts/vue_pagination/pagination_spec.js.es6 index 1a7f2bb5fb8..efb11211ce2 100644 --- a/spec/javascripts/vue_pagination/pagination_spec.js.es6 +++ b/spec/javascripts/vue_pagination/pagination_spec.js.es6 @@ -1,7 +1,6 @@ //= require vue //= require lib/utils/common_utils //= require vue_pagination/index -/* global fixture, gl */ describe('Pagination component', () => { let component; @@ -17,7 +16,7 @@ describe('Pagination component', () => { }; it('should render and start at page 1', () => { - fixture.set('<div class="test-pagination-container"></div>'); + setFixtures('<div class="test-pagination-container"></div>'); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), @@ -40,7 +39,7 @@ describe('Pagination component', () => { }); it('should go to the previous page', () => { - fixture.set('<div class="test-pagination-container"></div>'); + setFixtures('<div class="test-pagination-container"></div>'); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), @@ -61,7 +60,7 @@ describe('Pagination component', () => { }); it('should go to the next page', () => { - fixture.set('<div class="test-pagination-container"></div>'); + setFixtures('<div class="test-pagination-container"></div>'); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), @@ -82,7 +81,7 @@ describe('Pagination component', () => { }); it('should go to the last page', () => { - fixture.set('<div class="test-pagination-container"></div>'); + setFixtures('<div class="test-pagination-container"></div>'); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), @@ -103,7 +102,7 @@ describe('Pagination component', () => { }); it('should go to the first page', () => { - fixture.set('<div class="test-pagination-container"></div>'); + setFixtures('<div class="test-pagination-container"></div>'); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), @@ -124,7 +123,7 @@ describe('Pagination component', () => { }); it('should do nothing', () => { - fixture.set('<div class="test-pagination-container"></div>'); + setFixtures('<div class="test-pagination-container"></div>'); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), diff --git a/spec/lib/banzai/cross_project_reference_spec.rb b/spec/lib/banzai/cross_project_reference_spec.rb index 81b9a513ce3..deaabceef1c 100644 --- a/spec/lib/banzai/cross_project_reference_spec.rb +++ b/spec/lib/banzai/cross_project_reference_spec.rb @@ -24,7 +24,7 @@ describe Banzai::CrossProjectReference, lib: true do it 'returns the referenced project' do project2 = double('referenced project') - expect(Project).to receive(:find_with_namespace). + expect(Project).to receive(:find_by_full_path). with('cross/reference').and_return(project2) expect(project_from_ref('cross/reference')).to eq project2 diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb new file mode 100644 index 00000000000..f85a5dcbd8b --- /dev/null +++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Banzai::Filter::PlantumlFilter, lib: true do + include FilterSpecHelper + + it 'should replace plantuml pre tag with img tag' do + stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080") + input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>' + output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>' + doc = filter(input) + + expect(doc.to_s).to eq output + end + + it 'should not replace plantuml pre tag with img tag if disabled' do + stub_application_setting(plantuml_enabled: false) + input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>' + output = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre></pre></pre>' + doc = filter(input) + + expect(doc.to_s).to eq output + end + + it 'should not replace plantuml pre tag with img tag if url is invalid' do + stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid") + input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>' + output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> PlantUML Error: cannot connect to PlantUML server at "invalid"</pre></div></div>' + doc = filter(input) + + expect(doc.to_s).to eq output + 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 f824e2e1efe..49349035b3b 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -4,6 +4,33 @@ module Ci describe GitlabCiYamlProcessor, lib: true do let(:path) { 'path' } + describe '#build_attributes' do + context 'Coverage entry' do + subject { described_class.new(config, path).build_attributes(:rspec) } + + let(:config_base) { { rspec: { script: "rspec" } } } + let(:config) { YAML.dump(config_base) } + + context 'when config has coverage set at the global scope' do + before do + config_base.update(coverage: '/\(\d+\.\d+\) covered/') + end + + context "and 'rspec' job doesn't have coverage set" do + it { is_expected.to include(coverage_regex: '\(\d+\.\d+\) covered') } + end + + context "but 'rspec' job also has coverage set" do + before do + config_base[:rspec][:coverage] = '/Code coverage: \d+\.\d+/' + end + + it { is_expected.to include(coverage_regex: 'Code coverage: \d+\.\d+') } + end + end + end + end + describe "#builds_for_ref" do let(:type) { 'test' } @@ -21,6 +48,7 @@ module Ci stage_idx: 1, name: "rspec", commands: "pwd\nrspec", + coverage_regex: nil, tag_list: [], options: {}, allow_failure: false, @@ -435,6 +463,7 @@ module Ci stage_idx: 1, name: "rspec", commands: "pwd\nrspec", + coverage_regex: nil, tag_list: [], options: { image: "ruby:2.1", @@ -463,6 +492,7 @@ module Ci stage_idx: 1, name: "rspec", commands: "pwd\nrspec", + coverage_regex: nil, tag_list: [], options: { image: "ruby:2.5", @@ -702,6 +732,7 @@ module Ci stage_idx: 1, name: "rspec", commands: "pwd\nrspec", + coverage_regex: nil, tag_list: [], options: { image: "ruby:2.1", @@ -913,6 +944,7 @@ module Ci stage_idx: 1, name: "normal_job", commands: "test", + coverage_regex: nil, tag_list: [], options: {}, when: "on_success", @@ -958,6 +990,7 @@ module Ci stage_idx: 0, name: "job1", commands: "execute-script-for-job", + coverage_regex: nil, tag_list: [], options: {}, when: "on_success", @@ -970,6 +1003,7 @@ module Ci stage_idx: 0, name: "job2", commands: "execute-script-for-job", + coverage_regex: nil, tag_list: [], options: {}, when: "on_success", diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index f251c0dd25a..b234de4c772 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -58,58 +58,102 @@ describe Gitlab::Auth, lib: true do expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) end - it 'recognizes user lfs tokens' do - user = create(:user) - token = Gitlab::LfsToken.new(user).token + context 'while using LFS authenticate' do + it 'recognizes user lfs tokens' do + user = create(:user) + token = Gitlab::LfsToken.new(user).token - expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username) - expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities)) - end + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username) + expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities)) + end - it 'recognizes deploy key lfs tokens' do - key = create(:deploy_key) - token = Gitlab::LfsToken.new(key).token + it 'recognizes deploy key lfs tokens' do + key = create(:deploy_key) + token = Gitlab::LfsToken.new(key).token - expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}") - expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities)) - end + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}") + expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities)) + end - context "while using OAuth tokens as passwords" do - it 'succeeds for OAuth tokens with the `api` scope' do + it 'does not try password auth before oauth' do user = create(:user) - application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) - token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") + token = Gitlab::LfsToken.new(user).token + + expect(gl_auth).not_to receive(:find_with_user_password) + gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip') + end + end + + context 'while using OAuth tokens as passwords' do + let(:user) { create(:user) } + let(:token_w_api_scope) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') } + let(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) } + + it 'succeeds for OAuth tokens with the `api` scope' do expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'oauth2') - expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)) + expect(gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)) end it 'fails for OAuth tokens with other scopes' do - user = create(:user) - application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) - token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "read_user") + token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'read_user') expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'oauth2') expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) end + + it 'does not try password auth before oauth' do + expect(gl_auth).not_to receive(:find_with_user_password) + + gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip') + end end - context "while using personal access tokens as passwords" do - it 'succeeds for personal access tokens with the `api` scope' do - user = create(:user) - personal_access_token = create(:personal_access_token, user: user, scopes: ['api']) + context 'while using personal access tokens as passwords' do + let(:user) { create(:user) } + let(:token_w_api_scope) { create(:personal_access_token, user: user, scopes: ['api']) } + it 'succeeds for personal access tokens with the `api` scope' do expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.email) - expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities)) + expect(gl_auth.find_for_git_client(user.email, token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities)) end it 'fails for personal access tokens with other scopes' do - user = create(:user) personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: user.email) expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) end + + it 'does not try password auth before personal access tokens' do + expect(gl_auth).not_to receive(:find_with_user_password) + + gl_auth.find_for_git_client(user.email, token_w_api_scope.token, project: nil, ip: 'ip') + end + end + + context 'while using regular user and password' do + it 'falls through lfs authentication' do + user = create( + :user, + username: 'normal_user', + password: 'my-secret', + ) + + expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip')) + .to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) + end + + it 'falls through oauth authentication when the username is oauth2' do + user = create( + :user, + username: 'oauth2', + password: 'my-secret', + ) + + expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip')) + .to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) + end end it 'returns double nil for invalid credentials' do diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index 1e81eaef18c..b6e924d67be 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -24,7 +24,7 @@ describe Gitlab::ChatCommands::Command, service: true do it 'displays the help message' do expect(subject[:response_type]).to be(:ephemeral) - expect(subject[:text]).to start_with('Available commands') + expect(subject[:text]).to start_with('Unknown command') expect(subject[:text]).to match('/gitlab issue show') end end @@ -34,47 +34,7 @@ describe Gitlab::ChatCommands::Command, service: true do it 'rejects the actions' do expect(subject[:response_type]).to be(:ephemeral) - expect(subject[:text]).to start_with('Whoops! That action is not allowed') - end - end - - context 'issue is successfully created' do - let(:params) { { text: "issue create my new issue" } } - - before do - project.team << [user, :master] - end - - it 'presents the issue' do - expect(subject[:text]).to match("my new issue") - end - - it 'shows a link to the new issue' do - expect(subject[:text]).to match(/\/issues\/\d+/) - end - end - - context 'searching for an issue' do - let(:params) { { text: 'issue search find me' } } - let!(:issue) { create(:issue, project: project, title: 'find me') } - - before do - project.team << [user, :master] - end - - context 'a single issue is found' do - it 'presents the issue' do - expect(subject[:text]).to match(issue.title) - end - end - - context 'multiple issues found' do - let!(:issue2) { create(:issue, project: project, title: "someone find me") } - - it 'shows a link to the new issue' do - expect(subject[:text]).to match(issue.title) - expect(subject[:text]).to match(issue2.title) - end + expect(subject[:text]).to start_with('Whoops! This action is not allowed') end end @@ -90,7 +50,7 @@ describe Gitlab::ChatCommands::Command, service: true do context 'and user can not create deployment' do it 'returns action' do expect(subject[:response_type]).to be(:ephemeral) - expect(subject[:text]).to start_with('Whoops! That action is not allowed') + expect(subject[:text]).to start_with('Whoops! This action is not allowed') end end @@ -100,7 +60,7 @@ describe Gitlab::ChatCommands::Command, service: true do end it 'returns action' do - expect(subject[:text]).to include('Deployment from staging to production started.') + expect(subject[:text]).to include('Deployment started from staging to production') expect(subject[:response_type]).to be(:in_channel) end @@ -130,7 +90,7 @@ describe Gitlab::ChatCommands::Command, service: true do context 'IssueCreate is triggered' do let(:params) { { text: 'issue create my title' } } - it { is_expected.to eq(Gitlab::ChatCommands::IssueCreate) } + it { is_expected.to eq(Gitlab::ChatCommands::IssueNew) } end context 'IssueSearch is triggered' do diff --git a/spec/lib/gitlab/chat_commands/deploy_spec.rb b/spec/lib/gitlab/chat_commands/deploy_spec.rb index bd8099c92da..b3358a32161 100644 --- a/spec/lib/gitlab/chat_commands/deploy_spec.rb +++ b/spec/lib/gitlab/chat_commands/deploy_spec.rb @@ -15,8 +15,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do end context 'if no environment is defined' do - it 'returns nil' do - expect(subject).to be_nil + it 'does not execute an action' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq("No action found to be executed") end end @@ -26,8 +27,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do let!(:deployment) { create(:deployment, environment: staging, deployable: build) } context 'without actions' do - it 'returns nil' do - expect(subject).to be_nil + it 'does not execute an action' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq("No action found to be executed") end end @@ -37,8 +39,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do end it 'returns success result' do - expect(subject.type).to eq(:success) - expect(subject.message).to include('Deployment from staging to production started') + expect(subject[:response_type]).to be(:in_channel) + expect(subject[:text]).to start_with('Deployment started from staging to production') end context 'when duplicate action exists' do @@ -47,8 +49,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do end it 'returns error' do - expect(subject.type).to eq(:error) - expect(subject.message).to include('Too many actions defined') + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq('Too many actions defined') end end @@ -59,9 +61,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do name: 'teardown', environment: 'production') end - it 'returns success result' do - expect(subject.type).to eq(:success) - expect(subject.message).to include('Deployment from staging to production started') + it 'returns the success message' do + expect(subject[:response_type]).to be(:in_channel) + expect(subject[:text]).to start_with('Deployment started from staging to production') end end end diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_new_spec.rb index 6c71e79ff6d..84c22328064 100644 --- a/spec/lib/gitlab/chat_commands/issue_create_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_new_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::ChatCommands::IssueCreate, service: true do +describe Gitlab::ChatCommands::IssueNew, service: true do describe '#execute' do let(:project) { create(:empty_project) } let(:user) { create(:user) } @@ -18,7 +18,7 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do it 'creates the issue' do expect { subject }.to change { project.issues.count }.by(1) - expect(subject.title).to eq('bird is the word') + expect(subject[:response_type]).to be(:in_channel) end end @@ -41,6 +41,16 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do expect { subject }.to change { project.issues.count }.by(1) end end + + context 'issue cannot be created' do + let!(:issue) { create(:issue, project: project, title: 'bird is the word') } + let(:regex_match) { described_class.match("issue create #{'a' * 512}}") } + + it 'displays the errors' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to match("- Title is too long") + end + end end describe '.match' do diff --git a/spec/lib/gitlab/chat_commands/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/issue_search_spec.rb index 24c06a967fa..551ccb79a58 100644 --- a/spec/lib/gitlab/chat_commands/issue_search_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_search_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' describe Gitlab::ChatCommands::IssueSearch, service: true do describe '#execute' do - let!(:issue) { create(:issue, title: 'find me') } + let!(:issue) { create(:issue, project: project, title: 'find me') } let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') } - let(:project) { issue.project } + let(:project) { create(:empty_project) } let(:user) { issue.author } let(:regex_match) { described_class.match("issue search find") } @@ -14,7 +14,8 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do context 'when the user has no access' do it 'only returns the open issues' do - expect(subject).not_to include(confidential) + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to match("not found") end end @@ -24,13 +25,14 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do end it 'returns all results' do - expect(subject).to include(confidential, issue) + expect(subject).to have_key(:attachments) + expect(subject[:text]).to eq("Here are the 2 issues I found:") end end context 'without hits on the query' do it 'returns an empty collection' do - expect(subject).to be_empty + expect(subject[:text]).to match("not found") end end end diff --git a/spec/lib/gitlab/chat_commands/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/issue_show_spec.rb index 2eab73e49e5..1f20d0a44ce 100644 --- a/spec/lib/gitlab/chat_commands/issue_show_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_show_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' describe Gitlab::ChatCommands::IssueShow, service: true do describe '#execute' do - let(:issue) { create(:issue) } - let(:project) { issue.project } + let(:issue) { create(:issue, project: project) } + let(:project) { create(:empty_project) } let(:user) { issue.author } let(:regex_match) { described_class.match("issue show #{issue.iid}") } @@ -16,15 +16,19 @@ describe Gitlab::ChatCommands::IssueShow, service: true do end context 'the issue exists' do + let(:title) { subject[:attachments].first[:title] } + it 'returns the issue' do - expect(subject.iid).to be issue.iid + expect(subject[:response_type]).to be(:in_channel) + expect(title).to start_with(issue.title) end context 'when its reference is given' do let(:regex_match) { described_class.match("issue show #{issue.to_reference}") } it 'shows the issue' do - expect(subject.iid).to be issue.iid + expect(subject[:response_type]).to be(:in_channel) + expect(title).to start_with(issue.title) end end end @@ -32,17 +36,24 @@ describe Gitlab::ChatCommands::IssueShow, service: true do context 'the issue does not exist' do let(:regex_match) { described_class.match("issue show 2343242") } - it "returns nil" do - expect(subject).to be_nil + it "returns not found" do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to match("not found") end end end - describe 'self.match' do + describe '.match' do it 'matches the iid' do match = described_class.match("issue show 123") expect(match[:iid]).to eq("123") end + + it 'accepts a reference' do + match = described_class.match("issue show #{Issue.reference_prefix}123") + + expect(match[:iid]).to eq("123") + end end end diff --git a/spec/lib/gitlab/chat_commands/presenters/access_spec.rb b/spec/lib/gitlab/chat_commands/presenters/access_spec.rb new file mode 100644 index 00000000000..ae41d75ab0c --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/access_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::Access do + describe '#access_denied' do + subject { described_class.new.access_denied } + + it { is_expected.to be_a(Hash) } + + it 'displays an error message' do + expect(subject[:text]).to match("is not allowed") + expect(subject[:response_type]).to be(:ephemeral) + end + end + + describe '#not_found' do + subject { described_class.new.not_found } + + it { is_expected.to be_a(Hash) } + + it 'tells the user the resource was not found' do + expect(subject[:text]).to match("not found!") + expect(subject[:response_type]).to be(:ephemeral) + end + end + + describe '#authorize' do + context 'with an authorization URL' do + subject { described_class.new('http://authorize.me').authorize } + + it { is_expected.to be_a(Hash) } + + it 'tells the user to authorize' do + expect(subject[:text]).to match("connect your GitLab account") + expect(subject[:response_type]).to be(:ephemeral) + end + end + + context 'without authorization url' do + subject { described_class.new.authorize } + + it { is_expected.to be_a(Hash) } + + it 'tells the user to authorize' do + expect(subject[:text]).to match("Couldn't identify you") + expect(subject[:response_type]).to be(:ephemeral) + end + end + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb new file mode 100644 index 00000000000..dc2dd300072 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::Deploy do + let(:build) { create(:ci_build) } + + describe '#present' do + subject { described_class.new(build).present('staging', 'prod') } + + it { is_expected.to have_key(:text) } + it { is_expected.to have_key(:response_type) } + it { is_expected.to have_key(:status) } + it { is_expected.not_to have_key(:attachments) } + + it 'messages the channel of the deploy' do + expect(subject[:response_type]).to be(:in_channel) + expect(subject[:text]).to start_with("Deployment started from staging to prod") + end + end + + describe '#no_actions' do + subject { described_class.new(nil).no_actions } + + it { is_expected.to have_key(:text) } + it { is_expected.to have_key(:response_type) } + it { is_expected.to have_key(:status) } + it { is_expected.not_to have_key(:attachments) } + + it 'tells the user there is no action' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq("No action found to be executed") + end + end + + describe '#too_many_actions' do + subject { described_class.new([]).too_many_actions } + + it { is_expected.to have_key(:text) } + it { is_expected.to have_key(:response_type) } + it { is_expected.to have_key(:status) } + it { is_expected.not_to have_key(:attachments) } + + it 'tells the user there is no action' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq("Too many actions defined") + end + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb new file mode 100644 index 00000000000..17fcdbc2452 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::IssueNew do + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + let(:attachment) { subject[:attachments].first } + + subject { described_class.new(issue).present } + + it { is_expected.to be_a(Hash) } + + it 'shows the issue' do + expect(subject[:response_type]).to be(:in_channel) + expect(subject).to have_key(:attachments) + expect(attachment[:title]).to start_with(issue.title) + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb new file mode 100644 index 00000000000..ec6d3e34a96 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::IssueSearch do + let(:project) { create(:empty_project) } + let(:message) { subject[:text] } + + before { create_list(:issue, 2, project: project) } + + subject { described_class.new(project.issues).present } + + it 'formats the message correct' do + is_expected.to have_key(:text) + is_expected.to have_key(:status) + is_expected.to have_key(:response_type) + is_expected.to have_key(:attachments) + end + + it 'shows a list of results' do + expect(subject[:response_type]).to be(:ephemeral) + + expect(message).to start_with("Here are the 2 issues I found") + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb new file mode 100644 index 00000000000..5b678d31fce --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::IssueShow do + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + let(:attachment) { subject[:attachments].first } + + subject { described_class.new(issue).present } + + it { is_expected.to be_a(Hash) } + + it 'shows the issue' do + expect(subject[:response_type]).to be(:in_channel) + expect(subject).to have_key(:attachments) + expect(attachment[:title]).to start_with(issue.title) + end + + context 'with upvotes' do + before do + create(:award_emoji, :upvote, awardable: issue) + end + + it 'shows the upvote count' do + expect(subject[:response_type]).to be(:in_channel) + expect(attachment[:text]).to start_with("**Open** · :+1: 1") + end + end + + context 'confidential issue' do + let(:issue) { create(:issue, project: project) } + + it 'shows an ephemeral response' do + expect(subject[:response_type]).to be(:in_channel) + expect(attachment[:text]).to start_with("**Open**") + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb new file mode 100644 index 00000000000..4c6bd859552 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Entry::Coverage do + let(:entry) { described_class.new(config) } + + describe 'validations' do + context "when entry config value doesn't have the surrounding '/'" do + let(:config) { 'Code coverage: \d+\.\d+' } + + describe '#errors' do + subject { entry.errors } + it { is_expected.to include(/coverage config must be a regular expression/) } + end + + describe '#valid?' do + subject { entry } + it { is_expected.not_to be_valid } + end + end + + context "when entry config value has the surrounding '/'" do + let(:config) { '/Code coverage: \d+\.\d+/' } + + describe '#value' do + subject { entry.value } + it { is_expected.to eq(config[1...-1]) } + end + + describe '#errors' do + subject { entry.errors } + it { is_expected.to be_empty } + end + + describe '#valid?' do + subject { entry } + it { is_expected.to be_valid } + end + end + + context 'when entry value is not valid' do + let(:config) { '(malformed regexp' } + + describe '#errors' do + subject { entry.errors } + it { is_expected.to include(/coverage config must be a regular expression/) } + end + + describe '#valid?' do + subject { entry } + it { is_expected.not_to be_valid } + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index e64c8d46bd8..d4f1780b174 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -4,12 +4,17 @@ describe Gitlab::Ci::Config::Entry::Global do let(:global) { described_class.new(hash) } describe '.nodes' do - it 'can contain global config keys' do - expect(described_class.nodes).to include :before_script + it 'returns a hash' do + expect(described_class.nodes).to be_a(Hash) end - it 'returns a hash' do - expect(described_class.nodes).to be_a Hash + context 'when filtering all the entry/node names' do + it 'contains the expected node names' do + node_names = described_class.nodes.keys + expect(node_names).to match_array(%i[before_script image services + after_script variables stages + types cache coverage]) + end end end @@ -35,7 +40,7 @@ describe Gitlab::Ci::Config::Entry::Global do end it 'creates node object for each entry' do - expect(global.descendants.count).to eq 8 + expect(global.descendants.count).to eq 9 end it 'creates node object using valid class' do @@ -176,7 +181,7 @@ describe Gitlab::Ci::Config::Entry::Global do describe '#nodes' do it 'instantizes all nodes' do - expect(global.descendants.count).to eq 8 + expect(global.descendants.count).to eq 9 end it 'contains unspecified nodes' do diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index fc9b8b86dc4..d20f4ec207d 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -3,6 +3,20 @@ require 'spec_helper' describe Gitlab::Ci::Config::Entry::Job do let(:entry) { described_class.new(config, name: :rspec) } + describe '.nodes' do + context 'when filtering all the entry/node names' do + subject { described_class.nodes.keys } + + let(:result) do + %i[before_script script stage type after_script cache + image services only except variables artifacts + environment coverage] + end + + it { is_expected.to match_array result } + end + end + describe 'validations' do before { entry.compose! } diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb index b3c07347de1..8ad9b7cdf07 100644 --- a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb +++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb @@ -62,7 +62,7 @@ describe Gitlab::Ci::Status::Build::Cancelable do end describe '#action_icon' do - it { expect(subject.action_icon).to eq 'ban' } + it { expect(subject.action_icon).to eq 'icon_action_cancel' } end describe '#action_title' do diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb index f1b50a59ce6..f3e72ea1796 100644 --- a/spec/lib/gitlab/ci/status/build/play_spec.rb +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -44,7 +44,7 @@ describe Gitlab::Ci::Status::Build::Play do end describe '#action_icon' do - it { expect(subject.action_icon).to eq 'play' } + it { expect(subject.action_icon).to eq 'icon_action_play' } end describe '#action_title' do diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb index 62036f8ec5d..2db0f8d29bd 100644 --- a/spec/lib/gitlab/ci/status/build/retryable_spec.rb +++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb @@ -62,7 +62,7 @@ describe Gitlab::Ci::Status::Build::Retryable do end describe '#action_icon' do - it { expect(subject.action_icon).to eq 'refresh' } + it { expect(subject.action_icon).to eq 'icon_action_retry' } end describe '#action_title' do diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb index 597e02e86e4..41c2b624774 100644 --- a/spec/lib/gitlab/ci/status/build/stop_spec.rb +++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb @@ -46,7 +46,7 @@ describe Gitlab::Ci::Status::Build::Stop do end describe '#action_icon' do - it { expect(subject.action_icon).to eq 'stop' } + it { expect(subject.action_icon).to eq 'icon_action_stop' } end describe '#action_title' do diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index 1e21270d928..5893485634d 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -12,11 +12,11 @@ describe Gitlab::Diff::Highlight, lib: true do context "with a diff file" do let(:subject) { Gitlab::Diff::Highlight.new(diff_file, repository: project.repository).highlight } - it 'should return Gitlab::Diff::Line elements' do + it 'returns Gitlab::Diff::Line elements' do expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line) end - it 'should not modify "match" lines' do + it 'does not modify "match" lines' do expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') end @@ -43,11 +43,11 @@ describe Gitlab::Diff::Highlight, lib: true do context "with diff lines" do let(:subject) { Gitlab::Diff::Highlight.new(diff_file.diff_lines, repository: project.repository).highlight } - it 'should return Gitlab::Diff::Line elements' do + it 'returns Gitlab::Diff::Line elements' do expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line) end - it 'should not modify "match" lines' do + it 'does not modify "match" lines' do expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') end diff --git a/spec/lib/gitlab/diff/parallel_diff_spec.rb b/spec/lib/gitlab/diff/parallel_diff_spec.rb index fe5fa048413..0f779339c54 100644 --- a/spec/lib/gitlab/diff/parallel_diff_spec.rb +++ b/spec/lib/gitlab/diff/parallel_diff_spec.rb @@ -12,7 +12,7 @@ describe Gitlab::Diff::ParallelDiff, lib: true do subject { described_class.new(diff_file) } describe '#parallelize' do - it 'should return an array of arrays containing the parsed diff' do + it 'returns an array of arrays containing the parsed diff' do diff_lines = diff_file.highlighted_diff_lines expected = [ # Unchanged lines diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index f5822fed37c..8e3e4034c8f 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -99,7 +99,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do Files::CreateService.new( project, current_user, - source_branch: branch_name, + start_branch: branch_name, target_branch: branch_name, commit_message: "Create file", file_path: file_name, @@ -112,7 +112,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do Files::UpdateService.new( project, current_user, - source_branch: branch_name, + start_branch: branch_name, target_branch: branch_name, commit_message: "Update file", file_path: file_name, @@ -125,7 +125,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do Files::DeleteService.new( project, current_user, - source_branch: branch_name, + start_branch: branch_name, target_branch: branch_name, commit_message: "Delete file", file_path: file_name @@ -1640,7 +1640,9 @@ describe Gitlab::Diff::PositionTracer, lib: true do } merge_request = create(:merge_request, source_branch: second_create_file_commit.sha, target_branch: branch_name, source_project: project) - repository.merge(current_user, merge_request, options) + + repository.merge(current_user, merge_request.diff_head_sha, merge_request, options) + project.commit(branch_name) end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index b080be62b34..116ab16ae74 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -209,7 +209,13 @@ describe Gitlab::GitAccess, lib: true do stub_git_hooks project.repository.add_branch(user, unprotected_branch, 'feature') target_branch = project.repository.lookup('feature') - source_branch = project.repository.commit_file(user, FFaker::InternetSE.login_user_name, FFaker::HipsterIpsum.paragraph, FFaker::HipsterIpsum.sentence, unprotected_branch, false) + source_branch = project.repository.commit_file( + user, + FFaker::InternetSE.login_user_name, + FFaker::HipsterIpsum.paragraph, + message: FFaker::HipsterIpsum.sentence, + branch_name: unprotected_branch, + update: false) rugged = project.repository.rugged author = { email: "email@example.com", time: Time.now, name: "Example Git User" } diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index fadfe4d378e..e177d883158 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -12,7 +12,7 @@ describe Gitlab::Highlight, lib: true do Gitlab::Highlight.highlight_lines(project.repository, commit.id, 'files/ruby/popen.rb') end - it 'should properly highlight all the lines' do + it 'highlights all the lines properly' do expect(lines[4]).to eq(%Q{<span id="LC5" class="line"> <span class="kp">extend</span> <span class="nb">self</span></span>\n}) expect(lines[21]).to eq(%Q{<span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n}) expect(lines[26]).to eq(%Q{<span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n}) diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 7fb6829f582..20241d4d63e 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -52,6 +52,7 @@ snippets: - project - notes - award_emoji +- user_agent_detail releases: - project project_members: diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb index 0b7984d6ca9..f2cb028206f 100644 --- a/spec/lib/gitlab/import_export/members_mapper_spec.rb +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -92,5 +92,29 @@ describe Gitlab::ImportExport::MembersMapper, services: true do expect(members_mapper.map[exported_user_id]).to eq(user2.id) end end + + context 'importer same as group member' do + let(:user2) { create(:admin, authorized_projects_populated: true) } + let(:group) { create(:group) } + let(:project) { create(:empty_project, :public, name: 'searchable_project', namespace: group) } + let(:members_mapper) do + described_class.new( + exported_members: exported_members, user: user2, project: project) + end + + before do + group.add_users([user, user2], GroupMember::DEVELOPER) + end + + it 'maps the project member' do + expect(members_mapper.map[exported_user_id]).to eq(user2.id) + end + + it 'maps the project member if it already exists' do + project.add_master(user2) + + expect(members_mapper.map[exported_user_id]).to eq(user2.id) + end + end end end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 2c0750c3377..2e9f60432b4 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -6981,11 +6981,16 @@ ] } ], - "variables": [ - - ], "triggers": [ - + { + "id": 123, + "token": "cdbfasdf44a5958c83654733449e585", + "project_id": null, + "deleted_at": null, + "created_at": "2017-01-16T15:25:28.637Z", + "updated_at": "2017-01-16T15:25:28.637Z", + "gl_project_id": 123 + } ], "deploy_keys": [ 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 4b07fa53bf5..40d7d59f03b 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -197,6 +197,20 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do expect(restored_project_json).to be true end end + + context 'tokens are regenerated' do + before do + restored_project_json + end + + it 'has a new CI trigger token' do + expect(Ci::Trigger.where(token: 'cdbfasdf44a5958c83654733449e585')).to be_empty + end + + it 'has a new CI build token' do + expect(Ci::Build.where(token: 'abcd')).to be_empty + end + end end end end diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb index db0084d6823..57e412b0cef 100644 --- a/spec/lib/gitlab/import_export/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb @@ -55,8 +55,8 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do expect(created_object.project_id).to eq(project.id) end - it 'has a token' do - expect(created_object.token).to eq(token) + it 'has a nil token' do + expect(created_object.token).to eq(nil) end context 'original service exists' do @@ -178,4 +178,15 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do expect(created_object.author).to eq(new_user) end end + + context 'encrypted attributes' do + let(:relation_sym) { 'Ci::Variable' } + let(:relation_hash) do + create(:ci_variable).as_json + end + + it 'has no value for the encrypted attribute' do + expect(created_object.value).to be_nil + end + end end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 493bc2db21a..95b230e4f5c 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -222,6 +222,7 @@ CommitStatus: - queued_at - token - lock_version +- coverage_regex Ci::Variable: - id - project_id diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index b9d12c3c24c..9dd997aa7dc 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::LDAP::Access, lib: true do it { is_expected.to be_falsey } - it 'should block user in GitLab' do + it 'blocks user in GitLab' do expect(access).to receive(:block_user).with(user, 'does not exist anymore') access.allowed? diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb index 45cec65a284..1335a2b8f35 100644 --- a/spec/lib/gitlab/template/issue_template_spec.rb +++ b/spec/lib/gitlab/template/issue_template_spec.rb @@ -4,16 +4,14 @@ describe Gitlab::Template::IssueTemplate do subject { described_class } let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:file_path_1) { '.gitlab/issue_templates/bug.md' } - let(:file_path_2) { '.gitlab/issue_templates/template_test.md' } - let(:file_path_3) { '.gitlab/issue_templates/feature_proposal.md' } - - before do - project.add_user(user, Gitlab::Access::MASTER) - project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false) - project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false) - project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false) + + let(:project) do + create(:project, + :repository, + create_template: { + user: user, + access: Gitlab::Access::MASTER, + path: 'issue_templates' }) end describe '.all' do diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb index ae51b79be22..320b870309a 100644 --- a/spec/lib/gitlab/template/merge_request_template_spec.rb +++ b/spec/lib/gitlab/template/merge_request_template_spec.rb @@ -4,16 +4,14 @@ describe Gitlab::Template::MergeRequestTemplate do subject { described_class } let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:file_path_1) { '.gitlab/merge_request_templates/bug.md' } - let(:file_path_2) { '.gitlab/merge_request_templates/template_test.md' } - let(:file_path_3) { '.gitlab/merge_request_templates/feature_proposal.md' } - - before do - project.add_user(user, Gitlab::Access::MASTER) - project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false) - project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false) - project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false) + + let(:project) do + create(:project, + :repository, + create_template: { + user: user, + access: Gitlab::Access::MASTER, + path: 'merge_request_templates' }) end describe '.all' do diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 47cd5075a7d..4080092405d 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -221,6 +221,47 @@ describe Ci::Build, :models do end end + describe '#coverage_regex' do + subject { build.coverage_regex } + + context 'when project has build_coverage_regex set' do + let(:project_regex) { '\(\d+\.\d+\) covered' } + + before do + project.build_coverage_regex = project_regex + end + + context 'and coverage_regex attribute is not set' do + it { is_expected.to eq(project_regex) } + end + + context 'but coverage_regex attribute is also set' do + let(:build_regex) { 'Code coverage: \d+\.\d+' } + + before do + build.coverage_regex = build_regex + end + + it { is_expected.to eq(build_regex) } + end + end + + context 'when neither project nor build has coverage regex set' do + it { is_expected.to be_nil } + end + end + + describe '#update_coverage' do + context "regarding coverage_regex's value," do + it "saves the correct extracted coverage value" do + build.coverage_regex = '\(\d+.\d+\%\) covered' + allow(build).to receive(:trace) { 'Coverage 1033 / 1051 LOC (98.29%) covered' } + expect(build).to receive(:update_attributes).with(coverage: 98.29) { true } + expect(build.update_coverage).to be true + end + end + end + describe 'deployment' do describe '#last_deployment' do subject { build.last_deployment } @@ -443,11 +484,11 @@ describe Ci::Build, :models do let!(:build) { create(:ci_build, :trace, :success, :artifacts) } subject { build.erased? } - context 'build has not been erased' do + context 'job has not been erased' do it { is_expected.to be_falsey } end - context 'build has been erased' do + context 'job has been erased' do before do build.erase end diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb index 3b7cc7d9e2e..9053485939e 100644 --- a/spec/models/cycle_analytics/code_spec.rb +++ b/spec/models/cycle_analytics/code_spec.rb @@ -27,15 +27,13 @@ describe 'CycleAnalytics#code', feature: true do context "when a regular merge request (that doesn't close the issue) is created" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) + issue = create(:issue, project: project) - create_commit_referencing_issue(issue) - create_merge_request_closing_issue(issue, message: "Closes nothing") + create_commit_referencing_issue(issue) + create_merge_request_closing_issue(issue, message: "Closes nothing") - merge_merge_requests_closing_issue(issue) - deploy_master - end + merge_merge_requests_closing_issue(issue) + deploy_master expect(subject[:code].median).to be_nil end @@ -60,14 +58,12 @@ describe 'CycleAnalytics#code', feature: true do context "when a regular merge request (that doesn't close the issue) is created" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) + issue = create(:issue, project: project) - create_commit_referencing_issue(issue) - create_merge_request_closing_issue(issue, message: "Closes nothing") + create_commit_referencing_issue(issue) + create_merge_request_closing_issue(issue, message: "Closes nothing") - merge_merge_requests_closing_issue(issue) - end + merge_merge_requests_closing_issue(issue) expect(subject[:code].median).to be_nil end diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb index 5c73edbbc53..fc7d18bd40e 100644 --- a/spec/models/cycle_analytics/issue_spec.rb +++ b/spec/models/cycle_analytics/issue_spec.rb @@ -33,14 +33,12 @@ describe 'CycleAnalytics#issue', models: true do context "when a regular label (instead of a list label) is added to the issue" do it "returns nil" do - 5.times do - regular_label = create(:label) - issue = create(:issue, project: project) - issue.update(label_ids: [regular_label.id]) + regular_label = create(:label) + issue = create(:issue, project: project) + issue.update(label_ids: [regular_label.id]) - create_merge_request_closing_issue(issue) - merge_merge_requests_closing_issue(issue) - end + create_merge_request_closing_issue(issue) + merge_merge_requests_closing_issue(issue) expect(subject[:issue].median).to be_nil end diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb index 591bbdddf55..b9fe492fe2c 100644 --- a/spec/models/cycle_analytics/production_spec.rb +++ b/spec/models/cycle_analytics/production_spec.rb @@ -21,7 +21,13 @@ describe 'CycleAnalytics#production', feature: true do ["production deploy happens after merge request is merged (along with other changes)", lambda do |context, data| # Make other changes on master - sha = context.project.repository.commit_file(context.user, context.random_git_name, "content", "commit message", 'master', false) + sha = context.project.repository.commit_file( + context.user, + context.random_git_name, + 'content', + message: 'commit message', + branch_name: 'master', + update: false) context.project.repository.commit(sha) context.deploy_master @@ -29,11 +35,9 @@ describe 'CycleAnalytics#production', feature: true do context "when a regular merge request (that doesn't close the issue) is merged and deployed" do it "returns nil" do - 5.times do - merge_request = create(:merge_request) - MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master - end + merge_request = create(:merge_request) + MergeRequests::MergeService.new(project, user).execute(merge_request) + deploy_master expect(subject[:production].median).to be_nil end @@ -41,12 +45,10 @@ describe 'CycleAnalytics#production', feature: true do context "when the deployment happens to a non-production environment" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) - MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master(environment: 'staging') - end + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + MergeRequests::MergeService.new(project, user).execute(merge_request) + deploy_master(environment: 'staging') expect(subject[:production].median).to be_nil end diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb index 33d2c0a7416..febb18c9884 100644 --- a/spec/models/cycle_analytics/review_spec.rb +++ b/spec/models/cycle_analytics/review_spec.rb @@ -23,9 +23,7 @@ describe 'CycleAnalytics#review', feature: true do context "when a regular merge request (that doesn't close the issue) is created and merged" do it "returns nil" do - 5.times do - MergeRequests::MergeService.new(project, user).execute(create(:merge_request)) - end + MergeRequests::MergeService.new(project, user).execute(create(:merge_request)) expect(subject[:review].median).to be_nil end diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb index 00693d67475..9a024d533a1 100644 --- a/spec/models/cycle_analytics/staging_spec.rb +++ b/spec/models/cycle_analytics/staging_spec.rb @@ -29,10 +29,10 @@ describe 'CycleAnalytics#staging', feature: true do sha = context.project.repository.commit_file( context.user, context.random_git_name, - "content", - "commit message", - 'master', - false) + 'content', + message: 'commit message', + branch_name: 'master', + update: false) context.project.repository.commit(sha) context.deploy_master @@ -40,11 +40,9 @@ describe 'CycleAnalytics#staging', feature: true do context "when a regular merge request (that doesn't close the issue) is merged and deployed" do it "returns nil" do - 5.times do - merge_request = create(:merge_request) - MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master - end + merge_request = create(:merge_request) + MergeRequests::MergeService.new(project, user).execute(merge_request) + deploy_master expect(subject[:staging].median).to be_nil end @@ -52,12 +50,10 @@ describe 'CycleAnalytics#staging', feature: true do context "when the deployment happens to a non-production environment" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) - MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master(environment: 'staging') - end + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + MergeRequests::MergeService.new(project, user).execute(merge_request) + deploy_master(environment: 'staging') expect(subject[:staging].median).to be_nil end diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb index f857ea6cbec..c2ba012a0e6 100644 --- a/spec/models/cycle_analytics/test_spec.rb +++ b/spec/models/cycle_analytics/test_spec.rb @@ -24,16 +24,14 @@ describe 'CycleAnalytics#test', feature: true do context "when the pipeline is for a regular merge request (that doesn't close an issue)" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) - pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) - pipeline.run! - pipeline.succeed! + pipeline.run! + pipeline.succeed! - merge_merge_requests_closing_issue(issue) - end + merge_merge_requests_closing_issue(issue) expect(subject[:test].median).to be_nil end @@ -41,12 +39,10 @@ describe 'CycleAnalytics#test', feature: true do context "when the pipeline is not for a merge request" do it "returns nil" do - 5.times do - pipeline = create(:ci_pipeline, ref: "refs/heads/master", sha: project.repository.commit('master').sha) + pipeline = create(:ci_pipeline, ref: "refs/heads/master", sha: project.repository.commit('master').sha) - pipeline.run! - pipeline.succeed! - end + pipeline.run! + pipeline.succeed! expect(subject[:test].median).to be_nil end @@ -54,16 +50,14 @@ describe 'CycleAnalytics#test', feature: true do context "when the pipeline is dropped (failed)" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) - pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) - pipeline.run! - pipeline.drop! + pipeline.run! + pipeline.drop! - merge_merge_requests_closing_issue(issue) - end + merge_merge_requests_closing_issue(issue) expect(subject[:test].median).to be_nil end @@ -71,16 +65,14 @@ describe 'CycleAnalytics#test', feature: true do context "when the pipeline is cancelled" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) - pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) - pipeline.run! - pipeline.cancel! + pipeline.run! + pipeline.cancel! - merge_merge_requests_closing_issue(issue) - end + merge_merge_requests_closing_issue(issue) expect(subject[:test].median).to be_nil end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index b2e06541e66..eba392044bf 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -288,6 +288,11 @@ describe Environment, models: true do "1-foo" => "env-1-foo" + SUFFIX, "1/foo" => "env-1-foo" + SUFFIX, "foo-" => "foo" + SUFFIX, + "foo--bar" => "foo-bar" + SUFFIX, + "foo**bar" => "foo-bar" + SUFFIX, + "*-foo" => "env-foo" + SUFFIX, + "staging-12345678-" => "staging-12345678" + SUFFIX, + "staging-12345678-01234567" => "staging-12345678" + SUFFIX, }.each do |name, matcher| it "returns a slug matching #{matcher}, given #{name}" do slug = described_class.new(name: name).generate_slug diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 40c0a75c364..bba9058f394 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -23,21 +23,74 @@ describe Issue, models: true do end describe '#to_reference' do - let(:project) { build(:empty_project, name: 'sample-project') } - let(:issue) { build(:issue, iid: 1, project: project) } + let(:namespace) { build(:namespace, path: 'sample-namespace') } + let(:project) { build(:empty_project, name: 'sample-project', namespace: namespace) } + let(:issue) { build(:issue, iid: 1, project: project) } + let(:group) { create(:group, name: 'Group', path: 'sample-group') } + + context 'when nil argument' do + it 'returns issue id' do + expect(issue.to_reference).to eq "#1" + end + end + + context 'when full is true' do + it 'returns complete path to the issue' do + expect(issue.to_reference(full: true)).to eq 'sample-namespace/sample-project#1' + expect(issue.to_reference(project, full: true)).to eq 'sample-namespace/sample-project#1' + expect(issue.to_reference(group, full: true)).to eq 'sample-namespace/sample-project#1' + end + end - it 'returns a String reference to the object' do - expect(issue.to_reference).to eq "#1" + context 'when same project argument' do + it 'returns issue id' do + expect(issue.to_reference(project)).to eq("#1") + end end - it 'returns a String reference with the full path' do - expect(issue.to_reference(full: true)).to eq(project.path_with_namespace + '#1') + context 'when cross namespace project argument' do + let(:another_namespace_project) { create(:empty_project, name: 'another-project') } + + it 'returns complete path to the issue' do + expect(issue.to_reference(another_namespace_project)).to eq 'sample-namespace/sample-project#1' + end end it 'supports a cross-project reference' do another_project = build(:empty_project, name: 'another-project', namespace: project.namespace) expect(issue.to_reference(another_project)).to eq "sample-project#1" end + + context 'when same namespace / cross-project argument' do + let(:another_project) { create(:empty_project, namespace: namespace) } + + it 'returns path to the issue with the project name' do + expect(issue.to_reference(another_project)).to eq 'sample-project#1' + end + end + + context 'when different namespace / cross-project argument' do + let(:another_namespace) { create(:namespace, path: 'another-namespace') } + let(:another_project) { create(:empty_project, path: 'another-project', namespace: another_namespace) } + + it 'returns full path to the issue' do + expect(issue.to_reference(another_project)).to eq 'sample-namespace/sample-project#1' + end + end + + context 'when argument is a namespace' do + context 'with same project path' do + it 'returns path to the issue with the project name' do + expect(issue.to_reference(namespace)).to eq 'sample-project#1' + end + end + + context 'with different project path' do + it 'returns full path to the issue' do + expect(issue.to_reference(group)).to eq 'sample-namespace/sample-project#1' + end + end + end end describe '#is_being_reassigned?' do diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index 90d14c2c0b9..e4be0aba7a6 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -117,7 +117,7 @@ describe ProjectMember, models: true do users = create_list(:user, 2) described_class.add_users_to_projects( - [projects.first.id, projects.second], + [projects.first.id, projects.second.id], [users.first.id, users.second], described_class::MASTER) diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb index c879edddfdd..98f3d420c8a 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -113,10 +113,7 @@ describe MattermostSlashCommandsService, :models do end it 'shows error messages' do - teams, message = subject - - expect(teams).to be_empty - expect(message).to eq('Failed to get team list.') + expect(subject).to eq([[], "Failed to get team list."]) end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 646a1311462..48b085781e7 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -282,9 +282,10 @@ describe Project, models: true do end describe '#to_reference' do - let(:owner) { create(:user, name: 'Gitlab') } + let(:owner) { create(:user, name: 'Gitlab') } let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) } - let(:project) { create(:empty_project, path: 'sample-project', namespace: namespace) } + let(:project) { create(:empty_project, path: 'sample-project', namespace: namespace) } + let(:group) { create(:group, name: 'Group', path: 'sample-group', owner: owner) } context 'when nil argument' do it 'returns nil' do @@ -292,6 +293,14 @@ describe Project, models: true do end end + context 'when full is true' do + it 'returns complete path to the project' do + expect(project.to_reference(full: true)).to eq 'sample-namespace/sample-project' + expect(project.to_reference(project, full: true)).to eq 'sample-namespace/sample-project' + expect(project.to_reference(group, full: true)).to eq 'sample-namespace/sample-project' + end + end + context 'when same project argument' do it 'returns nil' do expect(project.to_reference(project)).to be_nil @@ -309,10 +318,33 @@ describe Project, models: true do context 'when same namespace / cross-project argument' do let(:another_project) { create(:empty_project, namespace: namespace) } - it 'returns complete path to the project' do + it 'returns path to the project' do expect(project.to_reference(another_project)).to eq 'sample-project' end end + + context 'when different namespace / cross-project argument' do + let(:another_namespace) { create(:namespace, path: 'another-namespace', owner: owner) } + let(:another_project) { create(:empty_project, path: 'another-project', namespace: another_namespace) } + + it 'returns full path to the project' do + expect(project.to_reference(another_project)).to eq 'sample-namespace/sample-project' + end + end + + context 'when argument is a namespace' do + context 'with same project path' do + it 'returns path to the project' do + expect(project.to_reference(namespace)).to eq 'sample-project' + end + end + + context 'with different project path' do + it 'returns full path to the project' do + expect(project.to_reference(group)).to eq 'sample-namespace/sample-project' + end + end + end end describe '#to_human_reference' do @@ -1801,6 +1833,14 @@ describe Project, models: true do end end + describe 'inside_path' do + let!(:project1) { create(:empty_project) } + let!(:project2) { create(:empty_project) } + let!(:path) { project1.namespace.path } + + it { expect(Project.inside_path(path)).to eq([project1]) } + end + def enable_lfs allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 829b69093c9..53b98ba05f8 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -15,7 +15,12 @@ describe Repository, models: true do let(:merge_commit) do merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) - merge_commit_id = repository.merge(user, merge_request, commit_options) + + merge_commit_id = repository.merge(user, + merge_request.diff_head_sha, + merge_request, + commit_options) + repository.commit(merge_commit_id) end @@ -289,17 +294,39 @@ describe Repository, models: true do describe "#commit_dir" do it "commits a change that creates a new directory" do expect do - repository.commit_dir(user, 'newdir', 'Create newdir', 'master') + repository.commit_dir(user, 'newdir', + message: 'Create newdir', branch_name: 'master') end.to change { repository.commits('master').count }.by(1) newdir = repository.tree('master', 'newdir') expect(newdir.path).to eq('newdir') end + context "when committing to another project" do + let(:forked_project) { create(:project) } + + it "creates a fork and commit to the forked project" do + expect do + repository.commit_dir(user, 'newdir', + message: 'Create newdir', branch_name: 'patch', + start_branch_name: 'master', start_project: forked_project) + end.to change { repository.commits('master').count }.by(0) + + expect(repository.branch_exists?('patch')).to be_truthy + expect(forked_project.repository.branch_exists?('patch')).to be_falsy + + newdir = repository.tree('patch', 'newdir') + expect(newdir.path).to eq('newdir') + end + end + context "when an author is specified" do it "uses the given email/name to set the commit's author" do expect do - repository.commit_dir(user, "newdir", "Add newdir", 'master', author_email: author_email, author_name: author_name) + repository.commit_dir(user, 'newdir', + message: 'Add newdir', + branch_name: 'master', + author_email: author_email, author_name: author_name) end.to change { repository.commits('master').count }.by(1) last_commit = repository.commit @@ -314,8 +341,9 @@ describe Repository, models: true do it 'commits change to a file successfully' do expect do repository.commit_file(user, 'CHANGELOG', 'Changelog!', - 'Updates file content', - 'master', true) + message: 'Updates file content', + branch_name: 'master', + update: true) end.to change { repository.commits('master').count }.by(1) blob = repository.blob_at('master', 'CHANGELOG') @@ -326,8 +354,12 @@ describe Repository, models: true do context "when an author is specified" do it "uses the given email/name to set the commit's author" do expect do - repository.commit_file(user, "README", 'README!', 'Add README', - 'master', true, author_email: author_email, author_name: author_name) + repository.commit_file(user, 'README', 'README!', + message: 'Add README', + branch_name: 'master', + update: true, + author_email: author_email, + author_name: author_name) end.to change { repository.commits('master').count }.by(1) last_commit = repository.commit @@ -342,7 +374,7 @@ describe Repository, models: true do it 'updates filename successfully' do expect do repository.update_file(user, 'NEWLICENSE', 'Copyright!', - branch: 'master', + branch_name: 'master', previous_path: 'LICENSE', message: 'Changes filename') end.to change { repository.commits('master').count }.by(1) @@ -355,15 +387,16 @@ describe Repository, models: true do context "when an author is specified" do it "uses the given email/name to set the commit's author" do - repository.commit_file(user, "README", 'README!', 'Add README', 'master', true) + repository.commit_file(user, 'README', 'README!', + message: 'Add README', branch_name: 'master', update: true) expect do - repository.update_file(user, 'README', "Updated README!", - branch: 'master', - previous_path: 'README', - message: 'Update README', - author_email: author_email, - author_name: author_name) + repository.update_file(user, 'README', 'Updated README!', + branch_name: 'master', + previous_path: 'README', + message: 'Update README', + author_email: author_email, + author_name: author_name) end.to change { repository.commits('master').count }.by(1) last_commit = repository.commit @@ -376,10 +409,12 @@ describe Repository, models: true do describe "#remove_file" do it 'removes file successfully' do - repository.commit_file(user, "README", 'README!', 'Add README', 'master', true) + repository.commit_file(user, 'README', 'README!', + message: 'Add README', branch_name: 'master', update: true) expect do - repository.remove_file(user, "README", "Remove README", 'master') + repository.remove_file(user, 'README', + message: 'Remove README', branch_name: 'master') end.to change { repository.commits('master').count }.by(1) expect(repository.blob_at('master', 'README')).to be_nil @@ -387,10 +422,13 @@ describe Repository, models: true do context "when an author is specified" do it "uses the given email/name to set the commit's author" do - repository.commit_file(user, "README", 'README!', 'Add README', 'master', true) + repository.commit_file(user, 'README', 'README!', + message: 'Add README', branch_name: 'master', update: true) expect do - repository.remove_file(user, "README", "Remove README", 'master', author_email: author_email, author_name: author_name) + repository.remove_file(user, 'README', + message: 'Remove README', branch_name: 'master', + author_email: author_email, author_name: author_name) end.to change { repository.commits('master').count }.by(1) last_commit = repository.commit @@ -538,11 +576,14 @@ describe Repository, models: true do describe "#license_blob", caching: true do before do - repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master') + repository.remove_file( + user, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master') end it 'handles when HEAD points to non-existent ref' do - repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false) + repository.commit_file( + user, 'LICENSE', 'Copyright!', + message: 'Add LICENSE', branch_name: 'master', update: false) allow(repository).to receive(:file_on_head). and_raise(Rugged::ReferenceError) @@ -551,21 +592,27 @@ describe Repository, models: true do end it 'looks in the root_ref only' do - repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'markdown') - repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'markdown', false) + repository.remove_file(user, 'LICENSE', + message: 'Remove LICENSE', branch_name: 'markdown') + repository.commit_file(user, 'LICENSE', + Licensee::License.new('mit').content, + message: 'Add LICENSE', branch_name: 'markdown', update: false) expect(repository.license_blob).to be_nil end it 'detects license file with no recognizable open-source license content' do - repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false) + repository.commit_file(user, 'LICENSE', 'Copyright!', + message: 'Add LICENSE', branch_name: 'master', update: false) expect(repository.license_blob.name).to eq('LICENSE') end %w[LICENSE LICENCE LiCensE LICENSE.md LICENSE.foo COPYING COPYING.md].each do |filename| it "detects '#{filename}'" do - repository.commit_file(user, filename, Licensee::License.new('mit').content, "Add #{filename}", 'master', false) + repository.commit_file(user, filename, + Licensee::License.new('mit').content, + message: "Add #{filename}", branch_name: 'master', update: false) expect(repository.license_blob.name).to eq(filename) end @@ -574,7 +621,8 @@ describe Repository, models: true do describe '#license_key', caching: true do before do - repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master') + repository.remove_file(user, 'LICENSE', + message: 'Remove LICENSE', branch_name: 'master') end it 'returns nil when no license is detected' do @@ -588,13 +636,16 @@ describe Repository, models: true do end it 'detects license file with no recognizable open-source license content' do - repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false) + repository.commit_file(user, 'LICENSE', 'Copyright!', + message: 'Add LICENSE', branch_name: 'master', update: false) expect(repository.license_key).to be_nil end it 'returns the license key' do - repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false) + repository.commit_file(user, 'LICENSE', + Licensee::License.new('mit').content, + message: 'Add LICENSE', branch_name: 'master', update: false) expect(repository.license_key).to eq('mit') end @@ -707,7 +758,7 @@ describe Repository, models: true do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) expect do - repository.rm_branch(user, 'new_feature') + repository.rm_branch(user, 'feature') end.to raise_error(GitHooksService::PreReceiveError) end @@ -728,36 +779,51 @@ describe Repository, models: true do context 'when pre hooks were successful' do before do - expect_any_instance_of(GitHooksService).to receive(:execute). - with(user, repository.path_to_repo, old_rev, new_rev, 'refs/heads/feature'). - and_yield.and_return(true) + service = GitHooksService.new + expect(GitHooksService).to receive(:new).and_return(service) + expect(service).to receive(:execute). + with( + user, + repository.path_to_repo, + old_rev, + new_rev, + 'refs/heads/feature'). + and_yield(service).and_return(true) end it 'runs without errors' do expect do - repository.update_branch_with_hooks(user, 'feature') { new_rev } + GitOperationService.new(user, repository).with_branch('feature') do + new_rev + end end.not_to raise_error end it 'ensures the autocrlf Git option is set to :input' do - expect(repository).to receive(:update_autocrlf_option) + service = GitOperationService.new(user, repository) - repository.update_branch_with_hooks(user, 'feature') { new_rev } + expect(service).to receive(:update_autocrlf_option) + + service.with_branch('feature') { new_rev } end context "when the branch wasn't empty" do it 'updates the head' do expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev) - repository.update_branch_with_hooks(user, 'feature') { new_rev } + + GitOperationService.new(user, repository).with_branch('feature') do + new_rev + end + expect(repository.find_branch('feature').dereferenced_target.id).to eq(new_rev) end end end context 'when the update adds more than one commit' do - it 'runs without errors' do - old_rev = '33f3729a45c02fc67d00adb1b8bca394b0e761d9' + let(:old_rev) { '33f3729a45c02fc67d00adb1b8bca394b0e761d9' } + it 'runs without errors' do # old_rev is an ancestor of new_rev expect(repository.rugged.merge_base(old_rev, new_rev)).to eq(old_rev) @@ -767,22 +833,28 @@ describe Repository, models: true do branch = 'feature-ff-target' repository.add_branch(user, branch, old_rev) - expect { repository.update_branch_with_hooks(user, branch) { new_rev } }.not_to raise_error + expect do + GitOperationService.new(user, repository).with_branch(branch) do + new_rev + end + end.not_to raise_error end end context 'when the update would remove commits from the target branch' do - it 'raises an exception' do - branch = 'master' - old_rev = repository.find_branch(branch).dereferenced_target.sha + let(:branch) { 'master' } + let(:old_rev) { repository.find_branch(branch).dereferenced_target.sha } + it 'raises an exception' do # The 'master' branch is NOT an ancestor of new_rev. expect(repository.rugged.merge_base(old_rev, new_rev)).not_to eq(old_rev) # Updating 'master' to new_rev would lose the commits on 'master' that # are not contained in new_rev. This should not be allowed. expect do - repository.update_branch_with_hooks(user, branch) { new_rev } + GitOperationService.new(user, repository).with_branch(branch) do + new_rev + end end.to raise_error(Repository::CommitError) end end @@ -792,7 +864,9 @@ describe Repository, models: true do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) expect do - repository.update_branch_with_hooks(user, 'feature') { new_rev } + GitOperationService.new(user, repository).with_branch('feature') do + new_rev + end end.to raise_error(GitHooksService::PreReceiveError) end end @@ -800,7 +874,6 @@ describe Repository, models: true do context 'when target branch is different from source branch' do before do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, '']) - allow(repository).to receive(:update_ref!) end it 'expires branch cache' do @@ -809,7 +882,10 @@ describe Repository, models: true do expect(repository).not_to receive(:expire_emptiness_caches) expect(repository).to receive(:expire_branches_cache) - repository.update_branch_with_hooks(user, 'new-feature') { new_rev } + GitOperationService.new(user, repository). + with_branch('new-feature') do + new_rev + end end end @@ -827,7 +903,9 @@ describe Repository, models: true do expect(empty_repository).to receive(:expire_branches_cache) empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!', - 'Updates file content', 'master', false) + message: 'Updates file content', + branch_name: 'master', + update: false) end end end @@ -877,7 +955,7 @@ describe Repository, models: true do end it 'sets autocrlf to :input' do - repository.update_autocrlf_option + GitOperationService.new(nil, repository).send(:update_autocrlf_option) expect(repository.raw_repository.autocrlf).to eq(:input) end @@ -892,7 +970,7 @@ describe Repository, models: true do expect(repository.raw_repository).not_to receive(:autocrlf=). with(:input) - repository.update_autocrlf_option + GitOperationService.new(nil, repository).send(:update_autocrlf_option) end end end @@ -1009,8 +1087,11 @@ describe Repository, models: true do it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) - merge_commit_id = repository.merge(user, merge_request, commit_options) - repository.commit(merge_commit_id) + + merge_commit_id = repository.merge(user, + merge_request.diff_head_sha, + merge_request, + commit_options) expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id) end @@ -1388,9 +1469,10 @@ describe Repository, models: true do describe '#rm_tag' do it 'removes a tag' do expect(repository).to receive(:before_remove_tag) - expect(repository.rugged.tags).to receive(:delete).with('v1.1.0') - repository.rm_tag('v1.1.0') + repository.rm_tag(create(:user), 'v1.1.0') + + expect(repository.find_tag('v1.1.0')).to be_nil end end @@ -1458,16 +1540,16 @@ describe Repository, models: true do end end - describe '#update_ref!' do + describe '#update_ref' do it 'can create a ref' do - repository.update_ref!('refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + GitOperationService.new(nil, repository).send(:update_ref, 'refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) expect(repository.find_branch('foobar')).not_to be_nil end it 'raises CommitError when the ref update fails' do expect do - repository.update_ref!('refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + GitOperationService.new(nil, repository).send(:update_ref, 'refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA) end.to raise_error(Repository::CommitError) end end diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb index 8017d1c3324..581305ad39f 100644 --- a/spec/models/todo_spec.rb +++ b/spec/models/todo_spec.rb @@ -109,7 +109,7 @@ describe Todo, models: true do end describe '#target_reference' do - it 'returns the short commit id for commits' do + it 'returns commit full reference with short id' do project = create(:project, :repository) commit = project.commit @@ -117,12 +117,12 @@ describe Todo, models: true do subject.target_type = 'Commit' subject.commit_id = commit.id - expect(subject.target_reference).to eq commit.short_id + expect(subject.target_reference).to eq commit.reference_link_text(full: true) end - it 'returns reference for issuables' do + it 'returns full reference for issuables' do subject.target = issue - expect(subject.target_reference).to eq issue.to_reference + expect(subject.target_reference).to eq issue.to_reference(full: true) end end end diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb new file mode 100644 index 00000000000..0f280f32eac --- /dev/null +++ b/spec/policies/ci/build_policy_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' + +describe Ci::BuildPolicy, :models do + let(:user) { create(:user) } + let(:build) { create(:ci_build, pipeline: pipeline) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + + let(:policies) do + described_class.abilities(user, build).to_set + end + + shared_context 'public pipelines disabled' do + before { project.update_attribute(:public_builds, false) } + end + + describe '#rules' do + context 'when user does not have access to the project' do + let(:project) { create(:empty_project, :private) } + + context 'when public builds are enabled' do + it 'does not include ability to read build' do + expect(policies).not_to include :read_build + end + end + + context 'when public builds are disabled' do + include_context 'public pipelines disabled' + + it 'does not include ability to read build' do + expect(policies).not_to include :read_build + end + end + end + + context 'when anonymous user has access to the project' do + let(:project) { create(:empty_project, :public) } + + context 'when public builds are enabled' do + it 'includes ability to read build' do + expect(policies).to include :read_build + end + end + + context 'when public builds are disabled' do + include_context 'public pipelines disabled' + + it 'does not include ability to read build' do + expect(policies).not_to include :read_build + end + end + end + + context 'when team member has access to the project' do + let(:project) { create(:empty_project, :public) } + + context 'team member is a guest' do + before { project.team << [user, :guest] } + + context 'when public builds are enabled' do + it 'includes ability to read build' do + expect(policies).to include :read_build + end + end + + context 'when public builds are disabled' do + include_context 'public pipelines disabled' + + it 'does not include ability to read build' do + expect(policies).not_to include :read_build + end + end + end + + context 'team member is a reporter' do + before { project.team << [user, :reporter] } + + context 'when public builds are enabled' do + it 'includes ability to read build' do + expect(policies).to include :read_build + end + end + + context 'when public builds are disabled' do + include_context 'public pipelines disabled' + + it 'does not include ability to read build' do + expect(policies).to include :read_build + end + end + end + end + end +end diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 645e36683bc..f197fadebab 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -67,7 +67,7 @@ describe API::Builds, api: true do context 'unauthorized user' do let(:api_user) { nil } - it 'should not return project builds' do + it 'does not return project builds' do expect(response).to have_http_status(401) end end @@ -86,7 +86,7 @@ describe API::Builds, api: true do context 'when commit exists in repository' do context 'when user is authorized' do - context 'when pipeline has builds' do + context 'when pipeline has jobs' do before do create(:ci_pipeline, project: project, sha: project.commit.id) create(:ci_build, pipeline: pipeline) @@ -95,7 +95,7 @@ describe API::Builds, api: true do get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user) end - it 'returns project builds for specific commit' do + it 'returns project jobs for specific commit' do expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.size).to eq 2 @@ -111,7 +111,7 @@ describe API::Builds, api: true do end end - context 'when pipeline has no builds' do + context 'when pipeline has no jobs' do before do branch_head = project.commit('feature').id get api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user) @@ -133,7 +133,7 @@ describe API::Builds, api: true do get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil) end - it 'does not return project builds' do + it 'does not return project jobs' do expect(response).to have_http_status(401) expect(json_response.except('message')).to be_empty end @@ -147,7 +147,7 @@ describe API::Builds, api: true do end context 'authorized user' do - it 'returns specific build data' do + it 'returns specific job data' do expect(response).to have_http_status(200) expect(json_response['name']).to eq('test') end @@ -165,7 +165,7 @@ describe API::Builds, api: true do context 'unauthorized user' do let(:api_user) { nil } - it 'does not return specific build data' do + it 'does not return specific job data' do expect(response).to have_http_status(401) end end @@ -176,7 +176,7 @@ describe API::Builds, api: true do get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) end - context 'build with artifacts' do + context 'job with artifacts' do let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } context 'authorized user' do @@ -185,7 +185,7 @@ describe API::Builds, api: true do 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } end - it 'returns specific build artifacts' do + it 'returns specific job artifacts' do expect(response).to have_http_status(200) expect(response.headers).to include(download_headers) end @@ -194,13 +194,13 @@ describe API::Builds, api: true do context 'unauthorized user' do let(:api_user) { nil } - it 'does not return specific build artifacts' do + it 'does not return specific job artifacts' do expect(response).to have_http_status(401) end end end - it 'does not return build artifacts if not uploaded' do + it 'does not return job artifacts if not uploaded' do expect(response).to have_http_status(404) end end @@ -241,7 +241,7 @@ describe API::Builds, api: true do end end - context 'non-existing build' do + context 'non-existing job' do shared_examples 'not found' do it { expect(response).to have_http_status(:not_found) } end @@ -254,7 +254,7 @@ describe API::Builds, api: true do it_behaves_like 'not found' end - context 'has no such build' do + context 'has no such job' do before do get path_for_ref(pipeline.ref, 'NOBUILD') end @@ -263,7 +263,7 @@ describe API::Builds, api: true do end end - context 'find proper build' do + context 'find proper job' do shared_examples 'a valid file' do let(:download_headers) do { 'Content-Transfer-Encoding' => 'binary', @@ -311,7 +311,7 @@ describe API::Builds, api: true do end context 'authorized user' do - it 'returns specific build trace' do + it 'returns specific job trace' do expect(response).to have_http_status(200) expect(response.body).to eq(build.trace) end @@ -320,7 +320,7 @@ describe API::Builds, api: true do context 'unauthorized user' do let(:api_user) { nil } - it 'does not return specific build trace' do + it 'does not return specific job trace' do expect(response).to have_http_status(401) end end @@ -333,7 +333,7 @@ describe API::Builds, api: true do context 'authorized user' do context 'user with :update_build persmission' do - it 'cancels running or pending build' do + it 'cancels running or pending job' do expect(response).to have_http_status(201) expect(project.builds.first.status).to eq('canceled') end @@ -342,7 +342,7 @@ describe API::Builds, api: true do context 'user without :update_build permission' do let(:api_user) { reporter.user } - it 'does not cancel build' do + it 'does not cancel job' do expect(response).to have_http_status(403) end end @@ -351,7 +351,7 @@ describe API::Builds, api: true do context 'unauthorized user' do let(:api_user) { nil } - it 'does not cancel build' do + it 'does not cancel job' do expect(response).to have_http_status(401) end end @@ -366,7 +366,7 @@ describe API::Builds, api: true do context 'authorized user' do context 'user with :update_build permission' do - it 'retries non-running build' do + it 'retries non-running job' do expect(response).to have_http_status(201) expect(project.builds.first.status).to eq('canceled') expect(json_response['status']).to eq('pending') @@ -376,7 +376,7 @@ describe API::Builds, api: true do context 'user without :update_build permission' do let(:api_user) { reporter.user } - it 'does not retry build' do + it 'does not retry job' do expect(response).to have_http_status(403) end end @@ -385,7 +385,7 @@ describe API::Builds, api: true do context 'unauthorized user' do let(:api_user) { nil } - it 'does not retry build' do + it 'does not retry job' do expect(response).to have_http_status(401) end end @@ -396,23 +396,23 @@ describe API::Builds, api: true do post api("/projects/#{project.id}/builds/#{build.id}/erase", user) end - context 'build is erasable' do + context 'job is erasable' do let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) } - it 'erases build content' do + it 'erases job content' do expect(response.status).to eq 201 expect(build.trace).to be_empty expect(build.artifacts_file.exists?).to be_falsy expect(build.artifacts_metadata.exists?).to be_falsy end - it 'updates build' do + it 'updates job' do expect(build.reload.erased_at).to be_truthy expect(build.reload.erased_by).to eq user end end - context 'build is not erasable' do + context 'job is not erasable' do let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) } it 'responds with forbidden' do @@ -452,20 +452,20 @@ describe API::Builds, api: true do post api("/projects/#{project.id}/builds/#{build.id}/play", user) end - context 'on an playable build' do + context 'on an playable job' do let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) } - it 'plays the build' do + it 'plays the job' do expect(response).to have_http_status 200 expect(json_response['user']['id']).to eq(user.id) expect(json_response['id']).to eq(build.id) end end - context 'on a non-playable build' do + context 'on a non-playable job' do it 'returns a status code 400, Bad Request' do expect(response).to have_http_status 400 - expect(response.body).to match("Unplayable Build") + expect(response.body).to match("Unplayable Job") end end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index edbf0140583..a027c23bb88 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -176,6 +176,9 @@ describe API::Groups, api: true do expect(json_response['visibility_level']).to eq(group1.visibility_level) expect(json_response['avatar_url']).to eq(group1.avatar_url) expect(json_response['web_url']).to eq(group1.web_url) + expect(json_response['request_access_enabled']).to eq(group1.request_access_enabled) + expect(json_response['full_name']).to eq(group1.full_name) + expect(json_response['full_path']).to eq(group1.full_path) expect(json_response['projects']).to be_an Array expect(json_response['projects'].length).to eq(2) expect(json_response['shared_projects']).to be_an Array @@ -323,7 +326,7 @@ describe API::Groups, api: true do expect(response).to have_http_status(404) end - it "should only return projects to which user has access" do + it "only returns projects to which user has access" do project3.team << [user3, :developer] get api("/groups/#{group1.id}/projects", user3) @@ -335,7 +338,7 @@ describe API::Groups, api: true do end context "when authenticated as admin" do - it "should return any existing group" do + it "returns any existing group" do get api("/groups/#{group2.id}/projects", admin) expect(response).to have_http_status(200) @@ -343,7 +346,7 @@ describe API::Groups, api: true do expect(json_response.first['name']).to eq(project2.name) end - it "should not return a non existing group" do + it "does not return a non existing group" do get api("/groups/1328/projects", admin) expect(response).to have_http_status(404) @@ -351,7 +354,7 @@ describe API::Groups, api: true do end context 'when using group path in URL' do - it 'should return any existing group' do + it 'returns any existing group' do get api("/groups/#{group1.path}/projects", admin) expect(response).to have_http_status(200) diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 01032c0929b..45d5ae267c5 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -4,6 +4,7 @@ describe API::ProjectSnippets, api: true do include ApiHelpers let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } let(:admin) { create(:admin) } describe 'GET /projects/:project_id/snippets/:id' do @@ -22,7 +23,7 @@ describe API::ProjectSnippets, api: true do let(:user) { create(:user) } it 'returns all snippets available to team member' do - project.team << [user, :developer] + project.add_developer(user) public_snippet = create(:project_snippet, :public, project: project) internal_snippet = create(:project_snippet, :internal, project: project) private_snippet = create(:project_snippet, :private, project: project) @@ -50,7 +51,7 @@ describe API::ProjectSnippets, api: true do title: 'Test Title', file_name: 'test.rb', code: 'puts "hello world"', - visibility_level: Gitlab::VisibilityLevel::PUBLIC + visibility_level: Snippet::PUBLIC } end @@ -72,6 +73,51 @@ describe API::ProjectSnippets, api: true do expect(response).to have_http_status(400) end + + context 'when the snippet is spam' do + def create_snippet(project, snippet_params = {}) + project.add_developer(user) + + post api("/projects/#{project.id}/snippets", user), params.merge(snippet_params) + end + + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the project is private' do + let(:private_project) { create(:project_empty_repo, :private) } + + context 'when the snippet is public' do + it 'creates the snippet' do + expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }. + to change { Snippet.count }.by(1) + end + end + end + + context 'when the project is public' do + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }. + to change { Snippet.count }.by(1) + end + end + + context 'when the snippet is public' do + it 'rejects the shippet' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + not_to change { Snippet.count } + expect(response).to have_http_status(400) + end + + it 'creates a spam log' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + end + end + end end describe 'PUT /projects/:project_id/snippets/:id/' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index a1db81ce18c..753dde0ca3a 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -459,7 +459,7 @@ describe API::Projects, api: true do before { project } before { admin } - it 'should create new project without path and return 201' do + it 'creates new project without path and return 201' do expect { post api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1) expect(response).to have_http_status(201) end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index f6fb6ea5506..6b9a739b439 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -80,7 +80,7 @@ describe API::Snippets, api: true do title: 'Test Title', file_name: 'test.rb', content: 'puts "hello world"', - visibility_level: Gitlab::VisibilityLevel::PUBLIC + visibility_level: Snippet::PUBLIC } end @@ -101,6 +101,36 @@ describe API::Snippets, api: true do expect(response).to have_http_status(400) end + + context 'when the snippet is spam' do + def create_snippet(snippet_params = {}) + post api('/snippets', user), params.merge(snippet_params) + end + + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(visibility_level: Snippet::PRIVATE) }. + to change { Snippet.count }.by(1) + end + end + + context 'when the snippet is public' do + it 'rejects the shippet' do + expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + not_to change { Snippet.count } + expect(response).to have_http_status(400) + end + + it 'creates a spam log' do + expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + end + end end describe 'PUT /snippets/:id' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 5bf5bf0739e..8692f9da976 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -305,6 +305,13 @@ describe API::Users, api: true do expect(user.reload.bio).to eq('new test bio') end + it "updates user with new password and forces reset on next login" do + put api("/users/#{user.id}", admin), password: '12345678' + + expect(response).to have_http_status(200) + expect(user.reload.password_expires_at).to be <= Time.now + end + it "updates user with organization" do put api("/users/#{user.id}", admin), { organization: 'GitLab' } diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 8dbe5f0b025..d85afdeab42 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -288,7 +288,7 @@ describe Ci::API::Builds do expect(build.reload.trace).to eq 'BUILD TRACE' end - context 'build has been erased' do + context 'job has been erased' do let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) } it 'responds with forbidden' do @@ -458,7 +458,7 @@ describe Ci::API::Builds do before { build.run! } describe "POST /builds/:id/artifacts/authorize" do - context "should authorize posting artifact to running build" do + context "authorizes posting artifact to running build" do it "using token as parameter" do post authorize_url, { token: build.token }, headers @@ -492,7 +492,7 @@ describe Ci::API::Builds do end end - context "should fail to post too large artifact" do + context "fails to post too large artifact" do it "using token as parameter" do stub_application_setting(max_artifacts_size: 0) diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 77549db2927..96889abee79 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' describe 'project routing' do before do - allow(Project).to receive(:find_with_namespace).and_return(false) - allow(Project).to receive(:find_with_namespace).with('gitlab/gitlabhq').and_return(true) + allow(Project).to receive(:find_by_full_path).and_return(false) + allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq').and_return(true) end # Shared examples for a resource inside a Project @@ -86,13 +86,13 @@ describe 'project routing' do end context 'name with dot' do - before { allow(Project).to receive(:find_with_namespace).with('gitlab/gitlabhq.keys').and_return(true) } + before { allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq.keys').and_return(true) } it { expect(get('/gitlab/gitlabhq.keys')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq.keys') } end context 'with nested group' do - before { allow(Project).to receive(:find_with_namespace).with('gitlab/subgroup/gitlabhq').and_return(true) } + before { allow(Project).to receive(:find_by_full_path).with('gitlab/subgroup/gitlabhq').and_return(true) } it { expect(get('/gitlab/subgroup/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlabhq') } end diff --git a/spec/serializers/analytics_build_serializer_spec.rb b/spec/serializers/analytics_build_serializer_spec.rb index f0551c78671..e3b1dd93dc2 100644 --- a/spec/serializers/analytics_build_serializer_spec.rb +++ b/spec/serializers/analytics_build_serializer_spec.rb @@ -1,17 +1,13 @@ require 'spec_helper' describe AnalyticsBuildSerializer do - let(:serializer) do - described_class - .new.represent(resource) - end - - let(:json) { serializer.as_json } let(:resource) { create(:ci_build) } + subject { described_class.new.represent(resource) } + context 'when there is a single object provided' do it 'contains important elements of analyticsBuild' do - expect(json) + expect(subject) .to include(:name, :branch, :short_sha, :date, :total_time, :url, :author) end end diff --git a/spec/serializers/analytics_issue_serializer_spec.rb b/spec/serializers/analytics_issue_serializer_spec.rb index 6afbb2df35c..2f08958a783 100644 --- a/spec/serializers/analytics_issue_serializer_spec.rb +++ b/spec/serializers/analytics_issue_serializer_spec.rb @@ -1,14 +1,13 @@ require 'spec_helper' describe AnalyticsIssueSerializer do - let(:serializer) do + subject do described_class .new(project: project, entity: :merge_request) .represent(resource) end let(:user) { create(:user) } - let(:json) { serializer.as_json } let(:project) { create(:project) } let(:resource) do { @@ -23,7 +22,7 @@ describe AnalyticsIssueSerializer do context 'when there is a single object provided' do it 'contains important elements of the issue' do - expect(json).to include(:title, :iid, :created_at, :total_time, :url, :author) + expect(subject).to include(:title, :iid, :created_at, :total_time, :url, :author) end end end diff --git a/spec/serializers/analytics_merge_request_serializer_spec.rb b/spec/serializers/analytics_merge_request_serializer_spec.rb index cdfae27193f..62067cc0ef2 100644 --- a/spec/serializers/analytics_merge_request_serializer_spec.rb +++ b/spec/serializers/analytics_merge_request_serializer_spec.rb @@ -1,14 +1,13 @@ require 'spec_helper' describe AnalyticsMergeRequestSerializer do - let(:serializer) do + subject do described_class .new(project: project, entity: :merge_request) .represent(resource) end let(:user) { create(:user) } - let(:json) { serializer.as_json } let(:project) { create(:project) } let(:resource) do { @@ -24,7 +23,7 @@ describe AnalyticsMergeRequestSerializer do context 'when there is a single object provided' do it 'contains important elements of the merge request' do - expect(json).to include(:title, :iid, :created_at, :total_time, :url, :author, :state) + expect(subject).to include(:title, :iid, :created_at, :total_time, :url, :author, :state) end end end diff --git a/spec/serializers/analytics_stage_serializer_spec.rb b/spec/serializers/analytics_stage_serializer_spec.rb index f9951826683..be6aa7c65c3 100644 --- a/spec/serializers/analytics_stage_serializer_spec.rb +++ b/spec/serializers/analytics_stage_serializer_spec.rb @@ -1,13 +1,13 @@ require 'spec_helper' describe AnalyticsStageSerializer do - let(:serializer) do - described_class - .new.represent(resource) + subject do + described_class.new.represent(resource) end - let(:json) { serializer.as_json } - let(:resource) { Gitlab::CycleAnalytics::CodeStage.new(project: double, options: {}) } + let(:resource) do + Gitlab::CycleAnalytics::CodeStage.new(project: double, options: {}) + end before do allow_any_instance_of(Gitlab::CycleAnalytics::BaseStage).to receive(:median).and_return(1.12) @@ -15,10 +15,10 @@ describe AnalyticsStageSerializer do end it 'it generates payload for single object' do - expect(json).to be_kind_of Hash + expect(subject).to be_kind_of Hash end it 'contains important elements of AnalyticsStage' do - expect(json).to include(:title, :description, :value) + expect(subject).to include(:title, :description, :value) end end diff --git a/spec/serializers/analytics_summary_serializer_spec.rb b/spec/serializers/analytics_summary_serializer_spec.rb index 7a84c8b0b40..5d7a94c2d02 100644 --- a/spec/serializers/analytics_summary_serializer_spec.rb +++ b/spec/serializers/analytics_summary_serializer_spec.rb @@ -1,29 +1,28 @@ require 'spec_helper' describe AnalyticsSummarySerializer do - let(:serializer) do - described_class - .new.represent(resource) + subject do + described_class.new.represent(resource) end - let(:json) { serializer.as_json } let(:project) { create(:empty_project) } let(:user) { create(:user) } + let(:resource) do - Gitlab::CycleAnalytics::Summary::Issue.new(project: double, - from: 1.day.ago, - current_user: user) + Gitlab::CycleAnalytics::Summary::Issue + .new(project: double, from: 1.day.ago, current_user: user) end before do - allow_any_instance_of(Gitlab::CycleAnalytics::Summary::Issue).to receive(:value).and_return(1.12) + allow_any_instance_of(Gitlab::CycleAnalytics::Summary::Issue) + .to receive(:value).and_return(1.12) end it 'it generates payload for single object' do - expect(json).to be_kind_of Hash + expect(subject).to be_kind_of Hash end it 'contains important elements of AnalyticsStage' do - expect(json).to include(:title, :value) + expect(subject).to include(:title, :value) end end diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb index b7ed4eb0239..3c37660885d 100644 --- a/spec/serializers/environment_serializer_spec.rb +++ b/spec/serializers/environment_serializer_spec.rb @@ -1,16 +1,15 @@ require 'spec_helper' describe EnvironmentSerializer do - let(:serializer) do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:json) do described_class .new(user: user, project: project) .represent(resource) end - let(:json) { serializer.as_json } - let(:user) { create(:user) } - let(:project) { create(:project) } - context 'when there is a single object provided' do before do create(:ci_build, :manual, name: 'manual1', diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index 3a32cb394dd..7cbf131e41e 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -7,11 +7,7 @@ describe PipelineSerializer do described_class.new(user: user) end - let(:entity) do - serializer.represent(resource) - end - - subject { entity.as_json } + subject { serializer.represent(resource) } describe '#represent' do context 'when used without pagination' do diff --git a/spec/services/compare_service_spec.rb b/spec/services/compare_service_spec.rb index 3760f19aaa2..0a7fc58523f 100644 --- a/spec/services/compare_service_spec.rb +++ b/spec/services/compare_service_spec.rb @@ -3,17 +3,17 @@ require 'spec_helper' describe CompareService, services: true do let(:project) { create(:project) } let(:user) { create(:user) } - let(:service) { described_class.new } + let(:service) { described_class.new(project, 'feature') } describe '#execute' do context 'compare with base, like feature...fix' do - subject { service.execute(project, 'feature', project, 'fix', straight: false) } + subject { service.execute(project, 'fix', straight: false) } it { expect(subject.diffs.size).to eq(1) } end context 'straight compare, like feature..fix' do - subject { service.execute(project, 'feature', project, 'fix', straight: true) } + subject { service.execute(project, 'fix', straight: true) } it { expect(subject.diffs.size).to eq(3) } end diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index b7dc99ed887..f2c2009bcbf 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -9,7 +9,7 @@ describe EventCreateService, services: true do it { expect(service.open_issue(issue, issue.author)).to be_truthy } - it "should create new event" do + it "creates new event" do expect { service.open_issue(issue, issue.author) }.to change { Event.count } end end @@ -19,7 +19,7 @@ describe EventCreateService, services: true do it { expect(service.close_issue(issue, issue.author)).to be_truthy } - it "should create new event" do + it "creates new event" do expect { service.close_issue(issue, issue.author) }.to change { Event.count } end end @@ -29,7 +29,7 @@ describe EventCreateService, services: true do it { expect(service.reopen_issue(issue, issue.author)).to be_truthy } - it "should create new event" do + it "creates new event" do expect { service.reopen_issue(issue, issue.author) }.to change { Event.count } end end diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb index d3c37c7820f..35e6e139238 100644 --- a/spec/services/files/update_service_spec.rb +++ b/spec/services/files/update_service_spec.rb @@ -6,7 +6,10 @@ describe Files::UpdateService do let(:project) { create(:project) } let(:user) { create(:user) } let(:file_path) { 'files/ruby/popen.rb' } - let(:new_contents) { "New Content" } + let(:new_contents) { 'New Content' } + let(:target_branch) { project.default_branch } + let(:last_commit_sha) { nil } + let(:commit_params) do { file_path: file_path, @@ -14,9 +17,9 @@ describe Files::UpdateService do file_content: new_contents, file_content_encoding: "text", last_commit_sha: last_commit_sha, - source_project: project, - source_branch: project.default_branch, - target_branch: project.default_branch, + start_project: project, + start_branch: project.default_branch, + target_branch: target_branch } end @@ -54,18 +57,6 @@ describe Files::UpdateService do end context "when the last_commit_sha is not supplied" do - let(:commit_params) do - { - file_path: file_path, - commit_message: "Update File", - file_content: new_contents, - file_content_encoding: "text", - source_project: project, - source_branch: project.default_branch, - target_branch: project.default_branch, - } - end - it "returns a hash with the :success status " do results = subject.execute @@ -80,5 +71,15 @@ describe Files::UpdateService do expect(results.data).to eq(new_contents) end end + + context 'when target branch is different than source branch' do + let(:target_branch) { "#{project.default_branch}-new" } + + it 'fires hooks only once' do + expect(GitHooksService).to receive(:new).once.and_call_original + + subject.execute + end + end end end diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb index 41b0968b8b4..3318dfb22b6 100644 --- a/spec/services/git_hooks_service_spec.rb +++ b/spec/services/git_hooks_service_spec.rb @@ -21,7 +21,7 @@ describe GitHooksService, services: true do hook = double(trigger: [true, nil]) expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) - expect(service.execute(user, @repo_path, @blankrev, @newrev, @ref) { }).to eq([true, nil]) + service.execute(user, @repo_path, @blankrev, @newrev, @ref) { } end end diff --git a/spec/services/labels/promote_service_spec.rb b/spec/services/labels/promote_service_spec.rb new file mode 100644 index 00000000000..4b90ad19640 --- /dev/null +++ b/spec/services/labels/promote_service_spec.rb @@ -0,0 +1,187 @@ +require 'spec_helper' + +describe Labels::PromoteService, services: true do + describe '#execute' do + let!(:user) { create(:user) } + + context 'project without group' do + let!(:project_1) { create(:empty_project) } + + let!(:project_label_1_1) { create(:label, project: project_1) } + + subject(:service) { described_class.new(project_1, user) } + + it 'fails on project without group' do + expect(service.execute(project_label_1_1)).to be_falsey + end + end + + context 'project with group' do + let!(:promoted_label_name) { "Promoted Label" } + let!(:untouched_label_name) { "Untouched Label" } + let!(:promoted_description) { "Promoted Description" } + let!(:promoted_color) { "#0000FF" } + let!(:label_2_1_priority) { 1 } + let!(:label_3_1_priority) { 2 } + + let!(:group_1) { create(:group) } + let!(:group_2) { create(:group) } + + let!(:project_1) { create(:empty_project, namespace: group_1) } + let!(:project_2) { create(:empty_project, namespace: group_1) } + let!(:project_3) { create(:empty_project, namespace: group_1) } + let!(:project_4) { create(:empty_project, namespace: group_2) } + + # Labels/issues can't be lazily created so we might as well eager initialize + # all other objects too since we use them inside + let!(:project_label_1_1) { create(:label, project: project_1, name: promoted_label_name, color: promoted_color, description: promoted_description) } + let!(:project_label_1_2) { create(:label, project: project_1, name: untouched_label_name) } + let!(:project_label_2_1) { create(:label, project: project_2, priority: label_2_1_priority, name: promoted_label_name, color: "#FF0000") } + let!(:project_label_3_1) { create(:label, project: project_3, priority: label_3_1_priority, name: promoted_label_name) } + let!(:project_label_3_2) { create(:label, project: project_3, priority: 1, name: untouched_label_name) } + let!(:project_label_4_1) { create(:label, project: project_4, name: promoted_label_name) } + + let!(:issue_1_1) { create(:labeled_issue, project: project_1, labels: [project_label_1_1, project_label_1_2]) } + let!(:issue_1_2) { create(:labeled_issue, project: project_1, labels: [project_label_1_2]) } + let!(:issue_2_1) { create(:labeled_issue, project: project_2, labels: [project_label_2_1]) } + let!(:issue_4_1) { create(:labeled_issue, project: project_4, labels: [project_label_4_1]) } + + let!(:merge_3_1) { create(:labeled_merge_request, source_project: project_3, target_project: project_3, labels: [project_label_3_1, project_label_3_2]) } + + let!(:issue_board_2_1) { create(:board, project: project_2) } + let!(:issue_board_list_2_1) { create(:list, board: issue_board_2_1, label: project_label_2_1) } + + let(:new_label) { group_1.labels.find_by(title: promoted_label_name) } + + subject(:service) { described_class.new(project_1, user) } + + it 'fails on group label' do + group_label = create(:group_label, group: group_1) + + expect(service.execute(group_label)).to be_falsey + end + + it 'is truthy on success' do + expect(service.execute(project_label_1_1)).to be_truthy + end + + it 'recreates the label as a group label' do + expect { service.execute(project_label_1_1) }. + to change(project_1.labels, :count).by(-1). + and change(group_1.labels, :count).by(1) + expect(new_label).not_to be_nil + end + + it 'copies title, description and color' do + service.execute(project_label_1_1) + + expect(new_label.title).to eq(promoted_label_name) + expect(new_label.description).to eq(promoted_description) + expect(new_label.color).to eq(promoted_color) + end + + it 'merges labels with the same name in group' do + expect { service.execute(project_label_1_1) }.to change(project_2.labels, :count).by(-1).and \ + change(project_3.labels, :count).by(-1) + end + + it 'recreates priorities' do + service.execute(project_label_1_1) + + expect(new_label.priority(project_1)).to be_nil + expect(new_label.priority(project_2)).to eq(label_2_1_priority) + expect(new_label.priority(project_3)).to eq(label_3_1_priority) + end + + it 'does not touch project out of promoted group' do + service.execute(project_label_1_1) + project_4_new_label = project_4.labels.find_by(title: promoted_label_name) + + expect(project_4_new_label).not_to be_nil + expect(project_4_new_label.id).to eq(project_label_4_1.id) + end + + it 'does not touch out of group priority' do + service.execute(project_label_1_1) + + expect(new_label.priority(project_4)).to be_nil + end + + it 'relinks issue with the promoted label' do + service.execute(project_label_1_1) + issue_label = issue_1_1.labels.find_by(title: promoted_label_name) + + expect(issue_label).not_to be_nil + expect(issue_label.id).to eq(new_label.id) + end + + it 'does not remove untouched labels from issue' do + expect { service.execute(project_label_1_1) }.not_to change(issue_1_1.labels, :count) + end + + it 'does not relink untouched label in issue' do + service.execute(project_label_1_1) + issue_label = issue_1_2.labels.find_by(title: untouched_label_name) + + expect(issue_label).not_to be_nil + expect(issue_label.id).to eq(project_label_1_2.id) + end + + it 'relinks issues with merged labels' do + service.execute(project_label_1_1) + issue_label = issue_2_1.labels.find_by(title: promoted_label_name) + + expect(issue_label).not_to be_nil + expect(issue_label.id).to eq(new_label.id) + end + + it 'does not relink issues from other group' do + service.execute(project_label_1_1) + issue_label = issue_4_1.labels.find_by(title: promoted_label_name) + + expect(issue_label).not_to be_nil + expect(issue_label.id).to eq(project_label_4_1.id) + end + + it 'updates merge request' do + service.execute(project_label_1_1) + merge_label = merge_3_1.labels.find_by(title: promoted_label_name) + + expect(merge_label).not_to be_nil + expect(merge_label.id).to eq(new_label.id) + end + + it 'updates board lists' do + service.execute(project_label_1_1) + list = issue_board_2_1.lists.find_by(label: new_label) + + expect(list).not_to be_nil + end + + # In case someone adds a new relation to Label.rb and forgets to relink it + # and the database doesn't have foreign key constraints + it 'relinks all relations' do + service.execute(project_label_1_1) + + Label.reflect_on_all_associations.each do |association| + expect(project_label_1_1.send(association.name).any?).to be_falsey + end + end + + context 'with invalid group label' do + before do + allow(service).to receive(:clone_label_to_group_label).and_wrap_original do |m, *args| + label = m.call(*args) + allow(label).to receive(:valid?).and_return(false) + + label + end + end + + it 'raises an exception' do + expect { service.execute(project_label_1_1) }.to raise_error(ActiveRecord::RecordInvalid) + end + end + end + end +end diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index 5f6a7716beb..d55a7657c0e 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -29,7 +29,7 @@ describe MergeRequests::CloseService, services: true do it { expect(@merge_request).to be_valid } it { expect(@merge_request).to be_closed } - it 'should execute hooks with close action' do + it 'executes hooks with close action' do expect(service).to have_received(:execute_hooks). with(@merge_request, 'close') end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 314ea670a71..2cc21acab7b 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -89,7 +89,7 @@ describe MergeRequests::RefreshService, services: true do # Merge master -> feature branch author = { email: 'test@gitlab.com', time: Time.now, name: "Me" } commit_options = { message: 'Test message', committer: author, author: author } - @project.repository.merge(@user, @merge_request, commit_options) + @project.repository.merge(@user, @merge_request.diff_head_sha, @merge_request, commit_options) commit = @project.repository.commit('feature') service.new(@project, @user).execute(@oldrev, commit.id, 'refs/heads/feature') reload_mrs diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb index 388abb6a0df..a0e51681725 100644 --- a/spec/services/merge_requests/resolve_service_spec.rb +++ b/spec/services/merge_requests/resolve_service_spec.rb @@ -66,7 +66,13 @@ describe MergeRequests::ResolveService do context 'when the source project is a fork and does not contain the HEAD of the target branch' do let!(:target_head) do - project.repository.commit_file(user, 'new-file-in-target', '', 'Add new file in target', 'conflict-start', false) + project.repository.commit_file( + user, + 'new-file-in-target', + '', + message: 'Add new file in target', + branch_name: 'conflict-start', + update: false) end before do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 309985a5d90..7cf2cd9968f 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -443,6 +443,8 @@ describe NotificationService, services: true do before do build_team(issue.project) + build_group(issue.project) + add_users_with_subscription(issue.project, issue) reset_delivered_emails! update_custom_notification(:new_issue, @u_guest_custom, project) @@ -459,6 +461,8 @@ describe NotificationService, services: true do should_email(@u_guest_custom) should_email(@u_custom_global) should_email(@u_participant_mentioned) + should_email(@g_global_watcher) + should_email(@g_watcher) should_not_email(@u_mentioned) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -1218,6 +1222,22 @@ describe NotificationService, services: true do project.add_master(@u_custom_global) end + # Users in the project's group but not part of project's team + # with different notification settings + def build_group(project) + group = create(:group, :public) + project.group = group + + # Group member: global=disabled, group=watch + @g_watcher = create_user_with_notification(:watch, 'group_watcher', project.group) + @g_watcher.notification_settings_for(nil).disabled! + + # Group member: global=watch, group=global + @g_global_watcher = create_global_setting_for(create(:user), :watch) + group.add_users([@g_watcher, @g_global_watcher], :master) + group + end + def create_global_setting_for(user, level) setting = user.global_notification_setting setting.level = level @@ -1226,9 +1246,9 @@ describe NotificationService, services: true do user end - def create_user_with_notification(level, username) + def create_user_with_notification(level, username, resource = project) user = create(:user, username: username) - setting = user.notification_settings_for(project) + setting = user.notification_settings_for(resource) setting.level = level setting.save diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb index bd89c4a7c11..bed1031e40a 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -41,6 +41,25 @@ describe 'Search::GlobalService', services: true do results = context.execute expect(results.objects('projects')).to match_array [found_project] end + + context 'nested group' do + let!(:nested_group) { create(:group, :nested) } + let!(:project) { create(:project, namespace: nested_group) } + + before { project.add_master(user) } + + it 'returns result from nested group' do + context = Search::GlobalService.new(user, search: project.path) + results = context.execute + expect(results.objects('projects')).to match_array [project] + end + + it 'returns result from descendants when search inside group' do + context = Search::GlobalService.new(user, search: project.path, group_id: nested_group.parent) + results = context.execute + expect(results.objects('projects')).to match_array [project] + end + end end end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 9f5a0ac4ec6..bd7269045e1 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -245,6 +245,8 @@ describe SystemNoteService, services: true do end describe '.change_title' do + let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum') } + subject { described_class.change_title(noteable, project, author, 'Old title') } context 'when noteable responds to `title`' do @@ -252,7 +254,7 @@ describe SystemNoteService, services: true do it 'sets the note text' do expect(subject.note). - to eq "changed title from **{-Old title-}** to **{+#{noteable.title}+}**" + to eq "changed title from **{-Old title-}** to **{+Lorem ipsum+}**" end end end diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb index 75c95d70951..6ed55289ed9 100644 --- a/spec/support/cycle_analytics_helpers.rb +++ b/spec/support/cycle_analytics_helpers.rb @@ -35,7 +35,13 @@ module CycleAnalyticsHelpers project.repository.add_branch(user, source_branch, 'master') end - sha = project.repository.commit_file(user, random_git_name, "content", "commit message", source_branch, false) + sha = project.repository.commit_file( + user, + random_git_name, + 'content', + message: 'commit message', + branch_name: source_branch, + update: false) project.repository.commit(sha) opts = { diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb index 10b90b40ba7..19b32c84d81 100644 --- a/spec/support/cycle_analytics_helpers/test_generation.rb +++ b/spec/support/cycle_analytics_helpers/test_generation.rb @@ -63,22 +63,20 @@ module CycleAnalyticsHelpers # test case. allow(self).to receive(:project) { other_project } - 5.times do - data = data_fn[self] - start_time = Time.now - end_time = rand(1..10).days.from_now - - start_time_conditions.each do |condition_name, condition_fn| - Timecop.freeze(start_time) { condition_fn[self, data] } - end + data = data_fn[self] + start_time = Time.now + end_time = rand(1..10).days.from_now - end_time_conditions.each do |condition_name, condition_fn| - Timecop.freeze(end_time) { condition_fn[self, data] } - end + start_time_conditions.each do |condition_name, condition_fn| + Timecop.freeze(start_time) { condition_fn[self, data] } + end - Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn + end_time_conditions.each do |condition_name, condition_fn| + Timecop.freeze(end_time) { condition_fn[self, data] } end + Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn + # Turn off the stub before checking assertions allow(self).to receive(:project).and_call_original @@ -114,17 +112,15 @@ module CycleAnalyticsHelpers context "start condition NOT PRESENT: #{start_time_conditions.map(&:first).to_sentence}" do context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do it "returns nil" do - 5.times do - data = data_fn[self] - end_time = rand(1..10).days.from_now - - end_time_conditions.each_with_index do |(condition_name, condition_fn), index| - Timecop.freeze(end_time + index.days) { condition_fn[self, data] } - end + data = data_fn[self] + end_time = rand(1..10).days.from_now - Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn + end_time_conditions.each_with_index do |(condition_name, condition_fn), index| + Timecop.freeze(end_time + index.days) { condition_fn[self, data] } end + Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn + expect(subject[phase].median).to be_nil end end @@ -133,17 +129,15 @@ module CycleAnalyticsHelpers context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do context "end condition NOT PRESENT: #{end_time_conditions.map(&:first).to_sentence}" do it "returns nil" do - 5.times do - data = data_fn[self] - start_time = Time.now - - start_time_conditions.each do |condition_name, condition_fn| - Timecop.freeze(start_time) { condition_fn[self, data] } - end + data = data_fn[self] + start_time = Time.now - post_fn[self, data] if post_fn + start_time_conditions.each do |condition_name, condition_fn| + Timecop.freeze(start_time) { condition_fn[self, data] } end + post_fn[self, data] if post_fn + expect(subject[phase].median).to be_nil end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 90f1a9c8798..b87232a350b 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -36,7 +36,8 @@ module TestEnv 'conflict-non-utf8' => 'd0a293c', 'conflict-too-large' => '39fa04f', 'deleted-image-test' => '6c17798', - 'wip' => 'b9238ee' + 'wip' => 'b9238ee', + 'csv' => '3dd0896' } # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily diff --git a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb index 80fc8c48fed..8d1cff7a261 100644 --- a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb +++ b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb @@ -20,7 +20,7 @@ describe 'gitlab:mail_google_schema_whitelisting rake task' do Rake.application.invoke_task "gitlab:mail_google_schema_whitelisting" end - it 'should run the task without errors' do + it 'runs the task without errors' do expect { run_rake_task }.not_to raise_error end end diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb index 44870cfcfb3..b6f6e7b7a2b 100644 --- a/spec/views/projects/builds/show.html.haml_spec.rb +++ b/spec/views/projects/builds/show.html.haml_spec.rb @@ -15,7 +15,7 @@ describe 'projects/builds/show', :view do allow(view).to receive(:can?).and_return(true) end - describe 'build information in header' do + describe 'job information in header' do let(:build) do create(:ci_build, :success, environment: 'staging') end @@ -28,11 +28,11 @@ describe 'projects/builds/show', :view do expect(rendered).to have_css('.ci-status.ci-success', text: 'passed') end - it 'does not render a link to the build' do + it 'does not render a link to the job' do expect(rendered).not_to have_link('passed') end - it 'shows build id' do + it 'shows job id' do expect(rendered).to have_css('.js-build-id', text: build.id) end @@ -45,8 +45,8 @@ describe 'projects/builds/show', :view do end end - describe 'environment info in build view' do - context 'build with latest deployment' do + describe 'environment info in job view' do + context 'job with latest deployment' do let(:build) do create(:ci_build, :success, environment: 'staging') end @@ -57,7 +57,7 @@ describe 'projects/builds/show', :view do end it 'shows deployment message' do - expected_text = 'This build is the most recent deployment' + expected_text = 'This job is the most recent deployment' render expect(rendered).to have_css( @@ -65,7 +65,7 @@ describe 'projects/builds/show', :view do end end - context 'build with outdated deployment' do + context 'job with outdated deployment' do let(:build) do create(:ci_build, :success, environment: 'staging', pipeline: pipeline) end @@ -87,7 +87,7 @@ describe 'projects/builds/show', :view do end it 'shows deployment message' do - expected_text = 'This build is an out-of-date deployment ' \ + expected_text = 'This job is an out-of-date deployment ' \ "to staging.\nView the most recent deployment ##{second_deployment.iid}." render @@ -95,7 +95,7 @@ describe 'projects/builds/show', :view do end end - context 'build failed to deploy' do + context 'job failed to deploy' do let(:build) do create(:ci_build, :failed, environment: 'staging', pipeline: pipeline) end @@ -105,7 +105,7 @@ describe 'projects/builds/show', :view do end it 'shows deployment message' do - expected_text = 'The deployment of this build to staging did not succeed.' + expected_text = 'The deployment of this job to staging did not succeed.' render expect(rendered).to have_css( @@ -113,7 +113,7 @@ describe 'projects/builds/show', :view do end end - context 'build will deploy' do + context 'job will deploy' do let(:build) do create(:ci_build, :running, environment: 'staging', pipeline: pipeline) end @@ -124,7 +124,7 @@ describe 'projects/builds/show', :view do end it 'shows deployment message' do - expected_text = 'This build is creating a deployment to staging' + expected_text = 'This job is creating a deployment to staging' render expect(rendered).to have_css( @@ -137,7 +137,7 @@ describe 'projects/builds/show', :view do end it 'shows that deployment will be overwritten' do - expected_text = 'This build is creating a deployment to staging' + expected_text = 'This job is creating a deployment to staging' render expect(rendered).to have_css( @@ -150,7 +150,7 @@ describe 'projects/builds/show', :view do context 'when environment does not exist' do it 'shows deployment message' do - expected_text = 'This build is creating a deployment to staging' + expected_text = 'This job is creating a deployment to staging' render expect(rendered).to have_css( @@ -161,7 +161,7 @@ describe 'projects/builds/show', :view do end end - context 'build that failed to deploy and environment has not been created' do + context 'job that failed to deploy and environment has not been created' do let(:build) do create(:ci_build, :failed, environment: 'staging', pipeline: pipeline) end @@ -171,7 +171,7 @@ describe 'projects/builds/show', :view do end it 'shows deployment message' do - expected_text = 'The deployment of this build to staging did not succeed' + expected_text = 'The deployment of this job to staging did not succeed' render expect(rendered).to have_css( @@ -179,7 +179,7 @@ describe 'projects/builds/show', :view do end end - context 'build that will deploy and environment has not been created' do + context 'job that will deploy and environment has not been created' do let(:build) do create(:ci_build, :running, environment: 'staging', pipeline: pipeline) end @@ -189,7 +189,7 @@ describe 'projects/builds/show', :view do end it 'shows deployment message' do - expected_text = 'This build is creating a deployment to staging' + expected_text = 'This job is creating a deployment to staging' render expect(rendered).to have_css( @@ -200,7 +200,7 @@ describe 'projects/builds/show', :view do end end - context 'when build is running' do + context 'when job is running' do before do build.run! render @@ -211,7 +211,7 @@ describe 'projects/builds/show', :view do end end - context 'when build is not running' do + context 'when job is not running' do before do build.success! render diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index e471a68a49a..5ef8cf1105b 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -107,7 +107,8 @@ describe GitGarbageCollectWorker do tree: old_commit.tree, parents: [old_commit], ) - project.repository.update_ref!( + GitOperationService.new(nil, project.repository).send( + :update_ref, "refs/heads/#{SecureRandom.hex(6)}", new_commit_sha, Gitlab::Git::BLANK_SHA diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 984acdade36..5919b99a6ed 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -74,7 +74,7 @@ describe PostReceive do context "webhook" do it "fetches the correct project" do - expect(Project).to receive(:find_with_namespace).with(project.path_with_namespace).and_return(project) + expect(Project).to receive(:find_by_full_path).with(project.path_with_namespace).and_return(project) PostReceive.new.perform(pwd(project), key_id, base64_changes) end @@ -89,7 +89,7 @@ describe PostReceive do end it "asks the project to trigger all hooks" do - allow(Project).to receive(:find_with_namespace).and_return(project) + allow(Project).to receive(:find_by_full_path).and_return(project) expect(project).to receive(:execute_hooks).twice expect(project).to receive(:execute_services).twice @@ -97,7 +97,7 @@ describe PostReceive do end it "enqueues a UpdateMergeRequestsWorker job" do - allow(Project).to receive(:find_with_namespace).and_return(project) + allow(Project).to receive(:find_by_full_path).and_return(project) expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args) PostReceive.new.perform(pwd(project), key_id, base64_changes) diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb index 1b910d9b91e..1f4c39eb64a 100644 --- a/spec/workers/project_destroy_worker_spec.rb +++ b/spec/workers/project_destroy_worker_spec.rb @@ -8,14 +8,14 @@ describe ProjectDestroyWorker do describe "#perform" do it "deletes the project" do - subject.perform(project.id, project.owner, {}) + subject.perform(project.id, project.owner.id, {}) expect(Project.all).not_to include(project) expect(Dir.exist?(path)).to be_falsey end it "deletes the project but skips repo deletion" do - subject.perform(project.id, project.owner, { "skip_repo" => true }) + subject.perform(project.id, project.owner.id, { "skip_repo" => true }) expect(Project.all).not_to include(project) expect(Dir.exist?(path)).to be_truthy diff --git a/vendor/assets/javascripts/jquery.ba-resize.js b/vendor/assets/javascripts/jquery.ba-resize.js deleted file mode 100644 index 1f41d379153..00000000000 --- a/vendor/assets/javascripts/jquery.ba-resize.js +++ /dev/null @@ -1,246 +0,0 @@ -/*! - * jQuery resize event - v1.1 - 3/14/2010 - * http://benalman.com/projects/jquery-resize-plugin/ - * - * Copyright (c) 2010 "Cowboy" Ben Alman - * Dual licensed under the MIT and GPL licenses. - * http://benalman.com/about/license/ - */ - -// Script: jQuery resize event -// -// *Version: 1.1, Last updated: 3/14/2010* -// -// Project Home - http://benalman.com/projects/jquery-resize-plugin/ -// GitHub - http://github.com/cowboy/jquery-resize/ -// Source - http://github.com/cowboy/jquery-resize/raw/master/jquery.ba-resize.js -// (Minified) - http://github.com/cowboy/jquery-resize/raw/master/jquery.ba-resize.min.js (1.0kb) -// -// About: License -// -// Copyright (c) 2010 "Cowboy" Ben Alman, -// Dual licensed under the MIT and GPL licenses. -// http://benalman.com/about/license/ -// -// About: Examples -// -// This working example, complete with fully commented code, illustrates a few -// ways in which this plugin can be used. -// -// resize event - http://benalman.com/code/projects/jquery-resize/examples/resize/ -// -// About: Support and Testing -// -// Information about what version or versions of jQuery this plugin has been -// tested with, what browsers it has been tested in, and where the unit tests -// reside (so you can test it yourself). -// -// jQuery Versions - 1.3.2, 1.4.1, 1.4.2 -// Browsers Tested - Internet Explorer 6-8, Firefox 2-3.6, Safari 3-4, Chrome, Opera 9.6-10.1. -// Unit Tests - http://benalman.com/code/projects/jquery-resize/unit/ -// -// About: Release History -// -// 1.1 - (3/14/2010) Fixed a minor bug that was causing the event to trigger -// immediately after bind in some circumstances. Also changed $.fn.data -// to $.data to improve performance. -// 1.0 - (2/10/2010) Initial release - -(function($,window,undefined){ - '$:nomunge'; // Used by YUI compressor. - - // A jQuery object containing all non-window elements to which the resize - // event is bound. - var elems = $([]), - - // Extend $.resize if it already exists, otherwise create it. - jq_resize = $.resize = $.extend( $.resize, {} ), - - timeout_id, - - // Reused strings. - str_setTimeout = 'setTimeout', - str_resize = 'resize', - str_data = str_resize + '-special-event', - str_delay = 'delay', - str_throttle = 'throttleWindow'; - - // Property: jQuery.resize.delay - // - // The numeric interval (in milliseconds) at which the resize event polling - // loop executes. Defaults to 250. - - jq_resize[ str_delay ] = 250; - - // Property: jQuery.resize.throttleWindow - // - // Throttle the native window object resize event to fire no more than once - // every <jQuery.resize.delay> milliseconds. Defaults to true. - // - // Because the window object has its own resize event, it doesn't need to be - // provided by this plugin, and its execution can be left entirely up to the - // browser. However, since certain browsers fire the resize event continuously - // while others do not, enabling this will throttle the window resize event, - // making event behavior consistent across all elements in all browsers. - // - // While setting this property to false will disable window object resize - // event throttling, please note that this property must be changed before any - // window object resize event callbacks are bound. - - jq_resize[ str_throttle ] = true; - - // Event: resize event - // - // Fired when an element's width or height changes. Because browsers only - // provide this event for the window element, for other elements a polling - // loop is initialized, running every <jQuery.resize.delay> milliseconds - // to see if elements' dimensions have changed. You may bind with either - // .resize( fn ) or .bind( "resize", fn ), and unbind with .unbind( "resize" ). - // - // Usage: - // - // > jQuery('selector').bind( 'resize', function(e) { - // > // element's width or height has changed! - // > ... - // > }); - // - // Additional Notes: - // - // * The polling loop is not created until at least one callback is actually - // bound to the 'resize' event, and this single polling loop is shared - // across all elements. - // - // Double firing issue in jQuery 1.3.2: - // - // While this plugin works in jQuery 1.3.2, if an element's event callbacks - // are manually triggered via .trigger( 'resize' ) or .resize() those - // callbacks may double-fire, due to limitations in the jQuery 1.3.2 special - // events system. This is not an issue when using jQuery 1.4+. - // - // > // While this works in jQuery 1.4+ - // > $(elem).css({ width: new_w, height: new_h }).resize(); - // > - // > // In jQuery 1.3.2, you need to do this: - // > var elem = $(elem); - // > elem.css({ width: new_w, height: new_h }); - // > elem.data( 'resize-special-event', { width: elem.width(), height: elem.height() } ); - // > elem.resize(); - - $.event.special[ str_resize ] = { - - // Called only when the first 'resize' event callback is bound per element. - setup: function() { - // Since window has its own native 'resize' event, return false so that - // jQuery will bind the event using DOM methods. Since only 'window' - // objects have a .setTimeout method, this should be a sufficient test. - // Unless, of course, we're throttling the 'resize' event for window. - if ( !jq_resize[ str_throttle ] && this[ str_setTimeout ] ) { return false; } - - var elem = $(this); - - // Add this element to the list of internal elements to monitor. - elems = elems.add( elem ); - - // Initialize data store on the element. - $.data( this, str_data, { w: elem.width(), h: elem.height() } ); - - // If this is the first element added, start the polling loop. - if ( elems.length === 1 ) { - loopy(); - } - }, - - // Called only when the last 'resize' event callback is unbound per element. - teardown: function() { - // Since window has its own native 'resize' event, return false so that - // jQuery will unbind the event using DOM methods. Since only 'window' - // objects have a .setTimeout method, this should be a sufficient test. - // Unless, of course, we're throttling the 'resize' event for window. - if ( !jq_resize[ str_throttle ] && this[ str_setTimeout ] ) { return false; } - - var elem = $(this); - - // Remove this element from the list of internal elements to monitor. - elems = elems.not( elem ); - - // Remove any data stored on the element. - elem.removeData( str_data ); - - // If this is the last element removed, stop the polling loop. - if ( !elems.length ) { - clearTimeout( timeout_id ); - } - }, - - // Called every time a 'resize' event callback is bound per element (new in - // jQuery 1.4). - add: function( handleObj ) { - // Since window has its own native 'resize' event, return false so that - // jQuery doesn't modify the event object. Unless, of course, we're - // throttling the 'resize' event for window. - if ( !jq_resize[ str_throttle ] && this[ str_setTimeout ] ) { return false; } - - var old_handler; - - // The new_handler function is executed every time the event is triggered. - // This is used to update the internal element data store with the width - // and height when the event is triggered manually, to avoid double-firing - // of the event callback. See the "Double firing issue in jQuery 1.3.2" - // comments above for more information. - - function new_handler( e, w, h ) { - var elem = $(this), - data = $.data( this, str_data ); - - // If called from the polling loop, w and h will be passed in as - // arguments. If called manually, via .trigger( 'resize' ) or .resize(), - // those values will need to be computed. - data.w = w !== undefined ? w : elem.width(); - data.h = h !== undefined ? h : elem.height(); - - old_handler.apply( this, arguments ); - }; - - // This may seem a little complicated, but it normalizes the special event - // .add method between jQuery 1.4/1.4.1 and 1.4.2+ - if ( $.isFunction( handleObj ) ) { - // 1.4, 1.4.1 - old_handler = handleObj; - return new_handler; - } else { - // 1.4.2+ - old_handler = handleObj.handler; - handleObj.handler = new_handler; - } - } - - }; - - function loopy() { - - // Start the polling loop, asynchronously. - timeout_id = window[ str_setTimeout ](function(){ - - // Iterate over all elements to which the 'resize' event is bound. - elems.each(function(){ - var elem = $(this), - width = elem.width(), - height = elem.height(), - data = $.data( this, str_data ); - - // If element size has changed since the last time, update the element - // data store and trigger the 'resize' event. - if ( width !== data.w || height !== data.h ) { - elem.trigger( str_resize, [ data.w = width, data.h = height ] ); - } - - }); - - // Loop. - loopy(); - - }, jq_resize[ str_delay ] ); - - }; - -})(jQuery,this); diff --git a/vendor/assets/javascripts/xterm/fit.js b/vendor/assets/javascripts/xterm/fit.js index 7e24fd9b36e..55438452cad 100644 --- a/vendor/assets/javascripts/xterm/fit.js +++ b/vendor/assets/javascripts/xterm/fit.js @@ -16,12 +16,12 @@ /* * CommonJS environment */ - module.exports = fit(require('../../xterm')); + module.exports = fit(require('./xterm')); } else if (typeof define == 'function') { /* * Require.js is available */ - define(['../../xterm'], fit); + define(['./xterm'], fit); } else { /* * Plain browser environment |