diff options
author | Pawel Chojnacki <pawel@chojnacki.ws> | 2017-06-16 18:19:41 +0200 |
---|---|---|
committer | Pawel Chojnacki <pawel@chojnacki.ws> | 2017-06-16 18:19:41 +0200 |
commit | 9f2c992ff1520e35d9b7bc26d603d597bc189618 (patch) | |
tree | 17af71363c63d99a15c76b1edcbceddc87bfc12d | |
parent | 64bb0d37d4ef1f8574355019f198e40bc9b70224 (diff) | |
parent | 5f42009f8dcc29d559ee415e92c88858e361f063 (diff) | |
download | gitlab-ce-9f2c992ff1520e35d9b7bc26d603d597bc189618.tar.gz |
Merge remote-tracking branch 'upstream/master' into 28717-additional-metrics-review-branch
1189 files changed, 21912 insertions, 7088 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml index e5636a13783..42afed54371 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -10,10 +10,10 @@ engines: languages: - ruby - javascript + exclude_paths: + - "lib/api/v3/*" eslint: enabled: true - fixme: - enabled: true rubocop: enabled: true ratings: @@ -35,4 +35,13 @@ exclude_paths: - node_modules/ - spec/ - vendor/ -- lib/api/v3/ +- .yarn-cache/ +- tmp/ +- builds/ +- coverage/ +- public/ +- shared/ +- webpack-report/ +- log/ +- backups/ +- coverage-javascript/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b442e48a3d0..f0c266485b6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -441,21 +441,40 @@ gitlab:assets:compile: - webpack-report/ karma: + image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.7-chrome-59.0-node-7.1-postgresql-9.6" stage: test <<: *use-pg <<: *dedicated-runner <<: *except-docs variables: BABEL_ENV: "coverage" + CHROME_LOG_FILE: "chrome_debug.log" script: - bundle exec rake karma coverage: '/^Statements *: (\d+\.\d+%)/' artifacts: name: coverage-javascript expire_in: 31d + when: always paths: + - chrome_debug.log - coverage-javascript/ +codeclimate: + before_script: [] + image: docker:latest + stage: test + variables: + SETUP_DB: "false" + DOCKER_DRIVER: overlay + services: + - docker:dind + script: + - docker pull codeclimate/codeclimate + - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json + artifacts: + paths: [codeclimate.json] + coverage: stage: post-test services: [] diff --git a/.rubocop.yml b/.rubocop.yml index 8f611a96702..4537e710dd4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -397,7 +397,7 @@ Style/ParenthesesAroundCondition: # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: short, verbose Style/PreferredHashMethods: - Enabled: true + Enabled: false # Checks for an obsolete RuntimeException argument in raise/fail. Style/RedundantException: @@ -1064,6 +1064,13 @@ RSpec/NotToNot: RSpec/RepeatedDescription: Enabled: false +# Ensure RSpec hook blocks are always multi-line. +RSpec/SingleLineHook: + Enabled: true + Exclude: + - 'spec/factories/*' + - 'spec/requests/api/v3/*' + # Checks for stubbed test subjects. RSpec/SubjectStub: Enabled: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e6d8d398a5..f43858a00a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,41 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 9.2.6 (2017-06-16) + +- Fix the last coverage in trace log should be extracted. !11128 (dosuken123) +- Respect merge, instead of push, permissions for protected actions. !11648 +- Fix pipeline_schedules pages throwing error 500. !11706 (dosuken123) +- Make backup task to continue on corrupt repositories. !11962 +- Fix incorrect ETag cache key when relative instance URL is used. !11964 +- Fix math rendering on blob pages. +- Invalidate cache for issue and MR counters more granularly. +- Fix terminals support for Kubernetes Service. +- Fix LFS timeouts when trying to save large files. +- Strip trailing whitespaces in submodule URLs. +- Make sure reCAPTCHA configuration is loaded when spam checks are initiated. +- Remove foreigh key on ci_trigger_schedules only if it exists. + +## 9.2.5 (2017-06-07) + +- No changes. + +## 9.2.4 (2017-06-02) + +- Fix visibility when referencing snippets. + +## 9.2.3 (2017-05-31) + +- Move uploads from 'public/uploads' to 'public/uploads/system'. +- Escapes html content before appending it to the DOM. +- Restrict API X-Frame-Options to same origin. +- Allow users autocomplete by author_id only for authenticated users. + +## 9.2.2 (2017-05-25) + +- Fix issue where real time pipelines were not cached. !11615 +- Make all notes use equal padding. + ## 9.2.1 (2017-05-23) - Fix placement of note emoji on hover. @@ -207,6 +242,20 @@ entry. - Fix preemptive scroll bar on user activity calendar. - Pipeline chat notifications convert seconds to minutes and hours. +## 9.1.7 (2017-06-07) + +- No changes. + +## 9.1.6 (2017-06-02) + +- Fix visibility when referencing snippets. + +## 9.1.5 (2017-05-31) + +- Move uploads from 'public/uploads' to 'public/uploads/system'. +- Restrict API X-Frame-Options to same origin. +- Allow users autocomplete by author_id only for authenticated users. + ## 9.1.4 (2017-05-12) - Fix error on CI/CD Settings page related to invalid pipeline trigger. !10948 (dosuken123) @@ -505,6 +554,20 @@ entry. - Only send chat notifications for the default branch. - Don't fill in the default kubernetes namespace. +## 9.0.10 (2017-06-07) + +- No changes. + +## 9.0.9 (2017-06-02) + +- Fix visibility when referencing snippets. + +## 9.0.8 (2017-05-31) + +- Move uploads from 'public/uploads' to 'public/uploads/system'. +- Restrict API X-Frame-Options to same origin. +- Allow users autocomplete by author_id only for authenticated users. + ## 9.0.7 (2017-05-05) - Enforce project features when searching blobs and wikis. diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 78bc1abd14f..bc859cbd6d9 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.10.0 +0.11.2 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 227cea21564..3e3c2f1e5ed 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -2.0.0 +2.1.1 @@ -2,6 +2,7 @@ source 'https://rubygems.org' gem 'rails', '4.2.8' gem 'rails-deprecated_sanitizer', '~> 1.0.3' +gem 'bootsnap', '~> 1.0.0' # Responders respond_to and respond_with gem 'responders', '~> 2.0' @@ -17,7 +18,7 @@ gem 'pg', '~> 0.18.2', group: :postgres gem 'rugged', '~> 0.25.1.1' -gem 'faraday', '~> 0.11.0' +gem 'faraday', '~> 0.12' # Authentication libraries gem 'devise', '~> 4.2' @@ -258,16 +259,30 @@ gem 'sentry-raven', '~> 2.4.0' gem 'premailer-rails', '~> 1.9.0' # I18n -gem 'ruby_parser', '~> 3.8.4', require: false +gem 'ruby_parser', '~> 3.8', require: false gem 'gettext_i18n_rails', '~> 1.8.0' gem 'gettext_i18n_rails_js', '~> 1.2.0' gem 'gettext', '~> 3.2.2', require: false, group: :development +# Perf bar +gem 'peek', '~> 1.0.1' +gem 'peek-gc', '~> 0.0.2' +gem 'peek-host', '~> 1.0.0' +gem 'peek-mysql2', '~> 1.1.0', group: :mysql +gem 'peek-performance_bar', '~> 1.2.1' +gem 'peek-pg', '~> 1.3.0', group: :postgres +gem 'peek-rblineprof', '~> 0.2.0' +gem 'peek-redis', '~> 1.2.0' +gem 'peek-sidekiq', '~> 1.0.3' + # Metrics group :metrics do gem 'allocations', '~> 1.0', require: false, platform: :mri gem 'method_source', '~> 0.8', require: false gem 'influxdb', '~> 0.2', require: false + + # Prometheus + gem 'prometheus-client-mmap', '~>0.7.0.beta5' end group :development do @@ -355,7 +370,7 @@ gem 'html2text' gem 'ruby-prof', '~> 0.16.2' # OAuth -gem 'oauth2', '~> 1.3.0' +gem 'oauth2', '~> 1.4' # Soft deletion gem 'paranoia', '~> 2.2' diff --git a/Gemfile.lock b/Gemfile.lock index be1f6555851..6755c75e331 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -56,6 +56,7 @@ GEM asciidoctor-plantuml (0.0.7) asciidoctor (~> 1.5) ast (2.3.0) + atomic (1.1.99) attr_encrypted (3.0.3) encryptor (~> 3.0.0) attr_required (1.0.0) @@ -82,6 +83,8 @@ GEM bindata (2.3.5) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) + bootsnap (1.0.0) + msgpack (~> 1.0) bootstrap-sass (3.3.6) autoprefixer-rails (>= 5.2.1) sass (>= 3.3.4) @@ -129,6 +132,8 @@ GEM coffee-script-source (1.10.0) colorize (0.7.7) concurrent-ruby (1.0.5) + concurrent-ruby-ext (1.0.5) + concurrent-ruby (= 1.0.5) connection_pool (2.2.1) crack (0.4.3) safe_yaml (~> 1.0.0) @@ -191,7 +196,7 @@ GEM factory_girl_rails (4.7.0) factory_girl (~> 4.7.0) railties (>= 3.0.0) - faraday (0.11.0) + faraday (0.12.1) multipart-post (>= 1.2, < 3) faraday_middleware (0.11.0.1) faraday (>= 0.7.4, < 1.0) @@ -457,7 +462,9 @@ GEM mimemagic (0.3.0) mini_portile2 (2.1.0) minitest (5.7.0) + mmap2 (2.2.6) mousetrap-rails (1.4.6) + msgpack (1.1.0) multi_json (1.12.1) multi_xml (0.6.0) multipart-post (2.0.0) @@ -473,8 +480,8 @@ GEM mini_portile2 (~> 2.1.0) numerizer (0.1.1) oauth (0.5.1) - oauth2 (1.3.1) - faraday (>= 0.8, < 0.12) + oauth2 (1.4.0) + faraday (>= 0.8, < 0.13) jwt (~> 1.0) multi_json (~> 1.3) multi_xml (~> 0.5) @@ -544,6 +551,36 @@ GEM parser (2.4.0.0) ast (~> 2.2) path_expander (1.0.1) + peek (1.0.1) + concurrent-ruby (>= 0.9.0) + concurrent-ruby-ext (>= 0.9.0) + railties (>= 4.0.0) + peek-gc (0.0.2) + peek + peek-host (1.0.0) + peek + peek-mysql2 (1.1.0) + atomic (>= 1.0.0) + mysql2 + peek + peek-performance_bar (1.2.1) + peek (>= 0.1.0) + peek-pg (1.3.0) + concurrent-ruby + concurrent-ruby-ext + peek + pg + peek-rblineprof (0.2.0) + peek + rblineprof + peek-redis (1.2.0) + atomic (>= 1.0.0) + peek + redis + peek-sidekiq (1.0.3) + atomic (>= 1.0.0) + peek + sidekiq pg (0.18.4) po_to_json (1.0.1) json (>= 1.6.0) @@ -560,6 +597,8 @@ GEM premailer-rails (1.9.2) actionmailer (>= 3, < 6) premailer (~> 1.7, >= 1.7.9) + prometheus-client-mmap (0.7.0.beta5) + mmap2 (~> 2.2.6) pry (0.10.4) coderay (~> 1.1.0) method_source (~> 0.8.1) @@ -652,7 +691,7 @@ GEM retriable (1.4.1) rinku (2.0.0) rotp (2.1.2) - rouge (2.0.7) + rouge (2.1.0) rqrcode (0.7.0) chunky_png rqrcode-rails3 (0.1.7) @@ -700,7 +739,7 @@ GEM ruby-progressbar (1.8.1) ruby-saml (1.4.1) nokogiri (>= 1.5.10) - ruby_parser (3.8.4) + ruby_parser (3.9.0) sexp_processor (~> 4.1) rubyntlm (0.5.2) rubypants (0.2.0) @@ -733,7 +772,7 @@ GEM sentry-raven (2.4.0) faraday (>= 0.7.6, < 1.0) settingslogic (2.0.9) - sexp_processor (4.8.0) + sexp_processor (4.9.0) sham_rack (1.3.6) rack shoulda-matchers (2.8.0) @@ -885,6 +924,7 @@ DEPENDENCIES benchmark-ips (~> 2.3.0) better_errors (~> 2.1.0) binding_of_caller (~> 0.7.2) + bootsnap (~> 1.0.0) bootstrap-sass (~> 3.3.0) brakeman (~> 3.6.0) browser (~> 2.2) @@ -913,7 +953,7 @@ DEPENDENCIES email_reply_trimmer (~> 0.1) email_spec (~> 1.6.0) factory_girl_rails (~> 4.7.0) - faraday (~> 0.11.0) + faraday (~> 0.12) ffaker (~> 2.4) flay (~> 2.8.0) flipper (~> 0.10.2) @@ -972,7 +1012,7 @@ DEPENDENCIES mysql2 (~> 0.3.16) net-ssh (~> 3.0.1) nokogiri (~> 1.6.7, >= 1.6.7.2) - oauth2 (~> 1.3.0) + oauth2 (~> 1.4) octokit (~> 4.6.2) oj (~> 2.17.4) omniauth (~> 1.4.2) @@ -992,9 +1032,19 @@ DEPENDENCIES omniauth_crowd (~> 2.2.0) org-ruby (~> 0.9.12) paranoia (~> 2.2) + peek (~> 1.0.1) + peek-gc (~> 0.0.2) + peek-host (~> 1.0.0) + peek-mysql2 (~> 1.1.0) + peek-performance_bar (~> 1.2.1) + peek-pg (~> 1.3.0) + peek-rblineprof (~> 0.2.0) + peek-redis (~> 1.2.0) + peek-sidekiq (~> 1.0.3) pg (~> 0.18.2) poltergeist (~> 1.9.0) premailer-rails (~> 1.9.0) + prometheus-client-mmap (~> 0.7.0.beta5) pry-byebug (~> 3.4.1) pry-rails (~> 0.3.4) rack-attack (~> 4.4.1) @@ -1023,7 +1073,7 @@ DEPENDENCIES rubocop-rspec (~> 1.15.0) ruby-fogbugz (~> 0.2.1) ruby-prof (~> 0.16.2) - ruby_parser (~> 3.8.4) + ruby_parser (~> 3.8) rufus-scheduler (~> 3.4) rugged (~> 0.25.1.1) sanitize (~> 2.0) diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 6680834a8d1..56fa0d71a9a 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -77,7 +77,7 @@ const Api = { dataType: 'json', }) .done(label => callback(label)) - .error(message => callback(message.responseJSON)); + .fail(message => callback(message.responseJSON)); }, // Return group projects list. Filtered by query @@ -134,7 +134,7 @@ const Api = { dataType: 'json', }) .done(file => callback(null, file)) - .error(callback); + .fail(callback); }, users(query, options) { diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js index 23d91fdb259..36ce4fddb72 100644 --- a/app/assets/javascripts/behaviors/gl_emoji.js +++ b/app/assets/javascripts/behaviors/gl_emoji.js @@ -88,6 +88,7 @@ function installGlEmojiElement() { const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0; if ( + emojiUnicode && isEmojiUnicode && !isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion) ) { diff --git a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js index 20ab2d7e827..4f8884d05ac 100644 --- a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js +++ b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js @@ -28,7 +28,8 @@ function isSkinToneComboEmoji(emojiUnicode) { // doesn't support the skin tone versions of horse racing const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16) function isHorceRacingSkinToneComboEmoji(emojiUnicode) { - return Array.from(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint && + const firstCharacter = Array.from(emojiUnicode)[0]; + return firstCharacter && firstCharacter.codePointAt(0) === horseRacingCodePoint && isSkinToneComboEmoji(emojiUnicode); } diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index 4568b86f298..dc636050221 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -35,7 +35,7 @@ export default class BlobFileDropzone { this.removeFile(file); }); this.on('sending', function (file, xhr, formData) { - formData.append('branch_name', form.find('input[name="branch_name"]').val()); + formData.append('branch_name', form.find('.js-branch-name').val()); formData.append('create_merge_request', form.find('.js-create-merge-request').val()); formData.append('commit_message', form.find('.js-commit-message').val()); }); diff --git a/app/assets/javascripts/blob/create_branch_dropdown.js b/app/assets/javascripts/blob/create_branch_dropdown.js deleted file mode 100644 index 95517f51b1c..00000000000 --- a/app/assets/javascripts/blob/create_branch_dropdown.js +++ /dev/null @@ -1,88 +0,0 @@ -class CreateBranchDropdown { - constructor(el, targetBranchDropdown) { - this.targetBranchDropdown = targetBranchDropdown; - this.el = el; - this.dropdownBack = this.el.closest('.dropdown').querySelector('.dropdown-menu-back'); - this.cancelButton = this.el.querySelector('.js-cancel-branch-btn'); - this.newBranchField = this.el.querySelector('#new_branch_name'); - this.newBranchCreateButton = this.el.querySelector('.js-new-branch-btn'); - - this.newBranchCreateButton.setAttribute('disabled', ''); - - this.addBindings(); - this.cleanupWrapper = this.cleanup.bind(this); - document.addEventListener('beforeunload', this.cleanupWrapper); - } - - cleanup() { - this.cleanBindings(); - document.removeEventListener('beforeunload', this.cleanupWrapper); - } - - cleanBindings() { - this.newBranchField.removeEventListener('keyup', this.enableBranchCreateButtonWrapper); - this.newBranchField.removeEventListener('change', this.enableBranchCreateButtonWrapper); - this.newBranchField.removeEventListener('keydown', this.handleNewBranchKeydownWrapper); - this.dropdownBack.removeEventListener('click', this.resetFormWrapper); - this.cancelButton.removeEventListener('click', this.handleCancelClickWrapper); - this.newBranchCreateButton.removeEventListener('click', this.createBranchWrapper); - } - - addBindings() { - this.enableBranchCreateButtonWrapper = this.enableBranchCreateButton.bind(this); - this.handleNewBranchKeydownWrapper = this.handleNewBranchKeydown.bind(this); - this.resetFormWrapper = this.resetForm.bind(this); - this.handleCancelClickWrapper = this.handleCancelClick.bind(this); - this.createBranchWrapper = this.createBranch.bind(this); - - this.newBranchField.addEventListener('keyup', this.enableBranchCreateButtonWrapper); - this.newBranchField.addEventListener('change', this.enableBranchCreateButtonWrapper); - this.newBranchField.addEventListener('keydown', this.handleNewBranchKeydownWrapper); - this.dropdownBack.addEventListener('click', this.resetFormWrapper); - this.cancelButton.addEventListener('click', this.handleCancelClickWrapper); - this.newBranchCreateButton.addEventListener('click', this.createBranchWrapper); - } - - handleCancelClick(e) { - e.preventDefault(); - e.stopPropagation(); - - this.resetForm(); - this.dropdownBack.click(); - } - - handleNewBranchKeydown(e) { - const keyCode = e.which; - const ENTER_KEYCODE = 13; - if (keyCode === ENTER_KEYCODE) { - this.createBranch(e); - } - } - - enableBranchCreateButton() { - if (this.newBranchField.value !== '') { - this.newBranchCreateButton.removeAttribute('disabled'); - } else { - this.newBranchCreateButton.setAttribute('disabled', ''); - } - } - - resetForm() { - this.newBranchField.value = ''; - this.enableBranchCreateButtonWrapper(); - } - - createBranch(e) { - e.preventDefault(); - - if (this.newBranchCreateButton.getAttribute('disabled') === '') { - return; - } - const newBranchName = this.newBranchField.value; - this.targetBranchDropdown.setNewBranch(newBranchName); - this.resetForm(); - } -} - -window.gl = window.gl || {}; -gl.CreateBranchDropdown = CreateBranchDropdown; diff --git a/app/assets/javascripts/blob/target_branch_dropdown.js b/app/assets/javascripts/blob/target_branch_dropdown.js deleted file mode 100644 index d52d69b1274..00000000000 --- a/app/assets/javascripts/blob/target_branch_dropdown.js +++ /dev/null @@ -1,152 +0,0 @@ -/* eslint-disable class-methods-use-this */ -const SELECT_ITEM_MSG = 'Select'; - -class TargetBranchDropDown { - constructor(dropdown) { - this.dropdown = dropdown; - this.$dropdown = $(dropdown); - this.fieldName = this.dropdown.getAttribute('data-field-name'); - this.form = this.dropdown.closest('form'); - this.createDropdown(); - } - - static bootstrap() { - const dropdowns = document.querySelectorAll('.js-project-branches-dropdown'); - [].forEach.call(dropdowns, dropdown => new TargetBranchDropDown(dropdown)); - } - - createDropdown() { - const self = this; - this.$dropdown.glDropdown({ - selectable: true, - filterable: true, - search: { - fields: ['title'], - }, - data: (term, callback) => $.ajax({ - url: self.dropdown.getAttribute('data-refs-url'), - data: { - ref: self.dropdown.getAttribute('data-ref'), - show_all: true, - }, - dataType: 'json', - }).done(refs => callback(self.dropdownData(refs))), - toggleLabel(item, el) { - if (el.is('.is-active')) { - return item.text; - } - return SELECT_ITEM_MSG; - }, - clicked(options) { - options.e.preventDefault(); - self.onClick.call(self); - }, - fieldName: self.fieldName, - }); - return new gl.CreateBranchDropdown(this.form.querySelector('.dropdown-new-branch'), this); - } - - onClick() { - this.enableSubmit(); - this.$dropdown.trigger('change.branch'); - } - - enableSubmit() { - const submitBtn = this.form.querySelector('[type="submit"]'); - if (this.branchInput && this.branchInput.value) { - submitBtn.removeAttribute('disabled'); - } else { - submitBtn.setAttribute('disabled', ''); - } - } - - dropdownData(refs) { - const branchList = this.dropdownItems(refs); - this.cachedRefs = refs; - this.addDefaultBranch(branchList); - this.addNewBranch(branchList); - return { Branches: branchList }; - } - - dropdownItems(refs) { - return refs.map(this.dropdownItem); - } - - dropdownItem(ref) { - return { id: ref, text: ref, title: ref }; - } - - addDefaultBranch(branchList) { - // when no branch is selected do nothing - if (!this.branchInput) { - return; - } - - const branchInputVal = this.branchInput.value; - const currentBranchIndex = this.searchBranch(branchList, branchInputVal); - - if (currentBranchIndex === -1) { - this.unshiftBranch(branchList, this.dropdownItem(branchInputVal)); - } - } - - addNewBranch(branchList) { - if (this.newBranch) { - this.unshiftBranch(branchList, this.newBranch); - } - } - - searchBranch(branchList, branchName) { - return _.findIndex(branchList, el => branchName === el.id); - } - - unshiftBranch(branchList, branch) { - const branchIndex = this.searchBranch(branchList, branch.id); - - if (branchIndex === -1) { - branchList.unshift(branch); - } - } - - setNewBranch(newBranchName) { - this.newBranch = this.dropdownItem(newBranchName); - this.refreshData(); - this.selectBranch(this.searchBranch(this.glDropdown.fullData.Branches, newBranchName)); - } - - refreshData() { - this.glDropdown.fullData = this.dropdownData(this.cachedRefs); - this.clearFilter(); - } - - clearFilter() { - // apply an empty filter in order to refresh the data - this.glDropdown.filter.filter(''); - this.dropdown.closest('.dropdown').querySelector('.dropdown-page-one .dropdown-input-field').value = ''; - } - - selectBranch(index) { - const branch = this.dropdown.closest('.dropdown').querySelectorAll('li a')[index]; - - if (!branch.classList.contains('is-active')) { - branch.click(); - } else { - this.closeDropdown(); - } - } - - closeDropdown() { - this.dropdown.closest('.dropdown').querySelector('.dropdown-menu-close').click(); - } - - get branchInput() { - return this.form.querySelector(`input[name="${this.fieldName}"]`); - } - - get glDropdown() { - return this.$dropdown.data('glDropdown'); - } -} - -window.gl = window.gl || {}; -gl.TargetBranchDropDown = TargetBranchDropDown; diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 0e4aa39226b..b94009ee76b 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -88,6 +88,8 @@ $(() => { if (list.type === 'closed') { list.position = Infinity; list.label = { description: 'Shows all closed issues. Moving an issue to this list closes it' }; + } else if (list.type === 'backlog') { + list.position = -1; } }); @@ -128,7 +130,7 @@ $(() => { }, computed: { disabled() { - return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length; + return !this.store.lists.filter(list => !list.preset).length; }, tooltipTitle() { if (this.disabled) { diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index 9ba84489910..adb7360327c 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -1,6 +1,7 @@ /* eslint-disable comma-dangle, space-before-function-paren, one-var */ /* global Sortable */ import Vue from 'vue'; +import AccessorUtilities from '../../lib/utils/accessor'; import boardList from './board_list'; import boardBlankState from './board_blank_state'; import './board_delete'; @@ -22,6 +23,10 @@ gl.issueBoards.Board = Vue.extend({ disabled: Boolean, issueLinkBase: String, rootPath: String, + boardId: { + type: String, + required: true, + }, }, data () { return { @@ -78,7 +83,16 @@ gl.issueBoards.Board = Vue.extend({ methods: { showNewIssueForm() { this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm; - } + }, + toggleExpanded(e) { + if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) { + this.list.isExpanded = !this.list.isExpanded; + + if (AccessorUtilities.isLocalStorageAccessSafe()) { + localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded); + } + } + }, }, mounted () { this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({ @@ -102,4 +116,11 @@ gl.issueBoards.Board = Vue.extend({ this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions); }, + created() { + if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) { + const isCollapsed = localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false'; + + this.list.isExpanded = !isCollapsed; + } + }, }); diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index 386102032cb..c7afd4ead6b 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -32,9 +32,6 @@ gl.issueBoards.BoardSidebar = Vue.extend({ showSidebar () { return Object.keys(this.issue).length; }, - assigneeId() { - return this.issue.assignee ? this.issue.assignee.id : 0; - }, milestoneTitle() { return this.issue.milestone ? this.issue.milestone.title : 'No Milestone'; } diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index 4699ef5a51c..daef01bc93d 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -152,6 +152,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({ <div class="card-assignee"> <user-avatar-link v-for="(assignee, index) in issue.assignees" + :key="assignee.id" v-if="shouldRenderAssignee(index)" class="js-no-trigger" :link-href="assigneeUrl(assignee)" diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js index fe7ab2db85d..478a1335b2b 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js +++ b/app/assets/javascripts/boards/components/modal/footer.js @@ -26,7 +26,8 @@ gl.issueBoards.ModalFooter = Vue.extend({ }, methods: { addIssues() { - const list = this.modal.selectedList || this.state.lists[0]; + const firstListIndex = 1; + const list = this.modal.selectedList || this.state.lists[firstListIndex]; const selectedIssues = ModalStore.getSelectedIssues(); const issueIds = selectedIssues.map(issue => issue.globalId); diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js b/app/assets/javascripts/boards/components/modal/lists_dropdown.js index 8cd15df90fa..4684ea76647 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js @@ -11,7 +11,7 @@ gl.issueBoards.ModalFooterListsDropdown = Vue.extend({ }, computed: { selected() { - return this.modal.selectedList || this.state.lists[0]; + return this.modal.selectedList || this.state.lists[1]; }, }, destroyed() { diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index 8514e7df1d3..548de1a4c52 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -12,7 +12,9 @@ class List { this.position = obj.position; this.title = obj.title; this.type = obj.list_type; - this.preset = ['closed', 'blank'].indexOf(this.type) > -1; + this.preset = ['backlog', 'closed', 'blank'].indexOf(this.type) > -1; + this.isExpandable = ['backlog', 'closed'].indexOf(this.type) > -1; + this.isExpanded = true; this.page = 1; this.loading = true; this.loadingMore = false; diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 7bddcdc3c1d..1e12d4ca415 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -32,10 +32,14 @@ gl.issueBoards.BoardsStore = { }, new (listObj) { const list = this.addList(listObj); + const backlogList = this.findList('type', 'backlog', 'backlog'); list .save() .then(() => { + // Remove any new issues from the backlog + // as they will be visible in the new list + list.issues.forEach(backlogList.removeIssue.bind(backlogList)); this.state.lists = _.sortBy(this.state.lists, 'position'); }) .catch(() => { @@ -48,7 +52,7 @@ gl.issueBoards.BoardsStore = { }, shouldAddBlankState () { // Decide whether to add the blank state - return !(this.state.lists.filter(list => list.type !== 'closed')[0]); + return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0]); }, addBlankState () { if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; @@ -101,7 +105,7 @@ gl.issueBoards.BoardsStore = { issueTo.removeLabel(listFrom.label); } - if (listTo.type === 'closed') { + if (listTo.type === 'closed' && listFrom.type !== 'backlog') { issueLists.forEach((list) => { list.removeIssue(issue); }); diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 072a899e9f2..c28f6e151a0 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -20,6 +20,7 @@ window.Build = (function () { this.$document = $(document); this.logBytes = 0; this.scrollOffsetPadding = 30; + this.hasBeenScrolled = false; this.updateDropdown = this.updateDropdown.bind(this); this.getBuildTrace = this.getBuildTrace.bind(this); @@ -62,6 +63,15 @@ window.Build = (function () { .off('click') .on('click', this.scrollToBottom.bind(this)); + const scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100); + + this.$scrollContainer + .off('scroll') + .on('scroll', () => { + this.hasBeenScrolled = true; + scrollThrottled(); + }); + $(window) .off('resize.build') .on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100)); @@ -70,25 +80,16 @@ window.Build = (function () { // eslint-disable-next-line this.getBuildTrace() - .then(() => this.makeTraceScrollable()) - .then(() => this.scrollToBottom()); + .then(() => this.toggleScroll()) + .then(() => { + if (!this.hasBeenScrolled) { + this.scrollToBottom(); + } + }); this.verifyTopPosition(); } - Build.prototype.makeTraceScrollable = function () { - this.$scrollContainer.niceScroll({ - cursorcolor: '#fff', - cursoropacitymin: 1, - cursorwidth: '3px', - railpadding: { top: 5, bottom: 5, right: 5 }, - }); - - this.$scrollContainer.on('scroll', _.throttle(this.toggleScroll.bind(this), 100)); - - this.toggleScroll(); - }; - Build.prototype.canScroll = function () { return (this.$scrollContainer.prop('scrollHeight') - this.scrollOffsetPadding) > this.$scrollContainer.height(); }; @@ -104,12 +105,11 @@ window.Build = (function () { * */ Build.prototype.toggleScroll = function () { - const bottomScroll = this.$scrollContainer.scrollTop() + - this.scrollOffsetPadding + - this.$scrollContainer.height(); + const currentPosition = this.$scrollContainer.scrollTop(); + const bottomScroll = currentPosition + this.$scrollContainer.innerHeight(); if (this.canScroll()) { - if (this.$scrollContainer.scrollTop() === 0) { + if (currentPosition === 0) { this.toggleDisableButton(this.$scrollTopBtn, true); this.toggleDisableButton(this.$scrollBottomBtn, false); } else if (bottomScroll === this.$scrollContainer.prop('scrollHeight')) { @@ -123,12 +123,14 @@ window.Build = (function () { }; Build.prototype.scrollToTop = function () { - this.$scrollContainer.getNiceScroll(0).doScrollTop(0); + this.hasBeenScrolled = true; + this.$scrollContainer.scrollTop(0); this.toggleScroll(); }; Build.prototype.scrollToBottom = function () { - this.$scrollContainer.getNiceScroll(0).doScrollTo(this.$scrollContainer.prop('scrollHeight')); + this.hasBeenScrolled = true; + this.$scrollContainer.scrollTop(this.$scrollContainer.prop('scrollHeight')); this.toggleScroll(); }; @@ -147,27 +149,34 @@ window.Build = (function () { Build.prototype.verifyTopPosition = function () { const $buildPage = $('.build-page'); + const $flashError = $('.alert-wrapper'); const $header = $('.build-header', $buildPage); const $runnersStuck = $('.js-build-stuck', $buildPage); const $startsEnvironment = $('.js-environment-container', $buildPage); const $erased = $('.js-build-erased', $buildPage); + const prependTopDefault = 20; + // header + navigation + margin let topPostion = 168; - if ($header) { + if ($header.length) { topPostion += $header.outerHeight(); } - if ($runnersStuck) { + if ($runnersStuck.length) { topPostion += $runnersStuck.outerHeight(); } - if ($startsEnvironment) { - topPostion += $startsEnvironment.outerHeight(); + if ($startsEnvironment.length) { + topPostion += $startsEnvironment.outerHeight() + prependTopDefault; + } + + if ($erased.length) { + topPostion += $erased.outerHeight() + prependTopDefault; } - if ($erased) { - topPostion += $erased.outerHeight() + 10; + if ($flashError.length) { + topPostion += $flashError.outerHeight(); } this.$buildTrace.css({ @@ -216,7 +225,11 @@ window.Build = (function () { Build.timeout = setTimeout(() => { //eslint-disable-next-line this.getBuildTrace() - .then(() => this.scrollToBottom()); + .then(() => { + if (!this.hasBeenScrolled) { + this.scrollToBottom(); + } + }); }, 4000); } else { this.$buildRefreshAnimation.remove(); @@ -238,7 +251,8 @@ window.Build = (function () { }; Build.prototype.toggleSidebar = function (shouldHide) { - const shouldShow = !shouldHide; + const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined; + const $toggleButton = $('.js-sidebar-build-toggle-header'); this.$buildTrace .toggleClass('sidebar-expanded', shouldShow) @@ -246,6 +260,16 @@ window.Build = (function () { this.$sidebar .toggleClass('right-sidebar-expanded', shouldShow) .toggleClass('right-sidebar-collapsed', shouldHide); + + $('.js-build-page') + .toggleClass('sidebar-expanded', shouldShow) + .toggleClass('sidebar-collapsed', shouldHide); + + if (this.$sidebar.hasClass('right-sidebar-expanded')) { + $toggleButton.addClass('hidden'); + } else { + $toggleButton.removeClass('hidden'); + } }; Build.prototype.sidebarOnResize = function () { @@ -253,13 +277,14 @@ window.Build = (function () { this.verifyTopPosition(); - if (this.$scrollContainer.getNiceScroll(0)) { + if (this.canScroll()) { this.toggleScroll(); } }; Build.prototype.sidebarOnClick = function () { if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); + this.verifyTopPosition(); }; Build.prototype.updateArtifactRemoveDate = function () { diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js index 082fbafb740..70ba83ce5b9 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import Visibility from 'visibilityjs'; -import pipelinesTableComponent from '../../vue_shared/components/pipelines_table'; +import pipelinesTableComponent from '../../vue_shared/components/pipelines_table.vue'; import PipelinesService from '../../pipelines/services/pipelines_service'; import PipelineStore from '../../pipelines/stores/pipelines_store'; import eventHub from '../../pipelines/event_hub'; diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js index cb054a2a197..bc3e741f524 100644 --- a/app/assets/javascripts/commons/polyfills.js +++ b/app/assets/javascripts/commons/polyfills.js @@ -1,5 +1,6 @@ // ECMAScript polyfills import 'core-js/fn/array/find'; +import 'core-js/fn/array/find-index'; import 'core-js/fn/array/from'; import 'core-js/fn/array/includes'; import 'core-js/fn/object/assign'; diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue index 285124e9515..a663e30dfd0 100644 --- a/app/assets/javascripts/deploy_keys/components/app.vue +++ b/app/assets/javascripts/deploy_keys/components/app.vue @@ -80,21 +80,27 @@ v-if="isLoading && !hasKeys" size="2" label="Loading deploy keys" - /> + /> <div v-else-if="hasKeys"> <keys-panel title="Enabled deploy keys for this project" :keys="keys.enabled_keys" - :store="store" /> + :store="store" + :endpoint="endpoint" + /> <keys-panel title="Deploy keys from projects you have access to" :keys="keys.available_project_keys" - :store="store" /> + :store="store" + :endpoint="endpoint" + /> <keys-panel v-if="keys.public_keys.length" title="Public deploy keys available to any project" :keys="keys.public_keys" - :store="store" /> + :store="store" + :endpoint="endpoint" + /> </div> </div> </template> diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue index 0a06a481b96..904f7f64fa8 100644 --- a/app/assets/javascripts/deploy_keys/components/key.vue +++ b/app/assets/javascripts/deploy_keys/components/key.vue @@ -11,6 +11,10 @@ type: Object, required: true, }, + endpoint: { + type: String, + required: true, + }, }, components: { actionBtn, @@ -19,6 +23,9 @@ timeagoDate() { return gl.utils.getTimeago().format(this.deployKey.created_at); }, + editDeployKeyPath() { + return `${this.endpoint}/${this.deployKey.id}/edit`; + }, }, methods: { isEnabled(id) { @@ -33,7 +40,8 @@ <div class="pull-left append-right-10 hidden-xs"> <i aria-hidden="true" - class="fa fa-key key-icon"> + class="fa fa-key key-icon" + > </i> </div> <div class="deploy-key-content key-list-item-info"> @@ -45,7 +53,8 @@ </div> <div v-if="deployKey.can_push" - class="write-access-allowed"> + class="write-access-allowed" + > Write access allowed </div> </div> @@ -53,7 +62,8 @@ <a v-for="project in deployKey.projects" class="label deploy-project-label" - :href="project.full_path"> + :href="project.full_path" + > {{ project.full_name }} </a> </div> @@ -61,20 +71,30 @@ <span class="key-created-at"> created {{ timeagoDate }} </span> + <a + v-if="deployKey.can_edit" + class="btn btn-small" + :href="editDeployKeyPath" + > + Edit + </a> <action-btn v-if="!isEnabled(deployKey.id)" :deploy-key="deployKey" - type="enable"/> + type="enable" + /> <action-btn v-else-if="deployKey.destroyed_when_orphaned && deployKey.almost_orphaned" :deploy-key="deployKey" btn-css-class="btn-warning" - type="remove" /> + type="remove" + /> <action-btn v-else :deploy-key="deployKey" btn-css-class="btn-warning" - type="disable" /> + type="disable" + /> </div> </div> </template> diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue index eccc470578b..9e6fb244af6 100644 --- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue +++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue @@ -20,6 +20,10 @@ type: Object, required: true, }, + endpoint: { + type: String, + required: true, + }, }, components: { key, @@ -34,18 +38,22 @@ ({{ keys.length }}) </h5> <ul class="well-list" - v-if="keys.length"> + v-if="keys.length" + > <li v-for="deployKey in keys" :key="deployKey.id"> <key :deploy-key="deployKey" - :store="store" /> + :store="store" + :endpoint="endpoint" + /> </li> </ul> <div class="settings-message text-center" - v-else-if="showHelpBox"> + v-else-if="showHelpBox" + > No deploy keys found. Create one with the form above. </div> </div> diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index c3e096561e4..5f87a05067b 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -2,7 +2,6 @@ /* global UsernameValidator */ /* global ActiveTabMemoizer */ /* global ShortcutsNavigation */ -/* global Build */ /* global IssuableIndex */ /* global ShortcutsIssuable */ /* global ZenMode */ @@ -119,9 +118,6 @@ import initSettingsPanels from './settings_panels'; shortcut_handler = new ShortcutsNavigation(); new UsersSelect(); break; - case 'projects:jobs:show': - new Build(); - break; case 'projects:merge_requests:index': case 'projects:issues:index': if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) { @@ -160,9 +156,6 @@ import initSettingsPanels from './settings_panels'; case 'admin:projects:index': new ProjectsList(); break; - case 'dashboard:groups:index': - new GroupsList(); - break; case 'explore:groups:index': new GroupsList(); @@ -218,6 +211,16 @@ import initSettingsPanels from './settings_panels'; new gl.GLForm($('.tag-form')); new RefSelectDropdown($('.js-branch-select'), window.gl.availableRefs); break; + case 'projects:snippets:new': + case 'projects:snippets:edit': + case 'projects:snippets:create': + case 'projects:snippets:update': + case 'snippets:new': + case 'snippets:edit': + case 'snippets:create': + case 'snippets:update': + new gl.GLForm($('.snippet-form')); + break; case 'projects:releases:edit': new ZenMode(); new gl.GLForm($('.release-form')); @@ -322,25 +325,14 @@ import initSettingsPanels from './settings_panels'; shortcut_handler = new ShortcutsNavigation(); new TreeView(); new BlobViewer(); - gl.TargetBranchDropDown.bootstrap(); break; case 'projects:find_file:show': shortcut_handler = true; break; - case 'projects:blob:new': - gl.TargetBranchDropDown.bootstrap(); - break; - case 'projects:blob:create': - gl.TargetBranchDropDown.bootstrap(); - break; case 'projects:blob:show': new BlobViewer(); - gl.TargetBranchDropDown.bootstrap(); initBlob(); break; - case 'projects:blob:edit': - gl.TargetBranchDropDown.bootstrap(); - break; case 'projects:blame:show': initBlob(); break; @@ -364,9 +356,11 @@ import initSettingsPanels from './settings_panels'; new ProjectFork(); break; case 'projects:artifacts:browse': + new ShortcutsNavigation(); new BuildArtifacts(); break; case 'projects:artifacts:file': + new ShortcutsNavigation(); new BlobViewer(); break; case 'help:index': diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 111449bb8f7..98ddcc20036 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -5,7 +5,7 @@ import './preview_markdown'; window.DropzoneInput = (function() { function DropzoneInput(form) { - var updateAttachingMessage, $attachingFileMessage, $mdArea, $attachButton, $cancelButton, $retryLink, $uploadingErrorContainer, $uploadingErrorMessage, $uploadProgress, $uploadingProgressContainer, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divHover, divSpinner, dropzone, $formDropzone, formTextarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, maxFileSize, pasteText, uploadsPath, showError, showSpinner, uploadFile; + var updateAttachingMessage, $attachingFileMessage, $mdArea, $attachButton, $cancelButton, $retryLink, $uploadingErrorContainer, $uploadingErrorMessage, $uploadProgress, $uploadingProgressContainer, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divHover, divSpinner, dropzone, $formDropzone, formTextarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, maxFileSize, pasteText, uploadsPath, showError, showSpinner, uploadFile, addFileToForm; Dropzone.autoDiscover = false; divHover = '<div class="div-dropzone-hover"></div>'; iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>'; @@ -71,6 +71,7 @@ window.DropzoneInput = (function() { pasteText(response.link.markdown, shouldPad); // Show 'Attach a file' link only when all files have been uploaded. if (!processingFileCount) $attachButton.removeClass('hide'); + addFileToForm(response.link.url); }, error: function(file, errorMessage = 'Attaching the file failed.', xhr) { // If 'error' event is fired by dropzone, the second parameter is error message. @@ -198,6 +199,10 @@ window.DropzoneInput = (function() { return formTextarea.trigger('input'); }; + addFileToForm = function(path) { + $(form).append('<input type="hidden" name="files[]" value="' + _.escape(path) + '">'); + }; + getFilename = function(e) { var value; if (window.clipboardData && window.clipboardData.getData) { diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue index 28597c799df..8120ef182d4 100644 --- a/app/assets/javascripts/environments/components/environment.vue +++ b/app/assets/javascripts/environments/components/environment.vue @@ -230,7 +230,7 @@ export default { </div> </div> - <div class="content-list environments-container"> + <div class="environments-container"> <loading-icon label="Loading environments" size="3" diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index 41d5453f1b2..a2448520a5f 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -70,7 +70,7 @@ export default { </span> </button> - <ul class="dropdown-menu"> + <ul class="dropdown-menu dropdown-menu-align-right"> <li v-for="action in actions"> <button type="button" diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 03eb51ba1b2..809c147bf25 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -9,7 +9,7 @@ import StopComponent from './environment_stop.vue'; import RollbackComponent from './environment_rollback.vue'; import TerminalButtonComponent from './environment_terminal_button.vue'; import MonitoringButtonComponent from './environment_monitoring.vue'; -import CommitComponent from '../../vue_shared/components/commit'; +import CommitComponent from '../../vue_shared/components/commit.vue'; import eventHub from '../event_hub'; /** @@ -422,11 +422,13 @@ export default { </script> <template> <div - :class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, 'gl-responsive-table-row': !model.isFolder }"> + :class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, 'gl-responsive-table-row': !model.isFolder }" + role="row"> <div class="table-section section-10" role="gridcell"> <div v-if="!model.isFolder" - class="table-mobile-header"> + class="table-mobile-header" + role="rowheader"> Environment </div> <a @@ -497,6 +499,7 @@ export default { <div class="table-section section-25" role="gridcell"> <div v-if="!model.isFolder" + role="rowheader" class="table-mobile-header"> Commit </div> @@ -513,7 +516,7 @@ export default { </div> <div v-if="!model.isFolder && !hasLastDeploymentKey" - class="commit-title"> + class="commit-title table-mobile-content"> No deployments yet </div> </div> @@ -521,6 +524,7 @@ export default { <div class="table-section section-10" role="gridcell"> <div v-if="!model.isFolder" + role="rowheader" class="table-mobile-header"> Updated </div> @@ -531,10 +535,10 @@ export default { </span> </div> - <div class="table-section section-30 environments-actions table-button-footer" role="gridcell"> + <div class="table-section section-30 table-button-footer" role="gridcell"> <div v-if="!model.isFolder" - class="btn-group environment-action-buttons" + class="btn-group table-action-buttons" role="group"> <actions-component diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index f9262ab85c5..b1fd9db650b 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -47,19 +47,19 @@ export default { <template> <div class="ci-table" role="grid"> <div class="gl-responsive-table-row table-row-header" role="row"> - <div class="table-section section-10 environments-name" role="rowheader"> + <div class="table-section section-10 environments-name" role="columnheader"> Environment </div> - <div class="table-section section-10 environments-deploy" role="rowheader"> + <div class="table-section section-10 environments-deploy" role="columnheader"> Deployment </div> - <div class="table-section section-15 environments-build" role="rowheader"> + <div class="table-section section-15 environments-build" role="columnheader"> Job </div> - <div class="table-section section-25 environments-commit" role="rowheader"> + <div class="table-section section-25 environments-commit" role="columnheader"> Commit </div> - <div class="table-section section-10 environments-date" role="rowheader"> + <div class="table-section section-10 environments-date" role="columnheader"> Updated </div> </div> diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js index 8a2f6a473de..a5773dd7e4f 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js +++ b/app/assets/javascripts/environments/stores/environments_store.js @@ -158,5 +158,4 @@ export default class EnvironmentsStore { return environments.filter(env => env.isFolder && env.isOpen); } - } diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index aaaeb9bddb1..139206cc185 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -8,39 +8,87 @@ export default class FilterableList { this.filterForm = form; this.listFilterElement = filter; this.listHolderElement = holder; + this.isBusy = false; + } + + getFilterEndpoint() { + return `${this.filterForm.getAttribute('action')}?${$(this.filterForm).serialize()}`; + } + + getPagePath() { + return this.getFilterEndpoint(); } initSearch() { - this.debounceFilter = _.debounce(this.filterResults.bind(this), 500); + // Wrap to prevent passing event arguments to .filterResults; + this.debounceFilter = _.debounce(this.onFilterInput.bind(this), 500); - this.listFilterElement.removeEventListener('input', this.debounceFilter); + this.unbindEvents(); + this.bindEvents(); + } + + onFilterInput() { + const $form = $(this.filterForm); + const queryData = {}; + const filterGroupsParam = $form.find('[name="filter_groups"]').val(); + + if (filterGroupsParam) { + queryData.filter_groups = filterGroupsParam; + } + + this.filterResults(queryData); + + if (this.setDefaultFilterOption) { + this.setDefaultFilterOption(); + } + } + + bindEvents() { this.listFilterElement.addEventListener('input', this.debounceFilter); } - filterResults() { - const form = this.filterForm; - const filterUrl = `${form.getAttribute('action')}?${$(form).serialize()}`; + unbindEvents() { + this.listFilterElement.removeEventListener('input', this.debounceFilter); + } + + filterResults(queryData) { + if (this.isBusy) { + return false; + } $(this.listHolderElement).fadeTo(250, 0.5); return $.ajax({ - url: form.getAttribute('action'), - data: $(form).serialize(), + url: this.getFilterEndpoint(), + data: queryData, type: 'GET', dataType: 'json', context: this, - complete() { - $(this.listHolderElement).fadeTo(250, 1); + complete: this.onFilterComplete, + beforeSend: () => { + this.isBusy = true; }, - success(data) { - this.listHolderElement.innerHTML = data.html; - - // Change url so if user reload a page - search results are saved - return window.history.replaceState({ - page: filterUrl, - - }, document.title, filterUrl); + success: (response, textStatus, xhr) => { + this.onFilterSuccess(response, xhr, queryData); }, }); } + + onFilterSuccess(response, xhr, queryData) { + if (response.html) { + this.listHolderElement.innerHTML = response.html; + } + + // Change url so if user reload a page - search results are saved + const currentPath = this.getPagePath(queryData); + + return window.history.replaceState({ + page: currentPath, + }, document.title, currentPath); + } + + onFilterComplete() { + this.isBusy = false; + $(this.listHolderElement).fadeTo(250, 1); + } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 72214321be1..8f547bd8f1f 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -77,6 +77,41 @@ class FilteredSearchManager { } } + bindStateEvents() { + this.stateFilters = document.querySelector('.container-fluid .issues-state-filters'); + + if (this.stateFilters) { + this.searchStateWrapper = this.searchState.bind(this); + + this.stateFilters.querySelector('[data-state="opened"]') + .addEventListener('click', this.searchStateWrapper); + this.stateFilters.querySelector('[data-state="closed"]') + .addEventListener('click', this.searchStateWrapper); + this.stateFilters.querySelector('[data-state="all"]') + .addEventListener('click', this.searchStateWrapper); + + this.mergedState = this.stateFilters.querySelector('[data-state="merged"]'); + if (this.mergedState) { + this.mergedState.addEventListener('click', this.searchStateWrapper); + } + } + } + + unbindStateEvents() { + if (this.stateFilters) { + this.stateFilters.querySelector('[data-state="opened"]') + .removeEventListener('click', this.searchStateWrapper); + this.stateFilters.querySelector('[data-state="closed"]') + .removeEventListener('click', this.searchStateWrapper); + this.stateFilters.querySelector('[data-state="all"]') + .removeEventListener('click', this.searchStateWrapper); + + if (this.mergedState) { + this.mergedState.removeEventListener('click', this.searchStateWrapper); + } + } + } + bindEvents() { this.handleFormSubmit = this.handleFormSubmit.bind(this); this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); @@ -112,6 +147,8 @@ class FilteredSearchManager { document.addEventListener('click', this.removeInputContainerFocusWrapper); document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper); eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); + + this.bindStateEvents(); } unbindEvents() { @@ -132,6 +169,8 @@ class FilteredSearchManager { document.removeEventListener('click', this.removeInputContainerFocusWrapper); document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper); eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); + + this.unbindStateEvents(); } checkForBackspace(e) { @@ -447,7 +486,19 @@ class FilteredSearchManager { } } - search() { + searchState(e) { + const target = e.currentTarget; + // remove focus outline after click + target.blur(); + + const state = target.dataset && target.dataset.state; + + if (state) { + this.search(state); + } + } + + search(state = null) { const paths = []; const searchQuery = gl.DropdownUtils.getSearchQuery(); @@ -455,7 +506,7 @@ class FilteredSearchManager { const { tokens, searchToken } = this.tokenizer.processTokens(searchQuery, this.filteredSearchTokenKeys.getKeys()); - const currentState = gl.utils.getParameterByName('state') || 'opened'; + const currentState = state || gl.utils.getParameterByName('state') || 'opened'; paths.push(`state=${currentState}`); tokens.forEach((token) => { diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index d34561e5512..3babe273100 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -248,7 +248,7 @@ GitLabDropdown = (function() { return function(data) { _this.fullData = data; _this.parseData(_this.fullData); - _this.focusTextInput(); + _this.focusTextInput(true); if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') { return _this.filter.input.trigger('input'); } @@ -728,8 +728,20 @@ GitLabDropdown = (function() { return [selectedObject, isMarking]; }; - GitLabDropdown.prototype.focusTextInput = function() { - if (this.options.filterable) { this.filterInput.focus(); } + GitLabDropdown.prototype.focusTextInput = function(triggerFocus = false) { + if (this.options.filterable) { + $(':focus').blur(); + + this.dropdown.one('transitionend', () => { + this.filterInput.focus(); + }); + + if (triggerFocus) { + // This triggers after a ajax request + // in case of slow requests, the dropdown transition could already be finished + this.dropdown.trigger('transitionend'); + } + } }; GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) { diff --git a/app/assets/javascripts/groups/components/group_folder.vue b/app/assets/javascripts/groups/components/group_folder.vue new file mode 100644 index 00000000000..7cc6c4b0359 --- /dev/null +++ b/app/assets/javascripts/groups/components/group_folder.vue @@ -0,0 +1,27 @@ +<script> +export default { + props: { + groups: { + type: Object, + required: true, + }, + baseGroup: { + type: Object, + required: false, + default: () => ({}), + }, + }, +}; +</script> + +<template> + <ul class="content-list group-list-tree"> + <group-item + v-for="(group, index) in groups" + :key="index" + :group="group" + :base-group="baseGroup" + :collection="groups" + /> + </ul> +</template> diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue new file mode 100644 index 00000000000..b1db34b9c50 --- /dev/null +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -0,0 +1,220 @@ +<script> +import eventHub from '../event_hub'; + +export default { + props: { + group: { + type: Object, + required: true, + }, + baseGroup: { + type: Object, + required: false, + default: () => ({}), + }, + collection: { + type: Object, + required: false, + default: () => ({}), + }, + }, + methods: { + onClickRowGroup(e) { + e.stopPropagation(); + + // Skip for buttons + if (!(e.target.tagName === 'A') && !(e.target.tagName === 'I' && e.target.parentElement.tagName === 'A')) { + if (this.group.hasSubgroups) { + eventHub.$emit('toggleSubGroups', this.group); + } else { + window.location.href = this.group.groupPath; + } + } + }, + onLeaveGroup(e) { + e.preventDefault(); + + // eslint-disable-next-line no-alert + if (confirm(`Are you sure you want to leave the "${this.group.fullName}" group?`)) { + this.leaveGroup(); + } + }, + leaveGroup() { + eventHub.$emit('leaveGroup', this.group, this.collection); + }, + }, + computed: { + groupDomId() { + return `group-${this.group.id}`; + }, + rowClass() { + return { + 'group-row': true, + 'is-open': this.group.isOpen, + 'has-subgroups': this.group.hasSubgroups, + 'no-description': !this.group.description, + }; + }, + visibilityIcon() { + return { + fa: true, + 'fa-globe': this.group.visibility === 'public', + 'fa-shield': this.group.visibility === 'internal', + 'fa-lock': this.group.visibility === 'private', + }; + }, + fullPath() { + let fullPath = ''; + + if (this.group.isOrphan) { + // check if current group is baseGroup + if (Object.keys(this.baseGroup).length > 0 && this.baseGroup !== this.group) { + // Remove baseGroup prefix from our current group.fullName. e.g: + // baseGroup.fullName: `level1` + // group.fullName: `level1 / level2 / level3` + // Result: `level2 / level3` + const gfn = this.group.fullName; + const bfn = this.baseGroup.fullName; + const length = bfn.length; + const start = gfn.indexOf(bfn); + const extraPrefixChars = 3; + + fullPath = gfn.substr(start + length + extraPrefixChars); + } else { + fullPath = this.group.fullName; + } + } else { + fullPath = this.group.name; + } + + return fullPath; + }, + hasGroups() { + return Object.keys(this.group.subGroups).length > 0; + }, + }, +}; +</script> + +<template> + <li + @click.stop="onClickRowGroup" + :id="groupDomId" + :class="rowClass" + > + <div + class="group-row-contents"> + <div + class="controls"> + <a + v-if="group.canEdit" + class="edit-group btn" + :href="group.editPath"> + <i + class="fa fa-cogs" + aria-hidden="true" + > + </i> + </a> + <a + @click="onLeaveGroup" + :href="group.leavePath" + class="leave-group btn" + title="Leave this group"> + <i + class="fa fa-sign-out" + aria-hidden="true" + > + </i> + </a> + </div> + <div + class="stats"> + <span + class="number-projects"> + <i + class="fa fa-bookmark" + aria-hidden="true" + > + </i> + {{group.numberProjects}} + </span> + <span + class="number-users"> + <i + class="fa fa-users" + aria-hidden="true" + > + </i> + {{group.numberUsers}} + </span> + <span + class="group-visibility"> + <i + :class="visibilityIcon" + aria-hidden="true" + > + </i> + </span> + </div> + <div + class="folder-toggle-wrap"> + <span + class="folder-caret" + v-if="group.hasSubgroups"> + <i + v-if="group.isOpen" + class="fa fa-caret-down" + aria-hidden="true" + > + </i> + <i + v-if="!group.isOpen" + class="fa fa-caret-right" + aria-hidden="true" + > + </i> + </span> + <span class="folder-icon"> + <i + v-if="group.isOpen" + class="fa fa-folder-open" + aria-hidden="true" + > + </i> + <i + v-if="!group.isOpen" + class="fa fa-folder" + aria-hidden="true"> + </i> + </span> + </div> + <div + class="avatar-container s40 hidden-xs"> + <a + :href="group.groupPath"> + <img + class="avatar s40" + :src="group.avatarUrl" + /> + </a> + </div> + <div + class="title"> + <a + :href="group.groupPath">{{fullPath}}</a> + <template v-if="group.permissions.humanGroupAccess"> + as + <span class="access-type">{{group.permissions.humanGroupAccess}}</span> + </template> + </div> + <div + class="description">{{group.description}}</div> + </div> + <group-folder + v-if="group.isOpen && hasGroups" + :groups="group.subGroups" + :baseGroup="group" + /> + </li> +</template> diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue new file mode 100644 index 00000000000..36a04d4202f --- /dev/null +++ b/app/assets/javascripts/groups/components/groups.vue @@ -0,0 +1,39 @@ +<script> +import tablePagination from '~/vue_shared/components/table_pagination.vue'; +import eventHub from '../event_hub'; + +export default { + props: { + groups: { + type: Object, + required: true, + }, + pageInfo: { + type: Object, + required: true, + }, + }, + components: { + tablePagination, + }, + methods: { + change(page) { + const filterGroupsParam = gl.utils.getParameterByName('filter_groups'); + const sortParam = gl.utils.getParameterByName('sort'); + eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam); + }, + }, +}; +</script> + +<template> + <div class="groups-list-tree-container"> + <group-folder + :groups="groups" + /> + <table-pagination + :change="change" + :pageInfo="pageInfo" + /> + </div> +</template> diff --git a/app/assets/javascripts/groups/event_hub.js b/app/assets/javascripts/groups/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/groups/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js new file mode 100644 index 00000000000..439a931ddad --- /dev/null +++ b/app/assets/javascripts/groups/groups_filterable_list.js @@ -0,0 +1,87 @@ +import FilterableList from '~/filterable_list'; +import eventHub from './event_hub'; + +export default class GroupFilterableList extends FilterableList { + constructor({ form, filter, holder, filterEndpoint, pagePath }) { + super(form, filter, holder); + this.form = form; + this.filterEndpoint = filterEndpoint; + this.pagePath = pagePath; + this.$dropdown = $('.js-group-filter-dropdown-wrap'); + } + + getFilterEndpoint() { + return this.filterEndpoint; + } + + getPagePath(queryData) { + const params = queryData ? $.param(queryData) : ''; + const queryString = params ? `?${params}` : ''; + return `${this.pagePath}${queryString}`; + } + + bindEvents() { + super.bindEvents(); + + this.onFormSubmitWrapper = this.onFormSubmit.bind(this); + this.onFilterOptionClikWrapper = this.onOptionClick.bind(this); + + this.filterForm.addEventListener('submit', this.onFormSubmitWrapper); + this.$dropdown.on('click', 'a', this.onFilterOptionClikWrapper); + } + + onFormSubmit(e) { + e.preventDefault(); + + const $form = $(this.form); + const filterGroupsParam = $form.find('[name="filter_groups"]').val(); + const queryData = {}; + + if (filterGroupsParam) { + queryData.filter_groups = filterGroupsParam; + } + + this.filterResults(queryData); + this.setDefaultFilterOption(); + } + + setDefaultFilterOption() { + const defaultOption = $.trim(this.$dropdown.find('.dropdown-menu a:first-child').text()); + this.$dropdown.find('.dropdown-label').text(defaultOption); + } + + onOptionClick(e) { + e.preventDefault(); + + const queryData = {}; + const sortParam = gl.utils.getParameterByName('sort', e.currentTarget.href); + + if (sortParam) { + queryData.sort = sortParam; + } + + this.filterResults(queryData); + + // Active selected option + this.$dropdown.find('.dropdown-label').text($.trim(e.currentTarget.text)); + + // Clear current value on search form + this.form.querySelector('[name="filter_groups"]').value = ''; + } + + onFilterSuccess(data, xhr, queryData) { + super.onFilterSuccess(data, xhr, queryData); + + const paginationData = { + 'X-Per-Page': xhr.getResponseHeader('X-Per-Page'), + 'X-Page': xhr.getResponseHeader('X-Page'), + 'X-Total': xhr.getResponseHeader('X-Total'), + 'X-Total-Pages': xhr.getResponseHeader('X-Total-Pages'), + 'X-Next-Page': xhr.getResponseHeader('X-Next-Page'), + 'X-Prev-Page': xhr.getResponseHeader('X-Prev-Page'), + }; + + eventHub.$emit('updateGroups', data); + eventHub.$emit('updatePagination', paginationData); + } +} diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js new file mode 100644 index 00000000000..ff601db2aa6 --- /dev/null +++ b/app/assets/javascripts/groups/index.js @@ -0,0 +1,190 @@ +/* global Flash */ + +import Vue from 'vue'; +import GroupFilterableList from './groups_filterable_list'; +import GroupsComponent from './components/groups.vue'; +import GroupFolder from './components/group_folder.vue'; +import GroupItem from './components/group_item.vue'; +import GroupsStore from './stores/groups_store'; +import GroupsService from './services/groups_service'; +import eventHub from './event_hub'; + +document.addEventListener('DOMContentLoaded', () => { + const el = document.getElementById('dashboard-group-app'); + + // Don't do anything if element doesn't exist (No groups) + // This is for when the user enters directly to the page via URL + if (!el) { + return; + } + + Vue.component('groups-component', GroupsComponent); + Vue.component('group-folder', GroupFolder); + Vue.component('group-item', GroupItem); + + // eslint-disable-next-line no-new + new Vue({ + el, + data() { + this.store = new GroupsStore(); + this.service = new GroupsService(el.dataset.endpoint); + + return { + store: this.store, + isLoading: true, + state: this.store.state, + loading: true, + }; + }, + computed: { + isEmpty() { + return Object.keys(this.state.groups).length === 0; + }, + }, + methods: { + fetchGroups(parentGroup) { + let parentId = null; + let getGroups = null; + let page = null; + let sort = null; + let pageParam = null; + let sortParam = null; + let filterGroups = null; + let filterGroupsParam = null; + + if (parentGroup) { + parentId = parentGroup.id; + } else { + this.isLoading = true; + } + + pageParam = gl.utils.getParameterByName('page'); + if (pageParam) { + page = pageParam; + } + + filterGroupsParam = gl.utils.getParameterByName('filter_groups'); + if (filterGroupsParam) { + filterGroups = filterGroupsParam; + } + + sortParam = gl.utils.getParameterByName('sort'); + if (sortParam) { + sort = sortParam; + } + + getGroups = this.service.getGroups(parentId, page, filterGroups, sort); + getGroups + .then(response => response.json()) + .then((response) => { + this.isLoading = false; + + this.updateGroups(response, parentGroup); + }) + .catch(this.handleErrorResponse); + + return getGroups; + }, + fetchPage(page, filterGroups, sort) { + this.isLoading = true; + + return this.service + .getGroups(null, page, filterGroups, sort) + .then((response) => { + this.isLoading = false; + $.scrollTo(0); + + const currentPath = gl.utils.mergeUrlParams({ page }, window.location.href); + window.history.replaceState({ + page: currentPath, + }, document.title, currentPath); + + this.updateGroups(response.json()); + this.updatePagination(response.headers); + }) + .catch(this.handleErrorResponse); + }, + toggleSubGroups(parentGroup = null) { + if (!parentGroup.isOpen) { + this.store.resetGroups(parentGroup); + this.fetchGroups(parentGroup); + } + + this.store.toggleSubGroups(parentGroup); + }, + leaveGroup(group, collection) { + this.service.leaveGroup(group.leavePath) + .then((response) => { + $.scrollTo(0); + + this.store.removeGroup(group, collection); + + // eslint-disable-next-line no-new + new Flash(response.json().notice, 'notice'); + }) + .catch((response) => { + let message = 'An error occurred. Please try again.'; + + if (response.status === 403) { + message = 'Failed to leave the group. Please make sure you are not the only owner'; + } + + // eslint-disable-next-line no-new + new Flash(message); + }); + }, + updateGroups(groups, parentGroup) { + this.store.setGroups(groups, parentGroup); + }, + updatePagination(headers) { + this.store.storePagination(headers); + }, + handleErrorResponse() { + this.isLoading = false; + $.scrollTo(0); + + // eslint-disable-next-line no-new + new Flash('An error occurred. Please try again.'); + }, + }, + created() { + eventHub.$on('fetchPage', this.fetchPage); + eventHub.$on('toggleSubGroups', this.toggleSubGroups); + eventHub.$on('leaveGroup', this.leaveGroup); + eventHub.$on('updateGroups', this.updateGroups); + eventHub.$on('updatePagination', this.updatePagination); + }, + beforeMount() { + let groupFilterList = null; + const form = document.querySelector('form#group-filter-form'); + const filter = document.querySelector('.js-groups-list-filter'); + const holder = document.querySelector('.js-groups-list-holder'); + + const opts = { + form, + filter, + holder, + filterEndpoint: el.dataset.endpoint, + pagePath: el.dataset.path, + }; + + groupFilterList = new GroupFilterableList(opts); + groupFilterList.initSearch(); + }, + mounted() { + this.fetchGroups() + .then((response) => { + this.updatePagination(response.headers); + this.isLoading = false; + }) + .catch(this.handleErrorResponse); + }, + beforeDestroy() { + eventHub.$off('fetchPage', this.fetchPage); + eventHub.$off('toggleSubGroups', this.toggleSubGroups); + eventHub.$off('leaveGroup', this.leaveGroup); + eventHub.$off('updateGroups', this.updateGroups); + eventHub.$off('updatePagination', this.updatePagination); + }, + }); +}); diff --git a/app/assets/javascripts/groups/services/groups_service.js b/app/assets/javascripts/groups/services/groups_service.js new file mode 100644 index 00000000000..97e02fcb76d --- /dev/null +++ b/app/assets/javascripts/groups/services/groups_service.js @@ -0,0 +1,38 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +Vue.use(VueResource); + +export default class GroupsService { + constructor(endpoint) { + this.groups = Vue.resource(endpoint); + } + + getGroups(parentId, page, filterGroups, sort) { + const data = {}; + + if (parentId) { + data.parent_id = parentId; + } else { + // Do not send the following param for sub groups + if (page) { + data.page = page; + } + + if (filterGroups) { + data.filter_groups = filterGroups; + } + + if (sort) { + data.sort = sort; + } + } + + return this.groups.get(data); + } + + // eslint-disable-next-line class-methods-use-this + leaveGroup(endpoint) { + return Vue.http.delete(endpoint); + } +} diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js new file mode 100644 index 00000000000..f6dc4290fd5 --- /dev/null +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -0,0 +1,152 @@ +import Vue from 'vue'; + +export default class GroupsStore { + constructor() { + this.state = {}; + this.state.groups = {}; + this.state.pageInfo = {}; + } + + setGroups(rawGroups, parent) { + const parentGroup = parent; + const tree = this.buildTree(rawGroups, parentGroup); + + if (parentGroup) { + parentGroup.subGroups = tree; + } else { + this.state.groups = tree; + } + + return tree; + } + + // eslint-disable-next-line class-methods-use-this + resetGroups(parent) { + const parentGroup = parent; + parentGroup.subGroups = {}; + } + + storePagination(pagination = {}) { + let paginationInfo; + + if (Object.keys(pagination).length) { + const normalizedHeaders = gl.utils.normalizeHeaders(pagination); + paginationInfo = gl.utils.parseIntPagination(normalizedHeaders); + } else { + paginationInfo = pagination; + } + + this.state.pageInfo = paginationInfo; + } + + buildTree(rawGroups, parentGroup) { + const groups = this.decorateGroups(rawGroups); + const tree = {}; + const mappedGroups = {}; + const orphans = []; + + // Map groups to an object + groups.map((group) => { + mappedGroups[group.id] = group; + mappedGroups[group.id].subGroups = {}; + return group; + }); + + Object.keys(mappedGroups).map((key) => { + const currentGroup = mappedGroups[key]; + if (currentGroup.parentId) { + // If the group is not at the root level, add it to its parent array of subGroups. + const findParentGroup = mappedGroups[currentGroup.parentId]; + if (findParentGroup) { + mappedGroups[currentGroup.parentId].subGroups[currentGroup.id] = currentGroup; + mappedGroups[currentGroup.parentId].isOpen = true; // Expand group if it has subgroups + } else if (parentGroup && parentGroup.id === currentGroup.parentId) { + tree[currentGroup.id] = currentGroup; + } else { + // Means the groups hast no direct parent. + // Save for later processing, we will add them to its corresponding base group + orphans.push(currentGroup); + } + } else { + // If the group is at the root level, add it to first level elements array. + tree[currentGroup.id] = currentGroup; + } + + return key; + }); + + // Hopefully this array will be empty for most cases + if (orphans.length) { + orphans.map((orphan) => { + let found = false; + const currentOrphan = orphan; + + Object.keys(tree).map((key) => { + const group = tree[key]; + if (currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0) { + group.subGroups[currentOrphan.id] = currentOrphan; + group.isOpen = true; + currentOrphan.isOrphan = true; + found = true; + } + + return key; + }); + + if (!found) { + currentOrphan.isOrphan = true; + tree[currentOrphan.id] = currentOrphan; + } + + return orphan; + }); + } + + return tree; + } + + decorateGroups(rawGroups) { + this.groups = rawGroups.map(this.decorateGroup); + return this.groups; + } + + // eslint-disable-next-line class-methods-use-this + decorateGroup(rawGroup) { + return { + id: rawGroup.id, + fullName: rawGroup.full_name, + fullPath: rawGroup.full_path, + avatarUrl: rawGroup.avatar_url, + name: rawGroup.name, + hasSubgroups: rawGroup.has_subgroups, + canEdit: rawGroup.can_edit, + description: rawGroup.description, + webUrl: rawGroup.web_url, + groupPath: rawGroup.group_path, + parentId: rawGroup.parent_id, + visibility: rawGroup.visibility, + leavePath: rawGroup.leave_path, + editPath: rawGroup.edit_path, + isOpen: false, + isOrphan: false, + numberProjects: rawGroup.number_projects_with_delimiter, + numberUsers: rawGroup.number_users_with_delimiter, + permissions: { + humanGroupAccess: rawGroup.permissions.human_group_access, + }, + subGroups: {}, + }; + } + + // eslint-disable-next-line class-methods-use-this + removeGroup(group, collection) { + Vue.delete(collection, group.id); + } + + // eslint-disable-next-line class-methods-use-this + toggleSubGroups(toggleGroup) { + const group = toggleGroup; + group.isOpen = !group.isOpen; + return group; + } +} diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue new file mode 100644 index 00000000000..5b9cf577189 --- /dev/null +++ b/app/assets/javascripts/jobs/components/header.vue @@ -0,0 +1,83 @@ +<script> + import ciHeader from '../../vue_shared/components/header_ci_component.vue'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + + export default { + name: 'jobHeaderSection', + props: { + job: { + type: Object, + required: true, + }, + isLoading: { + type: Boolean, + required: true, + }, + }, + components: { + ciHeader, + loadingIcon, + }, + data() { + return { + actions: this.getActions(), + }; + }, + computed: { + status() { + return this.job && this.job.status; + }, + shouldRenderContent() { + return !this.isLoading && Object.keys(this.job).length; + }, + }, + methods: { + getActions() { + const actions = []; + + if (this.job.new_issue_path) { + actions.push({ + label: 'New issue', + path: this.job.new_issue_path, + cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block', + type: 'ujs-link', + }); + } + + if (this.job.retry_path) { + actions.push({ + label: 'Retry', + path: this.job.retry_path, + cssClass: 'js-retry-button btn btn-inverted-secondary visible-md-block visible-lg-block', + type: 'ujs-link', + }); + } + + return actions; + }, + }, + watch: { + job() { + this.actions = this.getActions(); + }, + }, + }; +</script> +<template> + <div class="js-build-header build-header top-area"> + <ci-header + v-if="shouldRenderContent" + :status="status" + item-name="Job" + :item-id="job.id" + :time="job.created_at" + :user="job.user" + :actions="actions" + :hasSidebarButton="true" + /> + <loading-icon + v-if="isLoading" + size="2" + /> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue new file mode 100644 index 00000000000..ab2bcd728a8 --- /dev/null +++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue @@ -0,0 +1,31 @@ +<script> + export default { + name: 'SidebarDetailRow', + props: { + title: { + type: String, + required: false, + default: '', + }, + value: { + type: String, + required: true, + }, + }, + computed: { + hasTitle() { + return this.title.length > 0; + }, + }, + }; +</script> +<template> + <p class="build-detail-row"> + <span + v-if="hasTitle" + class="build-light-text"> + {{title}}: + </span> + {{value}} + </p> +</template> diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue new file mode 100644 index 00000000000..4223a8fea49 --- /dev/null +++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue @@ -0,0 +1,150 @@ +<script> + import detailRow from './sidebar_detail_row.vue'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import timeagoMixin from '../../vue_shared/mixins/timeago'; + import { timeIntervalInWords } from '../../lib/utils/datetime_utility'; + + export default { + name: 'SidebarDetailsBlock', + props: { + job: { + type: Object, + required: true, + }, + isLoading: { + type: Boolean, + required: true, + }, + }, + mixins: [ + timeagoMixin, + ], + components: { + detailRow, + loadingIcon, + }, + computed: { + shouldRenderContent() { + return !this.isLoading && Object.keys(this.job).length > 0; + }, + coverage() { + return `${this.job.coverage}%`; + }, + duration() { + return timeIntervalInWords(this.job.duration); + }, + queued() { + return timeIntervalInWords(this.job.queued); + }, + runnerId() { + return `#${this.job.runner.id}`; + }, + }, + }; +</script> +<template> + <div> + <template v-if="shouldRenderContent"> + <div + class="block retry-link" + v-if="job.retry_path || job.new_issue_path"> + <a + v-if="job.new_issue_path" + class="js-new-issue btn btn-new btn-inverted" + :href="job.new_issue_path"> + New issue + </a> + <a + v-if="job.retry_path" + class="js-retry-job btn btn-inverted-secondary" + :href="job.retry_path" + data-method="post" + rel="nofollow"> + Retry + </a> + </div> + <div class="block"> + <p + class="build-detail-row js-job-mr" + v-if="job.merge_request"> + <span + class="build-light-text"> + Merge Request: + </span> + <a :href="job.merge_request.path"> + !{{job.merge_request.iid}} + </a> + </p> + + <detail-row + class="js-job-duration" + v-if="job.duration" + title="Duration" + :value="duration" + /> + <detail-row + class="js-job-finished" + v-if="job.finished_at" + title="Finished" + :value="timeFormated(job.finished_at)" + /> + <detail-row + class="js-job-erased" + v-if="job.erased_at" + title="Erased" + :value="timeFormated(job.erased_at)" + /> + <detail-row + class="js-job-queued" + v-if="job.queued" + title="Queued" + :value="queued" + /> + <detail-row + class="js-job-runner" + v-if="job.runner" + title="Runner" + :value="runnerId" + /> + <detail-row + class="js-job-coverage" + v-if="job.coverage" + title="Coverage" + :value="coverage" + /> + <p + class="build-detail-row js-job-tags" + v-if="job.tags.length"> + <span + class="build-light-text"> + Tags: + </span> + <span + v-for="tag in job.tags" + key="tag" + class="label label-primary"> + {{tag}} + </span> + </p> + + <div + v-if="job.cancel_path" + class="btn-group prepend-top-5" + role="group"> + <a + class="js-cancel-job btn btn-sm btn-default" + :href="job.cancel_path" + data-method="post" + rel="nofollow"> + Cancel + </a> + </div> + </div> + </template> + <loading-icon + class="prepend-top-10" + v-if="isLoading" + size="2" + /> + </div> +</template> diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js new file mode 100644 index 00000000000..939d17129de --- /dev/null +++ b/app/assets/javascripts/jobs/job_details_bundle.js @@ -0,0 +1,68 @@ +/* global Flash */ + +import Vue from 'vue'; +import JobMediator from './job_details_mediator'; +import jobHeader from './components/header.vue'; +import detailsBlock from './components/sidebar_details_block.vue'; + +document.addEventListener('DOMContentLoaded', () => { + const dataset = document.getElementById('js-job-details-vue').dataset; + const mediator = new JobMediator({ endpoint: dataset.endpoint }); + + mediator.fetchJob(); + + // Header + // eslint-disable-next-line no-new + new Vue({ + el: '#js-build-header-vue', + data() { + return { + mediator, + }; + }, + components: { + jobHeader, + }, + mounted() { + this.mediator.initBuildClass(); + }, + updated() { + // Wait for flash message to be appended + Vue.nextTick(() => { + if (this.mediator.build) { + this.mediator.build.verifyTopPosition(); + } + }); + }, + render(createElement) { + return createElement('job-header', { + props: { + isLoading: this.mediator.state.isLoading, + job: this.mediator.store.state.job, + }, + }); + }, + }); + + // Sidebar information block + // eslint-disable-next-line + new Vue({ + el: '#js-details-block-vue', + data() { + return { + mediator, + }; + }, + components: { + detailsBlock, + }, + render(createElement) { + return createElement('details-block', { + props: { + isLoading: this.mediator.state.isLoading, + job: this.mediator.store.state.job, + }, + }); + }, + }); +}); diff --git a/app/assets/javascripts/jobs/job_details_mediator.js b/app/assets/javascripts/jobs/job_details_mediator.js new file mode 100644 index 00000000000..063c52fac74 --- /dev/null +++ b/app/assets/javascripts/jobs/job_details_mediator.js @@ -0,0 +1,67 @@ +/* global Flash */ +/* global Build */ + +import Visibility from 'visibilityjs'; +import Poll from '../lib/utils/poll'; +import JobStore from './stores/job_store'; +import JobService from './services/job_service'; +import '../build'; + +export default class JobMediator { + constructor(options = {}) { + this.options = options; + + this.store = new JobStore(); + this.service = new JobService(options.endpoint); + + this.state = { + isLoading: false, + }; + } + + initBuildClass() { + this.build = new Build(); + } + + fetchJob() { + this.poll = new Poll({ + resource: this.service, + method: 'getJob', + successCallback: this.successCallback.bind(this), + errorCallback: this.errorCallback.bind(this), + }); + + if (!Visibility.hidden()) { + this.state.isLoading = true; + this.poll.makeRequest(); + } else { + this.getJob(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + this.poll.restart(); + } else { + this.poll.stop(); + } + }); + } + + getJob() { + return this.service.getJob() + .then(response => this.successCallback(response)) + .catch(() => this.errorCallback()); + } + + successCallback(response) { + const data = response.json(); + this.state.isLoading = false; + this.store.storeJob(data); + } + + errorCallback() { + this.state.isLoading = false; + + return new Flash('An error occurred while fetching the job.'); + } +} diff --git a/app/assets/javascripts/jobs/services/job_service.js b/app/assets/javascripts/jobs/services/job_service.js new file mode 100644 index 00000000000..eaf1c6e500a --- /dev/null +++ b/app/assets/javascripts/jobs/services/job_service.js @@ -0,0 +1,14 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +Vue.use(VueResource); + +export default class JobService { + constructor(endpoint) { + this.job = Vue.resource(endpoint); + } + + getJob() { + return this.job.get(); + } +} diff --git a/app/assets/javascripts/jobs/stores/job_store.js b/app/assets/javascripts/jobs/stores/job_store.js new file mode 100644 index 00000000000..766194b8387 --- /dev/null +++ b/app/assets/javascripts/jobs/stores/job_store.js @@ -0,0 +1,11 @@ +export default class JobStore { + constructor() { + this.state = { + job: {}, + }; + } + + storeJob(job = {}) { + this.state.job = job; + } +} diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index a537267643e..2aca86189fd 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -167,8 +167,8 @@ if the name does not exist this function will return `null` otherwise it will return the value of the param key provided */ - w.gl.utils.getParameterByName = (name) => { - const url = window.location.href; + w.gl.utils.getParameterByName = (name, parseUrl) => { + const url = parseUrl || window.location.href; name = name.replace(/[[\]]/g, '\\$&'); const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); const results = regex.exec(url); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index b2f48049bb4..54c0da3fc9c 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -3,6 +3,11 @@ import timeago from 'timeago.js'; import dateFormat from 'vendor/date.format'; +import { + lang, + s__, +} from '../../locale'; + window.timeago = timeago; window.dateFormat = dateFormat; @@ -48,26 +53,45 @@ window.dateFormat = dateFormat; var locale; if (!timeagoInstance) { + const localeRemaining = function(number, index) { + return [ + [s__('Timeago|less than a minute ago'), s__('Timeago|a while')], + [s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')], + [s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')], + [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')], + [s__('Timeago|about an hour ago'), s__('Timeago|1 hour remaining')], + [s__('Timeago|about %s hours ago'), s__('Timeago|%s hours remaining')], + [s__('Timeago|a day ago'), s__('Timeago|1 day remaining')], + [s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')], + [s__('Timeago|a week ago'), s__('Timeago|1 week remaining')], + [s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')], + [s__('Timeago|a month ago'), s__('Timeago|1 month remaining')], + [s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')], + [s__('Timeago|a year ago'), s__('Timeago|1 year remaining')], + [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')] + ][index]; + }; locale = function(number, index) { return [ - ['less than a minute ago', 'a while'], - ['less than a minute ago', 'in %s seconds'], - ['about a minute ago', 'in 1 minute'], - ['%s minutes ago', 'in %s minutes'], - ['about an hour ago', 'in 1 hour'], - ['about %s hours ago', 'in %s hours'], - ['a day ago', 'in 1 day'], - ['%s days ago', 'in %s days'], - ['a week ago', 'in 1 week'], - ['%s weeks ago', 'in %s weeks'], - ['a month ago', 'in 1 month'], - ['%s months ago', 'in %s months'], - ['a year ago', 'in 1 year'], - ['%s years ago', 'in %s years'] + [s__('Timeago|less than a minute ago'), s__('Timeago|a while')], + [s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')], + [s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')], + [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')], + [s__('Timeago|about an hour ago'), s__('Timeago|in 1 hour')], + [s__('Timeago|about %s hours ago'), s__('Timeago|in %s hours')], + [s__('Timeago|a day ago'), s__('Timeago|in 1 day')], + [s__('Timeago|%s days ago'), s__('Timeago|in %s days')], + [s__('Timeago|a week ago'), s__('Timeago|in 1 week')], + [s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')], + [s__('Timeago|a month ago'), s__('Timeago|in 1 month')], + [s__('Timeago|%s months ago'), s__('Timeago|in %s months')], + [s__('Timeago|a year ago'), s__('Timeago|in 1 year')], + [s__('Timeago|%s years ago'), s__('Timeago|in %s years')] ][index]; }; - timeago.register('gl_en', locale); + timeago.register(lang, locale); + timeago.register(`${lang}-remaining`, localeRemaining); timeagoInstance = timeago(); } @@ -79,13 +103,11 @@ window.dateFormat = dateFormat; if (!time) { return ''; } - suffix || (suffix = 'remaining'); - expiredLabel || (expiredLabel = 'Past due'); - timefor = gl.utils.getTimeago().format(time).replace('in', ''); - if (timefor.indexOf('ago') > -1) { + if (new Date(time) < new Date()) { + expiredLabel || (expiredLabel = s__('Timeago|Past due')); timefor = expiredLabel; } else { - timefor = timefor.trim() + ' ' + suffix; + timefor = gl.utils.getTimeago().format(time, `${lang}-remaining`).trim(); } return timefor; }; @@ -102,7 +124,7 @@ window.dateFormat = dateFormat; }; w.gl.utils.updateTimeagoText = function(el) { - const formattedDate = gl.utils.getTimeago().format(el.getAttribute('datetime'), 'gl_en'); + const formattedDate = gl.utils.getTimeago().format(el.getAttribute('datetime'), lang); if (el.textContent !== formattedDate) { el.textContent = formattedDate; @@ -124,3 +146,24 @@ window.dateFormat = dateFormat; }; })(window); }).call(window); + +/** + * Port of ruby helper time_interval_in_words. + * + * @param {Number} seconds + * @return {String} + */ +// eslint-disable-next-line import/prefer-default-export +export function timeIntervalInWords(intervalInSeconds) { + const secondsInteger = parseInt(intervalInSeconds, 10); + const minutes = Math.floor(secondsInteger / 60); + const seconds = secondsInteger - (minutes * 60); + let text = ''; + + if (minutes >= 1) { + text = `${minutes} ${gl.text.pluralize('minute', minutes)} ${seconds} ${gl.text.pluralize('second', seconds)}`; + } else { + text = `${seconds} ${gl.text.pluralize('second', seconds)}`; + } + return text; +} diff --git a/app/assets/javascripts/locale/bg/app.js b/app/assets/javascripts/locale/bg/app.js new file mode 100644 index 00000000000..ba56c0bea25 --- /dev/null +++ b/app/assets/javascripts/locale/bg/app.js @@ -0,0 +1 @@ +var locales = locales || {}; locales['bg'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","PO-Revision-Date":"2017-06-05 09:40-0400","Last-Translator":"Lyubomir Vasilev <lyubomirv@abv.bg>","Language-Team":"Bulgarian","Language":"bg","X-Generator":"Zanata 3.9.6","Plural-Forms":"nplurals=2; plural=(n != 1)","lang":"bg","domain":"app","plural_forms":"nplurals=2; plural=(n != 1)"},"ByAuthor|by":["от"],"Commit":["Подаване","Подавания"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Анализът на циклите дава общ поглед върху това колко време е нужно на една идея да се превърне в завършена функционалност в проекта."],"CycleAnalyticsStage|Code":["Програмиране"],"CycleAnalyticsStage|Issue":["Проблем"],"CycleAnalyticsStage|Plan":["Планиране"],"CycleAnalyticsStage|Production":["Издаване"],"CycleAnalyticsStage|Review":["Преглед и одобрение"],"CycleAnalyticsStage|Staging":["Подготовка за издаване"],"CycleAnalyticsStage|Test":["Тестване"],"Deploy":["Внедряване","Внедрявания"],"FirstPushedBy|First":["Първо"],"FirstPushedBy|pushed by":["изпращане на промени от"],"From issue creation until deploy to production":["От създаването на проблема до внедряването в крайната версия"],"From merge request merge until deploy to production":["От прилагането на заявката за сливане до внедряването в крайната версия"],"Introducing Cycle Analytics":["Представяме Ви анализът на циклите"],"Last %d day":["Последния %d ден","Последните %d дни"],"Limited to showing %d event at most":["Ограничено до показване на последното %d събитие","Ограничено до показване на последните %d събития"],"Median":["Медиана"],"New Issue":["Нов проблем","Нови проблема"],"Not available":["Не е налично"],"Not enough data":["Няма достатъчно данни"],"OpenedNDaysAgo|Opened":["Отворен"],"Pipeline Health":["Състояние"],"ProjectLifecycle|Stage":["Етап"],"Read more":["Прочетете повече"],"Related Commits":["Свързани подавания"],"Related Deployed Jobs":["Свързани задачи за внедряване"],"Related Issues":["Свързани проблеми"],"Related Jobs":["Свързани задачи"],"Related Merge Requests":["Свързани заявки за сливане"],"Related Merged Requests":["Свързани приложени заявки за сливане"],"Showing %d event":["Показване на %d събитие","Показване на %d събития"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["Етапът на програмиране показва времето от първото подаване до създаването на заявката за сливане. Данните ще бъдат добавени тук автоматично след като бъде създадена първата заявка за сливане."],"The collection of events added to the data gathered for that stage.":["Съвкупността от събития добавени към данните събрани за този етап."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["Етапът на проблемите показва колко е времето от създаването на проблем до определянето на целеви етап на проекта за него, или до добавянето му в списък на дъската за проблеми. Започнете да добавяте проблеми, за да видите данните за този етап."],"The phase of the development lifecycle.":["Етапът от цикъла на разработка"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["Етапът на планиране показва колко е времето от преходната стъпка до изпращането на първото подаване. Това време ще бъде добавено автоматично след като изпратите първото си подаване."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["Етапът на издаване показва общото време, което е нужно от създаването на проблем до внедряването на кода в крайната версия."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["Етапът на преглед и одобрение показва времето от създаването на заявката за сливане до прилагането ѝ. Данните ще бъдат добавени автоматично след като приложите първата си заявка за сливане."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["Етапът на подготовка за издаване показва времето между прилагането на заявката за сливане и внедряването на кода в средата на работещата крайна версия. Данните ще бъдат добавени автоматично след като направите първото си внедряване в крайната версия."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["Етапът на тестване показва времето, което е нужно на „Gitlab CI“ да изпълни всички задачи за свързаната заявка за сливане. Данните ще бъдат добавени автоматично след като приключи изпълнените на първата Ви такава задача."],"The time taken by each data entry gathered by that stage.":["Времето, което отнема всеки запис от данни за съответния етап."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["Стойността, която се намира в средата на последователността от наблюдавани данни. Например: медианата на 3, 5 и 9 е 5, а медианата на 3, 5, 7 и 8 е (5+7)/2 = 6."],"Time before an issue gets scheduled":["Време преди един проблем да бъде планиран за работа"],"Time before an issue starts implementation":["Време преди работата по проблем да започне"],"Time between merge request creation and merge/close":["Време между създаване на заявка за сливане и прилагането/отхвърлянето ѝ"],"Time until first merge request":["Време преди първата заявка за сливане"],"Time|hr":["час","часа"],"Time|min":["мин","мин"],"Time|s":["сек"],"Total Time":["Общо време"],"Total test time for all commits/merges":["Общо време за тестване на всички подавания/сливания"],"Want to see the data? Please ask an administrator for access.":["Искате ли да видите данните? Помолете администратор за достъп."],"We don't have enough data to show this stage.":["Няма достатъчно данни за този етап."],"You need permission.":["Нуждаете се от разрешение."],"day":["ден","дни"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/de/app.js b/app/assets/javascripts/locale/de/app.js index 9411f078ecf..e7d2b174405 100644 --- a/app/assets/javascripts/locale/de/app.js +++ b/app/assets/javascripts/locale/de/app.js @@ -1 +1 @@ -var locales = locales || {}; locales['de'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-09 13:44+0200","Language-Team":"German","Language":"de","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"","X-Generator":"Poedit 2.0.1","lang":"de","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"ByAuthor|by":["Von"],"Commit":["Commit","Commits"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Cycle Analytics liefern einen Überblick darüber, wie viel Zeit in Ihrem Projekt von einer Idee bis zum Produktivdeployment vergeht."],"CycleAnalyticsStage|Code":["Code"],"CycleAnalyticsStage|Issue":["Issue"],"CycleAnalyticsStage|Plan":["Planung"],"CycleAnalyticsStage|Production":["Produktiv"],"CycleAnalyticsStage|Review":["Review"],"CycleAnalyticsStage|Staging":["Staging"],"CycleAnalyticsStage|Test":["Test"],"Deploy":["Deployment","Deployments"],"FirstPushedBy|First":["Erster"],"FirstPushedBy|pushed by":["gepusht von"],"From issue creation until deploy to production":["Vom Anlegen des Issues bis zum Produktivdeployment"],"From merge request merge until deploy to production":["Vom Merge Request bis zum Produktivdeployment"],"Introducing Cycle Analytics":["Was sind Cycle Analytics?"],"Last %d day":["Letzter %d Tag","Letzten %d Tage"],"Limited to showing %d event at most":["Eingeschränkt auf maximal %d Ereignis","Eingeschränkt auf maximal %d Ereignisse"],"Median":["Median"],"New Issue":["Neues Issue","Neue Issues"],"Not available":["Nicht verfügbar"],"Not enough data":["Nicht genügend Daten"],"OpenedNDaysAgo|Opened":["Erstellt"],"Pipeline Health":["Pipeline Kennzahlen"],"ProjectLifecycle|Stage":["Phase"],"Read more":["Mehr"],"Related Commits":["Zugehörige Commits"],"Related Deployed Jobs":["Zugehörige Deploymentjobs"],"Related Issues":["Zugehörige Issues"],"Related Jobs":["Zugehörige Jobs"],"Related Merge Requests":["Zugehörige Merge Requests"],"Related Merged Requests":["Zugehörige abgeschlossene Merge Requests"],"Showing %d event":["Zeige %d Ereignis","Zeige %d Ereignisse"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["Die Code-Phase stellt die Zeit vom ersten Commit bis zum Erstellen eines Merge Requests dar. Sobald Sie Ihren ersten Merge Request anlegen, werden dessen Daten automatisch ergänzt."],"The collection of events added to the data gathered for that stage.":["Ereignisse, die für diese Phase ausgewertet wurden."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["Die Issue-Phase stellt die Zeit vom Anlegen eines Issues bis zum Zuweisen eines Meilensteins oder Hinzufügen zum Issue Board dar. Erstellen Sie einen Issue, damit dessen Daten hier erscheinen."],"The phase of the development lifecycle.":["Die Phase im Entwicklungsprozess."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["Die Planungsphase stellt die Zeit von der vorherigen Phase bis zum Pushen des ersten Commits dar. Sobald Sie den ersten Commit pushen, werden dessen Daten hier erscheinen."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["Die Produktiv-Phase stellt die Gesamtzeit vom Anlegen eines Issues bis zum Deployment auf dem Produktivsystem dar. Sobald Sie den vollständigen Entwicklungszyklus von einer Idee bis zum Produktivdeployment durchlaufen haben, erscheinen die zugehörigen Daten hier."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["Die Review-Phase stellt die Zeit vom Anlegen eines Merge Requests bis zum Mergen dar. Sobald Sie Ihren ersten Merge Request abschließen, werden dessen Daten hier automatisch angezeigt."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["Die Staging-Phase stellt die Zeit zwischen Mergen eines Merge Requests und dem Produktivdeployment dar. Sobald Sie das erste Produktivdeployment durchgeführt haben, werden dessen Daten hier automatisch angezeigt."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["Die Test-Phase stellt die Zeit dar, die GitLab CI benötigt um die Pipelines von Merge Requests abzuarbeiten. Sobald die erste Pipeline abgeschlossen ist, werden deren Daten hier automatisch angezeigt."],"The time taken by each data entry gathered by that stage.":["Zeit die für das jeweilige Ereignis in der Phase ermittelt wurde."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["Der mittlere aller erfassten Werte. Zum Beispiel ist für 3, 5, 9 der Median 5. Bei 3, 5, 7, 8 ist der Median (5+7)/2 = 6."],"Time before an issue gets scheduled":["Zeit bis ein Issue geplant wird"],"Time before an issue starts implementation":["Zeit bis die Implementierung für ein Issue beginnt"],"Time between merge request creation and merge/close":["Zeit zwischen Anlegen und Mergen/Schließen eines Merge Requests"],"Time until first merge request":["Zeit bis zum ersten Merge Request"],"Time|hr":["h","h"],"Time|min":["min","min"],"Time|s":["s"],"Total Time":["Gesamtzeit"],"Total test time for all commits/merges":["Gesamte Testlaufzeit für alle Commits/Merges"],"Want to see the data? Please ask an administrator for access.":["Um diese Daten einsehen zu können, wenden Sie sich bitte an Ihren Administrator."],"We don't have enough data to show this stage.":["Es liegen nicht genügend Daten vor, um diese Phase anzuzeigen."],"You need permission.":["Sie benötigen Zugriffsrechte."],"day":["Tag","Tage"]}}};
\ No newline at end of file +var locales = locales || {}; locales['de'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-09 13:44+0200","Language-Team":"German","Language":"de","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"","X-Generator":"Poedit 2.0.1","lang":"de","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":["Von"],"Cancel":[""],"Commit":["Commit","Commits"],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Cycle Analytics liefern einen Überblick darüber, wie viel Zeit in Ihrem Projekt von einer Idee bis zum Produktivdeployment vergeht."],"CycleAnalyticsStage|Code":["Code"],"CycleAnalyticsStage|Issue":["Issue"],"CycleAnalyticsStage|Plan":["Planung"],"CycleAnalyticsStage|Production":["Produktiv"],"CycleAnalyticsStage|Review":["Review"],"CycleAnalyticsStage|Staging":["Staging"],"CycleAnalyticsStage|Test":["Test"],"Delete":[""],"Deploy":["Deployment","Deployments"],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":["Erster"],"FirstPushedBy|pushed by":["gepusht von"],"From issue creation until deploy to production":["Vom Anlegen des Issues bis zum Produktivdeployment"],"From merge request merge until deploy to production":["Vom Merge Request bis zum Produktivdeployment"],"Interval Pattern":[""],"Introducing Cycle Analytics":["Was sind Cycle Analytics?"],"Last %d day":["Letzter %d Tag","Letzten %d Tage"],"Last Pipeline":[""],"Limited to showing %d event at most":["Eingeschränkt auf maximal %d Ereignis","Eingeschränkt auf maximal %d Ereignisse"],"Median":["Median"],"New Issue":["Neues Issue","Neue Issues"],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":["Nicht verfügbar"],"Not enough data":["Nicht genügend Daten"],"OpenedNDaysAgo|Opened":["Erstellt"],"Owner":[""],"Pipeline Health":["Pipeline Kennzahlen"],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":["Phase"],"Read more":["Mehr"],"Related Commits":["Zugehörige Commits"],"Related Deployed Jobs":["Zugehörige Deploymentjobs"],"Related Issues":["Zugehörige Issues"],"Related Jobs":["Zugehörige Jobs"],"Related Merge Requests":["Zugehörige Merge Requests"],"Related Merged Requests":["Zugehörige abgeschlossene Merge Requests"],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["Zeige %d Ereignis","Zeige %d Ereignisse"],"Target Branch":[""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["Die Code-Phase stellt die Zeit vom ersten Commit bis zum Erstellen eines Merge Requests dar. Sobald Sie Ihren ersten Merge Request anlegen, werden dessen Daten automatisch ergänzt."],"The collection of events added to the data gathered for that stage.":["Ereignisse, die für diese Phase ausgewertet wurden."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["Die Issue-Phase stellt die Zeit vom Anlegen eines Issues bis zum Zuweisen eines Meilensteins oder Hinzufügen zum Issue Board dar. Erstellen Sie einen Issue, damit dessen Daten hier erscheinen."],"The phase of the development lifecycle.":["Die Phase im Entwicklungsprozess."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["Die Planungsphase stellt die Zeit von der vorherigen Phase bis zum Pushen des ersten Commits dar. Sobald Sie den ersten Commit pushen, werden dessen Daten hier erscheinen."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["Die Produktiv-Phase stellt die Gesamtzeit vom Anlegen eines Issues bis zum Deployment auf dem Produktivsystem dar. Sobald Sie den vollständigen Entwicklungszyklus von einer Idee bis zum Produktivdeployment durchlaufen haben, erscheinen die zugehörigen Daten hier."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["Die Review-Phase stellt die Zeit vom Anlegen eines Merge Requests bis zum Mergen dar. Sobald Sie Ihren ersten Merge Request abschließen, werden dessen Daten hier automatisch angezeigt."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["Die Staging-Phase stellt die Zeit zwischen Mergen eines Merge Requests und dem Produktivdeployment dar. Sobald Sie das erste Produktivdeployment durchgeführt haben, werden dessen Daten hier automatisch angezeigt."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["Die Test-Phase stellt die Zeit dar, die GitLab CI benötigt um die Pipelines von Merge Requests abzuarbeiten. Sobald die erste Pipeline abgeschlossen ist, werden deren Daten hier automatisch angezeigt."],"The time taken by each data entry gathered by that stage.":["Zeit die für das jeweilige Ereignis in der Phase ermittelt wurde."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["Der mittlere aller erfassten Werte. Zum Beispiel ist für 3, 5, 9 der Median 5. Bei 3, 5, 7, 8 ist der Median (5+7)/2 = 6."],"Time before an issue gets scheduled":["Zeit bis ein Issue geplant wird"],"Time before an issue starts implementation":["Zeit bis die Implementierung für ein Issue beginnt"],"Time between merge request creation and merge/close":["Zeit zwischen Anlegen und Mergen/Schließen eines Merge Requests"],"Time until first merge request":["Zeit bis zum ersten Merge Request"],"Time|hr":["h","h"],"Time|min":["min","min"],"Time|s":["s"],"Total Time":["Gesamtzeit"],"Total test time for all commits/merges":["Gesamte Testlaufzeit für alle Commits/Merges"],"Want to see the data? Please ask an administrator for access.":["Um diese Daten einsehen zu können, wenden Sie sich bitte an Ihren Administrator."],"We don't have enough data to show this stage.":["Es liegen nicht genügend Daten vor, um diese Phase anzuzeigen."],"You need permission.":["Sie benötigen Zugriffsrechte."],"day":["Tag","Tage"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/en/app.js b/app/assets/javascripts/locale/en/app.js index ade9b667b3c..0bb76c80b7a 100644 --- a/app/assets/javascripts/locale/en/app.js +++ b/app/assets/javascripts/locale/en/app.js @@ -1 +1 @@ -var locales = locales || {}; locales['en'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-04-12 22:36-0500","Last-Translator":"FULL NAME <EMAIL@ADDRESS>","Language-Team":"English","Language":"en","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","lang":"en","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"ByAuthor|by":[""],"Commit":["",""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":[""],"CycleAnalyticsStage|Code":[""],"CycleAnalyticsStage|Issue":[""],"CycleAnalyticsStage|Plan":[""],"CycleAnalyticsStage|Production":[""],"CycleAnalyticsStage|Review":[""],"CycleAnalyticsStage|Staging":[""],"CycleAnalyticsStage|Test":[""],"Deploy":["",""],"FirstPushedBy|First":[""],"FirstPushedBy|pushed by":[""],"From issue creation until deploy to production":[""],"From merge request merge until deploy to production":[""],"Introducing Cycle Analytics":[""],"Last %d day":["",""],"Limited to showing %d event at most":["",""],"Median":[""],"New Issue":["",""],"Not available":[""],"Not enough data":[""],"OpenedNDaysAgo|Opened":[""],"Pipeline Health":[""],"ProjectLifecycle|Stage":[""],"Read more":[""],"Related Commits":[""],"Related Deployed Jobs":[""],"Related Issues":[""],"Related Jobs":[""],"Related Merge Requests":[""],"Related Merged Requests":[""],"Showing %d event":["",""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":[""],"The collection of events added to the data gathered for that stage.":[""],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":[""],"The phase of the development lifecycle.":[""],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":[""],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":[""],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":[""],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":[""],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":[""],"The time taken by each data entry gathered by that stage.":[""],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":[""],"Time before an issue gets scheduled":[""],"Time before an issue starts implementation":[""],"Time between merge request creation and merge/close":[""],"Time until first merge request":[""],"Time|hr":["",""],"Time|min":["",""],"Time|s":[""],"Total Time":[""],"Total test time for all commits/merges":[""],"Want to see the data? Please ask an administrator for access.":[""],"We don't have enough data to show this stage.":[""],"You need permission.":[""],"day":["",""]}}};
\ No newline at end of file +var locales = locales || {}; locales['en'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-04-12 22:36-0500","Last-Translator":"FULL NAME <EMAIL@ADDRESS>","Language-Team":"English","Language":"en","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","lang":"en","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":[""],"Cancel":[""],"Commit":["",""],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":[""],"CycleAnalyticsStage|Code":[""],"CycleAnalyticsStage|Issue":[""],"CycleAnalyticsStage|Plan":[""],"CycleAnalyticsStage|Production":[""],"CycleAnalyticsStage|Review":[""],"CycleAnalyticsStage|Staging":[""],"CycleAnalyticsStage|Test":[""],"Delete":[""],"Deploy":["",""],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":[""],"FirstPushedBy|pushed by":[""],"From issue creation until deploy to production":[""],"From merge request merge until deploy to production":[""],"Interval Pattern":[""],"Introducing Cycle Analytics":[""],"Last %d day":["",""],"Last Pipeline":[""],"Limited to showing %d event at most":["",""],"Median":[""],"New Issue":["",""],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":[""],"Not enough data":[""],"OpenedNDaysAgo|Opened":[""],"Owner":[""],"Pipeline Health":[""],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":[""],"Read more":[""],"Related Commits":[""],"Related Deployed Jobs":[""],"Related Issues":[""],"Related Jobs":[""],"Related Merge Requests":[""],"Related Merged Requests":[""],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["",""],"Target Branch":[""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":[""],"The collection of events added to the data gathered for that stage.":[""],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":[""],"The phase of the development lifecycle.":[""],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":[""],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":[""],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":[""],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":[""],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":[""],"The time taken by each data entry gathered by that stage.":[""],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":[""],"Time before an issue gets scheduled":[""],"Time before an issue starts implementation":[""],"Time between merge request creation and merge/close":[""],"Time until first merge request":[""],"Time|hr":["",""],"Time|min":["",""],"Time|s":[""],"Total Time":[""],"Total test time for all commits/merges":[""],"Want to see the data? Please ask an administrator for access.":[""],"We don't have enough data to show this stage.":[""],"You need permission.":[""],"day":["",""]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/es/app.js b/app/assets/javascripts/locale/es/app.js index f5f510d7c2b..6977625f4d8 100644 --- a/app/assets/javascripts/locale/es/app.js +++ b/app/assets/javascripts/locale/es/app.js @@ -1 +1 @@ -var locales = locales || {}; locales['es'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-20 22:37-0500","Language-Team":"Spanish","Language":"es","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"","X-Generator":"Poedit 2.0.1","lang":"es","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"ByAuthor|by":["por"],"Commit":["Cambio","Cambios"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Cycle Analytics ofrece una visión general de cuánto tiempo tarda en pasar de idea a producción en su proyecto."],"CycleAnalyticsStage|Code":["Código"],"CycleAnalyticsStage|Issue":["Incidencia"],"CycleAnalyticsStage|Plan":["Planificación"],"CycleAnalyticsStage|Production":["Producción"],"CycleAnalyticsStage|Review":["Revisión"],"CycleAnalyticsStage|Staging":["Puesta en escena"],"CycleAnalyticsStage|Test":["Pruebas"],"Deploy":["Despliegue","Despliegues"],"FirstPushedBy|First":["Primer"],"FirstPushedBy|pushed by":["enviado por"],"From issue creation until deploy to production":["Desde la creación de la incidencia hasta el despliegue a producción"],"From merge request merge until deploy to production":["Desde la integración de la solicitud de fusión hasta el despliegue a producción"],"Introducing Cycle Analytics":["Introducción a Cycle Analytics"],"Last %d day":["Último %d día","Últimos %d días"],"Limited to showing %d event at most":["Limitado a mostrar máximo %d evento","Limitado a mostrar máximo %d eventos"],"Median":["Mediana"],"New Issue":["Nueva incidencia","Nuevas incidencias"],"Not available":["No disponible"],"Not enough data":["No hay suficientes datos"],"OpenedNDaysAgo|Opened":["Abierto"],"Pipeline Health":["Estado del Pipeline"],"ProjectLifecycle|Stage":["Etapa"],"Read more":["Leer más"],"Related Commits":["Cambios Relacionados"],"Related Deployed Jobs":["Trabajos Desplegados Relacionados"],"Related Issues":["Incidencias Relacionadas"],"Related Jobs":["Trabajos Relacionados"],"Related Merge Requests":["Solicitudes de fusión Relacionadas"],"Related Merged Requests":["Solicitudes de fusión Relacionadas"],"Showing %d event":["Mostrando %d evento","Mostrando %d eventos"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["La etapa de desarrollo muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquí una vez creada tu primera solicitud de fusión."],"The collection of events added to the data gathered for that stage.":["La colección de eventos agregados a los datos recopilados para esa etapa."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["La etapa de incidencia muestra el tiempo que toma desde la creación de un tema hasta asignar el tema a un hito, o añadir el tema a una lista en el panel de temas. Empieza a crear temas para ver los datos de esta etapa."],"The phase of the development lifecycle.":["La etapa del ciclo de vida de desarrollo."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["La etapa de planificación muestra el tiempo desde el paso anterior hasta el envío de tu primera confirmación. Este tiempo se añadirá automáticamente una vez que usted envíe el primer cambio."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["La etapa de producción muestra el tiempo total que tarda entre la creación de una incidencia y el despliegue del código a producción. Los datos se añadirán automáticamente una vez haya finalizado por completo el ciclo de idea a producción."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["La etapa de revisión muestra el tiempo desde la creación de la solicitud de fusión hasta que los cambios se fusionaron. Los datos se añadirán automáticamente después de fusionar su primera solicitud de fusión."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["La etapa de puesta en escena muestra el tiempo entre la fusión y el despliegue de código en el entorno de producción. Los datos se añadirán automáticamente una vez que se despliega a producción por primera vez."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["La etapa de pruebas muestra el tiempo que GitLab CI toma para ejecutar cada pipeline para la solicitud de fusión relacionada. Los datos se añadirán automáticamente luego de que el primer pipeline termine de ejecutarse."],"The time taken by each data entry gathered by that stage.":["El tiempo utilizado por cada entrada de datos obtenido por esa etapa."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["El valor en el punto medio de una serie de valores observados. Por ejemplo, entre 3, 5, 9, la mediana es 5. Entre 3, 5, 7, 8, la mediana es (5 + 7) / 2 = 6."],"Time before an issue gets scheduled":["Tiempo antes de que una incidencia sea programada"],"Time before an issue starts implementation":["Tiempo antes de que empieze la implementación de una incidencia"],"Time between merge request creation and merge/close":["Tiempo entre la creación de la solicitud de fusión y la integración o cierre de ésta"],"Time until first merge request":["Tiempo hasta la primera solicitud de fusión"],"Time|hr":["hr","hrs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Tiempo Total"],"Total test time for all commits/merges":["Tiempo total de pruebas para todos los cambios o integraciones"],"Want to see the data? Please ask an administrator for access.":["¿Quieres ver los datos? Por favor pide acceso al administrador."],"We don't have enough data to show this stage.":["No hay suficientes datos para mostrar en esta etapa."],"You need permission.":["Necesitas permisos."],"day":["día","días"]}}};
\ No newline at end of file +var locales = locales || {}; locales['es'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-06-07 12:29-0500","Language-Team":"Spanish","Language":"es","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"Bob Van Landuyt <bob@gitlab.com>","X-Generator":"Poedit 2.0.2","lang":"es","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"About auto deploy":["Acerca del auto despliegue"],"Activity":["Actividad"],"Add Changelog":["Agregar Changelog"],"Add Contribution guide":["Agregar guía de contribución"],"Add License":["Agregar Licencia"],"Add an SSH key to your profile to pull or push via SSH.":["Agregar una clave SSH a tu perfil para actualizar o enviar a través de SSH."],"Add new directory":["Agregar nuevo directorio"],"Archived project! Repository is read-only":["¡Proyecto archivado! El repositorio es de sólo lectura"],"Branch":["Rama","Ramas"],"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}":["La rama <strong>%{branch_name}</strong> fue creada. Para configurar el auto despliegue, escoge una plantilla Yaml para GitLab CI y envía tus cambios. %{link_to_autodeploy_doc}"],"Branches":["Ramas"],"ByAuthor|by":["por"],"CI configuration":["Configuración de CI"],"Changelog":["Changelog"],"Charts":["Gráficos"],"CiStatusLabel|canceled":["cancelado"],"CiStatusLabel|created":["creado"],"CiStatusLabel|failed":["fallado"],"CiStatusLabel|manual action":["acción manual"],"CiStatusLabel|passed":["pasó"],"CiStatusLabel|passed with warnings":["pasó con advertencias"],"CiStatusLabel|pending":["pendiente"],"CiStatusLabel|skipped":["omitido"],"CiStatusLabel|waiting for manual action":["esperando acción manual"],"CiStatusText|blocked":["bloqueado"],"CiStatusText|canceled":["cancelado"],"CiStatusText|created":["creado"],"CiStatusText|failed":["fallado"],"CiStatusText|manual":["manual"],"CiStatusText|passed":["pasó"],"CiStatusText|pending":["pendiente"],"CiStatusText|skipped":["omitido"],"CiStatus|running":["en ejecución"],"Commit":["Cambio","Cambios"],"CommitMessage|Add %{file_name}":["Agregar %{file_name}"],"Commits":["Cambios"],"Commits|History":["Historial"],"Compare":["Comparar"],"Contribution guide":["Guía de contribución"],"Contributors":["Contribuidores"],"Copy URL to clipboard":["Copiar URL al portapapeles"],"Copy commit SHA to clipboard":["Copiar SHA del cambio al portapapeles"],"Create New Directory":["Crear Nuevo Directorio"],"Create directory":["Crear directorio"],"Create empty bare repository":["Crear repositorio vacío"],"Create merge request":["Crear solicitud de fusión"],"CreateNewFork|Fork":["Bifurcar"],"Custom notification events":["Eventos de notificaciones personalizadas"],"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 %{notification_link}.":["Los niveles de notificación personalizados son los mismos que los niveles participantes. Con los niveles de notificación personalizados, también recibirá notificaciones para eventos seleccionados. Para obtener más información, consulte %{notification_link}."],"Cycle Analytics":["Cycle Analytics"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Cycle Analytics ofrece una visión general de cuánto tiempo tarda en pasar de idea a producción en su proyecto."],"CycleAnalyticsStage|Code":["Código"],"CycleAnalyticsStage|Issue":["Incidencia"],"CycleAnalyticsStage|Plan":["Planificación"],"CycleAnalyticsStage|Production":["Producción"],"CycleAnalyticsStage|Review":["Revisión"],"CycleAnalyticsStage|Staging":["Puesta en escena"],"CycleAnalyticsStage|Test":["Pruebas"],"Deploy":["Despliegue","Despliegues"],"Directory name":["Nombre del directorio"],"Don't show again":["No mostrar de nuevo"],"Download tar":["Descargar tar"],"Download tar.bz2":["Descargar tar.bz2"],"Download tar.gz":["Descargar tar.gz"],"Download zip":["Descargar zip"],"DownloadArtifacts|Download":["Descargar"],"DownloadSource|Download":["Descargar"],"Files":["Archivos"],"Find by path":["Buscar por ruta"],"Find file":["Buscar archivo"],"FirstPushedBy|First":["Primer"],"FirstPushedBy|pushed by":["enviado por"],"ForkedFromProjectPath|Forked from":["Bifurcado de"],"Forks":["Bifurcaciones"],"From issue creation until deploy to production":["Desde la creación de la incidencia hasta el despliegue a producción"],"From merge request merge until deploy to production":["Desde la integración de la solicitud de fusión hasta el despliegue a producción"],"Go to your fork":["Ir a tu bifurcación"],"GoToYourFork|Fork":["Bifurcación"],"Home":["Inicio"],"Housekeeping successfully started":["Servicio de limpieza iniciado con éxito"],"Import repository":["Importar repositorio"],"Introducing Cycle Analytics":["Introducción a Cycle Analytics"],"LFSStatus|Disabled":["Deshabilitado"],"LFSStatus|Enabled":["Habilitado"],"Last %d day":["Último %d día","Últimos %d días"],"Last Update":["Última actualización"],"Last commit":["Último cambio"],"Leave group":["Abandonar grupo"],"Leave project":["Abandonar proyecto"],"Limited to showing %d event at most":["Limitado a mostrar máximo %d evento","Limitado a mostrar máximo %d eventos"],"Median":["Mediana"],"MissingSSHKeyWarningLink|add an SSH key":["agregar una clave SSH"],"New Issue":["Nueva incidencia","Nuevas incidencias"],"New branch":["Nueva rama"],"New directory":["Nuevo directorio"],"New file":["Nuevo archivo"],"New issue":["Nueva incidencia"],"New merge request":["Nueva solicitud de fusión"],"New snippet":["Nuevo fragmento de código"],"New tag":["Nueva etiqueta"],"No repository":["No hay repositorio"],"Not available":["No disponible"],"Not enough data":["No hay suficientes datos"],"Notification events":["Eventos de notificación"],"NotificationEvent|Close issue":["Cerrar incidencia"],"NotificationEvent|Close merge request":["Cerrar solicitud de fusión"],"NotificationEvent|Failed pipeline":["Pipeline fallido"],"NotificationEvent|Merge merge request":["Integrar solicitud de fusión"],"NotificationEvent|New issue":["Nueva incidencia"],"NotificationEvent|New merge request":["Nueva solicitud de fusión"],"NotificationEvent|New note":["Nueva nota"],"NotificationEvent|Reassign issue":["Reasignar incidencia"],"NotificationEvent|Reassign merge request":["Reasignar solicitud de fusión"],"NotificationEvent|Reopen issue":["Reabrir incidencia"],"NotificationEvent|Successful pipeline":["Pipeline exitoso"],"NotificationLevel|Custom":["Personalizado"],"NotificationLevel|Disabled":["Deshabilitado"],"NotificationLevel|Global":["Global"],"NotificationLevel|On mention":["Cuando me mencionan"],"NotificationLevel|Participate":["Participación"],"NotificationLevel|Watch":["Vigilancia"],"OpenedNDaysAgo|Opened":["Abierto"],"Pipeline Health":["Estado del Pipeline"],"Project '%{project_name}' queued for deletion.":["Proyecto ‘%{project_name}’ en cola para eliminación."],"Project '%{project_name}' was successfully created.":["Proyecto ‘%{project_name}’ fue creado satisfactoriamente."],"Project '%{project_name}' was successfully updated.":["Proyecto ‘%{project_name}’ fue actualizado satisfactoriamente."],"Project '%{project_name}' will be deleted.":["Proyecto ‘%{project_name}’ será eliminado."],"Project access must be granted explicitly to each user.":["El acceso al proyecto debe concederse explícitamente a cada usuario."],"Project export could not be deleted.":["No se pudo eliminar la exportación del proyecto."],"Project export has been deleted.":["La exportación del proyecto ha sido eliminada."],"Project export link has expired. Please generate a new export from your project settings.":["El enlace de exportación del proyecto ha caducado. Por favor, genera una nueva exportación desde la configuración del proyecto."],"Project export started. A download link will be sent by email.":["Se inició la exportación del proyecto. Se enviará un enlace de descarga por correo electrónico."],"Project home":["Inicio del proyecto"],"ProjectFeature|Disabled":["Deshabilitada"],"ProjectFeature|Everyone with access":["Todos con acceso"],"ProjectFeature|Only team members":["Solo miembros del equipo"],"ProjectFileTree|Name":["Nombre"],"ProjectLastActivity|Never":["Nunca"],"ProjectLifecycle|Stage":["Etapa"],"ProjectNetworkGraph|Graph":["Historial gráfico"],"Read more":["Leer más"],"Readme":["Readme"],"RefSwitcher|Branches":["Ramas"],"RefSwitcher|Tags":["Etiquetas"],"Related Commits":["Cambios Relacionados"],"Related Deployed Jobs":["Trabajos Desplegados Relacionados"],"Related Issues":["Incidencias Relacionadas"],"Related Jobs":["Trabajos Relacionados"],"Related Merge Requests":["Solicitudes de fusión Relacionadas"],"Related Merged Requests":["Solicitudes de fusión Relacionadas"],"Remind later":["Recordar después"],"Remove project":["Eliminar proyecto"],"Request Access":["Solicitar acceso"],"Search branches and tags":["Buscar ramas y etiquetas"],"Select Archive Format":["Seleccionar formato de archivo"],"Set a password on your account to pull or push via %{protocol}":["Establezca una contraseña en su cuenta para actualizar o enviar a través de% {protocol}"],"Set up CI":["Configurar CI"],"Set up Koding":["Configurar Koding"],"Set up auto deploy":["Configurar auto despliegue"],"SetPasswordToCloneLink|set a password":["establecer una contraseña"],"Showing %d event":["Mostrando %d evento","Mostrando %d eventos"],"Source code":["Código fuente"],"StarProject|Star":["Destacar"],"Switch branch/tag":["Cambiar rama/etiqueta"],"Tag":["Etiqueta","Etiquetas"],"Tags":["Etiquetas"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["La etapa de desarrollo muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquí una vez creada tu primera solicitud de fusión."],"The collection of events added to the data gathered for that stage.":["La colección de eventos agregados a los datos recopilados para esa etapa."],"The fork relationship has been removed.":["La relación con la bifurcación se ha eliminado."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["La etapa de incidencia muestra el tiempo que toma desde la creación de un tema hasta asignar el tema a un hito, o añadir el tema a una lista en el panel de temas. Empieza a crear temas para ver los datos de esta etapa."],"The phase of the development lifecycle.":["La etapa del ciclo de vida de desarrollo."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["La etapa de planificación muestra el tiempo desde el paso anterior hasta el envío de tu primera confirmación. Este tiempo se añadirá automáticamente una vez que usted envíe el primer cambio."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["La etapa de producción muestra el tiempo total que tarda entre la creación de una incidencia y el despliegue del código a producción. Los datos se añadirán automáticamente una vez haya finalizado por completo el ciclo de idea a producción."],"The project can be accessed by any logged in user.":["El proyecto puede ser accedido por cualquier usuario conectado."],"The project can be accessed without any authentication.":["El proyecto puede accederse sin ninguna autenticación."],"The repository for this project does not exist.":["El repositorio para este proyecto no existe."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["La etapa de revisión muestra el tiempo desde la creación de la solicitud de fusión hasta que los cambios se fusionaron. Los datos se añadirán automáticamente después de fusionar su primera solicitud de fusión."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["La etapa de puesta en escena muestra el tiempo entre la fusión y el despliegue de código en el entorno de producción. Los datos se añadirán automáticamente una vez que se despliega a producción por primera vez."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["La etapa de pruebas muestra el tiempo que GitLab CI toma para ejecutar cada pipeline para la solicitud de fusión relacionada. Los datos se añadirán automáticamente luego de que el primer pipeline termine de ejecutarse."],"The time taken by each data entry gathered by that stage.":["El tiempo utilizado por cada entrada de datos obtenido por esa etapa."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["El valor en el punto medio de una serie de valores observados. Por ejemplo, entre 3, 5, 9, la mediana es 5. Entre 3, 5, 7, 8, la mediana es (5 + 7) / 2 = 6."],"This means you can not push code until you create an empty repository or import existing one.":["Esto significa que no puede enviar código hasta que cree un repositorio vacío o importe uno existente."],"Time before an issue gets scheduled":["Tiempo antes de que una incidencia sea programada"],"Time before an issue starts implementation":["Tiempo antes de que empieze la implementación de una incidencia"],"Time between merge request creation and merge/close":["Tiempo entre la creación de la solicitud de fusión y la integración o cierre de ésta"],"Time until first merge request":["Tiempo hasta la primera solicitud de fusión"],"Timeago|%s days ago":["hace %s días"],"Timeago|%s days remaining":["%s días restantes"],"Timeago|%s hours remaining":["%s horas restantes"],"Timeago|%s minutes ago":["hace %s minutos"],"Timeago|%s minutes remaining":["%s minutos restantes"],"Timeago|%s months ago":["hace %s meses"],"Timeago|%s months remaining":["%s meses restantes"],"Timeago|%s seconds remaining":["%s segundos restantes"],"Timeago|%s weeks ago":["hace %s semanas"],"Timeago|%s weeks remaining":["%s semanas restantes"],"Timeago|%s years ago":["hace %s años"],"Timeago|%s years remaining":["%s años restantes"],"Timeago|1 day remaining":["1 día restante"],"Timeago|1 hour remaining":["1 hora restante"],"Timeago|1 minute remaining":["1 minuto restante"],"Timeago|1 month remaining":["1 mes restante"],"Timeago|1 week remaining":["1 semana restante"],"Timeago|1 year remaining":["1 año restante"],"Timeago|Past due":["Atrasado"],"Timeago|a day ago":["hace un día"],"Timeago|a month ago":["hace 1 mes"],"Timeago|a week ago":["hace 1 semana"],"Timeago|a while":["hace un momento"],"Timeago|a year ago":["hace 1 año"],"Timeago|about %s hours ago":["hace alrededor de %s horas"],"Timeago|about a minute ago":["hace alrededor de 1 minuto"],"Timeago|about an hour ago":["hace alrededor de 1 hora"],"Timeago|in %s days":["en %s días"],"Timeago|in %s hours":["en %s horas"],"Timeago|in %s minutes":["en %s minutos"],"Timeago|in %s months":["en %s meses"],"Timeago|in %s seconds":["en %s segundos"],"Timeago|in %s weeks":["en %s semanas"],"Timeago|in %s years":["en %s años"],"Timeago|in 1 day":["en 1 día"],"Timeago|in 1 hour":["en 1 hora"],"Timeago|in 1 minute":["en 1 minuto"],"Timeago|in 1 month":["en 1 mes"],"Timeago|in 1 week":["en 1 semana"],"Timeago|in 1 year":["en 1 año"],"Timeago|less than a minute ago":["hace menos de 1 minuto"],"Time|hr":["hr","hrs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Tiempo Total"],"Total test time for all commits/merges":["Tiempo total de pruebas para todos los cambios o integraciones"],"Unstar":["No Destacar"],"Upload New File":["Subir nuevo archivo"],"Upload file":["Subir archivo"],"Use your global notification setting":["Utiliza tu configuración de notificación global"],"VisibilityLevel|Internal":["Interno"],"VisibilityLevel|Private":["Privado"],"VisibilityLevel|Public":["Público"],"Want to see the data? Please ask an administrator for access.":["¿Quieres ver los datos? Por favor pide acceso al administrador."],"We don't have enough data to show this stage.":["No hay suficientes datos para mostrar en esta etapa."],"Withdraw Access Request":["Retirar Solicitud de Acceso"],"You are going to remove %{project_name_with_namespace}.\\nRemoved project CANNOT be restored!\\nAre you ABSOLUTELY sure?":["Va a eliminar %{project_name_with_namespace}.\\n¡El proyecto eliminado NO puede ser restaurado!\\n¿Estás TOTALMENTE seguro?"],"You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?":["Vas a eliminar el enlace de la bifurcación con el proyecto original %{forked_from_project}. ¿Estás TOTALMENTE seguro?"],"You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?":["Vas a transferir %{project_name_with_namespace} a otro propietario. ¿Estás TOTALMENTE seguro?"],"You can only add files when you are on a branch":["Sólo puede agregar archivos cuando estas en una rama"],"You must sign in to star a project":["Debes iniciar sesión para destacar un proyecto"],"You need permission.":["Necesitas permisos."],"You will not get any notifications via email":["No recibirás ninguna notificación por correo electrónico"],"You will only receive notifications for the events you choose":["Solo recibirás notificaciones de los eventos que elijas"],"You will only receive notifications for threads you have participated in":["Solo recibirás notificaciones de los temas en los que has participado"],"You will receive notifications for any activity":["Recibirás notificaciones para cualquier actividad"],"You will receive notifications only for comments in which you were @mentioned":["Recibirás notificaciones sólo para los comentarios en los que se te mencionó"],"You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account":["No podrás actualizar o enviar código al proyecto a través de %{protocol} hasta que %{set_password_link} en tu cuenta"],"You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile":["No podrás actualizar o enviar código al proyecto a través de SSH hasta que %{add_ssh_key_link} en su perfil"],"Your name":["Tu nombre"],"committed":["cambió"],"day":["día","días"],"notification emails":["correos electrónicos de notificación"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/fr/app.js b/app/assets/javascripts/locale/fr/app.js new file mode 100644 index 00000000000..f9904ea61ea --- /dev/null +++ b/app/assets/javascripts/locale/fr/app.js @@ -0,0 +1 @@ +var locales = locales || {}; locales['fr'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-06-15 20:38+0000","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","PO-Revision-Date":"2017-06-14 04:21-0400","Last-Translator":"Dremor <egeorget@opmbx.org>","Language-Team":"French (https://www.transifex.com/gitlab-fr/teams/75145/fr/)","Language":"fr","Plural-Forms":"nplurals=2; plural=(n > 1);","X-Generator":"Zanata 3.9.6","lang":"fr","domain":"app","plural_forms":"nplurals=2; plural=(n > 1);"},"ByAuthor|by":["par"],"Commit":["Validation","Validations"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["L’analyseur de cycle permet d’avoir une vue d’ensemble du temps nécessaire pour aller d’une idée à sa mise en production pour votre projet."],"CycleAnalyticsStage|Code":["Code"],"CycleAnalyticsStage|Issue":["Incident"],"CycleAnalyticsStage|Plan":["Planification"],"CycleAnalyticsStage|Production":["Production"],"CycleAnalyticsStage|Review":["Examen"],"CycleAnalyticsStage|Staging":["Pré-production"],"CycleAnalyticsStage|Test":["Test"],"Deploy":["Déploiement","Déploiements"],"FirstPushedBy|First":["En premier"],"FirstPushedBy|pushed by":["poussé par"],"From issue creation until deploy to production":["Depuis la création de l'incident jusqu'au déploiement en production"],"From merge request merge until deploy to production":["Depuis la fusion de la demande de fusion jusqu'au déploiement en production"],"Introducing Cycle Analytics":["Introduction à l'analyseur de cycle"],"Last %d day":["Le dernier %d jour","Les derniers %d jours"],"Limited to showing %d event at most":["Limiter l'affichage au plus à %d évènement","Limiter l'affichage au plus à %d évènements"],"Median":["Médian"],"New Issue":["Nouvel incident","Nouveaux incidents"],"Not available":["Indisponible"],"Not enough data":["Données insuffisantes"],"OpenedNDaysAgo|Opened":["Ouvert"],"Pipeline Health":["Santé du Pipeline"],"ProjectLifecycle|Stage":["Étape"],"Read more":["Lire plus"],"Related Commits":["Validations liés"],"Related Deployed Jobs":["Tâches de déploiement liés"],"Related Issues":["Incidents liés"],"Related Jobs":["Tâches liées"],"Related Merge Requests":["Demandes de fusion liées"],"Related Merged Requests":["Demandes fusionnées liées"],"Showing %d event":["Affichage de %d évènement","Affichage de %d évènements"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["L’étape de développement montre le temps entre la première validation et la création de la demande de fusion. Les données seront automatiquement ajoutées ici une fois que vous aurez créé votre première demande de fusion."],"The collection of events added to the data gathered for that stage.":["L’ensemble d’évènements ajoutés aux données récupérées pour cette étape."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["L'étape des incidents montre le temps nécessaire entre la création d'un incident et son assignation à un jalon, ou son ajout à une liste d'un tableau d'incident. Débutez à créer des incidents pour voir des données pour cette étape."],"The phase of the development lifecycle.":["Les étapes du cycle de développement."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["L’étape de planification montre le temps entre l’étape précédente et l’envoi de votre première validation. Ce temps sera automatiquement ajouté quand vous pousserez votre première validation."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["L’étape de mise en production montre le temps nécessaire entre la création d’un incident et le déploiement du code en production. Les données seront automatiquement ajoutées une fois que vous aurez complété le cycle complet, depuis l’idée jusqu’à la mise en production."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["L’étape d’évaluation montre le temps entre la création de la demande de fusion et la fusion effective de celle-ci. Ces données seront automatiquement ajoutées après que vous ayez fusionné votre première demande de fusion."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["L’étape de pré-production indique le temps entre la fusion de la RF et le déploiement du code dans l’environnent de production. Les données seront automatiquement ajoutées une fois que vous déploierez en production pour la première fois."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["L’étape de test montre le temps que le CI de GitLab met pour exécuter chaque pipeline liés à la demande de fusion. Les données seront automatiquement ajoutées après que votre premier pipeline s’achèvera."],"The time taken by each data entry gathered by that stage.":["Le temps pris par chaque entrée récoltée durant cette étape."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["La valeur située au point médian d’une série de valeur observée. C.à.d., entre 3, 5, 9, le médian est 5. Entre 3, 5, 7, 8, le médian est (5+7)/2 = 6."],"Time before an issue gets scheduled":["Temps avant qu’un incident ne soit planifié"],"Time before an issue starts implementation":["Temps avant que résolution ne débute"],"Time between merge request creation and merge/close":["Temps entre la création d'une demande de fusion et sa fusion/clôture"],"Time until first merge request":["Temps jusqu’à la première demande de fusion"],"Time|hr":["hr","hrs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Temps total"],"Total test time for all commits/merges":["Temps total de test pour toutes les validations/fusions"],"Want to see the data? Please ask an administrator for access.":["Vous voulez voir les données ? Merci de contacter un administrateur pour en obtenir l’accès."],"We don't have enough data to show this stage.":["Nous n'avons pas suffisamment de données pour afficher cette étape."],"You need permission.":["Vous avez besoin d’une autorisation."],"day":["jour","jours"],"%{commit_author_link} committed %{commit_timeago}":["%{commit_author_link} a validé %{commit_timeago}"],"About auto deploy":["A propos de l'auto-déploiement"],"Active":["Actif"],"Activity":["Activité"],"Add Changelog":["Ajouter un journal des modifications"],"Add Contribution guide":["Ajouter un guide de contribution"],"Add License":["Ajouter une licence"],"Add an SSH key to your profile to pull or push via SSH.":["Ajoutez une clef SSH à votre profil pour pouvoir récupérer et pousser par SSH."],"Add new directory":["Ajouter un nouveau dossier"],"Archived project! Repository is read-only":["Projet archivé ! Le dépôt est en lecture seule"],"Are you sure you want to delete this pipeline schedule?":["Êtes-vous sûr de vouloir supprimer ce pipeline programmé"],"Attach a file by drag & drop or %{upload_link}":["Attachez un fichier par glisser & déposer ou %{upload_link}"],"Branch":["Branche","Branches"],"#~ \"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, cho\"#~ \"ose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_do\"#~ \"c}\"":["#~ \"La branche <strong>%{branch_name}</strong> a été crée. Pour mettre en place le\"#~ \" déploiement automatisé, sélectionnez un modèle de fichier Yaml pour Gitlab CI\"#~ \", et validez les modifications. %{link_to_autodeploy_doc}\""],"Branches":["Branches"],"Browse files":["Parcourir les fichiers"],"CI configuration":["Configuration du CI"],"Cancel":["Annuler"],"ChangeTypeActionLabel|Pick into branch":["Sélectionner dans la branche"],"ChangeTypeActionLabel|Revert in branch":["Annuler dans la branche"],"ChangeTypeAction|Cherry-pick":["Sélectionner"],"ChangeType|commit":["validation"],"ChangeType|merge request":["demande de fusion"],"Changelog":["Journal des modifications"],"Charts":["Graphiques"],"Cherry-pick this commit":["Sélectionner cette validation"],"Cherry-pick this merge-request":["Sélectionner cette demande de fusion"],"CiStatusLabel|canceled":["annulé"],"CiStatusLabel|created":["créé"],"CiStatusLabel|failed":["échoué"],"CiStatusLabel|manual action":["action manuelle"],"CiStatusLabel|passed":["passé"],"CiStatusLabel|passed with warnings":["passé avec des avertissements"],"CiStatusLabel|pending":["en attente"],"CiStatusLabel|skipped":["ignoré"],"CiStatusLabel|waiting for manual action":["en attente d'action manuelle"],"CiStatusText|blocked":["bloqué"],"CiStatusText|canceled":["annulé "],"CiStatusText|created":["créé"],"CiStatusText|failed":["échoué"],"CiStatusText|manual":["manuel"],"CiStatusText|passed":["passé"],"CiStatusText|pending":["en attente"],"CiStatusText|skipped":["ignoré"],"CiStatus|running":["en cours"],"Commit message":["Message de validation"],"CommitMessage|Add %{file_name}":["Ajout de %{file_name}"],"Commits":["Validations"],"Commits|History":["Historique"],"Committed by":["Validé par"],"Compare":["Comparer"],"Contribution guide":["Guilde de contribution"],"Contributors":["Contributeurs"],"Copy URL to clipboard":["Copier l'URL dans le presse-papier"],"Copy commit SHA to clipboard":["Copier le SAH de la validation"],"Create New Directory":["Créer un nouveau dossier"],"Create directory":["Créer un dossier"],"Create empty bare repository":["Créer un dépôt vide"],"Create merge request":["Créer une demande de fusion"],"Create new...":["Créer nouveau..."],"CreateNewFork|Fork":["Fork"],"CreateTag|Tag":["Étiquette"],"Cron Timezone":["Fuseau horaire de Cron"],"Cron syntax":["Syntaxe CRON"],"Custom":["Personnalisé"],"Custom notification events":["Événements de notification personnalisés"],"#~ \"Custom notification levels are the same as participating levels. With custom n\"#~ \"otification levels you will also receive notifications for select events. To f\"#~ \"ind out more, check out %{notification_link}.\"":["#~ \"Le niveau de notification Personnalisé est similaire au niveau Participation. \"#~ \"Il permet cependant également de recevoir des notifications pour des événement\"#~ \"s sélectionnés. Pour plus d’information, vous pouvez consulter %{notification_\"#~ \"link}.\""],"Cycle Analytics":["Analyseur de cycle"],"Define a custom pattern with cron syntax":["Définir un schéma personnalisé avec une syntaxe CRON"],"Delete":["Supprimer"],"Description":["Description"],"Directory name":["Nom du dossier"],"Don't show again":["Ne plus montrer"],"Download":["Télécharger"],"Download tar":["Télécharger tar"],"Download tar.bz2":["Télécharger tar.bz2"],"Download tar.gz":["Télécharger tar.gz"],"Download zip":["Télécharger zip"],"DownloadArtifacts|Download":["Télécharger"],"DownloadCommit|Email Patches":["Patch email"],"DownloadCommit|Plain Diff":["Diff simple"],"DownloadSource|Download":["Télécharger"],"Edit":["Éditer"],"Edit Pipeline Schedule %{id}":["Éditer le pipeline programmé %{id}"],"Every day (at 4:00am)":["Chaque jour (à 4:00 du matin)"],"Every month (on the 1st at 4:00am)":["Chaque mois (le 1er à 4:00 du matin)"],"Every week (Sundays at 4:00am)":["Chaque semaine (Dimanche à 4:00 du matin)"],"Failed to change the owner":["Échec du changement de propriétaire"],"Failed to remove the pipeline schedule":["Échec de la suppression du pipeline programmé"],"Files":["Fichiers"],"Find by path":["Rechercher par chemin"],"Find file":["Rechercher un fichier"],"Fork":["Fork","Forks"],"ForkedFromProjectPath|Forked from":["Forké depuis"],"Go to your fork":["Aller à votre fork"],"GoToYourFork|Fork":["Fork"],"Home":["Accueil"],"Housekeeping successfully started":["Maintenance démarrée avec succès"],"Import repository":["Importer un dépôt"],"Interval Pattern":["Schéma d’intervalle"],"LFSStatus|Disabled":["Désactivé"],"LFSStatus|Enabled":["Activé"],"Last Pipeline":["Dernier pipeline"],"Last Update":["Dernière mise à jour"],"Last commit":["Dernière validation"],"Learn more in the":["En apprendre plus dans le"],"Leave group":["Quitter le groupe"],"Leave project":["Quitter le projet"],"MissingSSHKeyWarningLink|add an SSH key":["ajouter un clef SSH"],"New Pipeline Schedule":["Nouveau pipeline programmé"],"New branch":["Nouvelle branche"],"New directory":["Nouveau dossier"],"New file":["Nouveau Fichier"],"New issue":["Nouvel incident"],"New merge request":["Nouvelle demande de fusion"],"New schedule":["Nouveau programme"],"New snippet":["Nouvel extrait de code"],"New tag":["Nouvelle étiquette"],"No repository":["Pas de dépôt"],"No schedules":["Aucun programme"],"Notification events":["Événement de notifications"],"NotificationEvent|Close issue":["Clore l'incident"],"NotificationEvent|Close merge request":["Clore la demande de fusion"],"NotificationEvent|Failed pipeline":["Pipeline échoué"],"NotificationEvent|Merge merge request":["Fusionner le demande de fusion"],"NotificationEvent|New issue":["Nouvel incident"],"NotificationEvent|New merge request":["Nouvelle demande de fusion"],"NotificationEvent|New note":["Nouvelle note"],"NotificationEvent|Reassign issue":["Réassigner l'incident"],"NotificationEvent|Reassign merge request":["Réassigner la demande de fusion"],"NotificationEvent|Reopen issue":["Ré-ouvrir l'incident"],"NotificationEvent|Successful pipeline":["Pipeline réussi"],"NotificationLevel|Custom":["Personnalisé"],"NotificationLevel|Disabled":["Désactivé"],"NotificationLevel|Global":["Global"],"NotificationLevel|On mention":["En cas de mention"],"NotificationLevel|Participate":["Participation"],"NotificationLevel|Watch":["Surveillé"],"OfSearchInADropdown|Filter":["Filtre"],"Options":["Options"],"Owner":["Propriétaire"],"Pipeline":["Pipeline"],"Pipeline Schedule":["Programmation de pipeline"],"Pipeline Schedules":["Programmations de pipeline"],"PipelineSchedules|Activated":["Activé"],"PipelineSchedules|Active":["Active"],"PipelineSchedules|All":["Tous"],"PipelineSchedules|Inactive":["Inactive"],"PipelineSchedules|Next Run":["Prochaine exécution"],"PipelineSchedules|None":["Aucune"],"PipelineSchedules|Provide a short description for this pipeline":["Indiquez une courte description"],"PipelineSchedules|Take ownership":["S’approprier"],"PipelineSchedules|Target":["Cible"],"Project '%{project_name}' queued for deletion.":["Projet '%{project_name}' en attente de suppression."],"Project '%{project_name}' was successfully created.":["Projet '%{project_name}' créé avec succès."],"Project '%{project_name}' was successfully updated.":["Projet '%{project_name}' mis à jour avec succès."],"Project '%{project_name}' will be deleted.":["Projet '%{project_name}' sera supprimé."],"Project access must be granted explicitly to each user.":["L’accès au projet doit être explicitement accordé à chaque utilisateur."],"Project export could not be deleted.":["L'export du projet n'a pas pu être supprimé."],"Project export has been deleted.":["L'export du projet a été supprimé."],"#~ \"Project export link has expired. Please generate a new export from your projec\"#~ \"t settings.\"":["#~ \"Le lien de l’export du projet a expiré. Merci de générer un nouvel export depu\"#~ \"is les paramètres du projet.\""],"Project export started. A download link will be sent by email.":["#~ \"L'export du projet a débuté. Un lien de téléchargement sera envoyé par courrie\"#~ \"l.\""],"Project home":["Accueil du projet"],"ProjectFeature|Disabled":["Désactivé"],"ProjectFeature|Everyone with access":["Toute personne ayant accès"],"ProjectFeature|Only team members":["Seulement les membres de l'équipe"],"ProjectFileTree|Name":["Nom"],"ProjectLastActivity|Never":["Jamais"],"ProjectNetworkGraph|Graph":["Graphique "],"Readme":["LisezMoi"],"RefSwitcher|Branches":["Branches"],"RefSwitcher|Tags":["Étiquettes"],"Remind later":["Me le rappeler ultérieurement"],"Remove project":["Supprimer le projet"],"Request Access":["Demander l'accès"],"Revert this commit":["Annuler cette validation"],"Revert this merge-request":["Annuler cette demande de fusion"],"Save pipeline schedule":["Sauvegarder le pipeline programmé"],"Schedule a new pipeline":["Programmer un nouveau pipeline"],"Scheduling Pipelines":["Programmer des pipelines"],"Search branches and tags":["Rechercher dans les branches et les étiquettes"],"Select Archive Format":["Sélectionnez le format de l'archive"],"Select a timezone":["Sélectionnez un fuseau horaire"],"Select target branch":["Sélectionnez une branche cible"],"Set a password on your account to pull or push via %{protocol}":["#~ \"Définissez un mot de passe pour votre compte pour pouvoir tirer ou pousser par\"#~ \" %{protocol}\""],"Set up CI":["Mettre en place le CI"],"Set up Koding":["Mettre en place Koding"],"Set up auto deploy":["Mettre en place l’auto-déploiement "],"SetPasswordToCloneLink|set a password":["définir un mot de passe"],"Source code":["Code source"],"StarProject|Star":["S'abonner"],"Start a <strong>new merge request</strong> with these changes":["Créer une <strong>nouvelle demande de fusion</strong> avec ces changements"],"Switch branch/tag":["Changer de branche / d'étiquette"],"Tag":["Étiquette","Étiquettes"],"Tags":["Étiquettes"],"Target Branch":["Branche cible"],"The fork relationship has been removed.":["La relation de fork a été supprimée."],"#~ \"The pipelines schedule runs pipelines in the future, repeatedly, for specific \"#~ \"branches or tags. Those scheduled pipelines will inherit limited project acces\"#~ \"s based on their associated user.\"":["#~ \"Les pipelines programmés exécutent des pipelines dans le futur, de façon répét\"#~ \"ée, pour les branches et étiquettes spécifiées. Ces pipelines programmés hérit\"#~ \"ent d’un accès partiel au projet basé sur l’utilisateur que leurs est associé.\""],"The project can be accessed by any logged in user.":["Votre projet peut être accédé par n’importe quel utilisateur authentifié"],"The project can be accessed without any authentication.":["Votre projet peut être accédé sans aucune authentification."],"The repository for this project does not exist.":["Le dépôt pour ce projet n'existe pas."],"#~ \"This means you can not push code until you create an empty repository or impor\"#~ \"t existing one.\"":["#~ \"Cela signifie que vous ne pouvez pas pousser du code tant que vous ne créez pa\"#~ \"s un dépôt vide, ou importez une dépôt existant.\""],"Timeago|%s days ago":["Il y a %s jours"],"Timeago|%s days remaining":["Il reste %s jours"],"Timeago|%s hours remaining":["Il reste %s heures"],"Timeago|%s minutes ago":["Il y a %s minutes"],"Timeago|%s minutes remaining":["Il reste %s minutes"],"Timeago|%s months ago":["Il y a %s mois"],"Timeago|%s months remaining":["Il reste %s mois"],"Timeago|%s seconds remaining":["Il reste %s secondes"],"Timeago|%s weeks ago":["Il y a %s semaines"],"Timeago|%s weeks remaining":["Il reste %s semaines"],"Timeago|%s years ago":["Il y a %s ans"],"Timeago|%s years remaining":["Il reste %s ans"],"Timeago|1 day remaining":["Il reste un jour"],"Timeago|1 hour remaining":["Il reste une heure"],"Timeago|1 minute remaining":["Il reste une minute"],"Timeago|1 month remaining":["Il reste un mois"],"Timeago|1 week remaining":["Il reste une semaine"],"Timeago|1 year remaining":["Il reste un an"],"Timeago|Past due":["En retard"],"Timeago|a day ago":["Il y a un jour"],"Timeago|a month ago":["Il y a un mois"],"Timeago|a week ago":["Il y a une semaine"],"Timeago|a while":["Il y a un moment"],"Timeago|a year ago":["Il y a un an"],"Timeago|about %s hours ago":["Il y a environ %s heures"],"Timeago|about a minute ago":["Il y a environ une minute"],"Timeago|about an hour ago":["Il y a environ une heure"],"Timeago|in %s days":["Dans %s jours"],"Timeago|in %s hours":["Dans %s heures"],"Timeago|in %s minutes":["Dans %s minutes"],"Timeago|in %s months":["Dans %s mois"],"Timeago|in %s seconds":["Dans %s secondes"],"Timeago|in %s weeks":["Dans %s semaines"],"Timeago|in %s years":["Dans %s années"],"Timeago|in 1 day":["Dans 1 jour"],"Timeago|in 1 hour":["Dans 1 heure"],"Timeago|in 1 minute":["Dans 1 minute"],"Timeago|in 1 month":["Dans 1 mois"],"Timeago|in 1 week":["Dans 1 semaine"],"Timeago|in 1 year":["Dans 1 an"],"Timeago|less than a minute ago":["il y a moins d'une minute"],"Unstar":["Se désabonner"],"Upload New File":["Téléverser un nouveau fichier"],"Upload file":["Téléverser un fichier"],"Use your global notification setting":["Utiliser vos paramètres de notification globaux"],"VisibilityLevel|Internal":["Interne"],"VisibilityLevel|Private":["Privé"],"VisibilityLevel|Public":["Publique"],"Withdraw Access Request":["Retirer la demande d'accès"],"#~ \"You are going to remove %{project_name_with_namespace}.\\n\"#~ \"Removed project CANNOT be restored!\\n\"#~ \"Are you ABSOLUTELY sure?\"":["#~ \"Vous êtes sur le point de supprimer %{project_name_with_namespace}.\\n\"#~ \"Les projets supprimés NE PEUVENT PAS être restaurés !\\n\"#~ \"Êtes vous ABSOLUMENT sûr ? \""],"#~ \"You are going to remove the fork relationship to source project %{forked_from_\"#~ \"project}. Are you ABSOLUTELY sure?\"":["#~ \"Vous allez supprimer la relation de fork avec le projet source %{forked_from_p\"#~ \"roject}. Êtes-vous VRAIMENT sûr.\""],"#~ \"You are going to transfer %{project_name_with_namespace} to another owner. Are\"#~ \" you ABSOLUTELY sure?\"":["#~ \"Vous allez transférer %{project_name_with_namespace} à un nouveau propriétaire\"#~ \". Êtes vous VRAIMENT sûr ?\""],"You can only add files when you are on a branch":["Vous ne pouvez ajouter de fichier que dans une branche"],"You must sign in to star a project":["Vous devez vous identifier pour vous abonner à un projet"],"You will not get any notifications via email":["Vous ne recevrez aucune notification par courriel"],"You will only receive notifications for the events you choose":["#~ \"Vous ne recevrez de notification que pour les évènements que vous aurez choisi\"#~ \"s\""],"You will only receive notifications for threads you have participated in":["#~ \"Vous ne recevrez de notification que pour les sujets auxquels vous avez partic\"#~ \"ipé\""],"You will receive notifications for any activity":["Vous recevrez des notifications pour n’importe quelles activités"],"You will receive notifications only for comments in which you were @mentioned":["#~ \"Vous ne recevrez de notifications que pour les commentaires où vous êtes @ment\"#~ \"ionné\""],"#~ \"You won't be able to pull or push project code via %{protocol} until you %{set\"#~ \"_password_link} on your account\"":["#~ \"Vous ne pourrez pas récupérer ou pousser de code par %{protocol} tant que vo\"#~ \"us n'aurez pas %{set_password_link} pour votre compte\""],"#~ \"You won't be able to pull or push project code via SSH until you %{add_ssh_key\"#~ \"_link} to your profile\"":["#~ \"Vous ne pourrez pas récupérer ou pousser de code par SSH tant que vous n’aur\"#~ \"ez pas %{add_ssh_key_link} dans votre profil\""],"Your name":["Votre nom"],"notification emails":["courriels de notification"],"parent":["parent","parents"],"pipeline schedules documentation":["documentation des pipeline programmés"],"with stage":["avec l'étape","avec les étapes"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/pt_BR/app.js b/app/assets/javascripts/locale/pt_BR/app.js new file mode 100644 index 00000000000..f2eed3da064 --- /dev/null +++ b/app/assets/javascripts/locale/pt_BR/app.js @@ -0,0 +1 @@ +var locales = locales || {}; locales['pt_BR'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","PO-Revision-Date":"2017-06-05 03:29-0400","Last-Translator":"Alexandre Alencar <alexandre.alencar@gmail.com>","Language-Team":"Portuguese (Brazil)","Language":"pt-BR","X-Generator":"Zanata 3.9.6","Plural-Forms":"nplurals=2; plural=(n != 1)","lang":"pt_BR","domain":"app","plural_forms":"nplurals=2; plural=(n != 1)"},"ByAuthor|by":["por"],"Commit":["Commit","Commits"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["A Análise de Ciclo fornece uma visão geral de quanto tempo uma ideia demora para ir para produção em seu projeto."],"CycleAnalyticsStage|Code":["Código"],"CycleAnalyticsStage|Issue":["Tarefa"],"CycleAnalyticsStage|Plan":["Plano"],"CycleAnalyticsStage|Production":["Produção"],"CycleAnalyticsStage|Review":["Revisão"],"CycleAnalyticsStage|Staging":["Homologação"],"CycleAnalyticsStage|Test":["Teste"],"Deploy":["Implantação","Implantações"],"FirstPushedBy|First":["Primeiro"],"FirstPushedBy|pushed by":["publicado por"],"From issue creation until deploy to production":["Da criação de tarefas até a implantação para a produção"],"From merge request merge until deploy to production":["Da incorporação do merge request até a implantação em produção"],"Introducing Cycle Analytics":["Apresentando a Análise de Ciclo"],"Last %d day":["Último %d dia","Últimos %d dias"],"Limited to showing %d event at most":["Limitado a mostrar %d evento no máximo","Limitado a mostrar %d eventos no máximo"],"Median":["Mediana"],"New Issue":["Nova Tarefa","Novas Tarefas"],"Not available":["Não disponível"],"Not enough data":["Dados insuficientes"],"OpenedNDaysAgo|Opened":["Aberto"],"Pipeline Health":["Saúde da Pipeline"],"ProjectLifecycle|Stage":["Etapa"],"Read more":["Ler mais"],"Related Commits":["Commits Relacionados"],"Related Deployed Jobs":["Jobs Relacionados Incorporados"],"Related Issues":["Tarefas Relacionadas"],"Related Jobs":["Jobs Relacionados"],"Related Merge Requests":["Merge Requests Relacionados"],"Related Merged Requests":["Merge Requests Relacionados"],"Showing %d event":["Mostrando %d evento","Mostrando %d eventos"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["O estágio de codificação mostra o tempo desde o primeiro commit até a criação do merge request. \\nOs dados serão automaticamente adicionados aqui uma vez que você tenha criado seu primeiro merge request."],"The collection of events added to the data gathered for that stage.":["A coleção de eventos adicionados aos dados coletados para esse estágio."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["O estágio em questão mostra o tempo que leva desde a criação de uma tarefa até a sua assinatura para um milestone, ou a sua adição para a lista no seu Painel de Tarefas. Comece a criar tarefas para ver dados para esta etapa."],"The phase of the development lifecycle.":["A fase do ciclo de vida do desenvolvimento."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["A fase de planejamento mostra o tempo do passo anterior até empurrar o seu primeiro commit. Este tempo será adicionado automaticamente assim que você realizar seu primeiro commit."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["O estágio de produção mostra o tempo total que leva entre criar uma tarefa e implantar o código na produção. Os dados serão adicionados automaticamente até que você complete todo o ciclo de produção."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["A etapa de revisão mostra o tempo de criação de um merge request até que o merge seja feito. Os dados serão automaticamente adicionados depois que você fizer seu primeiro merge request."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["O estágio de estágio mostra o tempo entre a fusão do MR e o código de implantação para o ambiente de produção. Os dados serão automaticamente adicionados depois de implantar na produção pela primeira vez."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["A fase de teste mostra o tempo que o GitLab CI leva para executar cada pipeline para o merge request relacionado. Os dados serão automaticamente adicionados após a conclusão do primeiro pipeline."],"The time taken by each data entry gathered by that stage.":["O tempo necessário para cada entrada de dados reunida por essa etapa."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["O valor situado no ponto médio de uma série de valores observados. Ex., entre 3, 5, 9, a mediana é 5. Entre 3, 5, 7, 8, a mediana é (5 + 7) / 2 = 6."],"Time before an issue gets scheduled":["Tempo até que uma tarefa seja planejada"],"Time before an issue starts implementation":["Tempo até que uma tarefa comece a ser implementada"],"Time between merge request creation and merge/close":["Tempo entre a criação do merge request e o merge/fechamento"],"Time until first merge request":["Tempo até o primeiro merge request"],"Time|hr":["h","hs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Tempo Total"],"Total test time for all commits/merges":["Tempo de teste total para todos os commits/merges"],"Want to see the data? Please ask an administrator for access.":["Precisa visualizar os dados? Solicite acesso ao administrador."],"We don't have enough data to show this stage.":["Não temos dados suficientes para mostrar esta fase."],"You need permission.":["Você precisa de permissão."],"day":["dia","dias"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/zh_CN/app.js b/app/assets/javascripts/locale/zh_CN/app.js index 9525bc88190..d1335cfbc0f 100644 --- a/app/assets/javascripts/locale/zh_CN/app.js +++ b/app/assets/javascripts/locale/zh_CN/app.js @@ -1 +1 @@ -var locales = locales || {}; locales['zh_CN'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (China) (https://www.transifex.com/gitlab-zh/teams/75177/zh_CN/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_CN","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_CN","domain":"app","plural_forms":"nplurals=1; plural=0;"},"ByAuthor|by":["作者:"],"Commit":["提交"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["周期分析概述了项目从想法到产品实现的各阶段所需的时间。"],"CycleAnalyticsStage|Code":["编码"],"CycleAnalyticsStage|Issue":["议题"],"CycleAnalyticsStage|Plan":["计划"],"CycleAnalyticsStage|Production":["生产"],"CycleAnalyticsStage|Review":["评审"],"CycleAnalyticsStage|Staging":["预发布"],"CycleAnalyticsStage|Test":["测试"],"Deploy":["部署"],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["从创建议题到部署至生产环境"],"From merge request merge until deploy to production":["从合并请求被合并后到部署至生产环境"],"Introducing Cycle Analytics":["周期分析简介"],"Last %d day":["最后 %d 天"],"Limited to showing %d event at most":["最多显示 %d 个事件"],"Median":["中位数"],"New Issue":["新议题"],"Not available":["数据不足"],"Not enough data":["数据不足"],"OpenedNDaysAgo|Opened":["开始于"],"Pipeline Health":["流水线健康指标"],"ProjectLifecycle|Stage":["项目生命周期"],"Read more":["了解更多"],"Related Commits":["相关的提交"],"Related Deployed Jobs":["相关的部署作业"],"Related Issues":["相关的议题"],"Related Jobs":["相关的作业"],"Related Merge Requests":["相关的合并请求"],"Related Merged Requests":["相关已合并的合并请求"],"Showing %d event":["显示 %d 个事件"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["编码阶段概述了从第一次提交到创建合并请求的时间。创建第一个合并请求后,数据将自动添加到此处。"],"The collection of events added to the data gathered for that stage.":["与该阶段相关的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["议题阶段概述了从创建议题到将议题设置里程碑或将议题添加到议题看板的时间。开始创建议题以查看此阶段的数据。"],"The phase of the development lifecycle.":["项目生命周期中的各个阶段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["计划阶段概述了从议题添加到日程后到推送首次提交的时间。当首次推送提交后,数据将自动添加到此处。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["生产阶段概述了从创建一个议题到将代码部署到生产环境的总时间。当完成想法到部署生产的循环,数据将自动添加到此处。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["评审阶段概述了从创建合并请求到被合并的时间。当创建第一个合并请求后,数据将自动添加到此处。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["预发布阶段概述了从合并请求被合并到部署至生产环境的总时间。首次部署到生产环境后,数据将自动添加到此处。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["测试阶段概述了GitLab CI为相关合并请求运行每个流水线所需的时间。当第一个流水线运行完成后,数据将自动添加到此处。"],"The time taken by each data entry gathered by that stage.":["该阶段每条数据所花的时间"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位数是一个数列中最中间的值。例如在 3、5、9 之间,中位数是 5。在 3、5、7、8 之间,中位数是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["议题被列入日程表的时间"],"Time before an issue starts implementation":["开始进行编码前的时间"],"Time between merge request creation and merge/close":["从创建合并请求到被合并或关闭的时间"],"Time until first merge request":["创建第一个合并请求之前的时间"],"Time|hr":["小时"],"Time|min":["分钟"],"Time|s":["秒"],"Total Time":["总时间"],"Total test time for all commits/merges":["所有提交和合并的总测试时间"],"Want to see the data? Please ask an administrator for access.":["权限不足。如需查看相关数据,请向管理员申请权限。"],"We don't have enough data to show this stage.":["该阶段的数据不足,无法显示。"],"You need permission.":["您需要相关的权限。"],"day":["天"]}}};
\ No newline at end of file +var locales = locales || {}; locales['zh_CN'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (China) (https://www.transifex.com/gitlab-zh/teams/75177/zh_CN/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_CN","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_CN","domain":"app","plural_forms":"nplurals=1; plural=0;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":["作者:"],"Cancel":[""],"Commit":["提交"],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["周期分析概述了项目从想法到产品实现的各阶段所需的时间。"],"CycleAnalyticsStage|Code":["编码"],"CycleAnalyticsStage|Issue":["议题"],"CycleAnalyticsStage|Plan":["计划"],"CycleAnalyticsStage|Production":["生产"],"CycleAnalyticsStage|Review":["评审"],"CycleAnalyticsStage|Staging":["预发布"],"CycleAnalyticsStage|Test":["测试"],"Delete":[""],"Deploy":["部署"],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["从创建议题到部署至生产环境"],"From merge request merge until deploy to production":["从合并请求被合并后到部署至生产环境"],"Interval Pattern":[""],"Introducing Cycle Analytics":["周期分析简介"],"Last %d day":["最后 %d 天"],"Last Pipeline":[""],"Limited to showing %d event at most":["最多显示 %d 个事件"],"Median":["中位数"],"New Issue":["新议题"],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":["数据不足"],"Not enough data":["数据不足"],"OpenedNDaysAgo|Opened":["开始于"],"Owner":[""],"Pipeline Health":["流水线健康指标"],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":["项目生命周期"],"Read more":["了解更多"],"Related Commits":["相关的提交"],"Related Deployed Jobs":["相关的部署作业"],"Related Issues":["相关的议题"],"Related Jobs":["相关的作业"],"Related Merge Requests":["相关的合并请求"],"Related Merged Requests":["相关已合并的合并请求"],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["显示 %d 个事件"],"Target Branch":[""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["编码阶段概述了从第一次提交到创建合并请求的时间。创建第一个合并请求后,数据将自动添加到此处。"],"The collection of events added to the data gathered for that stage.":["与该阶段相关的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["议题阶段概述了从创建议题到将议题设置里程碑或将议题添加到议题看板的时间。开始创建议题以查看此阶段的数据。"],"The phase of the development lifecycle.":["项目生命周期中的各个阶段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["计划阶段概述了从议题添加到日程后到推送首次提交的时间。当首次推送提交后,数据将自动添加到此处。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["生产阶段概述了从创建一个议题到将代码部署到生产环境的总时间。当完成想法到部署生产的循环,数据将自动添加到此处。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["评审阶段概述了从创建合并请求到被合并的时间。当创建第一个合并请求后,数据将自动添加到此处。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["预发布阶段概述了从合并请求被合并到部署至生产环境的总时间。首次部署到生产环境后,数据将自动添加到此处。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["测试阶段概述了GitLab CI为相关合并请求运行每个流水线所需的时间。当第一个流水线运行完成后,数据将自动添加到此处。"],"The time taken by each data entry gathered by that stage.":["该阶段每条数据所花的时间"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位数是一个数列中最中间的值。例如在 3、5、9 之间,中位数是 5。在 3、5、7、8 之间,中位数是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["议题被列入日程表的时间"],"Time before an issue starts implementation":["开始进行编码前的时间"],"Time between merge request creation and merge/close":["从创建合并请求到被合并或关闭的时间"],"Time until first merge request":["创建第一个合并请求之前的时间"],"Time|hr":["小时"],"Time|min":["分钟"],"Time|s":["秒"],"Total Time":["总时间"],"Total test time for all commits/merges":["所有提交和合并的总测试时间"],"Want to see the data? Please ask an administrator for access.":["权限不足。如需查看相关数据,请向管理员申请权限。"],"We don't have enough data to show this stage.":["该阶段的数据不足,无法显示。"],"You need permission.":["您需要相关的权限。"],"day":["天"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/zh_HK/app.js b/app/assets/javascripts/locale/zh_HK/app.js index fd0bcd988c5..30cb1e6b89e 100644 --- a/app/assets/javascripts/locale/zh_HK/app.js +++ b/app/assets/javascripts/locale/zh_HK/app.js @@ -1 +1 @@ -var locales = locales || {}; locales['zh_HK'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (Hong Kong) (https://www.transifex.com/gitlab-zh/teams/75177/zh_HK/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_HK","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_HK","domain":"app","plural_forms":"nplurals=1; plural=0;"},"ByAuthor|by":["作者:"],"Commit":["提交"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["週期分析概述了項目從想法到產品實現的各階段所需的時間。"],"CycleAnalyticsStage|Code":["編碼"],"CycleAnalyticsStage|Issue":["議題"],"CycleAnalyticsStage|Plan":["計劃"],"CycleAnalyticsStage|Production":["生產"],"CycleAnalyticsStage|Review":["評審"],"CycleAnalyticsStage|Staging":["預發布"],"CycleAnalyticsStage|Test":["測試"],"Deploy":["部署"],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["從創建議題到部署到生產環境"],"From merge request merge until deploy to production":["從合併請求的合併到部署至生產環境"],"Introducing Cycle Analytics":["週期分析簡介"],"Last %d day":["最後 %d 天"],"Limited to showing %d event at most":["最多顯示 %d 個事件"],"Median":["中位數"],"New Issue":["新議題"],"Not available":["不可用"],"Not enough data":["數據不足"],"OpenedNDaysAgo|Opened":["開始於"],"Pipeline Health":["流水線健康指標"],"ProjectLifecycle|Stage":["項目生命週期"],"Read more":["了解更多"],"Related Commits":["相關的提交"],"Related Deployed Jobs":["相關的部署作業"],"Related Issues":["相關的議題"],"Related Jobs":["相關的作業"],"Related Merge Requests":["相關的合併請求"],"Related Merged Requests":["相關已合併的合並請求"],"Showing %d event":["顯示 %d 個事件"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["編碼階段概述了從第一次提交到創建合併請求的時間。創建第壹個合並請求後,數據將自動添加到此處。"],"The collection of events added to the data gathered for that stage.":["與該階段相關的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["議題階段概述了從創建議題到將議題設置裏程碑或將議題添加到議題看板的時間。創建一個議題後,數據將自動添加到此處。"],"The phase of the development lifecycle.":["項目生命週期中的各個階段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["計劃階段概述了從議題添加到日程後到推送首次提交的時間。當首次推送提交後,數據將自動添加到此處。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["生產階段概述了從創建議題到將代碼部署到生產環境的時間。當完成完整的想法到部署生產,數據將自動添加到此處。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["評審階段概述了從創建合並請求到合併的時間。當創建第壹個合並請求後,數據將自動添加到此處。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["預發布階段概述了合並請求的合併到部署代碼到生產環境的總時間。當首次部署到生產環境後,數據將自動添加到此處。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["測試階段概述了GitLab CI為相關合併請求運行每個流水線所需的時間。當第壹個流水線運行完成後,數據將自動添加到此處。"],"The time taken by each data entry gathered by that stage.":["該階段每條數據所花的時間"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["議題被列入日程表的時間"],"Time before an issue starts implementation":["開始進行編碼前的時間"],"Time between merge request creation and merge/close":["從創建合併請求到被合並或關閉的時間"],"Time until first merge request":["創建第壹個合併請求之前的時間"],"Time|hr":["小時"],"Time|min":["分鐘"],"Time|s":["秒"],"Total Time":["總時間"],"Total test time for all commits/merges":["所有提交和合併的總測試時間"],"Want to see the data? Please ask an administrator for access.":["權限不足。如需查看相關數據,請向管理員申請權限。"],"We don't have enough data to show this stage.":["該階段的數據不足,無法顯示。"],"You need permission.":["您需要相關的權限。"],"day":["天"]}}};
\ No newline at end of file +var locales = locales || {}; locales['zh_HK'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (Hong Kong) (https://www.transifex.com/gitlab-zh/teams/75177/zh_HK/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_HK","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_HK","domain":"app","plural_forms":"nplurals=1; plural=0;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":["作者:"],"Cancel":[""],"Commit":["提交"],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["週期分析概述了項目從想法到產品實現的各階段所需的時間。"],"CycleAnalyticsStage|Code":["編碼"],"CycleAnalyticsStage|Issue":["議題"],"CycleAnalyticsStage|Plan":["計劃"],"CycleAnalyticsStage|Production":["生產"],"CycleAnalyticsStage|Review":["評審"],"CycleAnalyticsStage|Staging":["預發布"],"CycleAnalyticsStage|Test":["測試"],"Delete":[""],"Deploy":["部署"],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["從創建議題到部署到生產環境"],"From merge request merge until deploy to production":["從合併請求的合併到部署至生產環境"],"Interval Pattern":[""],"Introducing Cycle Analytics":["週期分析簡介"],"Last %d day":["最後 %d 天"],"Last Pipeline":[""],"Limited to showing %d event at most":["最多顯示 %d 個事件"],"Median":["中位數"],"New Issue":["新議題"],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":["不可用"],"Not enough data":["數據不足"],"OpenedNDaysAgo|Opened":["開始於"],"Owner":[""],"Pipeline Health":["流水線健康指標"],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":["項目生命週期"],"Read more":["了解更多"],"Related Commits":["相關的提交"],"Related Deployed Jobs":["相關的部署作業"],"Related Issues":["相關的議題"],"Related Jobs":["相關的作業"],"Related Merge Requests":["相關的合併請求"],"Related Merged Requests":["相關已合併的合並請求"],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["顯示 %d 個事件"],"Target Branch":[""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["編碼階段概述了從第一次提交到創建合併請求的時間。創建第壹個合並請求後,數據將自動添加到此處。"],"The collection of events added to the data gathered for that stage.":["與該階段相關的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["議題階段概述了從創建議題到將議題設置裏程碑或將議題添加到議題看板的時間。創建一個議題後,數據將自動添加到此處。"],"The phase of the development lifecycle.":["項目生命週期中的各個階段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["計劃階段概述了從議題添加到日程後到推送首次提交的時間。當首次推送提交後,數據將自動添加到此處。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["生產階段概述了從創建議題到將代碼部署到生產環境的時間。當完成完整的想法到部署生產,數據將自動添加到此處。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["評審階段概述了從創建合並請求到合併的時間。當創建第壹個合並請求後,數據將自動添加到此處。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["預發布階段概述了合並請求的合併到部署代碼到生產環境的總時間。當首次部署到生產環境後,數據將自動添加到此處。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["測試階段概述了GitLab CI為相關合併請求運行每個流水線所需的時間。當第壹個流水線運行完成後,數據將自動添加到此處。"],"The time taken by each data entry gathered by that stage.":["該階段每條數據所花的時間"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["議題被列入日程表的時間"],"Time before an issue starts implementation":["開始進行編碼前的時間"],"Time between merge request creation and merge/close":["從創建合併請求到被合並或關閉的時間"],"Time until first merge request":["創建第壹個合併請求之前的時間"],"Time|hr":["小時"],"Time|min":["分鐘"],"Time|s":["秒"],"Total Time":["總時間"],"Total test time for all commits/merges":["所有提交和合併的總測試時間"],"Want to see the data? Please ask an administrator for access.":["權限不足。如需查看相關數據,請向管理員申請權限。"],"We don't have enough data to show this stage.":["該階段的數據不足,無法顯示。"],"You need permission.":["您需要相關的權限。"],"day":["天"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/zh_TW/app.js b/app/assets/javascripts/locale/zh_TW/app.js index 79904d17bf6..f0fe1e31f18 100644 --- a/app/assets/javascripts/locale/zh_TW/app.js +++ b/app/assets/javascripts/locale/zh_TW/app.js @@ -1 +1 @@ -var locales = locales || {}; locales['zh_TW'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (Taiwan) (https://www.transifex.com/gitlab-zh/teams/75177/zh_TW/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_TW","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_TW","domain":"app","plural_forms":"nplurals=1; plural=0;"},"ByAuthor|by":["作者:"],"Commit":["送交"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["週期分析概述了你的專案從想法到產品實現,各階段所需的時間。"],"CycleAnalyticsStage|Code":["程式開發"],"CycleAnalyticsStage|Issue":["議題"],"CycleAnalyticsStage|Plan":["計劃"],"CycleAnalyticsStage|Production":["上線"],"CycleAnalyticsStage|Review":["複閱"],"CycleAnalyticsStage|Staging":["預備"],"CycleAnalyticsStage|Test":["測試"],"Deploy":["部署"],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["從議題建立至線上部署"],"From merge request merge until deploy to production":["從請求被合併後至線上部署"],"Introducing Cycle Analytics":["週期分析簡介"],"Last %d day":["最後 %d 天"],"Limited to showing %d event at most":["最多顯示 %d 個事件"],"Median":["中位數"],"New Issue":["新議題"],"Not available":["無法使用"],"Not enough data":["資料不足"],"OpenedNDaysAgo|Opened":["開始於"],"Pipeline Health":["流水線健康指標"],"ProjectLifecycle|Stage":["專案生命週期"],"Read more":["了解更多"],"Related Commits":["相關的送交"],"Related Deployed Jobs":["相關的部署作業"],"Related Issues":["相關的議題"],"Related Jobs":["相關的作業"],"Related Merge Requests":["相關的合併請求"],"Related Merged Requests":["相關已合併的請求"],"Showing %d event":["顯示 %d 個事件"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["程式開發階段顯示從第一次送交到建立合併請求的時間。建立第一個合併請求後,資料將自動填入。"],"The collection of events added to the data gathered for that stage.":["與該階段相關的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["議題階段顯示從議題建立到設置里程碑、或將該議題加至議題看板的時間。建立第一個議題後,資料將自動填入。"],"The phase of the development lifecycle.":["專案開發生命週期的各個階段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["計劃階段顯示從議題添加到日程後至推送第一個送交的時間。當第一次推送送交後,資料將自動填入。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["上線階段顯示從建立一個議題到部署程式至線上的總時間。當完成從想法到產品實現的循環後,資料將自動填入。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["複閱階段顯示從合併請求建立後至被合併的時間。當建立第一個合併請求後,資料將自動填入。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["預備階段顯示從合併請求被合併後至部署上線的時間。當第一次部署上線後,資料將自動填入。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["測試階段顯示相關合併請求的流水線所花的時間。當第一個流水線運作完畢後,資料將自動填入。"],"The time taken by each data entry gathered by that stage.":["每筆該階段相關資料所花的時間。"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["議題被列入日程表的時間"],"Time before an issue starts implementation":["議題等待開始實作的時間"],"Time between merge request creation and merge/close":["合併請求被合併或是關閉的時間"],"Time until first merge request":["第一個合併請求被建立前的時間"],"Time|hr":["小時"],"Time|min":["分鐘"],"Time|s":["秒"],"Total Time":["總時間"],"Total test time for all commits/merges":["所有送交和合併的總測試時間"],"Want to see the data? Please ask an administrator for access.":["權限不足。如需查看相關資料,請向管理員申請權限。"],"We don't have enough data to show this stage.":["因該階段的資料不足而無法顯示相關資訊"],"You need permission.":["您需要相關的權限。"],"day":["天"]}}};
\ No newline at end of file +var locales = locales || {}; locales['zh_TW'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (Taiwan) (https://www.transifex.com/gitlab-zh/teams/75177/zh_TW/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_TW","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_TW","domain":"app","plural_forms":"nplurals=1; plural=0;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":["作者:"],"Cancel":[""],"Commit":["送交"],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["週期分析概述了你的專案從想法到產品實現,各階段所需的時間。"],"CycleAnalyticsStage|Code":["程式開發"],"CycleAnalyticsStage|Issue":["議題"],"CycleAnalyticsStage|Plan":["計劃"],"CycleAnalyticsStage|Production":["上線"],"CycleAnalyticsStage|Review":["複閱"],"CycleAnalyticsStage|Staging":["預備"],"CycleAnalyticsStage|Test":["測試"],"Delete":[""],"Deploy":["部署"],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["從議題建立至線上部署"],"From merge request merge until deploy to production":["從請求被合併後至線上部署"],"Interval Pattern":[""],"Introducing Cycle Analytics":["週期分析簡介"],"Last %d day":["最後 %d 天"],"Last Pipeline":[""],"Limited to showing %d event at most":["最多顯示 %d 個事件"],"Median":["中位數"],"New Issue":["新議題"],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":["無法使用"],"Not enough data":["資料不足"],"OpenedNDaysAgo|Opened":["開始於"],"Owner":[""],"Pipeline Health":["流水線健康指標"],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":["專案生命週期"],"Read more":["了解更多"],"Related Commits":["相關的送交"],"Related Deployed Jobs":["相關的部署作業"],"Related Issues":["相關的議題"],"Related Jobs":["相關的作業"],"Related Merge Requests":["相關的合併請求"],"Related Merged Requests":["相關已合併的請求"],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["顯示 %d 個事件"],"Target Branch":[""],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["程式開發階段顯示從第一次送交到建立合併請求的時間。建立第一個合併請求後,資料將自動填入。"],"The collection of events added to the data gathered for that stage.":["與該階段相關的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["議題階段顯示從議題建立到設置里程碑、或將該議題加至議題看板的時間。建立第一個議題後,資料將自動填入。"],"The phase of the development lifecycle.":["專案開發生命週期的各個階段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["計劃階段顯示從議題添加到日程後至推送第一個送交的時間。當第一次推送送交後,資料將自動填入。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["上線階段顯示從建立一個議題到部署程式至線上的總時間。當完成從想法到產品實現的循環後,資料將自動填入。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["複閱階段顯示從合併請求建立後至被合併的時間。當建立第一個合併請求後,資料將自動填入。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["預備階段顯示從合併請求被合併後至部署上線的時間。當第一次部署上線後,資料將自動填入。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["測試階段顯示相關合併請求的流水線所花的時間。當第一個流水線運作完畢後,資料將自動填入。"],"The time taken by each data entry gathered by that stage.":["每筆該階段相關資料所花的時間。"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["議題被列入日程表的時間"],"Time before an issue starts implementation":["議題等待開始實作的時間"],"Time between merge request creation and merge/close":["合併請求被合併或是關閉的時間"],"Time until first merge request":["第一個合併請求被建立前的時間"],"Time|hr":["小時"],"Time|min":["分鐘"],"Time|s":["秒"],"Total Time":["總時間"],"Total test time for all commits/merges":["所有送交和合併的總測試時間"],"Want to see the data? Please ask an administrator for access.":["權限不足。如需查看相關資料,請向管理員申請權限。"],"We don't have enough data to show this stage.":["因該階段的資料不足而無法顯示相關資訊"],"You need permission.":["您需要相關的權限。"],"day":["天"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index fe367d0c42a..ed7629948ca 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -39,10 +39,6 @@ import './shortcuts_network'; // behaviors import './behaviors/'; -// blob -import './blob/create_branch_dropdown'; -import './blob/target_branch_dropdown'; - // templates import './templates/issuable_template_selector'; import './templates/issuable_template_selectors'; diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 841b24a60a3..07ede5ee913 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -65,14 +65,18 @@ }; Milestone.successCallback = function(data, element) { - var img_tag; - if (data.assignee) { - img_tag = $('<img/>'); - img_tag.attr('src', data.assignee.avatar_url); - img_tag.addClass('avatar s16'); - $(element).find('.assignee-icon img').replaceWith(img_tag); - } else { - $(element).find('.assignee-icon').empty(); + const $avatarContainer = $(element).find('.assignee-icon'); + $avatarContainer.empty(); + + if (data.assignees && data.assignees.length > 0) { + const $avatars = data.assignees.map((assignee) => { + const img_tag = $('<img/>'); + img_tag.attr('src', assignee.avatar_url); + img_tag.addClass('avatar s16'); + return img_tag; + }); + + $avatarContainer.append($avatars); } }; @@ -161,9 +165,9 @@ data = (function() { switch (newState) { case 'ongoing': - return opts.fieldName + '[assignee_id]=' + gon.current_user_id; + return `${opts.fieldName}[assignee_ids][]=${gon.current_user_id}`; case 'unassigned': - return opts.fieldName + '[assignee_id]='; + return `${opts.fieldName}[assignee_ids][]=0`; case 'closed': return opts.fieldName + '[state_event]=close'; } diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js index 658879607e2..04073ef7270 100644 --- a/app/assets/javascripts/new_commit_form.js +++ b/app/assets/javascripts/new_commit_form.js @@ -1,23 +1,20 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-return-assign, max-len */ (function() { this.NewCommitForm = (function() { - function NewCommitForm(form, targetBranchName = 'target_branch') { + function NewCommitForm(form) { this.form = form; - this.targetBranchName = targetBranchName; this.renderDestination = this.renderDestination.bind(this); - this.targetBranchDropdown = form.find('button.js-target-branch'); + this.branchName = form.find('.js-branch-name'); this.originalBranch = form.find('.js-original-branch'); this.createMergeRequest = form.find('.js-create-merge-request'); this.createMergeRequestContainer = form.find('.js-create-merge-request-container'); - this.targetBranchDropdown.on('change.branch', this.renderDestination); + this.branchName.keyup(this.renderDestination); this.renderDestination(); } NewCommitForm.prototype.renderDestination = function() { var different; - var targetBranch = this.form.find(`input[name="${this.targetBranchName}"]`); - - different = targetBranch.val() !== this.originalBranch.val(); + different = this.branchName.val() !== this.originalBranch.val(); if (different) { this.createMergeRequestContainer.show(); if (!this.wasDifferent) { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 929965de5c1..d56cf959486 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -56,6 +56,7 @@ const normalizeNewlines = function(str) { this.toggleCommitList = this.toggleCommitList.bind(this); this.postComment = this.postComment.bind(this); this.clearFlashWrapper = this.clearFlash.bind(this); + this.onHashChange = this.onHashChange.bind(this); this.notes_url = notes_url; this.note_ids = note_ids; @@ -127,7 +128,9 @@ const normalizeNewlines = function(str) { $(document).on('ajax:success', '.js-main-target-form', this.resetMainTargetForm); $(document).on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton); // when a key is clicked on the notes - return $(document).on('keydown', '.js-note-text', this.keydownNoteText); + $(document).on('keydown', '.js-note-text', this.keydownNoteText); + // When the URL fragment/hash has changed, `#note_xxx` + return $(window).on('hashchange', this.onHashChange); }; Notes.prototype.cleanBinding = function() { @@ -148,6 +151,7 @@ const normalizeNewlines = function(str) { $(document).off('ajax:success', '.js-main-target-form'); $(document).off('ajax:success', '.js-discussion-note-form'); $(document).off('ajax:complete', '.js-main-target-form'); + $(window).off('hashchange', this.onHashChange); }; Notes.initCommentTypeToggle = function (form) { @@ -298,8 +302,27 @@ const normalizeNewlines = function(str) { Notes.prototype.setupNewNote = function($note) { // Update datetime format on the recent note gl.utils.localTimeAgo($note.find('.js-timeago'), false); + this.collapseLongCommitList(); this.taskList.init(); + + // This stops the note highlight, #note_xxx`, from being removed after real time update + // The `:target` selector does not re-evaluate after we replace element in the DOM + Notes.updateNoteTargetSelector($note); + this.$noteToCleanHighlight = $note; + }; + + Notes.prototype.onHashChange = function() { + if (this.$noteToCleanHighlight) { + Notes.updateNoteTargetSelector(this.$noteToCleanHighlight); + } + + this.$noteToCleanHighlight = null; + }; + + Notes.updateNoteTargetSelector = function($note) { + const hash = gl.utils.getLocationHash(); + $note.toggleClass('target', hash && $note.filter(`#${hash}`).length > 0); }; /* @@ -597,13 +620,12 @@ const normalizeNewlines = function(str) { $noteEntityEl = $(noteEntity.html); $noteEntityEl.addClass('fade-in-full'); this.revertNoteEditForm($targetNote); - gl.utils.localTimeAgo($('.js-timeago', $noteEntityEl)); $noteEntityEl.renderGFM(); - $noteEntityEl.find('.js-task-list-container').taskList('enable'); // Find the note's `li` element by ID and replace it with the updated HTML $note_li = $('.note-row-' + noteEntity.id); $note_li.replaceWith($noteEntityEl); + this.setupNewNote($noteEntityEl); if (typeof gl.diffNotesCompileComponents !== 'undefined') { gl.diffNotesCompileComponents(); @@ -1060,7 +1082,7 @@ const normalizeNewlines = function(str) { var targetId = $originalContentEl.data('target-id'); var targetType = $originalContentEl.data('target-type'); - new gl.GLForm($editForm.find('form')); + new gl.GLForm($editForm.find('form'), this.enableGFM); $editForm.find('form') .attr('action', postUrl) @@ -1478,7 +1500,7 @@ const normalizeNewlines = function(str) { const cachedNoteBodyText = $noteBodyText.html(); // Show updated comment content temporarily - $noteBodyText.html(formContent); + $noteBodyText.html(_.escape(formContent)); $editingNote.removeClass('is-editing fade-in-full').addClass('being-posted fade-in-half'); $editingNote.find('.note-headline-meta a').html('<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>'); @@ -1491,7 +1513,7 @@ const normalizeNewlines = function(str) { }) .fail(() => { // Submission failed, revert back to original note - $noteBodyText.html(cachedNoteBodyText); + $noteBodyText.html(_.escape(cachedNoteBodyText)); $editingNote.removeClass('being-posted fade-in'); $editingNote.find('.fa.fa-spinner').remove(); diff --git a/app/assets/javascripts/peek.js b/app/assets/javascripts/peek.js new file mode 100644 index 00000000000..de1a99fa3bd --- /dev/null +++ b/app/assets/javascripts/peek.js @@ -0,0 +1,16 @@ +import 'vendor/peek'; +import 'vendor/peek.performance_bar'; + +$(document).on('click', '#peek-show-queries', (e) => { + e.preventDefault(); + $('.peek-rblineprof-modal').hide(); + const $modal = $('#modal-peek-pg-queries'); + if ($modal.length) { + $modal.modal('toggle'); + } +}); + +$(document).on('click', '.js-lineprof-file', (e) => { + e.preventDefault(); + $(e.target).parents('.peek-rblineprof-file').find('.data').toggle(); +}); diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index 4f6c5c177cf..2a1ecac3707 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -91,7 +91,7 @@ export default { @actionClicked="postAction" /> <loading-icon - v-else + v-if="isLoading" size="2"/> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/nav_controls.js b/app/assets/javascripts/pipelines/components/nav_controls.js deleted file mode 100644 index 6aa10531034..00000000000 --- a/app/assets/javascripts/pipelines/components/nav_controls.js +++ /dev/null @@ -1,52 +0,0 @@ -export default { - props: { - newPipelinePath: { - type: String, - required: true, - }, - - hasCiEnabled: { - type: Boolean, - required: true, - }, - - helpPagePath: { - type: String, - required: true, - }, - - ciLintPath: { - type: String, - required: true, - }, - - canCreatePipeline: { - type: Boolean, - required: true, - }, - }, - - template: ` - <div class="nav-controls"> - <a - v-if="canCreatePipeline" - :href="newPipelinePath" - class="btn btn-create"> - Run Pipeline - </a> - - <a - v-if="!hasCiEnabled" - :href="helpPagePath" - class="btn btn-info"> - Get started with Pipelines - </a> - - <a - :href="ciLintPath" - class="btn btn-default"> - CI Lint - </a> - </div> - `, -}; diff --git a/app/assets/javascripts/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue new file mode 100644 index 00000000000..632fc167f2b --- /dev/null +++ b/app/assets/javascripts/pipelines/components/nav_controls.vue @@ -0,0 +1,54 @@ +<script> +export default { + name: 'PipelineNavControls', + props: { + newPipelinePath: { + type: String, + required: true, + }, + + hasCiEnabled: { + type: Boolean, + required: true, + }, + + helpPagePath: { + type: String, + required: true, + }, + + ciLintPath: { + type: String, + required: true, + }, + + canCreatePipeline: { + type: Boolean, + required: true, + }, + }, +}; +</script> +<template> + <div class="nav-controls"> + <a + v-if="canCreatePipeline" + :href="newPipelinePath" + class="btn btn-create"> + Run Pipeline + </a> + + <a + v-if="!hasCiEnabled" + :href="helpPagePath" + class="btn btn-info"> + Get started with Pipelines + </a> + + <a + :href="ciLintPath" + class="btn btn-default"> + CI Lint + </a> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/navigation_tabs.js b/app/assets/javascripts/pipelines/components/navigation_tabs.js deleted file mode 100644 index 1626ae17a30..00000000000 --- a/app/assets/javascripts/pipelines/components/navigation_tabs.js +++ /dev/null @@ -1,72 +0,0 @@ -export default { - props: { - scope: { - type: String, - required: true, - }, - - count: { - type: Object, - required: true, - }, - - paths: { - type: Object, - required: true, - }, - }, - - mounted() { - $(document).trigger('init.scrolling-tabs'); - }, - - template: ` - <ul class="nav-links scrolling-tabs"> - <li - class="js-pipelines-tab-all" - :class="{ 'active': scope === 'all'}"> - <a :href="paths.allPath"> - All - <span class="badge js-totalbuilds-count"> - {{count.all}} - </span> - </a> - </li> - <li class="js-pipelines-tab-pending" - :class="{ 'active': scope === 'pending'}"> - <a :href="paths.pendingPath"> - Pending - <span class="badge"> - {{count.pending}} - </span> - </a> - </li> - <li class="js-pipelines-tab-running" - :class="{ 'active': scope === 'running'}"> - <a :href="paths.runningPath"> - Running - <span class="badge"> - {{count.running}} - </span> - </a> - </li> - <li class="js-pipelines-tab-finished" - :class="{ 'active': scope === 'finished'}"> - <a :href="paths.finishedPath"> - Finished - <span class="badge"> - {{count.finished}} - </span> - </a> - </li> - <li class="js-pipelines-tab-branches" - :class="{ 'active': scope === 'branches'}"> - <a :href="paths.branchesPath">Branches</a> - </li> - <li class="js-pipelines-tab-tags" - :class="{ 'active': scope === 'tags'}"> - <a :href="paths.tagsPath">Tags</a> - </li> - </ul> - `, -}; diff --git a/app/assets/javascripts/pipelines/components/navigation_tabs.vue b/app/assets/javascripts/pipelines/components/navigation_tabs.vue new file mode 100644 index 00000000000..d2f6d47f043 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/navigation_tabs.vue @@ -0,0 +1,76 @@ +<script> +export default { + name: 'PipelineNavigationTabs', + props: { + scope: { + type: String, + required: true, + }, + count: { + type: Object, + required: true, + }, + paths: { + type: Object, + required: true, + }, + }, + mounted() { + $(document).trigger('init.scrolling-tabs'); + }, +}; +</script> +<template> + <ul class="nav-links scrolling-tabs"> + <li + class="js-pipelines-tab-all" + :class="{ active: scope === 'all'}"> + <a :href="paths.allPath"> + All + <span class="badge js-totalbuilds-count"> + {{count.all}} + </span> + </a> + </li> + <li + class="js-pipelines-tab-pending" + :class="{ active: scope === 'pending'}"> + <a :href="paths.pendingPath"> + Pending + <span class="badge"> + {{count.pending}} + </span> + </a> + </li> + <li + class="js-pipelines-tab-running" + :class="{ active: scope === 'running'}"> + <a :href="paths.runningPath"> + Running + <span class="badge"> + {{count.running}} + </span> + </a> + </li> + <li + class="js-pipelines-tab-finished" + :class="{ active: scope === 'finished'}"> + <a :href="paths.finishedPath"> + Finished + <span class="badge"> + {{count.finished}} + </span> + </a> + </li> + <li + class="js-pipelines-tab-branches" + :class="{ active: scope === 'branches'}"> + <a :href="paths.branchesPath">Branches</a> + </li> + <li + class="js-pipelines-tab-tags" + :class="{ active: scope === 'tags'}"> + <a :href="paths.tagsPath">Tags</a> + </li> + </ul> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index 4781a8ff1da..8333ec0fbc3 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -23,7 +23,7 @@ export default { }; </script> <template> - <td> + <div class="table-section section-15 hidden-xs hidden-sm"> <a :href="pipeline.path" class="js-pipeline-url-link"> @@ -42,24 +42,26 @@ export default { class="js-pipeline-url-api api"> API </span> - <span - v-if="pipeline.flags.latest" - class="js-pipeline-url-lastest label label-success" - title="Latest pipeline for this branch" - ref="tooltip"> - latest - </span> - <span - v-if="pipeline.flags.yaml_errors" - class="js-pipeline-url-yaml label label-danger" - :title="pipeline.yaml_errors" - ref="tooltip"> - yaml invalid - </span> - <span - v-if="pipeline.flags.stuck" - class="js-pipeline-url-stuck label label-warning"> - stuck - </span> - </td> + <div class="label-container"> + <span + v-if="pipeline.flags.latest" + class="js-pipeline-url-latest label label-success" + title="Latest pipeline for this branch" + ref="tooltip"> + latest + </span> + <span + v-if="pipeline.flags.yaml_errors" + class="js-pipeline-url-yaml label label-danger" + :title="pipeline.yaml_errors" + ref="tooltip"> + yaml invalid + </span> + <span + v-if="pipeline.flags.stuck" + class="js-pipeline-url-stuck label label-warning"> + stuck + </span> + </div> + </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue new file mode 100644 index 00000000000..fed42d23112 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -0,0 +1,289 @@ +<script> + import Visibility from 'visibilityjs'; + import PipelinesService from '../services/pipelines_service'; + import eventHub from '../event_hub'; + import pipelinesTableComponent from '../../vue_shared/components/pipelines_table.vue'; + import tablePagination from '../../vue_shared/components/table_pagination.vue'; + import emptyState from './empty_state.vue'; + import errorState from './error_state.vue'; + import navigationTabs from './navigation_tabs.vue'; + import navigationControls from './nav_controls.vue'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import Poll from '../../lib/utils/poll'; + + export default { + props: { + store: { + type: Object, + required: true, + }, + }, + components: { + tablePagination, + pipelinesTableComponent, + emptyState, + errorState, + navigationTabs, + navigationControls, + loadingIcon, + }, + data() { + const pipelinesData = document.querySelector('#pipelines-list-vue').dataset; + + return { + endpoint: pipelinesData.endpoint, + cssClass: pipelinesData.cssClass, + helpPagePath: pipelinesData.helpPagePath, + newPipelinePath: pipelinesData.newPipelinePath, + canCreatePipeline: pipelinesData.canCreatePipeline, + allPath: pipelinesData.allPath, + pendingPath: pipelinesData.pendingPath, + runningPath: pipelinesData.runningPath, + finishedPath: pipelinesData.finishedPath, + branchesPath: pipelinesData.branchesPath, + tagsPath: pipelinesData.tagsPath, + hasCi: pipelinesData.hasCi, + ciLintPath: pipelinesData.ciLintPath, + state: this.store.state, + apiScope: 'all', + pagenum: 1, + isLoading: false, + hasError: false, + isMakingRequest: false, + updateGraphDropdown: false, + hasMadeRequest: false, + }; + }, + computed: { + canCreatePipelineParsed() { + return gl.utils.convertPermissionToBoolean(this.canCreatePipeline); + }, + scope() { + const scope = gl.utils.getParameterByName('scope'); + return scope === null ? 'all' : scope; + }, + shouldRenderErrorState() { + return this.hasError && !this.isLoading; + }, + + /** + * The empty state should only be rendered when the request is made to fetch all pipelines + * and none is returned. + * + * @return {Boolean} + */ + shouldRenderEmptyState() { + return !this.isLoading && + !this.hasError && + this.hasMadeRequest && + !this.state.pipelines.length && + (this.scope === 'all' || this.scope === null); + }, + /** + * When a specific scope does not have pipelines we render a message. + * + * @return {Boolean} + */ + shouldRenderNoPipelinesMessage() { + return !this.isLoading && + !this.hasError && + !this.state.pipelines.length && + this.scope !== 'all' && + this.scope !== null; + }, + + shouldRenderTable() { + return !this.hasError && + !this.isLoading && this.state.pipelines.length; + }, + /** + * Pagination should only be rendered when there is more than one page. + * + * @return {Boolean} + */ + shouldRenderPagination() { + return !this.isLoading && + this.state.pipelines.length && + this.state.pageInfo.total > this.state.pageInfo.perPage; + }, + + hasCiEnabled() { + return this.hasCi !== undefined; + }, + paths() { + return { + allPath: this.allPath, + pendingPath: this.pendingPath, + finishedPath: this.finishedPath, + runningPath: this.runningPath, + branchesPath: this.branchesPath, + tagsPath: this.tagsPath, + }; + }, + pageParameter() { + return gl.utils.getParameterByName('page') || this.pagenum; + }, + scopeParameter() { + return gl.utils.getParameterByName('scope') || this.apiScope; + }, + }, + created() { + this.service = new PipelinesService(this.endpoint); + + const poll = new Poll({ + resource: this.service, + method: 'getPipelines', + data: { page: this.pageParameter, scope: this.scopeParameter }, + successCallback: this.successCallback, + errorCallback: this.errorCallback, + notificationCallback: this.setIsMakingRequest, + }); + + if (!Visibility.hidden()) { + this.isLoading = true; + poll.makeRequest(); + } else { + // If tab is not visible we need to make the first request so we don't show the empty + // state without knowing if there are any pipelines + this.fetchPipelines(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + poll.restart(); + } else { + poll.stop(); + } + }); + + eventHub.$on('refreshPipelines', this.fetchPipelines); + }, + beforeDestroy() { + eventHub.$off('refreshPipelines'); + }, + methods: { + /** + * Will change the page number and update the URL. + * + * @param {Number} pageNumber desired page to go to. + */ + change(pageNumber) { + const param = gl.utils.setParamInURL('page', pageNumber); + + gl.utils.visitUrl(param); + return param; + }, + + fetchPipelines() { + if (!this.isMakingRequest) { + this.isLoading = true; + + this.service.getPipelines({ scope: this.scopeParameter, page: this.pageParameter }) + .then(response => this.successCallback(response)) + .catch(() => this.errorCallback()); + } + }, + successCallback(resp) { + const response = { + headers: resp.headers, + body: resp.json(), + }; + + this.store.storeCount(response.body.count); + this.store.storePipelines(response.body.pipelines); + this.store.storePagination(response.headers); + + this.isLoading = false; + this.updateGraphDropdown = true; + this.hasMadeRequest = true; + }, + + errorCallback() { + this.hasError = true; + this.isLoading = false; + this.updateGraphDropdown = false; + }, + + setIsMakingRequest(isMakingRequest) { + this.isMakingRequest = isMakingRequest; + + if (isMakingRequest) { + this.updateGraphDropdown = false; + } + }, + }, + }; +</script> +<template> + <div :class="cssClass"> + + <div + class="top-area scrolling-tabs-container inner-page-scroll-tabs" + v-if="!isLoading && !shouldRenderEmptyState"> + <div class="fade-left"> + <i + class="fa fa-angle-left" + aria-hidden="true"> + </i> + </div> + <div class="fade-right"> + <i + class="fa fa-angle-right" + aria-hidden="true"> + </i> + </div> + <navigation-tabs + :scope="scope" + :count="state.count" + :paths="paths" + /> + + <navigation-controls + :new-pipeline-path="newPipelinePath" + :has-ci-enabled="hasCiEnabled" + :help-page-path="helpPagePath" + :ciLintPath="ciLintPath" + :can-create-pipeline="canCreatePipelineParsed " + /> + </div> + + <div class="content-list pipelines"> + + <loading-icon + label="Loading Pipelines" + size="3" + v-if="isLoading" + /> + + <empty-state + v-if="shouldRenderEmptyState" + :help-page-path="helpPagePath" + /> + + <error-state v-if="shouldRenderErrorState" /> + + <div + class="blank-state blank-state-no-icon" + v-if="shouldRenderNoPipelinesMessage"> + <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2> + </div> + + <div + class="table-holder" + v-if="shouldRenderTable"> + + <pipelines-table-component + :pipelines="state.pipelines" + :service="service" + :update-graph-dropdown="updateGraphDropdown" + /> + </div> + + <table-pagination + v-if="shouldRenderPagination" + :change="change" + :pageInfo="state.pageInfo" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.js b/app/assets/javascripts/pipelines/components/pipelines_actions.js deleted file mode 100644 index b9e066c5db1..00000000000 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.js +++ /dev/null @@ -1,91 +0,0 @@ -/* eslint-disable no-new */ -/* global Flash */ -import '~/flash'; -import playIconSvg from 'icons/_icon_play.svg'; -import eventHub from '../event_hub'; -import loadingIconComponent from '../../vue_shared/components/loading_icon.vue'; - -export default { - props: { - actions: { - type: Array, - required: true, - }, - - service: { - type: Object, - required: true, - }, - }, - - components: { - loadingIconComponent, - }, - - data() { - return { - playIconSvg, - isLoading: false, - }; - }, - - methods: { - onClickAction(endpoint) { - this.isLoading = true; - - $(this.$refs.tooltip).tooltip('destroy'); - - this.service.postAction(endpoint) - .then(() => { - this.isLoading = false; - eventHub.$emit('refreshPipelines'); - }) - .catch(() => { - this.isLoading = false; - new Flash('An error occured while making the request.'); - }); - }, - - isActionDisabled(action) { - if (action.playable === undefined) { - return false; - } - - return !action.playable; - }, - }, - - template: ` - <div class="btn-group" v-if="actions"> - <button - type="button" - class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions" - title="Manual job" - data-toggle="dropdown" - data-placement="top" - aria-label="Manual job" - ref="tooltip" - :disabled="isLoading"> - ${playIconSvg} - <i - class="fa fa-caret-down" - aria-hidden="true" /> - <loading-icon v-if="isLoading" /> - </button> - - <ul class="dropdown-menu dropdown-menu-align-right"> - <li v-for="action in actions"> - <button - type="button" - class="js-pipeline-action-link no-btn btn" - @click="onClickAction(action.path)" - :class="{ 'disabled': isActionDisabled(action) }" - :disabled="isActionDisabled(action)"> - ${playIconSvg} - <span>{{action.name}}</span> - </button> - </li> - </ul> - </div> - `, -}; diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue new file mode 100644 index 00000000000..97b4de26214 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -0,0 +1,88 @@ +<script> + /* global Flash */ + import '~/flash'; + import playIconSvg from 'icons/_icon_play.svg'; + import eventHub from '../event_hub'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + + export default { + props: { + actions: { + type: Array, + required: true, + }, + service: { + type: Object, + required: true, + }, + }, + components: { + loadingIcon, + }, + data() { + return { + playIconSvg, + isLoading: false, + }; + }, + methods: { + onClickAction(endpoint) { + this.isLoading = true; + + $(this.$refs.tooltip).tooltip('destroy'); + + this.service.postAction(endpoint) + .then(() => { + this.isLoading = false; + eventHub.$emit('refreshPipelines'); + }) + .catch(() => { + this.isLoading = false; + // eslint-disable-next-line no-new + new Flash('An error occured while making the request.'); + }); + }, + isActionDisabled(action) { + if (action.playable === undefined) { + return false; + } + + return !action.playable; + }, + }, + }; +</script> +<template> + <div class="btn-group"> + <button + type="button" + class="dropdown-new btn btn-default has-tooltip js-pipeline-dropdown-manual-actions" + title="Manual job" + data-toggle="dropdown" + data-placement="top" + aria-label="Manual job" + ref="tooltip" + :disabled="isLoading"> + <span v-html="playIconSvg"></span> + <i + class="fa fa-caret-down" + aria-hidden="true"> + </i> + <loading-icon v-if="isLoading" /> + </button> + + <ul class="dropdown-menu dropdown-menu-align-right"> + <li v-for="action in actions"> + <button + type="button" + class="js-pipeline-action-link no-btn btn" + @click="onClickAction(action.path)" + :class="{ disabled: isActionDisabled(action) }" + :disabled="isActionDisabled(action)"> + <span v-html="playIconSvg"></span> + <span>{{action.name}}</span> + </button> + </li> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.js b/app/assets/javascripts/pipelines/components/pipelines_artifacts.js deleted file mode 100644 index f18e2dfadaf..00000000000 --- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.js +++ /dev/null @@ -1,33 +0,0 @@ -export default { - props: { - artifacts: { - type: Array, - required: true, - }, - }, - - template: ` - <div class="btn-group" role="group"> - <button - class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download" - title="Artifacts" - data-placement="top" - data-toggle="dropdown" - aria-label="Artifacts"> - <i class="fa fa-download" aria-hidden="true"></i> - <i class="fa fa-caret-down" aria-hidden="true"></i> - </button> - <ul class="dropdown-menu dropdown-menu-align-right"> - <li v-for="artifact in artifacts"> - <a - rel="nofollow" - download - :href="artifact.path"> - <i class="fa fa-download" aria-hidden="true"></i> - <span>Download {{artifact.name}} artifacts</span> - </a> - </li> - </ul> - </div> - `, -}; diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue new file mode 100644 index 00000000000..b4520481cdc --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue @@ -0,0 +1,51 @@ +<script> + import tooltipMixin from '../../vue_shared/mixins/tooltip'; + + export default { + props: { + artifacts: { + type: Array, + required: true, + }, + }, + mixins: [ + tooltipMixin, + ], + }; +</script> +<template> + <div + class="btn-group" + role="group"> + <button + class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download" + title="Artifacts" + data-placement="top" + data-toggle="dropdown" + aria-label="Artifacts" + ref="tooltip"> + <i + class="fa fa-download" + aria-hidden="true"> + </i> + <i + class="fa fa-caret-down" + aria-hidden="true"> + </i> + </button> + <ul class="dropdown-menu dropdown-menu-align-right"> + <li v-for="artifact in artifacts"> + <a + rel="nofollow" + download + :href="artifact.path"> + <i + class="fa fa-download" + aria-hidden="true"> + </i> + <span>Download {{artifact.name}} artifacts</span> + </a> + </li> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index 7fc19fce1ff..c05c76c9a64 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -16,6 +16,7 @@ /* global Flash */ import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import tooltipMixin from '../../vue_shared/mixins/tooltip'; export default { props: { @@ -31,6 +32,10 @@ export default { }, }, + mixins: [ + tooltipMixin, + ], + data() { return { isLoading: false, @@ -127,9 +132,10 @@ export default { <template> <div class="dropdown"> <button + ref="tooltip" :class="triggerButtonClass" @click="onClickStage" - class="mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button" + class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button" :title="stage.title" data-placement="top" data-toggle="dropdown" diff --git a/app/assets/javascripts/pipelines/components/time_ago.js b/app/assets/javascripts/pipelines/components/time_ago.js deleted file mode 100644 index 188f74cc705..00000000000 --- a/app/assets/javascripts/pipelines/components/time_ago.js +++ /dev/null @@ -1,98 +0,0 @@ -import iconTimerSvg from 'icons/_icon_timer.svg'; -import '../../lib/utils/datetime_utility'; - -export default { - props: { - finishedTime: { - type: String, - required: true, - }, - - duration: { - type: Number, - required: true, - }, - }, - - data() { - return { - iconTimerSvg, - }; - }, - - updated() { - $(this.$refs.tooltip).tooltip('fixTitle'); - }, - - computed: { - hasDuration() { - return this.duration > 0; - }, - - hasFinishedTime() { - return this.finishedTime !== ''; - }, - - localTimeFinished() { - return gl.utils.formatDate(this.finishedTime); - }, - - durationFormated() { - const date = new Date(this.duration * 1000); - - let hh = date.getUTCHours(); - let mm = date.getUTCMinutes(); - let ss = date.getSeconds(); - - // left pad - if (hh < 10) { - hh = `0${hh}`; - } - if (mm < 10) { - mm = `0${mm}`; - } - if (ss < 10) { - ss = `0${ss}`; - } - - return `${hh}:${mm}:${ss}`; - }, - - finishedTimeFormated() { - const timeAgo = gl.utils.getTimeago(); - - return timeAgo.format(this.finishedTime); - }, - }, - - template: ` - <td class="pipelines-time-ago"> - <p - class="duration" - v-if="hasDuration"> - <span - v-html="iconTimerSvg"> - </span> - {{durationFormated}} - </p> - - <p - class="finished-at" - v-if="hasFinishedTime"> - - <i - class="fa fa-calendar" - aria-hidden="true" /> - - <time - ref="tooltip" - data-toggle="tooltip" - data-placement="top" - data-container="body" - :title="localTimeFinished"> - {{finishedTimeFormated}} - </time> - </p> - </td> - `, -}; diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue new file mode 100644 index 00000000000..be3f32afa09 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/time_ago.vue @@ -0,0 +1,93 @@ +<script> + import iconTimerSvg from 'icons/_icon_timer.svg'; + import '../../lib/utils/datetime_utility'; + import tooltipMixin from '../../vue_shared/mixins/tooltip'; + import timeagoMixin from '../../vue_shared/mixins/timeago'; + + export default { + props: { + finishedTime: { + type: String, + required: true, + }, + duration: { + type: Number, + required: true, + }, + }, + mixins: [ + tooltipMixin, + timeagoMixin, + ], + data() { + return { + iconTimerSvg, + }; + }, + computed: { + hasDuration() { + return this.duration > 0; + }, + hasFinishedTime() { + return this.finishedTime !== ''; + }, + durationFormated() { + const date = new Date(this.duration * 1000); + + let hh = date.getUTCHours(); + let mm = date.getUTCMinutes(); + let ss = date.getSeconds(); + + // left pad + if (hh < 10) { + hh = `0${hh}`; + } + if (mm < 10) { + mm = `0${mm}`; + } + if (ss < 10) { + ss = `0${ss}`; + } + + return `${hh}:${mm}:${ss}`; + }, + }, + }; +</script> +<template> + <div class="table-section section-15 pipelines-time-ago"> + <div + class="table-mobile-header" + role="rowheader"> + Duration + </div> + <div class="table-mobile-content"> + <p + class="duration" + v-if="hasDuration"> + <span + v-html="iconTimerSvg"> + </span> + {{durationFormated}} + </p> + + <p + class="finished-at hidden-xs hidden-sm" + v-if="hasFinishedTime"> + + <i + class="fa fa-calendar" + aria-hidden="true"> + </i> + + <time + ref="tooltip" + data-placement="top" + data-container="body" + :title="tooltipTitle(finishedTime)"> + {{timeFormated(finishedTime)}} + </time> + </p> + </div> + </div> +</script> diff --git a/app/assets/javascripts/pipelines/index.js b/app/assets/javascripts/pipelines/index.js deleted file mode 100644 index 48f9181a8d9..00000000000 --- a/app/assets/javascripts/pipelines/index.js +++ /dev/null @@ -1,22 +0,0 @@ -import Vue from 'vue'; -import PipelinesStore from './stores/pipelines_store'; -import PipelinesComponent from './pipelines'; -import '../vue_shared/vue_resource_interceptor'; - -$(() => new Vue({ - el: document.querySelector('#pipelines-list-vue'), - - data() { - const store = new PipelinesStore(); - - return { - store, - }; - }, - components: { - 'vue-pipelines': PipelinesComponent, - }, - template: ` - <vue-pipelines :store="store" /> - `, -})); diff --git a/app/assets/javascripts/pipelines/pipelines.js b/app/assets/javascripts/pipelines/pipelines.js deleted file mode 100644 index 9f247af1dec..00000000000 --- a/app/assets/javascripts/pipelines/pipelines.js +++ /dev/null @@ -1,295 +0,0 @@ -import Visibility from 'visibilityjs'; -import PipelinesService from './services/pipelines_service'; -import eventHub from './event_hub'; -import pipelinesTableComponent from '../vue_shared/components/pipelines_table'; -import tablePagination from '../vue_shared/components/table_pagination.vue'; -import emptyState from './components/empty_state.vue'; -import errorState from './components/error_state.vue'; -import navigationTabs from './components/navigation_tabs'; -import navigationControls from './components/nav_controls'; -import loadingIcon from '../vue_shared/components/loading_icon.vue'; -import Poll from '../lib/utils/poll'; - -export default { - props: { - store: { - type: Object, - required: true, - }, - }, - - components: { - tablePagination, - pipelinesTableComponent, - emptyState, - errorState, - navigationTabs, - navigationControls, - loadingIcon, - }, - - data() { - const pipelinesData = document.querySelector('#pipelines-list-vue').dataset; - - return { - endpoint: pipelinesData.endpoint, - cssClass: pipelinesData.cssClass, - helpPagePath: pipelinesData.helpPagePath, - newPipelinePath: pipelinesData.newPipelinePath, - canCreatePipeline: pipelinesData.canCreatePipeline, - allPath: pipelinesData.allPath, - pendingPath: pipelinesData.pendingPath, - runningPath: pipelinesData.runningPath, - finishedPath: pipelinesData.finishedPath, - branchesPath: pipelinesData.branchesPath, - tagsPath: pipelinesData.tagsPath, - hasCi: pipelinesData.hasCi, - ciLintPath: pipelinesData.ciLintPath, - state: this.store.state, - apiScope: 'all', - pagenum: 1, - isLoading: false, - hasError: false, - isMakingRequest: false, - updateGraphDropdown: false, - hasMadeRequest: false, - }; - }, - - computed: { - canCreatePipelineParsed() { - return gl.utils.convertPermissionToBoolean(this.canCreatePipeline); - }, - - scope() { - const scope = gl.utils.getParameterByName('scope'); - return scope === null ? 'all' : scope; - }, - - shouldRenderErrorState() { - return this.hasError && !this.isLoading; - }, - - /** - * The empty state should only be rendered when the request is made to fetch all pipelines - * and none is returned. - * - * @return {Boolean} - */ - shouldRenderEmptyState() { - return !this.isLoading && - !this.hasError && - this.hasMadeRequest && - !this.state.pipelines.length && - (this.scope === 'all' || this.scope === null); - }, - - /** - * When a specific scope does not have pipelines we render a message. - * - * @return {Boolean} - */ - shouldRenderNoPipelinesMessage() { - return !this.isLoading && - !this.hasError && - !this.state.pipelines.length && - this.scope !== 'all' && - this.scope !== null; - }, - - shouldRenderTable() { - return !this.hasError && - !this.isLoading && this.state.pipelines.length; - }, - - /** - * Pagination should only be rendered when there is more than one page. - * - * @return {Boolean} - */ - shouldRenderPagination() { - return !this.isLoading && - this.state.pipelines.length && - this.state.pageInfo.total > this.state.pageInfo.perPage; - }, - - hasCiEnabled() { - return this.hasCi !== undefined; - }, - - paths() { - return { - allPath: this.allPath, - pendingPath: this.pendingPath, - finishedPath: this.finishedPath, - runningPath: this.runningPath, - branchesPath: this.branchesPath, - tagsPath: this.tagsPath, - }; - }, - - pageParameter() { - return gl.utils.getParameterByName('page') || this.pagenum; - }, - - scopeParameter() { - return gl.utils.getParameterByName('scope') || this.apiScope; - }, - }, - - created() { - this.service = new PipelinesService(this.endpoint); - - const poll = new Poll({ - resource: this.service, - method: 'getPipelines', - data: { page: this.pageParameter, scope: this.scopeParameter }, - successCallback: this.successCallback, - errorCallback: this.errorCallback, - notificationCallback: this.setIsMakingRequest, - }); - - if (!Visibility.hidden()) { - this.isLoading = true; - poll.makeRequest(); - } else { - // If tab is not visible we need to make the first request so we don't show the empty - // state without knowing if there are any pipelines - this.fetchPipelines(); - } - - Visibility.change(() => { - if (!Visibility.hidden()) { - poll.restart(); - } else { - poll.stop(); - } - }); - - eventHub.$on('refreshPipelines', this.fetchPipelines); - }, - - beforeDestroy() { - eventHub.$off('refreshPipelines'); - }, - - methods: { - /** - * Will change the page number and update the URL. - * - * @param {Number} pageNumber desired page to go to. - */ - change(pageNumber) { - const param = gl.utils.setParamInURL('page', pageNumber); - - gl.utils.visitUrl(param); - return param; - }, - - fetchPipelines() { - if (!this.isMakingRequest) { - this.isLoading = true; - - this.service.getPipelines({ scope: this.scopeParameter, page: this.pageParameter }) - .then(response => this.successCallback(response)) - .catch(() => this.errorCallback()); - } - }, - - successCallback(resp) { - const response = { - headers: resp.headers, - body: resp.json(), - }; - - this.store.storeCount(response.body.count); - this.store.storePipelines(response.body.pipelines); - this.store.storePagination(response.headers); - - this.isLoading = false; - this.updateGraphDropdown = true; - this.hasMadeRequest = true; - }, - - errorCallback() { - this.hasError = true; - this.isLoading = false; - this.updateGraphDropdown = false; - }, - - setIsMakingRequest(isMakingRequest) { - this.isMakingRequest = isMakingRequest; - - if (isMakingRequest) { - this.updateGraphDropdown = false; - } - }, - }, - - template: ` - <div :class="cssClass"> - - <div - class="top-area scrolling-tabs-container inner-page-scroll-tabs" - v-if="!isLoading && !shouldRenderEmptyState"> - <div class="fade-left"> - <i class="fa fa-angle-left" aria-hidden="true"></i> - </div> - <div class="fade-right"> - <i class="fa fa-angle-right" aria-hidden="true"></i> - </div> - <navigation-tabs - :scope="scope" - :count="state.count" - :paths="paths" /> - - <navigation-controls - :new-pipeline-path="newPipelinePath" - :has-ci-enabled="hasCiEnabled" - :help-page-path="helpPagePath" - :ciLintPath="ciLintPath" - :can-create-pipeline="canCreatePipelineParsed " /> - </div> - - <div class="content-list pipelines"> - - <loading-icon - label="Loading Pipelines" - size="3" - v-if="isLoading" - /> - - <empty-state - v-if="shouldRenderEmptyState" - :help-page-path="helpPagePath" /> - - <error-state v-if="shouldRenderErrorState" /> - - <div - class="blank-state blank-state-no-icon" - v-if="shouldRenderNoPipelinesMessage"> - <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2> - </div> - - <div - class="table-holder" - v-if="shouldRenderTable"> - - <pipelines-table-component - :pipelines="state.pipelines" - :service="service" - :update-graph-dropdown="updateGraphDropdown" - /> - </div> - - <table-pagination - v-if="shouldRenderPagination" - :pagenum="pagenum" - :change="change" - :count="state.count.all" - :pageInfo="state.pageInfo" - /> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/pipelines/pipelines_bundle.js b/app/assets/javascripts/pipelines/pipelines_bundle.js new file mode 100644 index 00000000000..923d9bfb248 --- /dev/null +++ b/app/assets/javascripts/pipelines/pipelines_bundle.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import PipelinesStore from './stores/pipelines_store'; +import pipelinesComponent from './components/pipelines.vue'; + +document.addEventListener('DOMContentLoaded', () => new Vue({ + el: '#pipelines-list-vue', + data() { + const store = new PipelinesStore(); + + return { + store, + }; + }, + components: { + pipelinesComponent, + }, + render(createElement) { + return createElement('pipelines-component', { + props: { + store: this.store, + }, + }); + }, +})); diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index 8ac71797c14..a4a7f3fa944 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -1,6 +1,8 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */ /* global Mousetrap */ /* global findFileURL */ +import Cookies from 'js-cookie'; + import findAndFollowLink from './shortcuts_dashboard_navigation'; (function() { @@ -14,6 +16,7 @@ import findAndFollowLink from './shortcuts_dashboard_navigation'; Mousetrap.bind('?', this.onToggleHelp); Mousetrap.bind('s', Shortcuts.focusSearch); Mousetrap.bind('f', (e => this.focusFilter(e))); + Mousetrap.bind('p b', this.onTogglePerfBar); const $globalDropdownMenu = $('.global-dropdown-menu'); const $globalDropdownToggle = $('.global-dropdown-toggle'); @@ -53,6 +56,17 @@ import findAndFollowLink from './shortcuts_dashboard_navigation'; return Shortcuts.toggleHelp(this.enabledHelp); }; + Shortcuts.prototype.onTogglePerfBar = function(e) { + e.preventDefault(); + const performanceBarCookieName = 'perf_bar_enabled'; + if (Cookies.get(performanceBarCookieName) === 'true') { + Cookies.remove(performanceBarCookieName, { path: '/' }); + } else { + Cookies.set(performanceBarCookieName, true, { path: '/' }); + } + gl.utils.refreshCurrentPage(); + }; + Shortcuts.prototype.toggleMarkdownPreview = function(e) { // Check if short-cut was triggered while in Write Mode const $target = $(e.target); diff --git a/app/assets/javascripts/vue_shared/components/commit.js b/app/assets/javascripts/vue_shared/components/commit.js deleted file mode 100644 index ff5ae28e062..00000000000 --- a/app/assets/javascripts/vue_shared/components/commit.js +++ /dev/null @@ -1,159 +0,0 @@ -import commitIconSvg from 'icons/_icon_commit.svg'; -import userAvatarLink from './user_avatar/user_avatar_link.vue'; - -export default { - props: { - /** - * Indicates the existance of a tag. - * Used to render the correct icon, if true will render `fa-tag` icon, - * if false will render `fa-code-fork` icon. - */ - tag: { - type: Boolean, - required: false, - default: false, - }, - - /** - * If provided is used to render the branch name and url. - * Should contain the following properties: - * name - * ref_url - */ - commitRef: { - type: Object, - required: false, - default: () => ({}), - }, - - /** - * Used to link to the commit sha. - */ - commitUrl: { - type: String, - required: false, - default: '', - }, - - /** - * Used to show the commit short sha that links to the commit url. - */ - shortSha: { - type: String, - required: false, - default: '', - }, - - /** - * If provided shows the commit tile. - */ - title: { - type: String, - required: false, - default: '', - }, - - /** - * If provided renders information about the author of the commit. - * When provided should include: - * `avatar_url` to render the avatar icon - * `web_url` to link to user profile - * `username` to render alt and title tags - */ - author: { - type: Object, - required: false, - default: () => ({}), - }, - }, - - computed: { - /** - * Used to verify if all the properties needed to render the commit - * ref section were provided. - * - * TODO: Improve this! Use lodash _.has when we have it. - * - * @returns {Boolean} - */ - hasCommitRef() { - return this.commitRef && this.commitRef.name && this.commitRef.ref_url; - }, - - /** - * Used to verify if all the properties needed to render the commit - * author section were provided. - * - * TODO: Improve this! Use lodash _.has when we have it. - * - * @returns {Boolean} - */ - hasAuthor() { - return this.author && - this.author.avatar_url && - this.author.path && - this.author.username; - }, - - /** - * If information about the author is provided will return a string - * to be rendered as the alt attribute of the img tag. - * - * @returns {String} - */ - userImageAltDescription() { - return this.author && - this.author.username ? `${this.author.username}'s avatar` : null; - }, - }, - - data() { - return { commitIconSvg }; - }, - - components: { - userAvatarLink, - }, - template: ` - <div class="branch-commit"> - - <div v-if="hasCommitRef" class="icon-container"> - <i v-if="tag" class="fa fa-tag"></i> - <i v-if="!tag" class="fa fa-code-fork"></i> - </div> - - <a v-if="hasCommitRef" - class="ref-name" - :href="commitRef.ref_url"> - {{commitRef.name}} - </a> - - <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div> - - <a class="commit-sha" - :href="commitUrl"> - {{shortSha}} - </a> - - <div class="commit-title flex-truncate-parent"> - <span v-if="title" class="flex-truncate-child"> - <user-avatar-link - v-if="hasAuthor" - class="avatar-image-container" - :link-href="author.path" - :img-src="author.avatar_url" - :img-alt="userImageAltDescription" - :tooltip-text="author.username" - /> - <a class="commit-row-message" - :href="commitUrl"> - {{title}} - </a> - </span> - <span v-else> - Cant find HEAD commit for this branch - </span> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue new file mode 100644 index 00000000000..262584769e0 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -0,0 +1,166 @@ +<script> + import commitIconSvg from 'icons/_icon_commit.svg'; + import userAvatarLink from './user_avatar/user_avatar_link.vue'; + + export default { + props: { + /** + * Indicates the existance of a tag. + * Used to render the correct icon, if true will render `fa-tag` icon, + * if false will render `fa-code-fork` icon. + */ + tag: { + type: Boolean, + required: false, + default: false, + }, + /** + * If provided is used to render the branch name and url. + * Should contain the following properties: + * name + * ref_url + */ + commitRef: { + type: Object, + required: false, + default: () => ({}), + }, + /** + * Used to link to the commit sha. + */ + commitUrl: { + type: String, + required: false, + default: '', + }, + + /** + * Used to show the commit short sha that links to the commit url. + */ + shortSha: { + type: String, + required: false, + default: '', + }, + /** + * If provided shows the commit tile. + */ + title: { + type: String, + required: false, + default: '', + }, + /** + * If provided renders information about the author of the commit. + * When provided should include: + * `avatar_url` to render the avatar icon + * `web_url` to link to user profile + * `username` to render alt and title tags + */ + author: { + type: Object, + required: false, + default: () => ({}), + }, + }, + computed: { + /** + * Used to verify if all the properties needed to render the commit + * ref section were provided. + * + * TODO: Improve this! Use lodash _.has when we have it. + * + * @returns {Boolean} + */ + hasCommitRef() { + return this.commitRef && this.commitRef.name && this.commitRef.ref_url; + }, + /** + * Used to verify if all the properties needed to render the commit + * author section were provided. + * + * TODO: Improve this! Use lodash _.has when we have it. + * + * @returns {Boolean} + */ + hasAuthor() { + return this.author && + this.author.avatar_url && + this.author.path && + this.author.username; + }, + /** + * If information about the author is provided will return a string + * to be rendered as the alt attribute of the img tag. + * + * @returns {String} + */ + userImageAltDescription() { + return this.author && + this.author.username ? `${this.author.username}'s avatar` : null; + }, + }, + data() { + return { commitIconSvg }; + }, + components: { + userAvatarLink, + }, + }; +</script> +<template> + <div class="branch-commit"> + <div v-if="hasCommitRef" class="icon-container hidden-xs"> + <i + v-if="tag" + class="fa fa-tag" + aria-hidden="true"> + </i> + <i + v-if="!tag" + class="fa fa-code-fork" + aria-hidden="true"> + </i> + </div> + + <a + v-if="hasCommitRef" + class="ref-name hidden-xs" + :href="commitRef.ref_url"> + {{commitRef.name}} + </a> + + <div + v-html="commitIconSvg" + class="commit-icon js-commit-icon"> + </div> + + <a + class="commit-sha" + :href="commitUrl"> + {{shortSha}} + </a> + + <div class="commit-title flex-truncate-parent"> + <span + v-if="title" + class="flex-truncate-child"> + <user-avatar-link + v-if="hasAuthor" + class="avatar-image-container" + :link-href="author.path" + :img-src="author.avatar_url" + :img-alt="userImageAltDescription" + :tooltip-text="author.username" + /> + <a class="commit-row-message" + :href="commitUrl"> + {{title}} + </a> + </span> + <span v-else> + Cant find HEAD commit for this branch + </span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index fe6d6a792e7..1d4d90f75b6 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -40,6 +40,11 @@ export default { required: false, default: () => [], }, + hasSidebarButton: { + type: Boolean, + required: false, + default: false, + }, }, mixins: [ @@ -66,8 +71,9 @@ export default { }, }; </script> + <template> - <header class="page-content-header"> + <header class="page-content-header ci-header-container"> <section class="header-main-content"> <ci-icon-badge :status="status" /> @@ -102,7 +108,7 @@ export default { </section> <section - class="header-action-button nav-controls" + class="header-action-buttons" v-if="actions.length"> <template v-for="action in actions"> @@ -113,6 +119,15 @@ export default { {{action.label}} </a> + <a + v-if="action.type === 'ujs-link'" + :href="action.path" + data-method="post" + rel="nofollow" + :class="action.cssClass"> + {{action.label}} + </a> + <button v-else="action.type === 'button'" @click="onClickAction(action)" @@ -120,7 +135,6 @@ export default { :class="action.cssClass" type="button"> {{action.label}} - <i v-show="action.isLoading" class="fa fa-spin fa-spinner" @@ -128,6 +142,18 @@ export default { </i> </button> </template> + <button + v-if="hasSidebarButton" + type="button" + class="btn btn-default visible-xs-block visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header" + aria-label="Toggle Sidebar" + id="toggleSidebar"> + <i + class="fa fa-angle-double-left" + aria-hidden="true" + aria-labelledby="toggleSidebar"> + </i> + </button> </section> </header> </template> diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js b/app/assets/javascripts/vue_shared/components/pipelines_table.js deleted file mode 100644 index 48a39f18112..00000000000 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.js +++ /dev/null @@ -1,55 +0,0 @@ -import PipelinesTableRowComponent from './pipelines_table_row'; - -/** - * Pipelines Table Component. - * - * Given an array of objects, renders a table. - */ -export default { - props: { - pipelines: { - type: Array, - required: true, - }, - - service: { - type: Object, - required: true, - }, - - updateGraphDropdown: { - type: Boolean, - required: false, - default: false, - }, - }, - - components: { - 'pipelines-table-row-component': PipelinesTableRowComponent, - }, - - template: ` - <table class="table ci-table"> - <thead> - <tr> - <th class="js-pipeline-status pipeline-status">Status</th> - <th class="js-pipeline-info pipeline-info">Pipeline</th> - <th class="js-pipeline-commit pipeline-commit">Commit</th> - <th class="js-pipeline-stages pipeline-stages">Stages</th> - <th class="js-pipeline-date pipeline-date"></th> - <th class="js-pipeline-actions pipeline-actions"></th> - </tr> - </thead> - <tbody> - <template v-for="model in pipelines" - v-bind:model="model"> - <tr is="pipelines-table-row-component" - :pipeline="model" - :service="service" - :update-graph-dropdown="updateGraphDropdown" - /> - </template> - </tbody> - </table> - `, -}; diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.vue b/app/assets/javascripts/vue_shared/components/pipelines_table.vue new file mode 100644 index 00000000000..884f1ce9689 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.vue @@ -0,0 +1,64 @@ +<script> + import pipelinesTableRowComponent from './pipelines_table_row.vue'; + + /** + * Pipelines Table Component. + * + * Given an array of objects, renders a table. + */ + export default { + props: { + pipelines: { + type: Array, + required: true, + }, + service: { + type: Object, + required: true, + }, + updateGraphDropdown: { + type: Boolean, + required: false, + default: false, + }, + }, + components: { + pipelinesTableRowComponent, + }, + }; +</script> +<template> + <div class="ci-table"> + <div + class="gl-responsive-table-row table-row-header" + role="row"> + <div + class="table-section section-10 js-pipeline-status pipeline-status" + role="rowheader"> + Status + </div> + <div + class="table-section section-15 js-pipeline-info pipeline-info" + role="rowheader"> + Pipeline + </div> + <div + class="table-section section-25 js-pipeline-commit pipeline-commit" + role="rowheader"> + Commit + </div> + <div + class="table-section section-15 js-pipeline-stages pipeline-stages" + role="rowheader"> + Stages + </div> + </div> + <pipelines-table-row-component + v-for="model in pipelines" + :key="model.id" + :pipeline="model" + :service="service" + :update-graph-dropdown="updateGraphDropdown" + /> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/vue_shared/components/pipelines_table_row.vue index f60f8eeb43d..4d5ebe2e9ed 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js +++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.vue @@ -1,12 +1,13 @@ +<script> /* eslint-disable no-param-reassign */ -import AsyncButtonComponent from '../../pipelines/components/async_button.vue'; -import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions'; -import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts'; +import asyncButtonComponent from '../../pipelines/components/async_button.vue'; +import pipelinesActionsComponent from '../../pipelines/components/pipelines_actions.vue'; +import pipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts.vue'; import ciBadge from './ci_badge_link.vue'; -import PipelinesStageComponent from '../../pipelines/components/stage.vue'; -import PipelinesUrlComponent from '../../pipelines/components/pipeline_url.vue'; -import PipelinesTimeagoComponent from '../../pipelines/components/time_ago'; -import CommitComponent from './commit'; +import pipelineStage from '../../pipelines/components/stage.vue'; +import pipelineUrl from '../../pipelines/components/pipeline_url.vue'; +import pipelinesTimeago from '../../pipelines/components/time_ago.vue'; +import commitComponent from './commit.vue'; /** * Pipeline table row. @@ -19,30 +20,26 @@ export default { type: Object, required: true, }, - service: { type: Object, required: true, }, - updateGraphDropdown: { type: Boolean, required: false, default: false, }, }, - components: { - 'async-button-component': AsyncButtonComponent, - 'pipelines-actions-component': PipelinesActionsComponent, - 'pipelines-artifacts-component': PipelinesArtifactsComponent, - 'commit-component': CommitComponent, - 'dropdown-stage': PipelinesStageComponent, - 'pipeline-url': PipelinesUrlComponent, + asyncButtonComponent, + pipelinesActionsComponent, + pipelinesArtifactsComponent, + commitComponent, + pipelineStage, + pipelineUrl, ciBadge, - 'time-ago': PipelinesTimeagoComponent, + pipelinesTimeago, }, - computed: { /** * If provided, returns the commit tag. @@ -203,17 +200,37 @@ export default { } return {}; }, - }, - template: ` - <tr class="commit"> - <td class="commit-link"> + displayPipelineActions() { + return this.pipeline.flags.retryable || + this.pipeline.flags.cancelable || + this.pipeline.details.manual_actions.length || + this.pipeline.details.artifacts.length; + }, + }, +}; +</script> +<template> + <div class="commit gl-responsive-table-row"> + <div class="table-section section-10 commit-link"> + <div class="table-mobile-header" + role="rowheader"> + Status + </div> + <div class="table-mobile-content"> <ci-badge :status="pipelineStatus"/> - </td> + </div> + </div> - <pipeline-url :pipeline="pipeline"></pipeline-url> + <pipeline-url :pipeline="pipeline" /> - <td> + <div class="table-section section-25"> + <div + class="table-mobile-header" + role="rowheader"> + Commit + </div> + <div class="table-mobile-content"> <commit-component :tag="commitTag" :commit-ref="commitRef" @@ -221,52 +238,67 @@ export default { :short-sha="commitShortSha" :title="commitTitle" :author="commitAuthor"/> - </td> + </div> + </div> - <td class="stage-cell"> + <div class="table-section section-wrap section-15 stage-cell"> + <div + class="table-mobile-header" + role="rowheader"> + Stages + </div> + <div class="table-mobile-content"> <div class="stage-container dropdown js-mini-pipeline-graph" v-if="pipeline.details.stages.length > 0" v-for="stage in pipeline.details.stages"> - - <dropdown-stage + <pipeline-stage :stage="stage" - :update-dropdown="updateGraphDropdown"/> + :update-dropdown="updateGraphDropdown" + /> </div> - </td> + </div> + </div> - <time-ago - :duration="pipelineDuration" - :finished-time="pipelineFinishedAt" /> + <pipelines-timeago + :duration="pipelineDuration" + :finished-time="pipelineFinishedAt" + /> - <td class="pipeline-actions"> - <div class="pull-right btn-group"> - <pipelines-actions-component - v-if="pipeline.details.manual_actions.length" - :actions="pipeline.details.manual_actions" - :service="service" /> + <div + v-if="displayPipelineActions" + class="table-section section-20 table-button-footer pipeline-actions"> + <div class="btn-group table-action-buttons"> + <pipelines-actions-component + v-if="pipeline.details.manual_actions.length" + :actions="pipeline.details.manual_actions" + :service="service" + /> - <pipelines-artifacts-component - v-if="pipeline.details.artifacts.length" - :artifacts="pipeline.details.artifacts" /> + <pipelines-artifacts-component + v-if="pipeline.details.artifacts.length" + class="hidden-xs hidden-sm" + :artifacts="pipeline.details.artifacts" + /> - <async-button-component - v-if="pipeline.flags.retryable" - :service="service" - :endpoint="pipeline.retry_path" - css-class="js-pipelines-retry-button btn-default btn-retry" - title="Retry" - icon="repeat" /> + <async-button-component + v-if="pipeline.flags.retryable" + :service="service" + :endpoint="pipeline.retry_path" + css-class="js-pipelines-retry-button btn-default btn-retry" + title="Retry" + icon="repeat" + /> - <async-button-component - v-if="pipeline.flags.cancelable" - :service="service" - :endpoint="pipeline.cancel_path" - css-class="js-pipelines-cancel-button btn-remove" - title="Cancel" - icon="remove" - confirm-action-message="Are you sure you want to cancel this pipeline?" /> - </div> - </td> - </tr> - `, -}; + <async-button-component + v-if="pipeline.flags.cancelable" + :service="service" + :endpoint="pipeline.cancel_path" + css-class="js-pipelines-cancel-button btn-remove" + title="Cancel" + icon="remove" + confirm-action-message="Are you sure you want to cancel this pipeline?" + /> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue index af2b4c6786e..1c6ef071a6d 100644 --- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue +++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue @@ -20,12 +20,6 @@ export default { default: 'top', }, - shortFormat: { - type: Boolean, - required: false, - default: false, - }, - cssClass: { type: String, required: false, @@ -37,18 +31,12 @@ export default { tooltipMixin, timeagoMixin, ], - - computed: { - timeagoCssClass() { - return this.shortFormat ? 'js-short-timeago' : 'js-timeago'; - }, - }, }; </script> <template> <time - :class="[timeagoCssClass, cssClass]" - class="js-timeago js-timeago-render" + :class="cssClass" + class="js-vue-timeago" :title="tooltipTitle(time)" :data-placement="tooltipPlacement" data-container="body" diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index 75907c35b7e..19166757e64 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -1,4 +1,7 @@ .awards { + display: flex; + flex-wrap: wrap; + .emoji-icon { width: 20px; height: 20px; @@ -100,7 +103,6 @@ .award-menu-holder { display: inline-block; - position: absolute; .tooltip { white-space: nowrap; @@ -108,9 +110,11 @@ } .award-control { - margin: 0 5px 6px 0; + margin: 4px 8px 4px 0; outline: 0; position: relative; + display: block; + float: left; &.disabled { cursor: default; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 5ab48b6c874..cba890ce831 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -48,6 +48,10 @@ @include chevron-active; border-color: $gray-darkest; } + + [data-toggle="dropdown"] { + outline: 0; + } } .dropdown-toggle { @@ -109,6 +113,7 @@ &:focus:active { @include chevron-active; border-color: $dropdown-toggle-active-border-color; + outline: 0; } } @@ -201,6 +206,11 @@ width: 100%; } + &.dropdown-open-left { + right: 0; + left: auto; + } + &.is-loading { .dropdown-content { display: none; @@ -261,7 +271,14 @@ text-transform: capitalize; } - .separator + .dropdown-header { + .dropdown-bold-header { + font-weight: 600; + line-height: 22px; + padding: 0 16px; + } + + .separator + .dropdown-header, + .separator + .dropdown-bold-header { padding-top: 2px; } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index d08df05fd6c..b26d8fbd5fe 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -59,6 +59,43 @@ } } + .file-blame-legend { + background-color: $gray-light; + text-align: right; + padding: 8px $gl-padding; + + @media (max-width: $screen-xs-max) { + text-align: left; + } + + .left-label { + padding-right: 5px; + } + + .right-label { + padding-left: 5px; + } + + .legend-box { + display: inline-block; + width: 10px; + height: 10px; + padding: 0 2px; + } + + @for $i from 0 through 5 { + .legend-box-#{$i} { + background-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%); + } + } + + @for $i from 1 through 4 { + .legend-box-#{$i + 5} { + background-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%); + } + } + } + .file-content { background: $white-light; @@ -118,6 +155,19 @@ padding: 5px 10px; min-width: 400px; background: $gray-light; + border-left: 3px solid; + } + + @for $i from 0 through 5 { + td.blame-commit-age-#{$i} { + border-left-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%); + } + } + + @for $i from 1 through 4 { + td.blame-commit-age-#{$i + 5} { + border-left-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%); + } } td.line-numbers { diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 432024779fd..a78179e727f 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -148,7 +148,8 @@ label { margin-top: 35px; } -.form-group .control-label { +.form-group .control-label, +.form-group .control-label-full-width { font-weight: normal; } diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 9e8acf4e73c..49bff23452d 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -51,6 +51,10 @@ body { &.limit-container-width { max-width: $limited-layout-width; } + + &.limit-container-width-sm { + max-width: 790px; + } } .alert-wrapper { diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 49163653548..38727e15c6f 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -264,3 +264,103 @@ ul.controls { ul.indent-list { padding: 10px 0 0 30px; } + + +// Specific styles for tree list +.group-list-tree { + .folder-toggle-wrap { + float: left; + line-height: $list-text-height; + font-size: 0; + + span { + font-size: $gl-font-size; + } + } + + .folder-caret, + .folder-icon { + display: inline-block; + } + + .folder-caret { + width: 15px; + } + + .folder-icon { + width: 20px; + } + + > .group-row:not(.has-subgroups) { + .folder-caret .fa { + opacity: 0; + } + } + + .content-list li:last-child { + padding-bottom: 0; + } + + .group-list-tree { + margin-bottom: 0; + margin-left: 30px; + position: relative; + + &::before { + content: ''; + display: block; + width: 0; + position: absolute; + top: 5px; + bottom: 0; + left: -16px; + border-left: 2px solid $border-white-normal; + } + + .group-row { + position: relative; + + &::before { + content: ""; + display: block; + width: 10px; + height: 0; + border-top: 2px solid $border-white-normal; + position: absolute; + top: 30px; + left: -16px; + } + + &:last-child::before { + background: $white-light; + height: auto; + top: 30px; + bottom: 0; + } + } + } + + .group-row { + padding: 0; + border: none; + } + + .group-row-contents { + padding: 10px 10px 8px; + border-top: solid 1px transparent; + border-bottom: solid 1px $white-normal; + + &:hover { + border-color: $row-hover-border; + background-color: $row-hover; + cursor: pointer; + } + } +} + +.js-groups-list-holder { + .groups-list-loading { + font-size: 34px; + text-align: center; + } +} diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 28b2a7cfacd..3787ef370b2 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -45,7 +45,8 @@ li { display: flex; - a { + a, + .btn-link { padding: $gl-btn-padding; padding-bottom: 11px; font-size: 14px; @@ -67,7 +68,29 @@ } } - &.active a { + .btn-link { + padding-top: 16px; + padding-left: 15px; + padding-right: 15px; + border-left: none; + border-right: none; + border-top: none; + border-radius: 0; + + &:hover, + &:active, + &:focus { + background-color: transparent; + } + + &:active { + outline: 0; + box-shadow: none; + } + } + + &.active a, + &.active .btn-link { border-bottom: 2px solid $link-underline-blue; color: $black; font-weight: 600; diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page-header.scss index 5f4211147f3..f1ecd050a0a 100644 --- a/app/assets/stylesheets/framework/page-header.scss +++ b/app/assets/stylesheets/framework/page-header.scss @@ -59,4 +59,8 @@ margin: 0 2px 0 3px; } } + + .ci-status { + margin-right: 10px; + } } diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index 9d8d08dff88..fa364e68d22 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -34,6 +34,10 @@ } } + .panel-empty-heading { + border-bottom: 0; + } + .panel-body { padding: $gl-padding; diff --git a/app/assets/stylesheets/framework/responsive-tables.scss b/app/assets/stylesheets/framework/responsive-tables.scss index a24483fa431..d2c90908baa 100644 --- a/app/assets/stylesheets/framework/responsive-tables.scss +++ b/app/assets/stylesheets/framework/responsive-tables.scss @@ -19,10 +19,6 @@ .table-section { white-space: nowrap; - .branch-commit { - max-width: 100%; - } - $section-widths: 10 15 20 25 30 40; @each $width in $section-widths { &.section-#{$width} { @@ -40,13 +36,58 @@ align-self: stretch; padding: 10px; align-items: center; - height: 62px; + min-height: 62px; &:not(:first-of-type) { border-top: 1px solid $white-normal; } } } + + &.section-wrap { + white-space: normal; + + @media (max-width: $screen-sm-max) { + flex-wrap: wrap; + } + } + } +} + + +.table-button-footer { + @media (min-width: $screen-md-min) { + text-align: right; + } + + @media (max-width: $screen-sm-max) { + background-color: $gray-normal; + align-self: stretch; + border-top: 1px solid $border-color; + + .table-action-buttons { + padding: 10px 5px; + display: flex; + + .btn { + border-radius: 3px; + } + + > .btn-group, + > .external-url, + > .btn { + flex: 1 1 28px; + margin: 0 5px; + } + + .dropdown-new { + width: 100%; + } + + .dropdown-menu { + min-width: initial; + } + } } } @@ -60,6 +101,7 @@ .table-mobile-header { color: $gl-text-color-secondary; + text-align: left; @include flex-max-width(40); @media (min-width: $screen-md-min) { @@ -87,4 +129,9 @@ @media (min-width: $screen-md-min) { flex: 0 0 90%; } + + .avatar { + float: none; + margin-right: 4px; + } } diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 5ae833cd5f6..1b20c35ad98 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -109,10 +109,12 @@ line-height: 15px; background-color: $gray-light; background-image: none; + padding: 3px 18px 3px 5px; .select2-search-choice-close { - top: 4px; - left: 3px; + top: 5px; + left: initial; + right: 3px; } &.select2-search-choice-focus { diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index c9f345d24be..b666223b120 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -74,9 +74,9 @@ $pagination-hover-color: $gl-text-color; $pagination-hover-bg: $row-hover; $pagination-hover-border: $border-color; -$pagination-active-color: $blue-600; -$pagination-active-bg: $white-light; -$pagination-active-border: $border-color; +$pagination-active-color: $white-light; +$pagination-active-bg: $gl-link-color; +$pagination-active-border: $gl-link-color; $pagination-disabled-color: #cdcdcd; $pagination-disabled-bg: $gray-light; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 4114a050d9a..49ba0108228 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -365,6 +365,13 @@ $avatar-border: rgba(0, 0, 0, .1); $gl-avatar-size: 40px; /* +* Blame +*/ +$blame-gray: #ededed; +$blame-cyan: #acd5f2; +$blame-blue: #254e77; + +/* * Builds */ $builds-trace-bg: #111; diff --git a/app/assets/stylesheets/mailers/devise.scss b/app/assets/stylesheets/mailers/devise.scss deleted file mode 100644 index 9f613710cf4..00000000000 --- a/app/assets/stylesheets/mailers/devise.scss +++ /dev/null @@ -1,140 +0,0 @@ -@import "framework/variables"; - -// NOTE: This stylesheet is for the exclusive use of the `devise_mailer` layout -// used for Devise email templates, and _should not_ be included in any -// application stylesheets. -// -// Styles defined here are embedded directly into the resulting email HTML via -// the `premailer` gem. - -$body-background-color: #363636; -$message-background-color: #fafafa; - -$header-color: #6b4fbb; -$body-color: #444; -$cta-color: #e14329; -$footer-link-color: #7e7e7e; - -$font-family: Helvetica, Arial, sans-serif; - -body { - background-color: $body-background-color; - font-family: $font-family; - margin: 0; - padding: 0; -} - -table { - -premailer-cellpadding: 0; - -premailer-cellspacing: 0; - - border: 0; - border-collapse: separate; - - &#wrapper { - background-color: $body-background-color; - width: 100%; - } - - &#header { - margin: 0 auto; - text-align: left; - width: 600px; - - & > td { - text-align: center; - } - } - - &#body { - background-color: $message-background-color; - border: 1px solid $black; - border-radius: 4px; - margin: 0 auto; - width: 600px; - } - - &#footer { - color: $footer-link-color; - font-size: 14px; - text-align: center; - width: 100%; - } - - td { - &#body-container { - padding: 20px 40px; - } - } -} - -.center { - text-align: center; -} - -#logo { - border: none; - outline: none; - min-height: 88px; - width: 134px; -} - -#content { - h2 { - color: $header-color; - font-size: 30px; - font-weight: 400; - line-height: 34px; - margin-top: 0; - } - - p { - color: $body-color; - font-size: 17px; - line-height: 24px; - margin-bottom: 0; - } -} - -#cta { - border: 1px solid $cta-color; - border-radius: 3px; - display: inline-block; - margin: 20px 0; - padding: 12px 24px; - - a { - background-color: $message-background-color; - color: $cta-color; - display: inline-block; - text-decoration: none; - } -} - -#tanuki { - padding: 40px 0 0; - - img { - border: none; - outline: none; - width: 37px; - min-height: 36px; - } -} - -#tagline { - font-size: 22px; - font-weight: 100; - padding: 4px 0 40px; -} - -#social { - padding: 0 10px 20px; - width: 600px; - word-spacing: 20px; - - a { - color: $footer-link-color; - text-decoration: none; - } -} diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index e3e94c8ca50..85109fec91a 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -1,3 +1,5 @@ +@import "./issues/issue_count_badge"; + [v-cloak] { display: none; } @@ -96,9 +98,51 @@ @media (min-width: $screen-sm-min) { width: 400px; } + + &.is-expandable { + .board-header { + cursor: pointer; + } + } + + &.is-collapsed { + width: 50px; + + .board-header { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } + + .board-title { + position: initial; + padding: 0; + border-bottom: 0; + + > span { + display: block; + transform: rotate(90deg) translate(25px, 0); + } + } + + .board-title-expandable-toggle { + position: absolute; + top: 50%; + left: 50%; + margin-left: -10px; + } + + .board-list-component, + .issue-count-badge { + display: none; + } + } } .board-inner { + position: relative; height: 100%; font-size: $issue-boards-font-size; background: $gray-light; @@ -387,30 +431,6 @@ margin: 5px; } -.board-issue-count-holder { - margin-top: -3px; - - .btn { - line-height: 12px; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } -} - -.board-issue-count { - padding-right: 10px; - padding-left: 10px; - line-height: 21px; - border-radius: $border-radius-base; - border: 1px solid $border-color; - - &.has-btn { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-width: 1px 0 1px 1px; - } -} - .page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar { &.right-sidebar { top: 0; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index e35558ad8e8..7eee0a71c66 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -71,7 +71,9 @@ height: 35px; display: flex; justify-content: flex-end; - border-bottom: 1px outset $white-light; + background: $gray-light; + border: 1px solid $border-color; + color: $gl-text-color; .truncated-info { margin: 0 auto; @@ -82,7 +84,7 @@ } .raw-link { - color: inherit; + color: $gl-text-color; margin-left: 5px; text-decoration: underline; } @@ -93,17 +95,25 @@ display: flex; align-self: center; font-size: 15px; + margin-bottom: 4px; svg { height: 15px; display: block; - fill: $white-light; + fill: $gl-text-color; } - a, + .controllers-buttons, .btn-scroll { - margin: 0 8px; - color: $white-light; + color: $gl-text-color; + height: 15px; + vertical-align: middle; + padding: 0; + width: 12px; + } + + .controllers-buttons { + margin: 1px 10px; } .btn-scroll.animate { @@ -137,21 +147,23 @@ top: 35px; left: 10px; bottom: 0; - overflow-y: hidden; - padding-bottom: 20px; - padding-right: 20px; + overflow-y: scroll; + overflow-x: hidden; + padding: 10px 20px 20px 5px; + white-space: pre; } .environment-information { - background-color: $gray-light; border: 1px solid $border-color; - padding: 12px $gl-padding; + padding: 8px $gl-padding 12px; border-radius: $border-radius-default; svg { position: relative; - top: 1px; + top: 5px; margin-right: 5px; + width: 22px; + height: 22px; } } @@ -165,54 +177,31 @@ } } -.status-message { - display: inline-block; - color: $white-light; - - .status-icon { - display: inline-block; - width: 16px; - height: 33px; +.build-header { + .ci-header-container, + .header-action-buttons { + display: flex; } - .status-text { - float: left; - opacity: 0; - margin-right: 10px; - font-weight: normal; - line-height: 1.8; - transition: opacity 1s ease-out; - - &.animate { - animation: fade-out-status 2s ease; - } + .ci-header-container { + min-height: 54px; } - &:hover .status-text { - opacity: 1; + .page-content-header { + padding: 10px 0 9px; } -} - -.build-header { - position: relative; - padding: 0; - display: flex; - min-height: 58px; - align-items: center; - - @media (max-width: $screen-sm-max) { - padding-right: 40px; - margin-top: 6px; - .btn-inverted { - display: none; + .header-action-buttons { + @media (max-width: $screen-xs-max) { + .sidebar-toggle-btn { + margin-top: 0; + margin-left: 10px; + max-height: 34px; + } } } .header-content { - flex: 1; - line-height: 1.8; - a { color: $gl-text-color; @@ -235,7 +224,7 @@ } .right-sidebar.build-sidebar { - padding: $gl-padding 0; + padding: 0; &.right-sidebar-collapsed { display: none; @@ -248,6 +237,10 @@ .block { width: 100%; + &:last-child { + border-bottom: 1px solid $border-gray-normal; + } + &.coverage { padding: 0 16px 11px; } @@ -257,34 +250,39 @@ } } - .js-build-variable { + .trigger-build-variable { color: $code-color; } - .js-build-value { + .trigger-build-value { padding: 2px 4px; color: $black; background-color: $white-light; } - .build-sidebar-header { - padding: 0 $gl-padding $gl-padding; - - .gutter-toggle { - margin-top: 0; - } + .label { + margin-left: 2px; } .retry-link { - color: $gl-link-color; display: none; - &:hover { - text-decoration: underline; + .btn-inverted-secondary { + color: $blue-500; + + &:hover { + color: $white-light; + } } @media (max-width: $screen-sm-max) { display: block; + + .btn { + i { + margin-left: 5px; + } + } } } @@ -308,6 +306,12 @@ left: $gl-padding; width: auto; } + + svg { + position: relative; + top: 2px; + margin-right: 3px; + } } .builds-container { @@ -369,6 +373,10 @@ } } } + + .link-commit { + color: $blue-600; + } } .build-sidebar { diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index bb72f453d1b..9db0f2075cb 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -228,7 +228,7 @@ margin: 10px 0; background: $gray-light; display: none; - white-space: pre-line; + white-space: pre-wrap; word-break: normal; pre { diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 56f20778637..89bd437b362 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -124,38 +124,6 @@ } .gl-responsive-table-row { - .environments-actions { - @media (min-width: $screen-md-min) { - text-align: right; - } - - @media (max-width: $screen-sm-max) { - background-color: $gray-normal; - align-self: stretch; - border-top: 1px solid $border-color; - - .environment-action-buttons { - padding: 10px; - display: flex; - - .btn { - border-radius: 3px; - } - - > .btn-group, - .external-url, - .btn { - flex: 1; - flex-basis: 28px; - } - - .dropdown-new { - width: 100%; - } - } - } - } - .branch-commit { max-width: 100%; } diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 5b723f7c722..4c3fa1fb8d4 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -89,7 +89,6 @@ background: $gray-light; border-radius: 0; color: $events-pre-color; - margin: 0 20px; overflow: hidden; } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index c2346f2f1c3..b3f310ff67d 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -58,7 +58,7 @@ } .emoji-block { - padding: 10px 0 4px; + padding: 10px 0; } } @@ -729,3 +729,33 @@ } } } + +.confidential-issue-warning { + background-color: $gl-gray; + border-radius: 3px; + padding: $gl-btn-padding $gl-padding; + margin-top: $gl-padding-top; + font-size: 14px; + color: $white-light; + + .fa { + margin-right: 8px; + } + + a { + color: $white-light; + text-decoration: underline; + } + + &.affix { + position: static; + width: initial; + + @media (min-width: $screen-sm-min) { + position: sticky; + position: -webkit-sticky; + top: 60px; + z-index: 200; + } + } +} diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 702e7662527..8cdb3f34ae5 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -1,3 +1,5 @@ +@import "./issues/issue_count_badge"; + .issues-list { .issue { padding: 10px 0 10px $gl-padding; @@ -249,14 +251,19 @@ ul.related-merge-requests > li { } @media (min-width: $screen-sm-min) { - .new-branch-col { - padding-top: 0; - text-align: right; - } + .emoji-block .row { + display: flex; - .create-mr-dropdown-wrap { - .btn-group:not(.hide) { - display: inline-block; + .new-branch-col { + padding-top: 0; + text-align: right; + align-self: center; + } + + .create-mr-dropdown-wrap { + .btn-group:not(.hide) { + display: inline-block; + } } } } diff --git a/app/assets/stylesheets/pages/issues/issue_count_badge.scss b/app/assets/stylesheets/pages/issues/issue_count_badge.scss new file mode 100644 index 00000000000..ccb62bfed18 --- /dev/null +++ b/app/assets/stylesheets/pages/issues/issue_count_badge.scss @@ -0,0 +1,29 @@ +.issue-count-badge { + display: inline-flex; + align-items: stretch; + height: 24px; +} + +.issue-count-badge-count { + display: flex; + align-items: center; + padding-right: 10px; + padding-left: 10px; + border: 1px solid $border-color; + border-radius: $border-radius-base; + line-height: 1; + + &.has-btn { + border-right: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } +} + +.issue-count-badge-add-button { + display: flex; + align-items: center; + border: 1px solid $border-color; + border-radius: 0 $border-radius-base $border-radius-base 0; + line-height: 1; +} diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 8249e02b64a..3cbe8dededb 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -128,6 +128,7 @@ a { width: 100%; font-size: 18px; + margin-right: 0; &:hover { border: 1px solid transparent; @@ -140,6 +141,7 @@ a { border: none; border-bottom: 2px solid $link-underline-blue; + margin-right: 0; color: $black; &:hover { diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index 971d54e7472..4be0e133b69 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -3,6 +3,41 @@ border-bottom: 1px solid $border-color; } +.project-member-tabs { + background: $gray-light; + border: 1px solid $border-color; + + li { + width: 50%; + + &.active { + background: $white-light; + } + + &:first-child { + border-right: 1px solid $border-color; + } + + a { + width: 100%; + text-align: center; + } + } +} + +.users-project-form { + .btn-create { + margin-right: 10px; + } +} + +.project-member-tab-content { + padding: $gl-padding; + border: 1px solid $border-color; + border-top: 0; + margin-bottom: $gl-padding; +} + .member { .list-item-name { @media (min-width: $screen-sm-min) { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 0ddaab0da14..aa307414737 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -103,41 +103,6 @@ } } -.confidential-issue-warning { - background-color: $gray-normal; - border-radius: 3px; - padding: 3px 12px; - margin: auto; - margin-top: 0; - text-align: center; - font-size: 12px; - align-items: center; - - @media (max-width: $screen-md-max) { - // On smaller devices the warning becomes the fourth item in the list, - // rather than centering, and grows to span the full width of the - // comment area. - order: 4; - margin: 6px auto; - width: 100%; - } - - .fa { - margin-right: 8px; - } -} - -.right-sidebar-expanded { - .confidential-issue-warning { - // When the sidebar is open the warning becomes the fourth item in the list, - // rather than centering, and grows to span the full width of the - // comment area. - order: 4; - margin: 6px auto; - width: 100%; - } -} - .discussion-form { padding: $gl-padding-top $gl-padding $gl-padding; background-color: $white-light; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index f956e3757bf..a0442463390 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -38,9 +38,12 @@ ul.notes { } .discussion { - overflow: hidden; display: block; position: relative; + + .diff-content { + overflow: visible; + } } > li { @@ -443,6 +446,52 @@ ul.notes { .note-action-button { margin-left: 8px; } + + .more-actions-toggle { + margin-left: 2px; + } +} + +.more-actions { + display: inline; + + .tooltip { + white-space: nowrap; + } +} + +.more-actions-toggle { + padding: 0; + + &:hover .icon, + &:focus .icon { + color: $blue-600; + } + + .icon { + padding: 0 6px; + } +} + +.more-actions-dropdown { + width: 180px; + min-width: 180px; + margin-top: $gl-btn-padding; + + li > a, + li > .btn { + color: $gl-text-color; + padding: $gl-btn-padding; + width: 100%; + text-align: left; + + &:hover, + &:focus { + color: $gl-text-color; + background-color: $blue-25; + border-radius: $border-radius-default; + } + } } .discussion-actions { diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss index ab417948931..595eb40fec7 100644 --- a/app/assets/stylesheets/pages/pipeline_schedules.scss +++ b/app/assets/stylesheets/pages/pipeline_schedules.scss @@ -12,7 +12,7 @@ .interval-pattern-form-group { label { margin-right: 10px; - font-size: 12px; + font-weight: normal; &[for='custom'] { margin-right: 0; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 58b458cd837..a85ba3a5955 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -10,17 +10,13 @@ .table-holder { width: 100%; - - @media (max-width: $screen-sm-max) { - overflow: auto; - } } .commit-title { margin: 0; } - .table.ci-table { + .ci-table { .label { margin-bottom: 3px; @@ -30,11 +26,6 @@ color: $black; } - .stage-cell { - min-width: 130px; // Guarantees we show at least 4 stages in line - width: 20%; - } - .pipelines-time-ago { text-align: right; } @@ -108,39 +99,7 @@ } } -.table.ci-table { - - &.builds-page tbody tr { - height: 71px; - } - - tr { - th { - padding: 16px 8px; - border: none; - } - - td { - padding: 10px 8px; - } - - td.environments-actions { - padding-right: 0; - } - - td.stage-cell { - padding: 10px 0; - } - - .commit-link { - padding: 9px 8px 10px 2px; - } - } - - tbody { - border-top-width: 1px; - } - +.ci-table { .build.retried { background-color: $gray-lightest; } @@ -194,13 +153,6 @@ color: $gl-link-color; } - .commit-title { - max-width: 225px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - .label { margin-right: 4px; } @@ -253,11 +205,7 @@ } .stage-cell { - font-size: 0; - padding: 0 4px; - - > .stage-container > div > button > span > svg, - > .stage-container > button > svg { + .mini-pipeline-graph-dropdown-toggle svg { height: 22px; width: 22px; position: absolute; @@ -545,12 +493,13 @@ border: 1px solid $border-color; border-radius: 30px; background-color: $white-light; + } - &:hover { - background-color: $stage-hover-bg; - border: 1px solid $stage-hover-border; - color: $gl-text-color; - } + a.build-content:hover, + button.build-content:hover { + background-color: $stage-hover-bg; + border: 1px solid $stage-hover-border; + color: $gl-text-color; } @@ -630,6 +579,23 @@ font-weight: normal; } +@mixin mini-pipeline-graph-color($color-light, $color-main, $color-dark) { + border-color: $color-main; + color: $color-main; + + &:hover, + &:focus, + &:active { + background-color: $color-light; + border-color: $color-dark; + color: $color-dark; + + svg { + fill: $color-dark; + } + } +} + // Dropdown button in mini pipeline graph .mini-pipeline-graph-dropdown-toggle { border-radius: 100px; @@ -669,100 +635,32 @@ // Dropdown button animation in mini pipeline graph &.ci-status-icon-success { - border-color: $green-500; - color: $green-500; - - &:hover, - &:focus, - &:active { - background-color: $green-50; - border-color: $green-600; - color: $green-600; - - svg { - fill: $green-600; - } - } + @include mini-pipeline-graph-color($green-50, $green-500, $green-600); } &.ci-status-icon-failed { - border-color: $red-500; - color: $red-500; - - &:hover, - &:focus, - &:active { - background-color: $red-50; - border-color: $red-600; - color: $red-600; - - svg { - fill: $red-600; - } - } + @include mini-pipeline-graph-color($red-50, $red-500, $red-600); } &.ci-status-icon-pending, &.ci-status-icon-success_with_warnings { - border-color: $orange-500; - color: $orange-500; - - &:hover, - &:focus, - &:active { - background-color: $orange-50; - border-color: $orange-600; - color: $orange-600; - - svg { - fill: $orange-600; - } - } + @include mini-pipeline-graph-color($orange-50, $orange-500, $orange-600); } &.ci-status-icon-running { - border-color: $blue-400; - color: $blue-400; - - &:hover, - &:focus, - &:active { - background-color: $blue-50; - border-color: $blue-600; - color: $blue-600; - - svg { - fill: $blue-600; - } - } + @include mini-pipeline-graph-color($blue-50, $blue-400, $blue-600); } &.ci-status-icon-canceled, &.ci-status-icon-disabled, &.ci-status-icon-not-found, &.ci-status-icon-manual { - border-color: $gl-text-color; - color: $gl-text-color; - - &:hover, - &:focus, - &:active { - background-color: rgba($gl-text-color, 0.1); - border-color: $gl-text-color; - } + @include mini-pipeline-graph-color(rgba($gl-text-color, 0.1), $gl-text-color, $gl-text-color); } &.ci-status-icon-created, &.ci-status-icon-skipped { - border-color: $gray-darkest; - color: $gray-darkest; - - &:hover, - &:focus, - &:active { - background-color: rgba($gray-darkest, 0.1); - border-color: $gray-darkest; - } + @include mini-pipeline-graph-color(rgba($gray-darkest, 0.1), $gray-darkest, $gray-darkest); } } @@ -841,6 +739,10 @@ top: 1px; vertical-align: text-bottom; position: relative; + + @media (max-width: $screen-xs-max) { + max-width: 60%; + } } // status icon on the left @@ -928,8 +830,14 @@ border-color: transparent; border-style: solid; top: -6px; - left: 2px; + left: 50%; + transform: translate(-50%, 0); border-width: 0 5px 6px; + + @media (max-width: $screen-sm-max) { + left: 100%; + margin-left: -12px; + } } &::before { @@ -944,6 +852,20 @@ } /** + * Center dropdown menu in mini graph + */ +.mini-pipeline-graph-dropdown-menu.dropdown-menu { + transform: translate(-80%, 0); + min-width: 150px; + + @media(min-width: $screen-md-min) { + transform: translate(-50%, 0); + right: auto; + left: 50%; + min-width: 240px; + } +} +/** * Terminal */ .terminal-icon { @@ -985,10 +907,17 @@ } } -.pipeline-header-container { +.ci-header-container { min-height: 55px; .text-center { padding-top: 12px; } + + .header-action-buttons { + .btn, + a { + margin-left: 10px; + } + } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index a2f781a6a6e..062665bc634 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -769,8 +769,7 @@ pre.light-well { } .project-refs-form .dropdown-menu, -.dropdown-menu-projects, -.dropdown-menu-branches { +.dropdown-menu-projects { width: 300px; @media (min-width: $screen-sm-min) { diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index f83fa421de5..085706a5001 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -56,7 +56,7 @@ &.expanded { max-height: none; - overflow-y: hidden; + overflow-y: visible; animation: expandMaxHeight 300ms ease-in; } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 4a284247143..67ad1ae60af 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -1,142 +1,82 @@ -.container-fluid { - .ci-status { - padding: 2px 7px 4px; - margin-right: 10px; - border: 1px solid $gray-darker; - white-space: nowrap; - border-radius: 4px; - - &:hover, - &:focus { - text-decoration: none; - } - - svg { - height: 13px; - width: 13px; - position: relative; - top: 2px; - overflow: visible; - } - - &.ci-failed, - &.ci-failed_with_warnings { - color: $red-500; - border-color: $red-500; +@mixin status-color($color-light, $color-main, $color-dark) { + color: $color-main; + border-color: $color-main; - &:not(span):hover { - background-color: $red-50; - color: $red-600; - border-color: $red-600; + &:not(span):hover { + background-color: $color-light; + color: $color-dark; + border-color: $color-dark; - svg { - fill: $red-600; - } - } - - svg { - fill: $red-500; - } + svg { + fill: $color-dark; } + } - &.ci-success, - &.ci-success_with_warnings { - color: $green-600; - border-color: $green-500; - - &:not(span):hover { - background-color: $green-50; - color: $green-700; - border-color: $green-600; - - svg { - fill: $green-600; - } - } - - svg { - fill: $green-500; - } - } + svg { + fill: $color-main; + } +} - &.ci-canceled, - &.ci-disabled { - color: $gl-text-color; - border-color: $gl-text-color; +.ci-status { + padding: 2px 7px 4px; + border: 1px solid $gray-darker; + white-space: nowrap; + border-radius: 4px; - &:not(span):hover { - background-color: rgba($gl-text-color, .07); - } + &:hover, + &:focus { + text-decoration: none; + } - svg { - fill: $gl-text-color; - } - } + svg { + height: 13px; + width: 13px; + position: relative; + top: 2px; + overflow: visible; + } - &.ci-pending { - color: $orange-600; - border-color: $orange-500; + &.ci-failed { + @include status-color($red-50, $red-500, $red-600); + } - &:not(span):hover { - background-color: $orange-50; - color: $orange-700; - border-color: $orange-600; + &.ci-success { + @include status-color($green-50, $green-500, $green-700); + } - svg { - fill: $orange-600; - } - } + &.ci-canceled, + &.ci-disabled, + &.ci-manual { + color: $gl-text-color; + border-color: $gl-text-color; - svg { - fill: $orange-500; - } + &:not(span):hover { + background-color: rgba($gl-text-color, .07); } + } - &.ci-info, - &.ci-running { - color: $blue-500; - border-color: $blue-500; - - &:not(span):hover { - background-color: $blue-50; - color: $blue-600; - border-color: $blue-600; - - svg { - fill: $blue-600; - } - } - - svg { - fill: $blue-500; - } - } + &.ci-pending, + &.ci-failed_with_warnings, + &.ci-success_with_warnings { + @include status-color($orange-50, $orange-500, $orange-700); + } - &.ci-created, - &.ci-skipped { - color: $gl-text-color-secondary; - border-color: $gl-text-color-secondary; + &.ci-info, + &.ci-running { + @include status-color($blue-50, $blue-500, $blue-600); + } - &:not(span):hover { - background-color: rgba($gl-text-color-secondary, .07); - } + &.ci-created, + &.ci-skipped { + color: $gl-text-color-secondary; + border-color: $gl-text-color-secondary; - svg { - fill: $gl-text-color-secondary; - } + &:not(span):hover { + background-color: rgba($gl-text-color-secondary, .07); } - &.ci-manual { - color: $gl-text-color; - border-color: $gl-text-color; - - &:not(span):hover { - background-color: rgba($gl-text-color, .07); - } - - svg { - fill: $gl-text-color; - } + svg { + fill: $gl-text-color-secondary; } } } diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index b64b89485f7..94d0a39f397 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -42,9 +42,7 @@ } .git-access-header { - padding: 16px 40px 11px 0; - line-height: 28px; - font-size: 18px; + padding: $gl-padding 0 $gl-padding-top; } .git-clone-holder { @@ -66,6 +64,7 @@ .git-clone-holder { width: 480px; + padding-bottom: $gl-padding; } .nav-controls { @@ -89,9 +88,9 @@ margin: $gl-padding 0; h3 { - font-size: 22px; + font-size: 19px; font-weight: normal; - margin-top: 1.4em; + margin: $gl-padding 0; } } diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 152d7baad49..4d4b8a8425f 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -100,6 +100,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :enabled_git_access_protocol, :gravatar_enabled, :help_page_text, + :help_page_hide_commercial_content, + :help_page_support_url, :home_page_url, :housekeeping_bitmaps_enabled, :housekeeping_enabled, @@ -149,6 +151,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :version_check_enabled, :terminal_max_session_time, :polling_interval_multiplier, + :prometheus_metrics_enabled, :usage_ping_enabled, disabled_oauth_sign_in_sources: [], diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb index ab212ed15d0..e5cba774dcb 100644 --- a/app/controllers/admin/deploy_keys_controller.rb +++ b/app/controllers/admin/deploy_keys_controller.rb @@ -1,6 +1,6 @@ class Admin::DeployKeysController < Admin::ApplicationController before_action :deploy_keys, only: [:index] - before_action :deploy_key, only: [:destroy] + before_action :deploy_key, only: [:destroy, :edit, :update] def index end @@ -10,12 +10,24 @@ class Admin::DeployKeysController < Admin::ApplicationController end def create - @deploy_key = deploy_keys.new(deploy_key_params.merge(user: current_user)) + @deploy_key = deploy_keys.new(create_params.merge(user: current_user)) if @deploy_key.save redirect_to admin_deploy_keys_path else - render "new" + render 'new' + end + end + + def edit + end + + def update + if deploy_key.update_attributes(update_params) + flash[:notice] = 'Deploy key was successfully updated.' + redirect_to admin_deploy_keys_path + else + render 'edit' end end @@ -38,7 +50,11 @@ class Admin::DeployKeysController < Admin::ApplicationController @deploy_keys ||= DeployKey.are_public end - def deploy_key_params + def create_params params.require(:deploy_key).permit(:key, :title, :can_push) end + + def update_params + params.require(:deploy_key).permit(:title, :can_push) + end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 47ce21d238b..91694ebcd1d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -9,6 +9,7 @@ class ApplicationController < ActionController::Base include SentryHelper include WorkhorseHelper include EnforcesTwoFactorAuthentication + include Peek::Rblineprof::CustomControllerHelpers before_action :authenticate_user_from_private_token! before_action :authenticate_user_from_rss_token! @@ -18,7 +19,7 @@ class ApplicationController < ActionController::Base before_action :ldap_security_check before_action :sentry_context before_action :default_headers - before_action :add_gon_variables + before_action :add_gon_variables, unless: -> { request.path.start_with?('/-/peek') } before_action :configure_permitted_parameters, if: :devise_controller? before_action :require_email, unless: :devise_controller? @@ -63,6 +64,21 @@ class ApplicationController < ActionController::Base end end + def peek_enabled? + return false unless Gitlab::PerformanceBar.enabled? + return false unless current_user + + if RequestStore.active? + if RequestStore.store.key?(:peek_enabled) + RequestStore.store[:peek_enabled] + else + RequestStore.store[:peek_enabled] = cookies[:perf_bar_enabled].present? + end + else + cookies[:perf_bar_enabled].present? + end + end + protected # This filter handles both private tokens and personal access tokens diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 907717dcb96..fe331a883c1 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -21,7 +21,7 @@ class AutocompleteController < ApplicationController @users = [current_user, *@users].uniq end - if params[:author_id].present? + if params[:author_id].present? && current_user author = User.find_by_id(params[:author_id]) @users = [author, *@users].uniq if author end diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 183eb00ef67..36ad307a93b 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -1,11 +1,6 @@ module CreatesCommit extend ActiveSupport::Concern - def set_start_branch_to_branch_name - branch_exists = @repository.find_branch(@branch_name) - @start_branch = @branch_name if branch_exists - end - def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) if can?(current_user, :push_code, @project) @project_to_commit_into = @project diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb index b17c138d5c7..404559c8707 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issues_action.rb @@ -14,7 +14,7 @@ module IssuesAction respond_to do |format| format.html - format.atom { render layout: false } + format.atom { render layout: 'xml.atom' } end end end diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb index cefb9b4e766..8d07780f6c2 100644 --- a/app/controllers/concerns/membership_actions.rb +++ b/app/controllers/concerns/membership_actions.rb @@ -52,9 +52,14 @@ module MembershipActions "You left the \"#{membershipable.human_name}\" #{source_type}." end - redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize] + respond_to do |format| + format.html do + redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize] + redirect_to redirect_path, notice: notice + end - redirect_to redirect_path, notice: notice + format.json { render json: { notice: notice } } + end end protected diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb index 3e2a0fe4f8b..b2536a1c949 100644 --- a/app/controllers/concerns/milestone_actions.rb +++ b/app/controllers/concerns/milestone_actions.rb @@ -46,8 +46,10 @@ module MilestoneActions def milestone_redirect_path if @project namespace_project_milestone_path(@project.namespace, @project, @milestone) - else + elsif @group group_milestone_path(@group, @milestone.safe_title, title: @milestone.title) + else + dashboard_milestone_path(@milestone.safe_title, title: @milestone.title) end end end diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb index d0a692070d9..b68d76aeff0 100644 --- a/app/controllers/concerns/spammable_actions.rb +++ b/app/controllers/concerns/spammable_actions.rb @@ -17,10 +17,18 @@ module SpammableActions private + def ensure_spam_config_loaded! + return @spam_config_loaded if defined?(@spam_config_loaded) + + @spam_config_loaded = Gitlab::Recaptcha.load_configurations! + end + def recaptcha_check_with_fallback(&fallback) if spammable.valid? redirect_to spammable elsif render_recaptcha? + ensure_spam_config_loaded! + if params[:recaptcha_verification] flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' end @@ -35,7 +43,7 @@ module SpammableActions default_params = { request: request } recaptcha_check = params[:recaptcha_verification] && - Gitlab::Recaptcha.load_configurations! && + ensure_spam_config_loaded! && verify_recaptcha return default_params unless recaptcha_check diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb index d03265e9f20..742157d113d 100644 --- a/app/controllers/dashboard/groups_controller.rb +++ b/app/controllers/dashboard/groups_controller.rb @@ -1,16 +1,30 @@ class Dashboard::GroupsController < Dashboard::ApplicationController def index - @group_members = current_user.group_members.includes(source: :route).joins(:group) - @group_members = @group_members.merge(Group.search(params[:filter_groups])) if params[:filter_groups].present? - @group_members = @group_members.merge(Group.sort(@sort = params[:sort])) - @group_members = @group_members.page(params[:page]) + @groups = + if params[:parent_id] && Group.supports_nested_groups? + parent = Group.find_by(id: params[:parent_id]) + + if can?(current_user, :read_group, parent) + GroupsFinder.new(current_user, parent: parent).execute + else + Group.none + end + else + current_user.groups + end + + @groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present? + @groups = @groups.includes(:route) + @groups = @groups.sort(@sort = params[:sort]) + @groups = @groups.page(params[:page]) respond_to do |format| format.html format.json do - render json: { - html: view_to_html_string("dashboard/groups/_groups", locals: { group_members: @group_members }) - } + render json: GroupSerializer + .new(current_user: @current_user) + .with_pagination(request, response) + .represent(@groups) end end end diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index df528d10f6e..751dbbd8e96 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -1,6 +1,8 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController + include MilestoneActions + before_action :projects - before_action :milestone, only: [:show] + before_action :milestone, only: [:show, :merge_requests, :participants, :labels] def index respond_to do |format| diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index 3d49ea97591..641c502dbe4 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -11,7 +11,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController format.html format.atom do load_events - render layout: false + render layout: 'xml.atom' end format.json do render json: { diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index 623392c1240..28c90548cc1 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -47,11 +47,6 @@ class Dashboard::TodosController < Dashboard::ApplicationController render json: todos_counts end - # Used in TodosHelper also - def self.todos_count_format(count) - count >= 100 ? '99+' : count - end - private def find_todos diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index c08943d993a..27137ffde54 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -58,7 +58,7 @@ class GroupsController < Groups::ApplicationController format.atom do load_events - render layout: false + render layout: 'xml.atom' end end end diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb index 125746d0426..abc832e6ddc 100644 --- a/app/controllers/health_controller.rb +++ b/app/controllers/health_controller.rb @@ -20,25 +20,8 @@ class HealthController < ActionController::Base render_check_results(results) end - def metrics - results = CHECKS.flat_map(&:metrics) - - response = results.map(&method(:metric_to_prom_line)).join("\n") - - render text: response, content_type: 'text/plain; version=0.0.4' - end - private - def metric_to_prom_line(metric) - labels = metric.labels&.map { |key, value| "#{key}=\"#{value}\"" }&.join(',') || '' - if labels.empty? - "#{metric.name} #{metric.value}" - else - "#{metric.name}{#{labels}} #{metric.value}" - end - end - def render_check_results(results) flattened = results.flat_map do |name, result| if result.is_a?(Gitlab::HealthChecks::Result) diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 1c01be06451..11db164b3fa 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -25,8 +25,10 @@ class JwtController < ApplicationController authenticate_with_http_basic do |login, password| @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip) - render_unauthorized unless @authentication_result.success? && - (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User)) + if @authentication_result.failed? || + (@authentication_result.actor.present? && !@authentication_result.actor.is_a?(User)) + render_unauthorized + end end rescue Gitlab::Auth::MissingPersonalTokenError render_missing_personal_token @@ -37,7 +39,7 @@ class JwtController < ApplicationController errors: [ { code: 'UNAUTHORIZED', message: "HTTP Basic: Access denied\n" \ - "You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \ + "You must use a personal access token with 'api' scope for Git over HTTP.\n" \ "You can generate one at #{profile_personal_access_tokens_url}" } ] }, status: 401 diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb new file mode 100644 index 00000000000..0e9a19c0b6f --- /dev/null +++ b/app/controllers/metrics_controller.rb @@ -0,0 +1,21 @@ +class MetricsController < ActionController::Base + include RequiresHealthToken + + protect_from_forgery with: :exception + + before_action :validate_prometheus_metrics + + def index + render text: metrics_service.metrics_text, content_type: 'text/plain; verssion=0.0.4' + end + + private + + def metrics_service + @metrics_service ||= MetricsService.new + end + + def validate_prometheus_metrics + render_404 unless Gitlab::Metrics.prometheus_metrics_enabled? + end +end diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index 0abe7ea3c9b..f748d191ef4 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -38,7 +38,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController end def set_index_vars - @scopes = Gitlab::Auth::API_SCOPES + @scopes = Gitlab::Auth::AVAILABLE_SCOPES @personal_access_token = finder.build @inactive_personal_access_tokens = finder(state: 'inactive').execute diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 8cd1c47eb3f..72f34930ca8 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -9,7 +9,7 @@ class ProfilesController < Profiles::ApplicationController end def update - user_params.except!(:email) if @user.ldap_user? + user_params.except!(:email) if @user.external_email? respond_to do |format| if @user.update_attributes(user_params) @@ -76,7 +76,7 @@ class ProfilesController < Profiles::ApplicationController end def user_params - params.require(:user).permit( + @user_params ||= params.require(:user).permit( :avatar, :bio, :email, diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index cb4bd0ad5f5..603a51266da 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -80,10 +80,6 @@ class Projects::ApplicationController < ApplicationController cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present? end - def builds_enabled - return render_404 unless @project.feature_available?(:builds, current_user) - end - def require_pages_enabled! not_found unless Gitlab.config.pages.enabled end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 7025c7a1de6..66e6a9a451c 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -26,8 +26,6 @@ class Projects::BlobController < Projects::ApplicationController end def create - set_start_branch_to_branch_name - create_commit(Files::CreateService, success_notice: "The file has been successfully created.", success_path: -> { namespace_project_blob_path(@project.namespace, @project, File.join(@branch_name, @file_path)) }, failure_view: :new, @@ -55,7 +53,7 @@ class Projects::BlobController < Projects::ApplicationController def edit if can_collaborate_with_project? - blob.load_all_data!(@repository) + blob.load_all_data! else redirect_to action: 'show' end @@ -74,7 +72,7 @@ class Projects::BlobController < Projects::ApplicationController def preview @content = params[:content] - @blob.load_all_data!(@repository) + @blob.load_all_data! diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', include_diff_info: true) diff_lines = diffy.diff.scan(/.*\n/)[2..-1] diff_lines = Gitlab::Diff::Parser.new.parse(diff_lines) @@ -93,9 +91,11 @@ class Projects::BlobController < Projects::ApplicationController def diff apply_diff_view_cookie! - @form = UnfoldForm.new(params) - @lines = Gitlab::Highlight.highlight_lines(repository, @ref, @path) - @lines = @lines[@form.since - 1..@form.to - 1] + @blob.load_all_data! + @lines = Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: @repository).lines + + @form = UnfoldForm.new(params) + @lines = @lines[@form.since - 1..@form.to - 1].map(&:html_safe) if @form.bottom? @match_line = '' @@ -111,7 +111,7 @@ class Projects::BlobController < Projects::ApplicationController private def blob - @blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path), @project) + @blob ||= @repository.blob_at(@commit.id, @path) if @blob @blob diff --git a/app/controllers/projects/boards/lists_controller.rb b/app/controllers/projects/boards/lists_controller.rb index 67e3c9add81..ad53bb749a0 100644 --- a/app/controllers/projects/boards/lists_controller.rb +++ b/app/controllers/projects/boards/lists_controller.rb @@ -5,7 +5,9 @@ module Projects before_action :authorize_read_list!, only: [:index] def index - render json: serialize_as_json(board.lists) + lists = ::Boards::Lists::ListService.new(project, current_user).execute(board) + + render json: serialize_as_json(lists) end def create diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index d8ed470e461..70b06cfd9b4 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -10,10 +10,10 @@ class Projects::BranchesController < Projects::ApplicationController def index @sort = params[:sort].presence || sort_value_name @branches = BranchesFinder.new(@repository, params).execute + @branches = Kaminari.paginate_array(@branches).page(params[:page]) respond_to do |format| format.html do - paginate_branches @refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name)) @max_commits = @branches.reduce(0) do |memo, branch| @@ -22,7 +22,6 @@ class Projects::BranchesController < Projects::ApplicationController end end format.json do - paginate_branches unless params[:show_all] render json: @branches.map(&:name) end end @@ -106,10 +105,6 @@ class Projects::BranchesController < Projects::ApplicationController end end - def paginate_branches - @branches = Kaminari.paginate_array(@branches).page(params[:page]) - end - def url_to_autodeploy_setup(project, branch_name) namespace_project_new_blob_path( project.namespace, diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index ad92f05a42d..f33797ca310 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -26,7 +26,7 @@ class Projects::CommitsController < Projects::ApplicationController respond_to do |format| format.html - format.atom { render layout: false } + format.atom { render layout: 'xml.atom' } format.json do pager_json( diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index f27089b8590..7f1469e107d 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -4,6 +4,7 @@ class Projects::DeployKeysController < Projects::ApplicationController # Authorize before_action :authorize_admin_project! + before_action :authorize_update_deploy_key!, only: [:edit, :update] layout "project_settings" @@ -21,7 +22,7 @@ class Projects::DeployKeysController < Projects::ApplicationController end def create - @key = DeployKey.new(deploy_key_params.merge(user: current_user)) + @key = DeployKey.new(create_params.merge(user: current_user)) unless @key.valid? && @project.deploy_keys << @key flash[:alert] = @key.errors.full_messages.join(', ').html_safe @@ -29,6 +30,18 @@ class Projects::DeployKeysController < Projects::ApplicationController redirect_to_repository_settings(@project) end + def edit + end + + def update + if deploy_key.update_attributes(update_params) + flash[:notice] = 'Deploy key was successfully updated.' + redirect_to_repository_settings(@project) + else + render 'edit' + end + end + def enable Projects::EnableDeployKeyService.new(@project, current_user, params).execute @@ -52,7 +65,19 @@ class Projects::DeployKeysController < Projects::ApplicationController protected - def deploy_key_params + def deploy_key + @deploy_key ||= @project.deploy_keys.find(params[:id]) + end + + def create_params params.require(:deploy_key).permit(:key, :title, :can_push) end + + def update_params + params.require(:deploy_key).permit(:title, :can_push) + end + + def authorize_update_deploy_key! + access_denied! unless can?(current_user, :update_deploy_key, deploy_key) + end end diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 7f3205a8001..928f17e6a8e 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -104,7 +104,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController def render_missing_personal_token render plain: "HTTP Basic: Access denied\n" \ - "You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \ + "You must use a personal access token with 'api' scope for Git over HTTP.\n" \ "You can generate one at #{profile_personal_access_tokens_url}", status: 401 end diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index 43fc0c39801..df5221fe95f 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -5,7 +5,6 @@ class Projects::GraphsController < Projects::ApplicationController before_action :require_non_empty_project before_action :assign_ref_vars before_action :authorize_download_code! - before_action :builds_enabled, only: :ci def show respond_to do |format| diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 8b1efd0c572..56f76e752d0 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -10,11 +10,7 @@ class Projects::IssuesController < Projects::ApplicationController before_action :redirect_to_external_issue_tracker, only: [:index, :new] before_action :module_enabled - before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, - :related_branches, :can_create_branch, :realtime_changes, :create_merge_request] - - # Allow read any issue - before_action :authorize_read_issue!, only: [:show, :realtime_changes] + before_action :issue, except: [:index, :new, :create, :bulk_update] # Allow write(create) issue before_action :authorize_create_issue!, only: [:new, :create] @@ -55,7 +51,7 @@ class Projects::IssuesController < Projects::ApplicationController respond_to do |format| format.html - format.atom { render layout: false } + format.atom { render layout: 'xml.atom' } format.json do render json: { html: view_to_html_string("projects/issues/_issues"), @@ -229,18 +225,19 @@ class Projects::IssuesController < Projects::ApplicationController protected def issue + return @issue if defined?(@issue) # The Sortable default scope causes performance issues when used with find_by @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take! + + return render_404 unless can?(current_user, :read_issue, @issue) + + @issue end alias_method :subscribable_resource, :issue alias_method :issuable, :issue alias_method :awardable, :issue alias_method :spammable, :issue - def authorize_read_issue! - return render_404 unless can?(current_user, :read_issue, @issue) - end - def authorize_update_issue! return render_404 unless can?(current_user, :update_issue, @issue) end diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index ac151839f61..1beac202efe 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -8,7 +8,7 @@ class Projects::LabelsController < Projects::ApplicationController before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :generate, :destroy, :remove_priority, :set_priorities] - before_action :authorize_admin_group!, only: [:promote] + before_action :authorize_admin_group_labels!, only: [:promote] respond_to :js, :html @@ -161,7 +161,7 @@ class Projects::LabelsController < Projects::ApplicationController 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) + def authorize_admin_group_labels! + return render_404 unless can?(current_user, :admin_label, @project.group) end end diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb index 2662a146968..ef4f083b98f 100644 --- a/app/controllers/projects/pipeline_schedules_controller.rb +++ b/app/controllers/projects/pipeline_schedules_controller.rb @@ -43,7 +43,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController if schedule.update(owner: current_user) redirect_to pipeline_schedules_path(@project) else - redirect_to pipeline_schedules_path(@project), alert: "Failed to change the owner" + redirect_to pipeline_schedules_path(@project), alert: _("Failed to change the owner") end end @@ -53,7 +53,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController else redirect_to pipeline_schedules_path(@project), status: 302, - alert: "Failed to remove the pipeline schedule" + alert: _("Failed to remove the pipeline schedule") end end diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 87ec0df257a..8effb792689 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -4,7 +4,6 @@ class Projects::PipelinesController < Projects::ApplicationController before_action :authorize_read_pipeline! before_action :authorize_create_pipeline!, only: [:new, :create] before_action :authorize_update_pipeline!, only: [:retry, :cancel] - before_action :builds_enabled, only: :charts wrap_parameters Ci::Pipeline @@ -99,7 +98,7 @@ class Projects::PipelinesController < Projects::ApplicationController end def stage - @stage = pipeline.stage(params[:stage]) + @stage = pipeline.legacy_stage(params[:stage]) return not_found unless @stage respond_to do |format| diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 84deefb7875..8a8f8d6a27d 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -107,6 +107,6 @@ class Projects::SnippetsController < Projects::ApplicationController end def snippet_params - params.require(:project_snippet).permit(:title, :content, :file_name, :private, :visibility_level) + params.require(:project_snippet).permit(:title, :content, :file_name, :private, :visibility_level, :description) end end diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index f8eb8e00a5d..266a15c1cf9 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -36,7 +36,6 @@ class Projects::TreeController < Projects::ApplicationController def create_dir return render_404 unless @commit_params.values.all? - set_start_branch_to_branch_name create_commit(Files::CreateDirService, success_notice: "The directory has been successfully created.", success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@branch_name, @dir_name)), failure_path: namespace_project_tree_path(@project.namespace, @project, @ref)) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index aada031f117..5480814874b 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -34,7 +34,7 @@ class ProjectsController < Projects::ApplicationController redirect_to( project_path(@project), - notice: "Project '#{@project.name}' was successfully created." + notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name } ) else render 'new' @@ -49,7 +49,7 @@ class ProjectsController < Projects::ApplicationController respond_to do |format| if result[:status] == :success - flash[:notice] = "Project '#{@project.name}' was successfully updated." + flash[:notice] = _("Project '%{project_name}' was successfully updated.") % { project_name: @project.name } format.html do redirect_to(edit_project_path(@project)) end @@ -76,7 +76,7 @@ class ProjectsController < Projects::ApplicationController return access_denied! unless can?(current_user, :remove_fork_project, @project) if ::Projects::UnlinkForkService.new(@project, current_user).execute - flash[:notice] = 'The fork relationship has been removed.' + flash[:notice] = _('The fork relationship has been removed.') end end @@ -97,7 +97,7 @@ class ProjectsController < Projects::ApplicationController end if @project.pending_delete? - flash[:alert] = "Project #{@project.name} queued for deletion." + flash[:alert] = _("Project '%{project_name}' queued for deletion.") % { project_name: @project.name } end respond_to do |format| @@ -108,7 +108,7 @@ class ProjectsController < Projects::ApplicationController format.atom do load_events - render layout: false + render layout: 'xml.atom' end end end @@ -117,7 +117,7 @@ class ProjectsController < Projects::ApplicationController return access_denied! unless can?(current_user, :remove_project, @project) ::Projects::DestroyService.new(@project, current_user, {}).async_execute - flash[:alert] = "Project '#{@project.name_with_namespace}' will be deleted." + flash[:alert] = _("Project '%{project_name}' will be deleted.") % { project_name: @project.name_with_namespace } redirect_to dashboard_projects_path, status: 302 rescue Projects::DestroyService::DestroyError => ex @@ -156,7 +156,7 @@ class ProjectsController < Projects::ApplicationController redirect_to( project_path(@project), - notice: "Housekeeping successfully started" + notice: _("Housekeeping successfully started") ) rescue ::Projects::HousekeepingService::LeaseTaken => ex redirect_to( @@ -170,7 +170,7 @@ class ProjectsController < Projects::ApplicationController redirect_to( edit_project_path(@project), - notice: "Project export started. A download link will be sent by email." + notice: _("Project export started. A download link will be sent by email.") ) end @@ -182,16 +182,16 @@ class ProjectsController < Projects::ApplicationController else redirect_to( edit_project_path(@project), - alert: "Project export link has expired. Please generate a new export from your project settings." + alert: _("Project export link has expired. Please generate a new export from your project settings.") ) end end def remove_export if @project.remove_exports - flash[:notice] = "Project export has been deleted." + flash[:notice] = _("Project export has been deleted.") else - flash[:alert] = "Project export could not be deleted." + flash[:alert] = _("Project export could not be deleted.") end redirect_to(edit_project_path(@project)) end @@ -202,7 +202,7 @@ class ProjectsController < Projects::ApplicationController else redirect_to( edit_project_path(@project), - alert: "Project export could not be deleted." + alert: _("Project export could not be deleted.") ) end end @@ -220,13 +220,13 @@ class ProjectsController < Projects::ApplicationController branches = BranchesFinder.new(@repository, params).execute.map(&:name) options = { - 'Branches' => branches.take(100) + s_('RefSwitcher|Branches') => branches.take(100) } unless @repository.tag_count.zero? tags = TagsFinder.new(@repository, params).execute.map(&:name) - options['Tags'] = tags.take(100) + options[s_('RefSwitcher|Tags')] = tags.take(100) end # If reference is commit id - we should add it to branch/tag selectbox diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 10806895764..d7c702b94f8 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -47,6 +47,10 @@ class SessionsController < Devise::SessionsController private + def login_counter + @login_counter ||= Gitlab::Metrics.counter(:user_session_logins, 'User sign in count') + end + # Handle an "initial setup" state, where there's only one user, it's an admin, # and they require a password change. def check_initial_setup @@ -129,6 +133,7 @@ class SessionsController < Devise::SessionsController end def log_user_activity(user) + login_counter.increment Users::ActivityService.new(user, 'login').execute end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 8511457b53d..3d86dd2ea2c 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -45,6 +45,8 @@ class SnippetsController < ApplicationController @snippet = CreateSnippetService.new(nil, current_user, create_params).execute + move_temporary_files if @snippet.valid? && params[:files] + recaptcha_check_with_fallback { render :new } end @@ -124,6 +126,12 @@ class SnippetsController < ApplicationController end def snippet_params - params.require(:personal_snippet).permit(:title, :content, :file_name, :private, :visibility_level) + params.require(:personal_snippet).permit(:title, :content, :file_name, :private, :visibility_level, :description) + end + + def move_temporary_files + params[:files].each do |file| + FileMover.new(file, @snippet).execute + end end end diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index eef53730291..dc882b17143 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -9,12 +9,16 @@ class UploadsController < ApplicationController private def find_model + return nil unless params[:id] + return render_404 unless upload_model && upload_mount @model = upload_model.find(params[:id]) end def authorize_access! + return nil unless model + authorized = case model when Note @@ -33,6 +37,8 @@ class UploadsController < ApplicationController end def authorize_create_access! + return nil unless model + # for now we support only personal snippets comments authorized = can?(current_user, :comment_personal_snippet, model) @@ -73,7 +79,12 @@ class UploadsController < ApplicationController def uploader return @uploader if defined?(@uploader) - if model.is_a?(PersonalSnippet) + case model + when nil + @uploader = PersonalFileUploader.new(nil, params[:secret]) + + @uploader.retrieve_from_store!(params[:filename]) + when PersonalSnippet @uploader = PersonalFileUploader.new(model, params[:secret]) @uploader.retrieve_from_store!(params[:filename]) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 19fc1e5de49..c211106fbaa 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -10,7 +10,7 @@ class UsersController < ApplicationController format.atom do load_events - render layout: false + render layout: 'xml.atom' end format.json do diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 71154da7ec5..2bfc7586adc 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -204,6 +204,10 @@ module ApplicationHelper 'https://' + promo_host end + def support_url + current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/' + end + def page_filter_path(options = {}) without = options.delete(:without) add_label = options.delete(:label) diff --git a/app/helpers/blame_helper.rb b/app/helpers/blame_helper.rb new file mode 100644 index 00000000000..d1dc4d94560 --- /dev/null +++ b/app/helpers/blame_helper.rb @@ -0,0 +1,21 @@ +module BlameHelper + def age_map_duration(blame_groups, project) + now = Time.zone.now + start_date = blame_groups.map { |blame_group| blame_group[:commit].committed_date } + .append(project.created_at).min + + { + now: now, + started_days_ago: (now - start_date).to_i / 1.day + } + end + + def age_map_class(commit_date, duration) + commit_date_days_ago = (duration[:now] - commit_date).to_i / 1.day + # Numbers 0 to 10 come from this calculation, but only commits on the oldest + # day get number 10 (all other numbers can be multiple days), so the range + # is normalized to 0-9 + age_group = [(10 * commit_date_days_ago) / duration[:started_days_ago], 9].min + "blame-commit-age-#{age_group}" + end +end diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb index eb03ced67eb..0a15c29cfb5 100644 --- a/app/helpers/broadcast_messages_helper.rb +++ b/app/helpers/broadcast_messages_helper.rb @@ -1,5 +1,5 @@ module BroadcastMessagesHelper - def broadcast_message(message = BroadcastMessage.current) + def broadcast_message(message) return unless message.present? content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 0081bbd92b3..00464810054 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -61,7 +61,7 @@ module ButtonHelper html: true, placement: placement, container: 'body', - title: "Set a password on your account<br>to pull or push via #{protocol}" + title: _("Set a password on your account to pull or push via %{protocol}") % { protocol: protocol } } end @@ -76,7 +76,7 @@ module ButtonHelper html: true, placement: placement, container: 'body', - title: 'Add an SSH key to your profile<br>to pull or push via SSH.' + title: _('Add an SSH key to your profile to pull or push via SSH.') } end end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 32b1e7822af..21c0eb8b54c 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -16,16 +16,18 @@ module CiStatusHelper return status.label end - case status - when 'success' - 'passed' - when 'success_with_warnings' - 'passed with warnings' - when 'manual' - 'waiting for manual action' - else - status - end + label = case status + when 'success' + 'passed' + when 'success_with_warnings' + 'passed with warnings' + when 'manual' + 'waiting for manual action' + else + status + end + translation = "CiStatusLabel|#{label}" + s_(translation) end def ci_text_for_status(status) @@ -35,13 +37,22 @@ module CiStatusHelper case status when 'success' - 'passed' + s_('CiStatusText|passed') when 'success_with_warnings' - 'passed' + s_('CiStatusText|passed') when 'manual' - 'blocked' + s_('CiStatusText|blocked') else - status + # All states are already being translated inside the detailed statuses: + # :running => Gitlab::Ci::Status::Running + # :skipped => Gitlab::Ci::Status::Skipped + # :failed => Gitlab::Ci::Status::Failed + # :success => Gitlab::Ci::Status::Success + # :canceled => Gitlab::Ci::Status::Canceled + # The following states are customized above: + # :manual => Gitlab::Ci::Status::Manual + status_translation = "CiStatusText|#{status}" + s_(status_translation) end end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 2ae3a616933..06822747d11 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -124,6 +124,30 @@ module DiffHelper !diff_file.deleted_file? && @merge_request && @merge_request.source_project end + def diff_render_error_reason(viewer) + case viewer.render_error + when :too_large + "it is too large" + when :server_side_but_stored_externally + case viewer.diff_file.external_storage + when :lfs + 'it is stored in LFS' + else + 'it is stored externally' + end + end + end + + def diff_render_error_options(viewer) + diff_file = viewer.diff_file + options = [] + + blob_url = namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.content_sha, diff_file.file_path)) + options << link_to('view the blob', blob_url) + + options + end + private def diff_btn(title, name, selected) diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 3b24f183785..fdbca789d21 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -66,4 +66,17 @@ module EmailsHelper ) end end + + def email_default_heading(text) + content_tag :h1, text, style: [ + "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif", + 'color:#333333', + 'font-size:18px', + 'font-weight:400', + 'line-height:1.4', + 'padding:0', + 'margin:0', + 'text-align:center' + ].join(';') + end end diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index 53962b84618..014fc46b130 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -29,7 +29,7 @@ module FormHelper current_user: true, project_id: issuable.project.try(:id), field_name: "#{issuable.class.model_name.param_key}[assignee_ids][]", - default_label: 'Assignee', + default_label: 'Unassigned', 'max-select': 1, 'dropdown-header': 'Assignee', multi_select: true, diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 40864bed0ff..8c7af62e199 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -128,7 +128,7 @@ module GitlabRoutingHelper def preview_markdown_path(project, *args) if @snippet.is_a?(PersonalSnippet) - preview_markdown_snippet_path(@snippet) + preview_markdown_snippets_path else preview_markdown_namespace_project_path(project.namespace, project, *args) end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index a6014088e92..c003b01e226 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -8,7 +8,7 @@ module GroupsHelper group = Group.find_by_full_path(group) end - group.try(:avatar_url) || image_path('no_group_avatar.png') + group.try(:avatar_url) || ActionController::Base.helpers.image_path('no_group_avatar.png') end def group_title(group, name = nil, url = nil) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index c515774140c..a230db22fa2 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -121,6 +121,8 @@ module MilestonesHelper merge_requests_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json) elsif @group merge_requests_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json) + else + merge_requests_dashboard_milestone_path(milestone, title: milestone.title, format: :json) end end @@ -129,6 +131,8 @@ module MilestonesHelper participants_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json) elsif @group participants_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json) + else + participants_dashboard_milestone_path(milestone, title: milestone.title, format: :json) end end @@ -137,6 +141,8 @@ module MilestonesHelper labels_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json) elsif @group labels_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json) + else + labels_dashboard_milestone_path(milestone, title: milestone.title, format: :json) end end end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 17bfd07e00f..833d3c36b28 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -13,7 +13,7 @@ module NavHelper else "page-gutter right-sidebar-expanded" end - elsif current_path?('builds#show') + elsif current_path?('jobs#show') "page-gutter build-sidebar right-sidebar-expanded" elsif current_path?('wikis#show') || current_path?('wikis#edit') || @@ -27,6 +27,7 @@ module NavHelper def nav_header_class class_name = '' class_name << " with-horizontal-nav" if defined?(nav) && nav + class_name << " with-peek" if peek_enabled? class_name end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 3d4802290b5..c59d8dafc83 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -90,14 +90,18 @@ module NotesHelper end end - def note_url(note) + def note_url(note, project = @project) if note.noteable.is_a?(PersonalSnippet) snippet_note_path(note.noteable, note) else - namespace_project_note_path(@project.namespace, @project, note) + namespace_project_note_path(project.namespace, project, note) end end + def noteable_note_url(note) + Gitlab::UrlBuilder.build(note) + end + def form_resources if @snippet.is_a?(PersonalSnippet) [@note] diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 03cc8f2b6bd..fde961e2da4 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -21,30 +21,36 @@ module NotificationsHelper end def notification_title(level) + # Can be anything in `NotificationSetting.level: case level.to_sym when :participating - 'Participate' + s_('NotificationLevel|Participate') when :mention - 'On mention' + s_('NotificationLevel|On mention') else - level.to_s.titlecase + N_('NotificationLevel|Global') + N_('NotificationLevel|Watch') + N_('NotificationLevel|Disabled') + N_('NotificationLevel|Custom') + level = "NotificationLevel|#{level.to_s.humanize}" + s_(level) end end def notification_description(level) case level.to_sym when :participating - 'You will only receive notifications for threads you have participated in' + _('You will only receive notifications for threads you have participated in') when :mention - 'You will receive notifications only for comments in which you were @mentioned' + _('You will receive notifications only for comments in which you were @mentioned') when :watch - 'You will receive notifications for any activity' + _('You will receive notifications for any activity') when :disabled - 'You will not get any notifications via email' + _('You will not get any notifications via email') when :global - 'Use your global notification setting' + _('Use your global notification setting') when :custom - 'You will only receive notifications for the events you choose' + _('You will only receive notifications for the events you choose') end end @@ -76,11 +82,22 @@ module NotificationsHelper end def notification_event_name(event) + # All values from NotificationSetting::EMAIL_EVENTS case event when :success_pipeline - 'Successful pipeline' + s_('NotificationEvent|Successful pipeline') else - event.to_s.humanize + N_('NotificationEvent|New note') + N_('NotificationEvent|New issue') + N_('NotificationEvent|Reopen issue') + N_('NotificationEvent|Close issue') + N_('NotificationEvent|Reassign issue') + N_('NotificationEvent|New merge request') + N_('NotificationEvent|Close merge request') + N_('NotificationEvent|Reassign merge request') + N_('NotificationEvent|Merge merge request') + N_('NotificationEvent|Failed pipeline') + s_(event.to_s.humanize) end end end diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb new file mode 100644 index 00000000000..45238f12ac7 --- /dev/null +++ b/app/helpers/profiles_helper.rb @@ -0,0 +1,7 @@ +module ProfilesHelper + def email_provider_label + return unless current_user.external_email? + + current_user.email_provider.present? ? Gitlab::OAuth::Provider.label_for(current_user.email_provider) : "LDAP" + end +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index f74e61c9481..c11dd49f4a7 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -70,15 +70,18 @@ module ProjectsHelper end def remove_project_message(project) - "You are going to remove #{project.name_with_namespace}.\n Removed project CANNOT be restored!\n Are you ABSOLUTELY sure?" + _("You are going to remove %{project_name_with_namespace}.\nRemoved project CANNOT be restored!\nAre you ABSOLUTELY sure?") % + { project_name_with_namespace: project.name_with_namespace } end def transfer_project_message(project) - "You are going to transfer #{project.name_with_namespace} to another owner. Are you ABSOLUTELY sure?" + _("You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?") % + { project_name_with_namespace: project.name_with_namespace } end def remove_fork_project_message(project) - "You are going to remove the fork relationship to source project #{@project.forked_from_project.name_with_namespace}. Are you ABSOLUTELY sure?" + _("You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?") % + { forked_from_project: @project.forked_from_project.name_with_namespace } end def project_nav_tabs @@ -143,7 +146,7 @@ module ProjectsHelper end options = options_for_select( - options, + options.invert, selected: highest_available_option || @project.project_feature.public_send(field), disabled: disabled_option ) @@ -159,12 +162,13 @@ module ProjectsHelper end def link_to_autodeploy_doc - link_to 'About auto deploy', help_page_path('ci/autodeploy/index'), target: '_blank' + link_to _('About auto deploy'), help_page_path('ci/autodeploy/index'), target: '_blank' end def autodeploy_flash_notice(branch_name) - "Branch <strong>#{truncate(sanitize(branch_name))}</strong> was created. To set up auto deploy, \ - choose a GitLab CI Yaml template and commit your changes. #{link_to_autodeploy_doc}".html_safe + translation = _("Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}") % + { branch_name: truncate(sanitize(branch_name)), link_to_autodeploy_doc: link_to_autodeploy_doc } + translation.html_safe end def project_list_cache_key(project) @@ -214,6 +218,10 @@ module ProjectsHelper nav_tabs << :container_registry end + if project.builds_enabled? && can?(current_user, :read_pipeline, project) + nav_tabs << :pipelines + end + tab_ability_map.each do |tab, ability| if can?(current_user, ability, project) nav_tabs << tab @@ -227,7 +235,6 @@ module ProjectsHelper { environments: :read_environment, milestones: :read_milestone, - pipelines: :read_pipeline, snippets: :read_project_snippet, settings: :admin_project, builds: :read_build, @@ -250,11 +257,11 @@ module ProjectsHelper def project_lfs_status(project) if project.lfs_enabled? content_tag(:span, class: 'lfs-enabled') do - 'Enabled' + s_('LFSStatus|Enabled') end else content_tag(:span, class: 'lfs-disabled') do - 'Disabled' + s_('LFSStatus|Disabled') end end end @@ -263,7 +270,7 @@ module ProjectsHelper if current_user current_user.name else - "Your name" + _("Your name") end end @@ -300,17 +307,18 @@ module ProjectsHelper if project.last_activity_at time_ago_with_tooltip(project.last_activity_at, placement: 'bottom', html_class: 'last_activity_time_ago') else - "Never" + s_("ProjectLastActivity|Never") end end def add_special_file_path(project, file_name:, commit_message: nil, branch_name: nil, context: nil) + commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name.downcase } namespace_project_new_blob_path( project.namespace, project, project.default_branch || 'master', file_name: file_name, - commit_message: commit_message || "Add #{file_name.downcase}", + commit_message: commit_message, branch_name: branch_name, context: context ) @@ -447,9 +455,9 @@ module ProjectsHelper def project_feature_options { - 'Disabled' => ProjectFeature::DISABLED, - 'Only team members' => ProjectFeature::PRIVATE, - 'Everyone with access' => ProjectFeature::ENABLED + ProjectFeature::DISABLED => s_('ProjectFeature|Disabled'), + ProjectFeature::PRIVATE => s_('ProjectFeature|Only team members'), + ProjectFeature::ENABLED => s_('ProjectFeature|Everyone with access') } end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 19286fadb19..3d1b3a4711a 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -4,7 +4,7 @@ module TodosHelper end def todos_count_format(count) - count > 99 ? '99+' : count + count > 99 ? '99+' : count.to_s end def todos_done_count diff --git a/app/helpers/u2f_helper.rb b/app/helpers/u2f_helper.rb index 143b4ca6b51..81bfe5d4eeb 100644 --- a/app/helpers/u2f_helper.rb +++ b/app/helpers/u2f_helper.rb @@ -1,5 +1,5 @@ module U2fHelper def inject_u2f_api? - browser.chrome? && browser.version.to_i >= 41 && !browser.device.mobile? + ((browser.chrome? && browser.version.to_i >= 41) || (browser.opera? && browser.version.to_i >= 40)) && !browser.device.mobile? end end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index 50757b01538..35755bc149b 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -29,11 +29,11 @@ module VisibilityLevelHelper def project_visibility_level_description(level) case level when Gitlab::VisibilityLevel::PRIVATE - "Project access must be granted explicitly to each user." + _("Project access must be granted explicitly to each user.") when Gitlab::VisibilityLevel::INTERNAL - "The project can be accessed by any logged in user." + _("The project can be accessed by any logged in user.") when Gitlab::VisibilityLevel::PUBLIC - "The project can be accessed without any authentication." + _("The project can be accessed without any authentication.") end end @@ -81,7 +81,9 @@ module VisibilityLevelHelper end def visibility_level_label(level) - Project.visibility_levels.key(level) + # The visibility level can be: + # 'VisibilityLevel|Private', 'VisibilityLevel|Internal', 'VisibilityLevel|Public' + s_(Project.visibility_levels.key(level)) end def restricted_visibility_levels(show_all = false) diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb index f7ed61625f4..962570a0efd 100644 --- a/app/mailers/devise_mailer.rb +++ b/app/mailers/devise_mailer.rb @@ -2,7 +2,9 @@ class DeviseMailer < Devise::Mailer default from: "#{Gitlab.config.gitlab.email_display_name} <#{Gitlab.config.gitlab.email_from}>" default reply_to: Gitlab.config.gitlab.email_reply_to - layout 'devise_mailer' + layout 'mailer/devise' + + helper EmailsHelper protected diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 2192f76499d..668caef0d2c 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -37,7 +37,12 @@ class ApplicationSetting < ActiveRecord::Base validates :home_page_url, allow_blank: true, url: true, - if: :home_page_url_column_exist + if: :home_page_url_column_exists? + + validates :help_page_support_url, + allow_blank: true, + url: true, + if: :help_page_support_url_column_exists? validates :after_sign_out_path, allow_blank: true, @@ -215,6 +220,7 @@ class ApplicationSetting < ActiveRecord::Base domain_whitelist: Settings.gitlab['domain_whitelist'], gravatar_enabled: Settings.gravatar['enabled'], help_page_text: nil, + help_page_hide_commercial_content: false, unique_ips_limit_per_user: 10, unique_ips_limit_time_window: 3600, unique_ips_limit_enabled: false, @@ -263,10 +269,14 @@ class ApplicationSetting < ActiveRecord::Base end end - def home_page_url_column_exist + def home_page_url_column_exists? ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url) end + def help_page_support_url_column_exists? + ActiveRecord::Base.connection.column_exists?(:application_settings, :help_page_support_url) + end + def sidekiq_throttling_column_exists? ActiveRecord::Base.connection.column_exists?(:application_settings, :sidekiq_throttling_enabled) end diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index 6ada6fae4eb..ebe60441603 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -5,7 +5,7 @@ class AwardEmoji < ActiveRecord::Base include Participable include GhostUser - belongs_to :awardable, polymorphic: true + belongs_to :awardable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :user validates :awardable, :user, presence: true diff --git a/app/models/blob.rb b/app/models/blob.rb index 6a42a12891c..954d4e4d779 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -94,6 +94,10 @@ class Blob < SimpleDelegator end end + def load_all_data! + super(project.repository) if project + end + def no_highlighting? raw_size && raw_size > MAXIMUM_TEXT_HIGHLIGHT_SIZE end @@ -151,6 +155,10 @@ class Blob < SimpleDelegator @extension ||= extname.downcase.delete('.') end + def file_type + Gitlab::FileDetector.type_of(path) + end + def video? UploaderHelper::VIDEO_EXT.include?(extension) end @@ -176,16 +184,19 @@ class Blob < SimpleDelegator end def rendered_as_text?(ignore_errors: true) - simple_viewer.text? && (ignore_errors || simple_viewer.render_error.nil?) + simple_viewer.is_a?(BlobViewer::Text) && (ignore_errors || simple_viewer.render_error.nil?) end def show_viewer_switcher? rendered_as_text? && rich_viewer end + def expanded? + !!@expanded + end + def expand! - simple_viewer&.expanded = true - rich_viewer&.expanded = true + @expanded = true end private diff --git a/app/models/blob_viewer/base.rb b/app/models/blob_viewer/base.rb index e6119d25fab..35965d01692 100644 --- a/app/models/blob_viewer/base.rb +++ b/app/models/blob_viewer/base.rb @@ -6,15 +6,15 @@ module BlobViewer self.loading_partial_name = 'loading' - delegate :partial_path, :loading_partial_path, :rich?, :simple?, :text?, :binary?, to: :class + delegate :partial_path, :loading_partial_path, :rich?, :simple?, :load_async?, :text?, :binary?, to: :class attr_reader :blob - attr_accessor :expanded delegate :project, to: :blob def initialize(blob) @blob = blob + @initially_binary = blob.binary? end def self.partial_path @@ -52,19 +52,15 @@ module BlobViewer def self.can_render?(blob, verify_binary: true) return false if verify_binary && binary? != blob.binary? return true if extensions&.include?(blob.extension) - return true if file_types&.include?(Gitlab::FileDetector.type_of(blob.path)) + return true if file_types&.include?(blob.file_type) false end - def load_async? - self.class.load_async? && render_error.nil? - end - def collapsed? return @collapsed if defined?(@collapsed) - @collapsed = !expanded && collapse_limit && blob.raw_size > collapse_limit + @collapsed = !blob.expanded? && collapse_limit && blob.raw_size > collapse_limit end def too_large? @@ -73,6 +69,10 @@ module BlobViewer @too_large = size_limit && blob.raw_size > size_limit end + def binary_detected_after_load? + !@initially_binary && blob.binary? + end + # This method is used on the server side to check whether we can attempt to # render the blob at all. Human-readable error messages are found in the # `BlobHelper#blob_render_error_reason` helper. diff --git a/app/models/blob_viewer/empty.rb b/app/models/blob_viewer/empty.rb index d9d128eb273..2380578ed72 100644 --- a/app/models/blob_viewer/empty.rb +++ b/app/models/blob_viewer/empty.rb @@ -4,6 +4,5 @@ module BlobViewer include ServerSide self.partial_name = 'empty' - self.binary = true end end diff --git a/app/models/blob_viewer/server_side.rb b/app/models/blob_viewer/server_side.rb index 05a3dd7d913..fbc1b520c01 100644 --- a/app/models/blob_viewer/server_side.rb +++ b/app/models/blob_viewer/server_side.rb @@ -9,20 +9,16 @@ module BlobViewer end def prepare! - if blob.project - blob.load_all_data!(blob.project.repository) - end + blob.load_all_data! end def render_error - if blob.stored_externally? - # Files that are not stored in the repository, like LFS files and - # build artifacts, can only be rendered using a client-side viewer, - # since we do not want to read large amounts of data into memory on the - # server side. Client-side viewers use JS and can fetch the file from - # `blob_raw_url` using AJAX. - return :server_side_but_stored_externally - end + # Files that are not stored in the repository, like LFS files and + # build artifacts, can only be rendered using a client-side viewer, + # since we do not want to read large amounts of data into memory on the + # server side. Client-side viewers use JS and can fetch the file from + # `blob_raw_url` using AJAX. + return :server_side_but_stored_externally if blob.stored_externally? super end diff --git a/app/models/board.rb b/app/models/board.rb index cf8317891b5..18081a32157 100644 --- a/app/models/board.rb +++ b/app/models/board.rb @@ -5,6 +5,10 @@ class Board < ActiveRecord::Base validates :project, presence: true + def backlog_list + lists.merge(List.backlog).take + end + def closed_list lists.merge(List.closed).take end diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index cb40f33932a..944725d91c3 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -16,7 +16,7 @@ class BroadcastMessage < ActiveRecord::Base def self.current Rails.cache.fetch("broadcast_message_current", expires_in: 1.minute) do - where("ends_at > :now AND starts_at <= :now", now: Time.zone.now).last + where('ends_at > :now AND starts_at <= :now', now: Time.zone.now).order([:created_at, :id]).to_a end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index cec1ca89a6a..58758f7ca8a 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -33,7 +33,7 @@ module Ci scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } - scope :manual_actions, ->() { where(when: :manual).relevant } + scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) } mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader @@ -109,7 +109,7 @@ module Ci end def playable? - action? && manual? + action? && (manual? || complete?) end def action? diff --git a/app/models/ci/legacy_stage.rb b/app/models/ci/legacy_stage.rb new file mode 100644 index 00000000000..9b536af672b --- /dev/null +++ b/app/models/ci/legacy_stage.rb @@ -0,0 +1,64 @@ +module Ci + # Currently this is artificial object, constructed dynamically + # We should migrate this object to actual database record in the future + class LegacyStage + include StaticModel + + attr_reader :pipeline, :name + + delegate :project, to: :pipeline + + def initialize(pipeline, name:, status: nil, warnings: nil) + @pipeline = pipeline + @name = name + @status = status + @warnings = warnings + end + + def groups + @groups ||= statuses.ordered.latest + .sort_by(&:sortable_name).group_by(&:group_name) + .map do |group_name, grouped_statuses| + Ci::Group.new(self, name: group_name, jobs: grouped_statuses) + end + end + + def to_param + name + end + + def statuses_count + @statuses_count ||= statuses.count + end + + def status + @status ||= statuses.latest.status + end + + def detailed_status(current_user) + Gitlab::Ci::Status::Stage::Factory + .new(self, current_user) + .fabricate! + end + + def statuses + @statuses ||= pipeline.statuses.where(stage: name) + end + + def builds + @builds ||= pipeline.builds.where(stage: name) + end + + def success? + status.to_s == 'success' + end + + def has_warnings? + if @warnings.is_a?(Integer) + @warnings > 0 + else + statuses.latest.failed_but_allowed.any? + end + end + end +end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 425ca9278eb..9ddecba5183 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -11,9 +11,7 @@ module Ci belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline' belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule' - has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id' - has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id' - + has_many :stages has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id has_many :builds, foreign_key: :commit_id has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id @@ -25,8 +23,11 @@ module Ci has_many :pending_builds, -> { pending }, foreign_key: :commit_id, class_name: 'Ci::Build' has_many :retryable_builds, -> { latest.failed_or_canceled }, foreign_key: :commit_id, class_name: 'Ci::Build' has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus' - has_many :manual_actions, -> { latest.manual_actions }, foreign_key: :commit_id, class_name: 'Ci::Build' - has_many :artifacts, -> { latest.with_artifacts_not_expired }, foreign_key: :commit_id, class_name: 'Ci::Build' + has_many :manual_actions, -> { latest.manual_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build' + has_many :artifacts, -> { latest.with_artifacts_not_expired.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build' + + has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id' + has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id' delegate :id, to: :project, prefix: true @@ -162,21 +163,21 @@ module Ci where.not(duration: nil).sum(:duration) end - def stage(name) - stage = Ci::Stage.new(self, name: name) - stage unless stage.statuses_count.zero? - end - def stages_count statuses.select(:stage).distinct.count end - def stages_name + def stages_names statuses.order(:stage_idx).distinct. pluck(:stage, :stage_idx).map(&:first) end - def stages + def legacy_stage(name) + stage = Ci::LegacyStage.new(self, name: name) + stage unless stage.statuses_count.zero? + end + + def legacy_stages # TODO, this needs refactoring, see gitlab-ce#26481. stages_query = statuses @@ -191,7 +192,7 @@ module Ci .pluck('sg.stage', status_sql, "(#{warnings_sql})") stages_with_statuses.map do |stage| - Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)]) + Ci::LegacyStage.new(self, Hash[%i[name status warnings].zip(stage)]) end end @@ -291,12 +292,14 @@ module Ci end end - def config_builds_attributes + def stage_seeds return [] unless config_processor - config_processor. - builds_for_ref(ref, tag?, trigger_requests.first). - sort_by { |build| build[:stage_idx] } + @stage_seeds ||= config_processor.stage_seeds(self) + end + + def has_stage_seeds? + stage_seeds.any? end def has_warnings? @@ -304,7 +307,7 @@ module Ci end def config_processor - return nil unless ci_yaml_file + return unless ci_yaml_file return @config_processor if defined?(@config_processor) @config_processor ||= begin diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index 9bda3186c30..59570924c8d 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -1,64 +1,11 @@ module Ci - # Currently this is artificial object, constructed dynamically - # We should migrate this object to actual database record in the future - class Stage - include StaticModel + class Stage < ActiveRecord::Base + extend Ci::Model - attr_reader :pipeline, :name + belongs_to :project + belongs_to :pipeline - delegate :project, to: :pipeline - - def initialize(pipeline, name:, status: nil, warnings: nil) - @pipeline = pipeline - @name = name - @status = status - @warnings = warnings - end - - def groups - @groups ||= statuses.ordered.latest - .sort_by(&:sortable_name).group_by(&:group_name) - .map do |group_name, grouped_statuses| - Ci::Group.new(self, name: group_name, jobs: grouped_statuses) - end - end - - def to_param - name - end - - def statuses_count - @statuses_count ||= statuses.count - end - - def status - @status ||= statuses.latest.status - end - - def detailed_status(current_user) - Gitlab::Ci::Status::Stage::Factory - .new(self, current_user) - .fabricate! - end - - def statuses - @statuses ||= pipeline.statuses.where(stage: name) - end - - def builds - @builds ||= pipeline.builds.where(stage: name) - end - - def success? - status.to_s == 'success' - end - - def has_warnings? - if @warnings.is_a?(Integer) - @warnings > 0 - else - statuses.latest.failed_but_allowed.any? - end - end + has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id + has_many :builds, foreign_key: :commit_id end end diff --git a/app/models/commit.rb b/app/models/commit.rb index bfa3a624e70..20206d57c4c 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -114,16 +114,16 @@ class Commit # # Usually, the commit title is the first line of the commit message. # In case this first line is longer than 100 characters, it is cut off - # after 80 characters and ellipses (`&hellp;`) are appended. + # after 80 characters + `...` def title - full_title.length > 100 ? full_title[0..79] << "…" : full_title + return full_title if full_title.length < 100 + + full_title.truncate(81, separator: ' ', omission: '…') end # Returns the full commits title def full_title - return @full_title if @full_title - - @full_title = + @full_title ||= if safe_message.blank? no_commit_message else @@ -131,19 +131,14 @@ class Commit end end - # Returns the commits description - # - # cut off, ellipses (`&hellp;`) are prepended to the commit message. + # Returns full commit message if title is truncated (greater than 99 characters) + # otherwise returns commit message without first line def description - title_end = safe_message.index("\n") - @description ||= - if (!title_end && safe_message.length > 100) || (title_end && title_end > 100) - "…" << safe_message[80..-1] - else - safe_message.split("\n", 2)[1].try(:chomp) - end - end + return safe_message if full_title.length >= 100 + safe_message.split("\n", 2)[1].try(:chomp) + end + def description? description.present? end @@ -326,12 +321,11 @@ class Commit end def raw_diffs(*args) - # Uncomment when https://gitlab.com/gitlab-org/gitaly/merge_requests/170 is merged - # if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs) - # Gitlab::GitalyClient::Commit.new(project.repository).diff_from_parent(self, *args) - # else - raw.diffs(*args) - # end + if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs) + Gitlab::GitalyClient::Commit.new(project.repository).diff_from_parent(self, *args) + else + raw.diffs(*args) + end end def raw_deltas diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 8b4ed49269d..07cec63b939 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -5,17 +5,17 @@ class CommitStatus < ActiveRecord::Base self.table_name = 'ci_builds' + belongs_to :user belongs_to :project belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline' - belongs_to :user delegate :commit, to: :pipeline delegate :sha, :short_sha, to: :pipeline validates :pipeline, presence: true, unless: :importing? - validates :name, presence: true + validates :name, presence: true, unless: :importing? alias_attribute :author, :user @@ -112,7 +112,7 @@ class CommitStatus < ActiveRecord::Base end def group_name - name.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip + name.to_s.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip end def failed_but_allowed? @@ -132,6 +132,11 @@ class CommitStatus < ActiveRecord::Base false end + # To be overriden when inherrited from + def cancelable? + false + end + def stuck? false end @@ -151,7 +156,7 @@ class CommitStatus < ActiveRecord::Base end def sortable_name - name.split(/(\d+)/).map do |v| + name.to_s.split(/(\d+)/).map do |v| v =~ /\d+/ ? v.to_i : v end end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index fe7fa4e66b7..04d08f2cfd5 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -4,7 +4,7 @@ class Deployment < ActiveRecord::Base belongs_to :project, required: true, validate: true belongs_to :environment, required: true, validate: true belongs_to :user - belongs_to :deployable, polymorphic: true + belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations validates :sha, presence: true validates :ref, presence: true diff --git a/app/models/diff_viewer/added.rb b/app/models/diff_viewer/added.rb new file mode 100644 index 00000000000..1909e6ef9d8 --- /dev/null +++ b/app/models/diff_viewer/added.rb @@ -0,0 +1,8 @@ +module DiffViewer + class Added < Base + include Simple + include Static + + self.partial_name = 'added' + end +end diff --git a/app/models/diff_viewer/base.rb b/app/models/diff_viewer/base.rb new file mode 100644 index 00000000000..0cbe714288d --- /dev/null +++ b/app/models/diff_viewer/base.rb @@ -0,0 +1,87 @@ +module DiffViewer + class Base + PARTIAL_PATH_PREFIX = 'projects/diffs/viewers'.freeze + + class_attribute :partial_name, :type, :extensions, :file_types, :binary, :switcher_icon, :switcher_title + + # These limits relate to the sum of the old and new blob sizes. + # Limits related to the actual size of the diff are enforced in Gitlab::Diff::File. + class_attribute :collapse_limit, :size_limit + + delegate :partial_path, :loading_partial_path, :rich?, :simple?, :text?, :binary?, to: :class + + attr_reader :diff_file + + delegate :project, to: :diff_file + + def initialize(diff_file) + @diff_file = diff_file + @initially_binary = diff_file.binary? + end + + def self.partial_path + File.join(PARTIAL_PATH_PREFIX, partial_name) + end + + def self.rich? + type == :rich + end + + def self.simple? + type == :simple + end + + def self.binary? + binary + end + + def self.text? + !binary? + end + + def self.can_render?(diff_file, verify_binary: true) + can_render_blob?(diff_file.old_blob, verify_binary: verify_binary) && + can_render_blob?(diff_file.new_blob, verify_binary: verify_binary) + end + + def self.can_render_blob?(blob, verify_binary: true) + return true if blob.nil? + return false if verify_binary && binary? != blob.binary? + return true if extensions&.include?(blob.extension) + return true if file_types&.include?(blob.file_type) + + false + end + + def collapsed? + return @collapsed if defined?(@collapsed) + return @collapsed = true if diff_file.collapsed? + + @collapsed = !diff_file.expanded? && collapse_limit && diff_file.raw_size > collapse_limit + end + + def too_large? + return @too_large if defined?(@too_large) + return @too_large = true if diff_file.too_large? + + @too_large = size_limit && diff_file.raw_size > size_limit + end + + def binary_detected_after_load? + !@initially_binary && diff_file.binary? + end + + # This method is used on the server side to check whether we can attempt to + # render the diff_file at all. Human-readable error messages are found in the + # `BlobHelper#diff_render_error_reason` helper. + def render_error + if too_large? + :too_large + end + end + + def prepare! + # To be overridden by subclasses + end + end +end diff --git a/app/models/diff_viewer/client_side.rb b/app/models/diff_viewer/client_side.rb new file mode 100644 index 00000000000..cf41d07f8eb --- /dev/null +++ b/app/models/diff_viewer/client_side.rb @@ -0,0 +1,10 @@ +module DiffViewer + module ClientSide + extend ActiveSupport::Concern + + included do + self.collapse_limit = 1.megabyte + self.size_limit = 10.megabytes + end + end +end diff --git a/app/models/diff_viewer/deleted.rb b/app/models/diff_viewer/deleted.rb new file mode 100644 index 00000000000..9c129bac694 --- /dev/null +++ b/app/models/diff_viewer/deleted.rb @@ -0,0 +1,8 @@ +module DiffViewer + class Deleted < Base + include Simple + include Static + + self.partial_name = 'deleted' + end +end diff --git a/app/models/diff_viewer/image.rb b/app/models/diff_viewer/image.rb new file mode 100644 index 00000000000..759d9a36ebb --- /dev/null +++ b/app/models/diff_viewer/image.rb @@ -0,0 +1,12 @@ +module DiffViewer + class Image < Base + include Rich + include ClientSide + + self.partial_name = 'image' + self.extensions = UploaderHelper::IMAGE_EXT + self.binary = true + self.switcher_icon = 'picture-o' + self.switcher_title = 'image diff' + end +end diff --git a/app/models/diff_viewer/mode_changed.rb b/app/models/diff_viewer/mode_changed.rb new file mode 100644 index 00000000000..d487d996f8d --- /dev/null +++ b/app/models/diff_viewer/mode_changed.rb @@ -0,0 +1,8 @@ +module DiffViewer + class ModeChanged < Base + include Simple + include Static + + self.partial_name = 'mode_changed' + end +end diff --git a/app/models/diff_viewer/no_preview.rb b/app/models/diff_viewer/no_preview.rb new file mode 100644 index 00000000000..5455fee4490 --- /dev/null +++ b/app/models/diff_viewer/no_preview.rb @@ -0,0 +1,9 @@ +module DiffViewer + class NoPreview < Base + include Simple + include Static + + self.partial_name = 'no_preview' + self.binary = true + end +end diff --git a/app/models/diff_viewer/not_diffable.rb b/app/models/diff_viewer/not_diffable.rb new file mode 100644 index 00000000000..4f9638626ea --- /dev/null +++ b/app/models/diff_viewer/not_diffable.rb @@ -0,0 +1,9 @@ +module DiffViewer + class NotDiffable < Base + include Simple + include Static + + self.partial_name = 'not_diffable' + self.binary = true + end +end diff --git a/app/models/diff_viewer/renamed.rb b/app/models/diff_viewer/renamed.rb new file mode 100644 index 00000000000..f1fbfd8c6d5 --- /dev/null +++ b/app/models/diff_viewer/renamed.rb @@ -0,0 +1,8 @@ +module DiffViewer + class Renamed < Base + include Simple + include Static + + self.partial_name = 'renamed' + end +end diff --git a/app/models/diff_viewer/rich.rb b/app/models/diff_viewer/rich.rb new file mode 100644 index 00000000000..3b0ca6e4cff --- /dev/null +++ b/app/models/diff_viewer/rich.rb @@ -0,0 +1,11 @@ +module DiffViewer + module Rich + extend ActiveSupport::Concern + + included do + self.type = :rich + self.switcher_icon = 'file-text-o' + self.switcher_title = 'rendered diff' + end + end +end diff --git a/app/models/diff_viewer/server_side.rb b/app/models/diff_viewer/server_side.rb new file mode 100644 index 00000000000..aed1a0791b1 --- /dev/null +++ b/app/models/diff_viewer/server_side.rb @@ -0,0 +1,26 @@ +module DiffViewer + module ServerSide + extend ActiveSupport::Concern + + included do + self.collapse_limit = 1.megabyte + self.size_limit = 5.megabytes + end + + def prepare! + diff_file.old_blob&.load_all_data! + diff_file.new_blob&.load_all_data! + end + + def render_error + # Files that are not stored in the repository, like LFS files and + # build artifacts, can only be rendered using a client-side viewer, + # since we do not want to read large amounts of data into memory on the + # server side. Client-side viewers use JS and can fetch the file from + # `diff_file_blob_raw_path` and `diff_file_old_blob_raw_path` using AJAX. + return :server_side_but_stored_externally if diff_file.stored_externally? + + super + end + end +end diff --git a/app/models/diff_viewer/simple.rb b/app/models/diff_viewer/simple.rb new file mode 100644 index 00000000000..65750996ee4 --- /dev/null +++ b/app/models/diff_viewer/simple.rb @@ -0,0 +1,11 @@ +module DiffViewer + module Simple + extend ActiveSupport::Concern + + included do + self.type = :simple + self.switcher_icon = 'code' + self.switcher_title = 'source diff' + end + end +end diff --git a/app/models/diff_viewer/static.rb b/app/models/diff_viewer/static.rb new file mode 100644 index 00000000000..d761328b3f6 --- /dev/null +++ b/app/models/diff_viewer/static.rb @@ -0,0 +1,10 @@ +module DiffViewer + module Static + extend ActiveSupport::Concern + + # We can always render a static viewer, even if the diff is too large. + def render_error + nil + end + end +end diff --git a/app/models/diff_viewer/text.rb b/app/models/diff_viewer/text.rb new file mode 100644 index 00000000000..98f4b2aea2a --- /dev/null +++ b/app/models/diff_viewer/text.rb @@ -0,0 +1,15 @@ +module DiffViewer + class Text < Base + include Simple + include ServerSide + + self.partial_name = 'text' + self.binary = false + + # Since the text diff viewer doesn't render the old and new blobs in full, + # we only need the limits related to the actual size of the diff which are + # already enforced in Gitlab::Diff::File. + self.collapse_limit = nil + self.size_limit = nil + end +end diff --git a/app/models/environment.rb b/app/models/environment.rb index 94815f99e9f..62db7e958ab 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -223,7 +223,8 @@ class Environment < ActiveRecord::Base def etag_cache_key Gitlab::Routing.url_helpers.namespace_project_environments_path( project.namespace, - project) + project, + format: :json) end private diff --git a/app/models/event.rb b/app/models/event.rb index d6d39473774..fad6ff03927 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -47,7 +47,7 @@ class Event < ActiveRecord::Base belongs_to :author, class_name: "User" belongs_to :project - belongs_to :target, polymorphic: true + belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations # For Hash only serialize :data # rubocop:disable Cop/ActiverecordSerialize diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb index 8867ba0d2ff..532b8f4ad69 100644 --- a/app/models/generic_commit_status.rb +++ b/app/models/generic_commit_status.rb @@ -11,6 +11,7 @@ class GenericCommitStatus < CommitStatus def set_default_values self.context ||= 'default' self.stage ||= 'external' + self.stage_idx ||= 1000000 end def tags diff --git a/app/models/label_link.rb b/app/models/label_link.rb index 51b5c2b1f4c..d68e1f54317 100644 --- a/app/models/label_link.rb +++ b/app/models/label_link.rb @@ -1,7 +1,7 @@ class LabelLink < ActiveRecord::Base include Importable - belongs_to :target, polymorphic: true + belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :label validates :target, presence: true, unless: :importing? diff --git a/app/models/list.rb b/app/models/list.rb index ba7353a1325..918275be142 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -2,7 +2,7 @@ class List < ActiveRecord::Base belongs_to :board belongs_to :label - enum list_type: { label: 1, closed: 2 } + enum list_type: { backlog: 0, label: 1, closed: 2 } validates :board, :list_type, presence: true validates :label, :position, presence: true, if: :label? diff --git a/app/models/member.rb b/app/models/member.rb index 29f9d61e870..788a32dd8e3 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -8,7 +8,7 @@ class Member < ActiveRecord::Base belongs_to :created_by, class_name: "User" belongs_to :user - belongs_to :source, polymorphic: true + belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations delegate :name, :username, :email, to: :user, prefix: true diff --git a/app/models/note.rb b/app/models/note.rb index 563af47f314..244bf169c29 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -41,7 +41,7 @@ class Note < ActiveRecord::Base participant :author belongs_to :project - belongs_to :noteable, polymorphic: true, touch: true + belongs_to :noteable, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :author, class_name: "User" belongs_to :updated_by, class_name: "User" belongs_to :last_edited_by, class_name: 'User' diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index e4726e62e93..b0df7aeb323 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -4,7 +4,7 @@ class NotificationSetting < ActiveRecord::Base default_value_for :level, NotificationSetting.levels[:global] belongs_to :user - belongs_to :source, polymorphic: true + belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :project, foreign_key: 'source_id' validates :user, presence: true @@ -41,10 +41,8 @@ class NotificationSetting < ActiveRecord::Base :success_pipeline ].freeze - store :events, accessors: EMAIL_EVENTS, coder: JSON - - before_create :set_events - before_save :events_to_boolean + store :events, coder: JSON + before_save :convert_events def self.find_or_create_for(source) setting = find_or_initialize_by(source: source) @@ -56,21 +54,18 @@ class NotificationSetting < ActiveRecord::Base setting end - # Set all event attributes to false when level is not custom or being initialized for UX reasons - def set_events - return if custom? - - self.events = {} - end + # 1. Check if this event has a value stored in its database column. + # 2. If it does, return that value. + # 3. If it doesn't (the value is nil), return the value from the serialized + # JSON hash in `events`. + (EMAIL_EVENTS - [:failed_pipeline]).each do |event| + define_method(event) do + bool = super() - # Validates store accessors values as boolean - # It is a text field so it does not cast correct boolean values in JSON - def events_to_boolean - EMAIL_EVENTS.each do |event| - bool = ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(public_send(event)) - - events[event] = bool + bool.nil? ? !!events[event] : bool end + + alias_method :"#{event}?", event end # Allow people to receive failed pipeline notifications if they already have @@ -78,7 +73,23 @@ class NotificationSetting < ActiveRecord::Base # custom settings. def failed_pipeline bool = super + bool = events[:failed_pipeline] if bool.nil? bool.nil? || bool end + alias_method :failed_pipeline?, :failed_pipeline + + def event_enabled?(event) + respond_to?(event) && public_send(event) + end + + def convert_events + return if events_before_type_cast.nil? + + EMAIL_EVENTS.each do |event| + write_attribute(event, public_send(event)) + end + + write_attribute(:events, nil) + end end diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index ae9f71e7747..6e13f9b2089 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -15,11 +15,10 @@ class PersonalAccessToken < ActiveRecord::Base scope :without_impersonation, -> { where(impersonation: false) } validates :scopes, presence: true - validate :validate_api_scopes + validate :validate_scopes def revoke! - self.revoked = true - self.save + update!(revoked: true) end def active? @@ -28,9 +27,9 @@ class PersonalAccessToken < ActiveRecord::Base protected - def validate_api_scopes - unless scopes.all? { |scope| Gitlab::Auth::API_SCOPES.include?(scope.to_sym) } - errors.add :scopes, "can only contain API scopes" + def validate_scopes + unless scopes.all? { |scope| Gitlab::Auth::AVAILABLE_SCOPES.include?(scope.to_sym) } + errors.add :scopes, "can only contain available scopes" end end end diff --git a/app/models/project.rb b/app/models/project.rb index 0caf7387450..4c394646787 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -63,16 +63,6 @@ class Project < ActiveRecord::Base # update visibility_level of forks after_update :update_forks_visibility_level - def update_forks_visibility_level - return unless visibility_level < visibility_level_was - - forks.each do |forked_project| - if forked_project.visibility_level > visibility_level - forked_project.visibility_level = visibility_level - forked_project.save! - end - end - end after_validation :check_pending_delete @@ -165,7 +155,7 @@ class Project < ActiveRecord::Base has_many :todos, dependent: :destroy has_many :notification_settings, dependent: :destroy, as: :source - has_one :import_data, dependent: :delete, class_name: "ProjectImportData" + has_one :import_data, dependent: :delete, class_name: 'ProjectImportData' has_one :project_feature, dependent: :destroy has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete has_many :container_repositories, dependent: :destroy @@ -488,7 +478,11 @@ class Project < ActiveRecord::Base ProjectCacheWorker.perform_async(self.id) end - self.import_data&.destroy + remove_import_data + end + + def remove_import_data + import_data&.destroy end def import_url=(value) @@ -1060,6 +1054,17 @@ class Project < ActiveRecord::Base !!repository.exists? end + def update_forks_visibility_level + return unless visibility_level < visibility_level_was + + forks.each do |forked_project| + if forked_project.visibility_level > visibility_level + forked_project.visibility_level = visibility_level + forked_project.save! + end + end + end + def create_wiki ProjectWiki.new(self, self.owner).wiki true @@ -1068,6 +1073,10 @@ class Project < ActiveRecord::Base false end + def wiki + @wiki ||= ProjectWiki.new(self, self.owner) + end + def jira_tracker_active? jira_tracker? && jira_service.active end @@ -1190,10 +1199,6 @@ class Project < ActiveRecord::Base end end - def wiki - @wiki ||= ProjectWiki.new(self, self.owner) - end - def running_or_pending_build_count(force: false) Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do builds.running_or_pending.count(:all) diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 8977a7cdafe..48e7802c557 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -116,30 +116,19 @@ class KubernetesService < DeploymentService # short time later def terminals(environment) with_reactive_cache do |data| - pods = data.fetch(:pods, nil) - filter_pods(pods, app: environment.slug). - flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) }. - each { |terminal| add_terminal_auth(terminal, terminal_auth) } + pods = filter_by_label(data[:pods], app: environment.slug) + terminals = pods.flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) } + terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) } end end - # Caches all pods in the namespace so other calls don't need to block on - # network access. + # Caches resources in the namespace so other calls don't need to block on + # network access def calculate_reactive_cache return unless active? && project && !project.pending_delete? - kubeclient = build_kubeclient! - - # Store as hashes, rather than as third-party types - pods = begin - kubeclient.get_pods(namespace: actual_namespace).as_json - rescue KubeException => err - raise err unless err.error_code == 404 - [] - end - # We may want to cache extra things in the future - { pods: pods } + { pods: read_pods } end TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze @@ -166,6 +155,16 @@ class KubernetesService < DeploymentService ) end + # Returns a hash of all pods in the namespace + def read_pods + kubeclient = build_kubeclient! + + kubeclient.get_pods(namespace: actual_namespace).as_json + rescue KubeException => err + raise err unless err.error_code == 404 + [] + end + def kubeclient_ssl_options opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER } @@ -181,11 +180,11 @@ class KubernetesService < DeploymentService { bearer_token: token } end - def join_api_url(*parts) + def join_api_url(api_path) url = URI.parse(api_url) prefix = url.path.sub(%r{/+\z}, '') - url.path = [prefix, *parts].join("/") + url.path = [prefix, api_path].join("/") url.to_s end diff --git a/app/models/redirect_route.rb b/app/models/redirect_route.rb index 99812bcde53..964175ddab8 100644 --- a/app/models/redirect_route.rb +++ b/app/models/redirect_route.rb @@ -1,5 +1,5 @@ class RedirectRoute < ActiveRecord::Base - belongs_to :source, polymorphic: true + belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations validates :source, presence: true diff --git a/app/models/repository.rb b/app/models/repository.rb index 07e0b3bae4f..7460515fea8 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -946,6 +946,8 @@ class Repository end def is_ancestor?(ancestor_id, descendant_id) + return false if ancestor_id.nil? || descendant_id.nil? + Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled| if is_enabled raw_repository.is_ancestor?(ancestor_id, descendant_id) @@ -1102,7 +1104,7 @@ class Repository blob = blob_at(sha, path) return unless blob - blob.load_all_data!(self) + blob.load_all_data! blob.data end diff --git a/app/models/route.rb b/app/models/route.rb index be77b8b51a5..97e8a6ad9e9 100644 --- a/app/models/route.rb +++ b/app/models/route.rb @@ -1,5 +1,5 @@ class Route < ActiveRecord::Base - belongs_to :source, polymorphic: true + belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations validates :source, presence: true diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb index eed3ca7e179..edde7bedbab 100644 --- a/app/models/sent_notification.rb +++ b/app/models/sent_notification.rb @@ -2,7 +2,7 @@ class SentNotification < ActiveRecord::Base serialize :position, Gitlab::Diff::Position # rubocop:disable Cop/ActiverecordSerialize belongs_to :project - belongs_to :noteable, polymorphic: true + belongs_to :noteable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :recipient, class_name: "User" validates :project, :recipient, presence: true diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 6c3358685fe..54014df43b0 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -11,6 +11,7 @@ class Snippet < ActiveRecord::Base include Editable cache_markdown_field :title, pipeline: :single_line + cache_markdown_field :description cache_markdown_field :content # Aliases to make application_helper#edited_time_ago_with_tooltip helper work properly with snippets. diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 17869c8bac2..2f0c9640744 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -1,7 +1,7 @@ class Subscription < ActiveRecord::Base belongs_to :user belongs_to :project - belongs_to :subscribable, polymorphic: true + belongs_to :subscribable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations validates :user, :subscribable, presence: true diff --git a/app/models/todo.rb b/app/models/todo.rb index b011001b235..696d139af74 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -22,7 +22,7 @@ class Todo < ActiveRecord::Base belongs_to :author, class_name: "User" belongs_to :note belongs_to :project - belongs_to :target, polymorphic: true, touch: true + belongs_to :target, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :user delegate :name, :email, to: :author, prefix: true, allow_nil: true diff --git a/app/models/upload.rb b/app/models/upload.rb index 13987931b05..f194d7bdb80 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -2,7 +2,7 @@ class Upload < ActiveRecord::Base # Upper limit for foreground checksum processing CHECKSUM_THRESHOLD = 100.megabytes - belongs_to :model, polymorphic: true + belongs_to :model, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations validates :size, presence: true validates :path, presence: true diff --git a/app/models/user_agent_detail.rb b/app/models/user_agent_detail.rb index 0949c6ef083..2d05fdd3e54 100644 --- a/app/models/user_agent_detail.rb +++ b/app/models/user_agent_detail.rb @@ -1,5 +1,5 @@ class UserAgentDetail < ActiveRecord::Base - belongs_to :subject, polymorphic: true + belongs_to :subject, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations validates :user_agent, :ip_address, :subject_id, :subject_type, presence: true diff --git a/app/policies/deploy_key_policy.rb b/app/policies/deploy_key_policy.rb new file mode 100644 index 00000000000..ebab213e6be --- /dev/null +++ b/app/policies/deploy_key_policy.rb @@ -0,0 +1,11 @@ +class DeployKeyPolicy < BasePolicy + def rules + return unless @user + + can! :update_deploy_key if @user.admin? + + if @subject.private? && @user.project_deploy_keys.exists?(id: @subject.id) + can! :update_deploy_key + end + end +end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 3959b895f44..47518dddb61 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -203,7 +203,7 @@ class ProjectPolicy < BasePolicy unless project.feature_available?(:builds, user) && repository_enabled cannot!(*named_abilities(:build)) - cannot!(*named_abilities(:pipeline)) + cannot!(*named_abilities(:pipeline) - [:read_pipeline]) cannot!(*named_abilities(:pipeline_schedule)) cannot!(*named_abilities(:environment)) cannot!(*named_abilities(:deployment)) diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb index cf8ff92617f..bc5c4f32f79 100644 --- a/app/policies/project_snippet_policy.rb +++ b/app/policies/project_snippet_policy.rb @@ -1,5 +1,10 @@ class ProjectSnippetPolicy < BasePolicy def rules + # We have to check both project feature visibility and a snippet visibility and take the stricter one + # This will be simplified - check https://gitlab.com/gitlab-org/gitlab-ce/issues/27573 + return unless @subject.project.feature_available?(:snippets, @user) + return unless Ability.allowed?(@user, :read_project, @subject.project) + can! :read_project_snippet if @subject.public? return unless @user diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index 0db9e31031c..8bf35953d29 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -110,12 +110,24 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated end def closing_issues_links - markdown issues_sentence(project, closing_issues), pipeline: :gfm, author: author, project: project + markdown( + issues_sentence(project, closing_issues), + pipeline: :gfm, + author: author, + project: project, + issuable_state_filter_enabled: true + ) end def mentioned_issues_links mentioned_issues = issues_mentioned_but_not_closing(current_user) - markdown issues_sentence(project, mentioned_issues), pipeline: :gfm, author: author, project: project + markdown( + issues_sentence(project, mentioned_issues), + pipeline: :gfm, + author: author, + project: project, + issuable_state_filter_enabled: true + ) end def assign_to_closing_issues_link diff --git a/app/presenters/projects/settings/deploy_keys_presenter.rb b/app/presenters/projects/settings/deploy_keys_presenter.rb index 070b0c35e36..229311eb6ee 100644 --- a/app/presenters/projects/settings/deploy_keys_presenter.rb +++ b/app/presenters/projects/settings/deploy_keys_presenter.rb @@ -11,7 +11,7 @@ module Projects end def enabled_keys - @enabled_keys ||= project.deploy_keys + @enabled_keys ||= project.deploy_keys.includes(:projects) end def any_keys_enabled? @@ -23,11 +23,7 @@ module Projects end def available_project_keys - @available_project_keys ||= current_user.project_deploy_keys - enabled_keys - end - - def any_available_project_keys_enabled? - available_project_keys.any? + @available_project_keys ||= current_user.project_deploy_keys.includes(:projects) - enabled_keys end def key_available?(deploy_key) @@ -37,17 +33,13 @@ module Projects def available_public_keys return @available_public_keys if defined?(@available_public_keys) - @available_public_keys ||= DeployKey.are_public - enabled_keys + @available_public_keys ||= DeployKey.are_public.includes(:projects) - enabled_keys # Public keys that are already used by another accessible project are already # in @available_project_keys. @available_public_keys -= available_project_keys end - def any_available_public_keys_enabled? - available_public_keys.any? - end - def as_json serializer = DeployKeySerializer.new opts = { user: current_user } diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 0063920e603..eeb5399aa8b 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -1,18 +1,15 @@ -class BuildDetailsEntity < BuildEntity +class BuildDetailsEntity < JobEntity expose :coverage, :erased_at, :duration expose :tag_list, as: :tags - expose :user, using: UserEntity + expose :runner, using: RunnerEntity + expose :pipeline, using: PipelineEntity expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :update_build, project) } do |build| erase_namespace_project_job_path(project.namespace, project, build) end - expose :artifacts, using: BuildArtifactEntity - expose :runner, using: RunnerEntity - expose :pipeline, using: PipelineEntity - expose :merge_request, if: -> (*) { can?(current_user, :read_merge_request, build.merge_request) } do expose :iid do |build| build.merge_request.iid @@ -28,16 +25,14 @@ class BuildDetailsEntity < BuildEntity end expose :raw_path do |build| - raw_namespace_project_build_path(project.namespace, project, build) + raw_namespace_project_job_path(project.namespace, project, build) end private def build_failed_issue_options - { - title: "Build Failed ##{build.id}", - description: namespace_project_job_url(project.namespace, project, build) - } + { title: "Build Failed ##{build.id}", + description: namespace_project_job_path(project.namespace, project, build) } end def current_user diff --git a/app/serializers/build_serializer.rb b/app/serializers/build_serializer.rb index 79b67001199..bae9932847f 100644 --- a/app/serializers/build_serializer.rb +++ b/app/serializers/build_serializer.rb @@ -1,5 +1,5 @@ class BuildSerializer < BaseSerializer - entity BuildEntity + entity JobEntity def represent_status(resource) data = represent(resource, { only: [:status] }) diff --git a/app/serializers/deploy_key_entity.rb b/app/serializers/deploy_key_entity.rb index d75a83d0fa5..068013c8829 100644 --- a/app/serializers/deploy_key_entity.rb +++ b/app/serializers/deploy_key_entity.rb @@ -11,4 +11,11 @@ class DeployKeyEntity < Grape::Entity expose :projects, using: ProjectEntity do |deploy_key| deploy_key.projects.select { |project| options[:user].can?(:read_project, project) } end + expose :can_edit + + private + + def can_edit + options[:user].can?(:update_deploy_key, object) + end end diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb index 8b3de1bed0f..e493c9162fd 100644 --- a/app/serializers/deployment_entity.rb +++ b/app/serializers/deployment_entity.rb @@ -24,6 +24,6 @@ class DeploymentEntity < Grape::Entity expose :user, using: UserEntity expose :commit, using: CommitEntity - expose :deployable, using: BuildEntity - expose :manual_actions, using: BuildEntity + expose :deployable, using: JobEntity + expose :manual_actions, using: JobEntity end diff --git a/app/serializers/group_entity.rb b/app/serializers/group_entity.rb new file mode 100644 index 00000000000..7c872a3e986 --- /dev/null +++ b/app/serializers/group_entity.rb @@ -0,0 +1,50 @@ +class GroupEntity < Grape::Entity + include ActionView::Helpers::NumberHelper + include RequestAwareEntity + include MembersHelper + include GroupsHelper + + expose :id, :name, :path, :description, :visibility + expose :full_name, :full_path + expose :web_url + expose :parent_id + expose :created_at, :updated_at + + expose :group_path do |group| + group_path(group) + end + + expose :permissions do + expose :human_group_access do |group, options| + group.group_members.find_by(user_id: request.current_user)&.human_access + end + end + + expose :edit_path do |group| + edit_group_path(group) + end + + expose :leave_path do |group| + leave_group_group_members_path(group) + end + + expose :can_edit do |group| + can?(request.current_user, :admin_group, group) + end + + expose :has_subgroups do |group| + GroupsFinder.new(request.current_user, parent: group).execute.any? + end + + expose :number_projects_with_delimiter do |group| + number_with_delimiter(GroupProjectsFinder.new(group: group, current_user: request.current_user).execute.count) + end + + expose :number_users_with_delimiter do |group| + number_with_delimiter(group.users.count) + end + + expose :avatar_url do |group| + group_icon(group) + end +end diff --git a/app/serializers/group_serializer.rb b/app/serializers/group_serializer.rb new file mode 100644 index 00000000000..26e8566828b --- /dev/null +++ b/app/serializers/group_serializer.rb @@ -0,0 +1,19 @@ +class GroupSerializer < BaseSerializer + entity GroupEntity + + def with_pagination(request, response) + tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) } + end + + def paginated? + @paginator.present? + end + + def represent(resource, opts = {}) + if paginated? + super(@paginator.paginate(resource), opts) + else + super(resource, opts) + end + end +end diff --git a/app/serializers/build_entity.rb b/app/serializers/job_entity.rb index c01efa9dd5c..d6de43bcbcb 100644 --- a/app/serializers/build_entity.rb +++ b/app/serializers/job_entity.rb @@ -1,17 +1,21 @@ -class BuildEntity < Grape::Entity +class JobEntity < Grape::Entity include RequestAwareEntity expose :id expose :name expose :build_path do |build| - path_to(:namespace_project_job, build) + build.target_url || path_to(:namespace_project_job, build) end - expose :retry_path, if: -> (*) { build&.retryable? } do |build| + expose :retry_path, if: -> (*) { retryable? } do |build| path_to(:retry_namespace_project_job, build) end + expose :cancel_path, if: -> (*) { cancelable? } do |build| + path_to(:cancel_namespace_project_job, build) + end + expose :play_path, if: -> (*) { playable? } do |build| path_to(:play_namespace_project_job, build) end @@ -25,6 +29,14 @@ class BuildEntity < Grape::Entity alias_method :build, :object + def cancelable? + build.cancelable? && can?(request.current_user, :update_build, build) + end + + def retryable? + build.retryable? && can?(request.current_user, :update_build, build) + end + def playable? build.playable? && can?(request.current_user, :update_build, build) end diff --git a/app/serializers/job_group_entity.rb b/app/serializers/job_group_entity.rb index 04487e59009..8554de55517 100644 --- a/app/serializers/job_group_entity.rb +++ b/app/serializers/job_group_entity.rb @@ -4,7 +4,7 @@ class JobGroupEntity < Grape::Entity expose :name expose :size expose :detailed_status, as: :status, with: StatusEntity - expose :jobs, with: BuildEntity + expose :jobs, with: JobEntity private diff --git a/app/serializers/pipeline_details_entity.rb b/app/serializers/pipeline_details_entity.rb index d58572a5f87..130968a44c1 100644 --- a/app/serializers/pipeline_details_entity.rb +++ b/app/serializers/pipeline_details_entity.rb @@ -1,6 +1,6 @@ class PipelineDetailsEntity < PipelineEntity expose :details do - expose :stages, using: StageEntity + expose :legacy_stages, as: :stages, using: StageEntity expose :artifacts, using: BuildArtifactEntity expose :manual_actions, using: BuildActionEntity end diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index b428ff69fe8..661bf17983c 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -13,14 +13,15 @@ class PipelineSerializer < BaseSerializer def represent(resource, opts = {}) if resource.is_a?(ActiveRecord::Relation) + resource = resource.preload([ :retryable_builds, :cancelable_statuses, :trigger_requests, :project, - { pending_builds: :project }, - { manual_actions: :project }, - { artifacts: :project } + :manual_actions, + :artifacts, + { pending_builds: :project } ]) end diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb index fd9ff115eab..68f6a8619e5 100644 --- a/app/services/boards/create_service.rb +++ b/app/services/boards/create_service.rb @@ -12,6 +12,7 @@ module Boards def create_board! board = project.boards.create + board.lists.create(list_type: :backlog) board.lists.create(list_type: :closed) board diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 533e6787855..418fa9afd6e 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -3,7 +3,7 @@ module Boards class ListService < BaseService def execute issues = IssuesFinder.new(current_user, filter_params).execute - issues = without_board_labels(issues) unless list + issues = without_board_labels(issues) unless movable_list? issues = with_list_label(issues) if movable_list? issues.order_by_position_and_priority end diff --git a/app/services/boards/lists/list_service.rb b/app/services/boards/lists/list_service.rb index c579ed4c869..df2a01a69e5 100644 --- a/app/services/boards/lists/list_service.rb +++ b/app/services/boards/lists/list_service.rb @@ -2,6 +2,8 @@ module Boards module Lists class ListService < BaseService def execute(board) + board.lists.create(list_type: :backlog) unless board.lists.backlog.exists? + board.lists end end diff --git a/app/services/ci/create_pipeline_builds_service.rb b/app/services/ci/create_pipeline_builds_service.rb deleted file mode 100644 index 70fb2c5e38f..00000000000 --- a/app/services/ci/create_pipeline_builds_service.rb +++ /dev/null @@ -1,51 +0,0 @@ -module Ci - class CreatePipelineBuildsService < BaseService - attr_reader :pipeline - - def execute(pipeline) - @pipeline = pipeline - - new_builds.map do |build_attributes| - create_build(build_attributes) - end - end - - delegate :project, to: :pipeline - - private - - def create_build(build_attributes) - build_attributes = build_attributes.merge( - pipeline: pipeline, - project: project, - ref: pipeline.ref, - tag: pipeline.tag, - user: current_user, - trigger_request: trigger_request - ) - build = pipeline.builds.create(build_attributes) - - # Create the environment before the build starts. This sets its slug and - # makes it available as an environment variable - project.environments.find_or_create_by(name: build.expanded_environment_name) if - build.has_environment? - - build - end - - def new_builds - @new_builds ||= pipeline.config_builds_attributes. - reject { |build| existing_build_names.include?(build[:name]) } - end - - def existing_build_names - @existing_build_names ||= pipeline.builds.pluck(:name) - end - - def trigger_request - return @trigger_request if defined?(@trigger_request) - - @trigger_request ||= pipeline.trigger_requests.first - end - end -end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 13baa63220d..769749c9925 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -43,20 +43,22 @@ module Ci return pipeline end - unless pipeline.config_builds_attributes.present? - return error('No builds for this pipeline.') + unless pipeline.has_stage_seeds? + return error('No stages / jobs for this pipeline.') end Ci::Pipeline.transaction do update_merge_requests_head_pipeline if pipeline.save - Ci::CreatePipelineBuildsService + Ci::CreatePipelineStagesService .new(project, current_user) .execute(pipeline) end cancel_pending_pipelines if project.auto_cancel_pending_pipelines? + pipeline_created_counter.increment(source: source) + pipeline.tap(&:process!) end @@ -131,5 +133,9 @@ module Ci pipeline.drop if save pipeline end + + def pipeline_created_counter + @pipeline_created_counter ||= Gitlab::Metrics.counter(:pipelines_created_count, "Pipelines created count") + end end end diff --git a/app/services/ci/create_pipeline_stages_service.rb b/app/services/ci/create_pipeline_stages_service.rb new file mode 100644 index 00000000000..f2c175adee6 --- /dev/null +++ b/app/services/ci/create_pipeline_stages_service.rb @@ -0,0 +1,20 @@ +module Ci + class CreatePipelineStagesService < BaseService + def execute(pipeline) + pipeline.stage_seeds.each do |seed| + seed.user = current_user + + seed.create! do |build| + ## + # Create the environment before the build starts. This sets its slug and + # makes it available as an environment variable + # + if build.has_environment? + environment_name = build.expanded_environment_name + project.environments.find_or_create_by(name: environment_name) + end + end + end + end + end +end diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index f51e9fd1d54..6372e5755db 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -1,7 +1,7 @@ module Ci class RetryBuildService < ::BaseService CLONE_ACCESSORS = %i[pipeline project ref tag options commands name - allow_failure stage stage_idx trigger_request + allow_failure stage_id stage stage_idx trigger_request yaml_variables when environment coverage_regex description tag_list].freeze diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index ab4c02a97a0..a5ae4927412 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -17,18 +17,18 @@ class CompareService start_branch_name) do |commit| break unless commit - compare(commit.sha, target_project, target_branch, straight) + compare(commit.sha, target_project, target_branch, straight: straight) end end private - def compare(source_sha, target_project, target_branch, straight) + def compare(source_sha, target_project, target_branch, straight:) raw_compare = Gitlab::Git::Compare.new( target_project.repository.raw_repository, target_branch, source_sha, - straight + straight: straight ) Compare.new(raw_compare, target_project, straight: straight) diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index f080e6326a1..fb1d4aed58b 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -101,12 +101,12 @@ class GitPushService < BaseService UpdateMergeRequestsWorker .perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref]) - SystemHookPushWorker.perform_async(build_push_data.dup, :push_hooks) - EventCreateService.new.push(@project, current_user, build_push_data) + Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute(:push) + + SystemHookPushWorker.perform_async(build_push_data.dup, :push_hooks) @project.execute_hooks(build_push_data.dup, :push_hooks) @project.execute_services(build_push_data.dup, :push_hooks) - Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute(:push) if push_remove_branch? AfterBranchDeleteService diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 7c424fba428..9917a39b795 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -8,10 +8,12 @@ class GitTagPushService < BaseService @push_data = build_push_data EventCreateService.new.push(project, current_user, @push_data) + Ci::CreatePipelineService.new(project, current_user, @push_data).execute(:push) + SystemHooksService.new.execute_hooks(build_system_push_data.dup, :tag_push_hooks) project.execute_hooks(@push_data.dup, :tag_push_hooks) project.execute_services(@push_data.dup, :tag_push_hooks) - Ci::CreatePipelineService.new(project, current_user, @push_data).execute(:push) + ProjectCacheWorker.perform_async(project.id, [], [:commit_count, :repository_size]) true diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index e77a3e3eac1..cd4d180824f 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -236,8 +236,9 @@ class IssuableBaseService < BaseService ) if old_assignees != issuable.assignees - assignees = old_assignees + issuable.assignees.to_a - invalidate_cache_counts(assignees.compact, issuable) + new_assignees = issuable.assignees.to_a + affected_assignees = (old_assignees + new_assignees) - (old_assignees & new_assignees) + invalidate_cache_counts(affected_assignees.compact, issuable) end after_update(issuable) @@ -313,11 +314,13 @@ class IssuableBaseService < BaseService end if issuable.previous_changes.include?('description') - create_description_change_note(issuable) - end - - if issuable.previous_changes.include?('description') && issuable.tasks? - create_task_status_note(issuable) + if issuable.tasks? && issuable.updated_tasks.any? + create_task_status_note(issuable) + else + # TODO: Show this note if non-task content was modified. + # https://gitlab.com/gitlab-org/gitlab-ce/issues/33577 + create_description_change_note(issuable) + end end if issuable.previous_changes.include?('time_estimate') diff --git a/app/services/metrics_service.rb b/app/services/metrics_service.rb new file mode 100644 index 00000000000..d726db4e99b --- /dev/null +++ b/app/services/metrics_service.rb @@ -0,0 +1,33 @@ +require 'prometheus/client/formats/text' + +class MetricsService + CHECKS = [ + Gitlab::HealthChecks::DbCheck, + Gitlab::HealthChecks::RedisCheck, + Gitlab::HealthChecks::FsShardsCheck + ].freeze + + def prometheus_metrics_text + Prometheus::Client::Formats::Text.marshal_multiprocess(multiprocess_metrics_path) + end + + def health_metrics_text + metrics = CHECKS.flat_map(&:metrics) + + formatter.marshal(metrics) + end + + def metrics_text + "#{health_metrics_text}#{prometheus_metrics_text}" + end + + private + + def formatter + @formatter ||= Gitlab::HealthChecks::PrometheusTextFormat.new + end + + def multiprocess_metrics_path + @multiprocess_metrics_path ||= Rails.root.join(ENV['prometheus_multiproc_dir']).freeze + end +end diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 988bd0a7cdb..8d1820bc504 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -8,7 +8,7 @@ class NotificationRecipientService @project = project end - def build_recipients(target, current_user, action: nil, previous_assignee: nil, skip_current_user: true) + def build_recipients(target, current_user, action:, previous_assignee: nil, skip_current_user: true) custom_action = build_custom_key(action, target) recipients = target.participants(current_user) @@ -59,7 +59,7 @@ class NotificationRecipientService return [] if notification_setting.mention? || notification_setting.disabled? - return [] if notification_setting.custom? && !notification_setting.public_send(custom_action) + return [] if notification_setting.custom? && !notification_setting.event_enabled?(custom_action) return [] if (notification_setting.watch? || notification_setting.participating?) && NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action) @@ -176,7 +176,7 @@ class NotificationRecipientService if notification_level settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level]) - settings = settings.select { |setting| setting.events[action] } if action.present? + settings = settings.select { |setting| setting.event_enabled?(action) } if action.present? settings.map(&:user_id) else resource.notification_settings.pluck(:user_id) @@ -225,7 +225,7 @@ class NotificationRecipientService def user_ids_with_global_level_custom(ids, action) settings = settings_with_global_level_of(:custom, ids) - settings = settings.select { |setting| setting.events[action] } + settings = settings.select { |setting| setting.event_enabled?(action) } settings.map(&:user_id) end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 646ccbdb2bf..3a98a5f6b64 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -273,7 +273,7 @@ class NotificationService end def issue_moved(issue, new_issue, current_user) - recipients = NotificationRecipientService.new(issue.project).build_recipients(issue, current_user) + recipients = NotificationRecipientService.new(issue.project).build_recipients(issue, current_user, action: 'moved') recipients.map do |recipient| email = mailer.issue_moved_email(recipient, issue, new_issue, current_user) diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index a7e13648b54..83144b1e011 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/slash_commands/interpret_service.rb @@ -92,26 +92,20 @@ module SlashCommands desc 'Assign' explanation do |users| - "Assigns #{users.map(&:to_reference).to_sentence}." if users.any? + "Assigns #{users.first.to_reference}." if users.any? end params '@user' condition do current_user.can?(:"admin_#{issuable.to_ability_name}", project) end parse_params do |assignee_param| - users = extract_references(assignee_param, :user) - - if users.empty? - users = User.where(username: assignee_param.split(' ').map(&:strip)) - end - - users + extract_users(assignee_param) end command :assign do |users| next if users.empty? if issuable.is_a?(Issue) - @updates[:assignee_ids] = users.map(&:id) + @updates[:assignee_ids] = [users.last.id] else @updates[:assignee_id] = users.last.id end @@ -416,7 +410,7 @@ module SlashCommands params '@user' command :cc - desc 'Define target branch for MR' + desc 'Set target branch' explanation do |branch_name| "Sets target branch to #{branch_name}." end @@ -459,6 +453,18 @@ module SlashCommands end end + def extract_users(params) + return [] if params.nil? + + users = extract_references(params, :user) + + if users.empty? + users = User.where(username: params.split(' ').map(&:strip)) + end + + users + end + def find_labels(labels_param) extract_references(labels_param, :label) | LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb index 3bc0408f557..14addb6cf14 100644 --- a/app/uploaders/artifact_uploader.rb +++ b/app/uploaders/artifact_uploader.rb @@ -23,6 +23,10 @@ class ArtifactUploader < GitlabUploader File.join(self.class.local_artifacts_store, 'tmp/cache') end + def work_dir + File.join(self.class.local_artifacts_store, 'tmp/work') + end + private def default_local_path diff --git a/app/uploaders/file_mover.rb b/app/uploaders/file_mover.rb new file mode 100644 index 00000000000..00c2888d224 --- /dev/null +++ b/app/uploaders/file_mover.rb @@ -0,0 +1,63 @@ +class FileMover + attr_reader :secret, :file_name, :model, :update_field + + def initialize(file_path, model, update_field = :description) + @secret = File.split(File.dirname(file_path)).last + @file_name = File.basename(file_path) + @model = model + @update_field = update_field + end + + def execute + move + uploader.record_upload if update_markdown + end + + private + + def move + FileUtils.mkdir_p(File.dirname(file_path)) + FileUtils.move(temp_file_path, file_path) + end + + def update_markdown + updated_text = model.read_attribute(update_field).gsub(temp_file_uploader.to_markdown, uploader.to_markdown) + model.update_attribute(update_field, updated_text) + + true + rescue + revert + + false + end + + def temp_file_path + return @temp_file_path if @temp_file_path + + temp_file_uploader.retrieve_from_store!(file_name) + + @temp_file_path = temp_file_uploader.file.path + end + + def file_path + return @file_path if @file_path + + uploader.retrieve_from_store!(file_name) + + @file_path = uploader.file.path + end + + def uploader + @uploader ||= PersonalFileUploader.new(model, secret) + end + + def temp_file_uploader + @temp_file_uploader ||= PersonalFileUploader.new(nil, secret) + end + + def revert + Rails.logger.warn("Markdown not updated, file move reverted for #{model}") + + FileUtils.move(file_path, temp_file_path) + end +end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 7e94218c23d..652277e3b78 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -13,6 +13,13 @@ class FileUploader < GitlabUploader ) end + # Not using `GitlabUploader.base_dir` because all project namespaces are in + # the `public/uploads` dir. + # + def self.base_dir + root_dir + end + # Returns the part of `store_dir` that can change based on the model's current # path # diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb index 02afddb8c6a..0da7a025591 100644 --- a/app/uploaders/gitlab_uploader.rb +++ b/app/uploaders/gitlab_uploader.rb @@ -3,16 +3,28 @@ class GitlabUploader < CarrierWave::Uploader::Base File.join(CarrierWave.root, upload_record.path) end - def self.base_dir + def self.root_dir 'uploads' end - delegate :base_dir, to: :class + # When object storage is used, keep the `root_dir` as `base_dir`. + # The files aren't really in folders there, they just have a name. + # The files that contain user input in their name, also contain a hash, so + # the names are still unique + # + # This method is overridden in the `FileUploader` + def self.base_dir + return root_dir unless file_storage? - def file_storage? - storage.is_a?(CarrierWave::Storage::File) + File.join(root_dir, 'system') end + def self.file_storage? + self.storage == CarrierWave::Storage::File + end + + delegate :base_dir, :file_storage?, to: :class + def file_cache_storage? cache_storage.is_a?(CarrierWave::Storage::File) end @@ -41,4 +53,27 @@ class GitlabUploader < CarrierWave::Uploader::Base def exists? file.try(:exists?) end + + # Override this if you don't want to save files by default to the Rails.root directory + def work_dir + # Default path set by CarrierWave: + # https://github.com/carrierwaveuploader/carrierwave/blob/v1.0.0/lib/carrierwave/uploader/cache.rb#L182 + CarrierWave.tmp_path + end + + def filename + super || file&.filename + end + + private + + # To prevent files from moving across filesystems, override the default + # implementation: + # http://github.com/carrierwaveuploader/carrierwave/blob/v1.0.0/lib/carrierwave/uploader/cache.rb#L181-L183 + def workfile_path(for_file = original_filename) + # To be safe, keep this directory outside of the the cache directory + # because calling CarrierWave.clean_cache_files! will remove any files in + # the cache directory. + File.join(work_dir, @cache_id, version_name.to_s, for_file) + end end diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb index 02589959c2f..d11ebf0f9ca 100644 --- a/app/uploaders/lfs_object_uploader.rb +++ b/app/uploaders/lfs_object_uploader.rb @@ -16,16 +16,4 @@ class LfsObjectUploader < GitlabUploader def work_dir File.join(Gitlab.config.lfs.storage_path, 'tmp', 'work') end - - private - - # To prevent LFS files from moving across filesystems, override the default - # implementation: - # http://github.com/carrierwaveuploader/carrierwave/blob/v1.0.0/lib/carrierwave/uploader/cache.rb#L181-L183 - def workfile_path(for_file = original_filename) - # To be safe, keep this directory outside of the the cache directory - # because calling CarrierWave.clean_cache_files! will remove any files in - # the cache directory. - File.join(work_dir, @cache_id, version_name.to_s, for_file) - end end diff --git a/app/uploaders/personal_file_uploader.rb b/app/uploaders/personal_file_uploader.rb index 969b0a20d38..7f857765fbf 100644 --- a/app/uploaders/personal_file_uploader.rb +++ b/app/uploaders/personal_file_uploader.rb @@ -10,6 +10,10 @@ class PersonalFileUploader < FileUploader end def self.model_path(model) - File.join("/#{base_dir}", model.class.to_s.underscore, model.id.to_s) + if model + File.join("/#{base_dir}", model.class.to_s.underscore, model.id.to_s) + else + File.join("/#{base_dir}", 'temp') + end end end diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb index 4c127f29250..feb4f04d7b7 100644 --- a/app/uploaders/records_uploads.rb +++ b/app/uploaders/records_uploads.rb @@ -6,8 +6,6 @@ module RecordsUploads before :remove, :destroy_upload end - private - # After storing an attachment, create a corresponding Upload record # # NOTE: We're ignoring the argument passed to this callback because we want @@ -15,13 +13,16 @@ module RecordsUploads # `Tempfile` object the callback gets. # # Called `after :store` - def record_upload(_tempfile) + def record_upload(_tempfile = nil) + return unless model return unless file_storage? return unless file.exists? Upload.record(self) end + private + # Before removing an attachment, destroy any Upload records at the same path # # Called `before :remove` diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index e1b4e34cd2b..95dffdafabe 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -180,11 +180,25 @@ .col-sm-10 = f.text_area :sign_in_text, class: 'form-control', rows: 4 .help-block Markdown enabled + + %fieldset + %legend Help Page .form-group = f.label :help_page_text, class: 'control-label col-sm-2' .col-sm-10 = f.text_area :help_page_text, class: 'form-control', rows: 4 .help-block Markdown enabled + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :help_page_hide_commercial_content do + = f.check_box :help_page_hide_commercial_content + Hide marketing-related entries from help + .form-group + = f.label :help_page_support_url, 'Support page URL', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :help_page_support_url, class: 'form-control', placeholder: 'http://company.example.com/getting-help', :'aria-describedby' => 'support_help_block' + %span.help-block#support_help_block Alternate support URL for help page %fieldset %legend Pages @@ -232,7 +246,7 @@ = f.number_field :container_registry_token_expire_delay, class: 'form-control' %fieldset - %legend Metrics + %legend Metrics - Influx %p Setup InfluxDB to measure a wide variety of statistics like the time spent in running SQL queries. These settings require a @@ -297,6 +311,22 @@ results in fewer but larger UDP packets being sent. %fieldset + %legend Metrics - Prometheus + %p + Enable a Prometheus metrics endpoint at `#{metrics_path}` to expose a variety of statistics on the health and performance of GitLab. Additional information on authenticating and connecting to the metrics endpoint is available + = link_to 'here', admin_health_check_path + \. This setting requires a + = link_to 'restart', help_page_path('administration/restart_gitlab') + to take effect. + = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/introduction') + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :prometheus_metrics_enabled do + = f.check_box :prometheus_metrics_enabled + Enable Prometheus Metrics + + %fieldset %legend Background Jobs %p These settings require a restart to take effect. diff --git a/app/views/admin/deploy_keys/edit.html.haml b/app/views/admin/deploy_keys/edit.html.haml new file mode 100644 index 00000000000..3a59282e578 --- /dev/null +++ b/app/views/admin/deploy_keys/edit.html.haml @@ -0,0 +1,10 @@ +- page_title 'Edit Deploy Key' +%h3.page-title Edit public deploy key +%hr + +%div + = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form form-horizontal' } do |f| + = render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key } + .form-actions + = f.submit 'Save changes', class: 'btn-save btn' + = link_to 'Cancel', admin_deploy_keys_path, class: 'btn btn-cancel' diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml index 007da8c1d29..92370034baa 100644 --- a/app/views/admin/deploy_keys/index.html.haml +++ b/app/views/admin/deploy_keys/index.html.haml @@ -31,4 +31,6 @@ %span.cgray added #{time_ago_with_tooltip(deploy_key.created_at)} %td - = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove delete-key pull-right' + .pull-right + = link_to 'Edit', edit_admin_deploy_key_path(deploy_key), class: 'btn btn-sm' + = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove delete-key' diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml index a064efc231f..13f5259698f 100644 --- a/app/views/admin/deploy_keys/new.html.haml +++ b/app/views/admin/deploy_keys/new.html.haml @@ -1,31 +1,10 @@ -- page_title "New Deploy Key" +- page_title 'New Deploy Key' %h3.page-title New public deploy key %hr %div = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form form-horizontal' } do |f| - = form_errors(@deploy_key) - - .form-group - = f.label :title, class: "control-label" - .col-sm-10= f.text_field :title, class: 'form-control' - .form-group - = f.label :key, class: "control-label" - .col-sm-10 - %p.light - Paste a machine public key here. Read more about how to generate it - = link_to "here", help_page_path("ssh/README") - = f.text_area :key, class: "form-control thin_area", rows: 5 - .form-group - .control-label - .col-sm-10 - = f.label :can_push do - = f.check_box :can_push - %strong Write access allowed - %p.light.append-bottom-0 - Allow this key to push to repository as well? (Default only allows pull access.) - + = render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key } .form-actions - = f.submit 'Create', class: "btn-create btn" - = link_to "Cancel", admin_deploy_keys_path, class: "btn btn-cancel" - + = f.submit 'Create', class: 'btn-create btn' + = link_to 'Cancel', admin_deploy_keys_path, class: 'btn btn-cancel' diff --git a/app/views/dashboard/groups/_groups.html.haml b/app/views/dashboard/groups/_groups.html.haml index 6c3bf1a2b3b..168e6272d8e 100644 --- a/app/views/dashboard/groups/_groups.html.haml +++ b/app/views/dashboard/groups/_groups.html.haml @@ -1,6 +1,9 @@ .js-groups-list-holder - %ul.content-list - - @group_members.each do |group_member| - = render 'shared/groups/group', group: group_member.group, group_member: group_member - - = paginate @group_members, theme: 'gitlab' + #dashboard-group-app{ data: { endpoint: dashboard_groups_path(format: :json), path: dashboard_groups_path } } + .groups-list-loading + = icon('spinner spin', 'v-show' => 'isLoading') + %template{ 'v-if' => '!isLoading && isEmpty' } + %div{ 'v-cloak' => true } + = render 'empty_state' + %template{ 'v-else-if' => '!isLoading && !isEmpty' } + %groups-component{ ':groups' => 'state.groups', ':page-info' => 'state.pageInfo' } diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 73ab2c95ff9..f9b45a539a1 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -2,7 +2,10 @@ - header_title "Groups", dashboard_groups_path = render 'dashboard/groups_head' -- if @group_members.empty? += webpack_bundle_tag 'common_vue' += webpack_bundle_tag 'groups' + +- if @groups.empty? = render 'empty_state' - else = render 'groups' diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder index 06fb531b546..70ec6bc6257 100644 --- a/app/views/dashboard/issues.atom.builder +++ b/app/views/dashboard/issues.atom.builder @@ -1,10 +1,7 @@ -xml.instruct! -xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do - xml.title "#{current_user.name} issues" - xml.link href: url_for(params), rel: "self", type: "application/atom+xml" - xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html" - xml.id issues_dashboard_url - xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? +xml.title "#{current_user.name} issues" +xml.link href: url_for(params), rel: "self", type: "application/atom+xml" +xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html" +xml.id issues_dashboard_url +xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? - xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any? -end +xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any? diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder index 13f7a8ddcec..747c53b440e 100644 --- a/app/views/dashboard/projects/index.atom.builder +++ b/app/views/dashboard/projects/index.atom.builder @@ -1,10 +1,7 @@ -xml.instruct! -xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do - xml.title "Activity" - xml.link href: dashboard_projects_url(rss_url_options), rel: "self", type: "application/atom+xml" - xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html" - xml.id dashboard_projects_url - xml.updated @events[0].updated_at.xmlschema if @events[0] +xml.title "Activity" +xml.link href: dashboard_projects_url(rss_url_options), rel: "self", type: "application/atom+xml" +xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html" +xml.id dashboard_projects_url +xml.updated @events[0].updated_at.xmlschema if @events[0] - xml << render(partial: 'events/event', collection: @events) if @events.any? -end +xml << render(partial: 'events/event', collection: @events) if @events.any? diff --git a/app/views/devise/mailer/confirmation_instructions.html.haml b/app/views/devise/mailer/confirmation_instructions.html.haml index 086bb8e083d..a508b7537a2 100644 --- a/app/views/devise/mailer/confirmation_instructions.html.haml +++ b/app/views/devise/mailer/confirmation_instructions.html.haml @@ -1,16 +1,15 @@ -.center - - if @resource.unconfirmed_email.present? - #content - %h2= @resource.unconfirmed_email - %p Click the link below to confirm your email address. - #cta - = link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token) - - else - #content - - if Gitlab.com? - %h2 Thanks for signing up to GitLab! - - else - %h2 Welcome, #{@resource.name}! - %p To get started, click the link below to confirm your account. - #cta - = link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token) +- if @resource.unconfirmed_email.present? + #content + = email_default_heading(@resource.unconfirmed_email) + %p Click the link below to confirm your email address. + #cta + = link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token) +- else + #content + - if Gitlab.com? + = email_default_heading('Thanks for signing up to GitLab!') + - else + = email_default_heading("Welcome, #{@resource.name}!") + %p To get started, click the link below to confirm your account. + #cta + = link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token) diff --git a/app/views/devise/mailer/password_change.html.haml b/app/views/devise/mailer/password_change.html.haml index 3349ee84807..5ec515285f2 100644 --- a/app/views/devise/mailer/password_change.html.haml +++ b/app/views/devise/mailer/password_change.html.haml @@ -1,10 +1,8 @@ -.center - #content - %h2 Hello, #{@resource.name}! - %p - The password for your GitLab account on - #{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url)} - has successfully been changed. - %p - If you did not initiate this change, please contact your administrator - immediately. += email_default_heading("Hello, #{@resource.name}!") +%p + The password for your GitLab account on + #{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url)} + has successfully been changed. +%p + If you did not initiate this change, please contact your administrator + immediately. diff --git a/app/views/devise/mailer/reset_password_instructions.html.haml b/app/views/devise/mailer/reset_password_instructions.html.haml index e91c9522520..47e192afa52 100644 --- a/app/views/devise/mailer/reset_password_instructions.html.haml +++ b/app/views/devise/mailer/reset_password_instructions.html.haml @@ -1,12 +1,10 @@ -.center - #content - %h2 Hello, #{@resource.name}! - %p - Someone, hopefully you, has requested to reset the password for your - GitLab account on #{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url)}. - %p - If you did not perform this request, you can safely ignore this email. - %p - Otherwise, click the link below to complete the process. - #cta - = link_to('Reset password', edit_password_url(@resource, reset_password_token: @token)) += email_default_heading("Hello, #{@resource.name}!") +%p + Someone, hopefully you, has requested to reset the password for your + GitLab account on #{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url)}. +%p + If you did not perform this request, you can safely ignore this email. +%p + Otherwise, click the link below to complete the process. +#cta + = link_to('Reset password', edit_password_url(@resource, reset_password_token: @token)) diff --git a/app/views/devise/mailer/unlock_instructions.html.haml b/app/views/devise/mailer/unlock_instructions.html.haml index 9990d1ccac6..79e3a35cc9a 100644 --- a/app/views/devise/mailer/unlock_instructions.html.haml +++ b/app/views/devise/mailer/unlock_instructions.html.haml @@ -1,9 +1,8 @@ -.center - #content - %h2 Hello, #{@resource.name}! - %p - Your GitLab account has been locked due to an excessive amount of unsuccessful - sign in attempts. Your account will automatically unlock in #{time_ago_in_words(Devise.unlock_in.from_now)} - or you may click the link below to unlock now. - #cta - = link_to('Unlock account', unlock_url(@resource, unlock_token: @token)) +#content + = email_default_heading("Hello, #{@resource.name}!") + %p + Your GitLab account has been locked due to an excessive amount of unsuccessful + sign in attempts. Your account will automatically unlock in #{time_ago_in_words(Devise.unlock_in.from_now)} + or you may click the link below to unlock now. + #cta + = link_to('Unlock account', unlock_url(@resource, unlock_token: @token)) diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml index 70042dee20f..4a41be972da 100644 --- a/app/views/discussions/_diff_with_notes.html.haml +++ b/app/views/discussions/_diff_with_notes.html.haml @@ -2,8 +2,9 @@ - blob = discussion.blob .diff-file.file-holder - .js-file-title.file-title - = render "projects/diffs/file_header", diff_file: diff_file, url: discussion_path(discussion), show_toggle: false + .js-file-title.file-title.file-title-flex-parent + .file-header-content + = render "projects/diffs/file_header", diff_file: diff_file, url: discussion_path(discussion), show_toggle: false .diff-content.code.js-syntax-highlight %table diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder index 469768d83f2..a239ea8caf0 100644 --- a/app/views/groups/issues.atom.builder +++ b/app/views/groups/issues.atom.builder @@ -1,10 +1,7 @@ -xml.instruct! -xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do - xml.title "#{@group.name} issues" - xml.link href: url_for(params), rel: "self", type: "application/atom+xml" - xml.link href: issues_group_url, rel: "alternate", type: "text/html" - xml.id issues_group_url - xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? +xml.title "#{@group.name} issues" +xml.link href: url_for(params), rel: "self", type: "application/atom+xml" +xml.link href: issues_group_url, rel: "alternate", type: "text/html" +xml.id issues_group_url +xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? - xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any? -end +xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any? diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder index 914091dfd15..0f67b15c301 100644 --- a/app/views/groups/show.atom.builder +++ b/app/views/groups/show.atom.builder @@ -1,10 +1,7 @@ -xml.instruct! -xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do - xml.title "#{@group.name} activity" - xml.link href: group_url(@group, rss_url_options), rel: "self", type: "application/atom+xml" - xml.link href: group_url(@group), rel: "alternate", type: "text/html" - xml.id group_url(@group) - xml.updated @events[0].updated_at.xmlschema if @events[0] +xml.title "#{@group.name} activity" +xml.link href: group_url(@group, rss_url_options), rel: "self", type: "application/atom+xml" +xml.link href: group_url(@group), rel: "alternate", type: "text/html" +xml.id group_url(@group) +xml.updated @events[0].updated_at.xmlschema if @events[0] - xml << render(@events) if @events.any? -end +xml << render(@events) if @events.any? diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index ea8bbe92d86..331d1181220 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -29,6 +29,10 @@ %td Focus Filter %tr %td.shortcut + .key p b + %td Show/hide the Performance Bar + %tr + %td.shortcut .key ? %td Show/hide this dialog %tr diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index b20e3a22133..c25eae63eec 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -1,10 +1,15 @@ %div +- if current_application_settings.help_page_text.present? + = markdown_field(current_application_settings, :help_page_text) + %hr + +- unless current_application_settings.help_page_hide_commercial_content? %h1 GitLab Community Edition - if user_signed_in? %span= Gitlab::VERSION - %small= Gitlab::REVISION + %small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION) = version_status_badge %p.slead GitLab is open source software to collaborate on code. @@ -18,13 +23,9 @@ Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises. %br Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}. - - if current_application_settings.help_page_text.present? - %hr - = markdown_field(current_application_settings, :help_page_text) - -%hr + %hr -.row +.row.prepend-top-default .col-md-8 .documentation-index = markdown(@help_index) @@ -33,8 +34,9 @@ .panel-heading Quick help %ul.well-list - %li= link_to 'See our website for getting help', promo_url + '/getting-help/' + %li= link_to 'See our website for getting help', support_url %li= link_to 'Use the search bar on the top of this page', '#', onclick: 'Shortcuts.focusSearch(event)' %li= link_to 'Use shortcuts', '#', onclick: 'Shortcuts.toggleHelp()' - %li= link_to 'Get a support subscription', 'https://about.gitlab.com/pricing/' - %li= link_to 'Compare GitLab editions', 'https://about.gitlab.com/features/#compare' + - unless current_application_settings.help_page_hide_commercial_content? + %li= link_to 'Get a support subscription', 'https://about.gitlab.com/pricing/' + %li= link_to 'Compare GitLab editions', 'https://about.gitlab.com/features/#compare' diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml index f6ebd76af9d..c07c148a12a 100644 --- a/app/views/help/show.html.haml +++ b/app/views/help/show.html.haml @@ -1,3 +1,3 @@ - page_title @path.split("/").reverse.map(&:humanize) -.documentation.wiki +.documentation.wiki.prepend-top-default = markdown @markdown diff --git a/app/views/layouts/_broadcast.html.haml b/app/views/layouts/_broadcast.html.haml index 3a7e0929c16..bcd2f03e83c 100644 --- a/app/views/layouts/_broadcast.html.haml +++ b/app/views/layouts/_broadcast.html.haml @@ -1 +1,2 @@ -= broadcast_message +- BroadcastMessage.current.each do |message| + = broadcast_message(message) diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 9e354987401..eea33b5966f 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -28,14 +28,17 @@ = stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "print", media: "print" = stylesheet_link_tag "test", media: "all" if Rails.env.test? + = stylesheet_link_tag 'peek' if peek_enabled? = Gon::Base.render_data = webpack_bundle_tag "runtime" = webpack_bundle_tag "common" + = webpack_bundle_tag "locale" = webpack_bundle_tag "main" = webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled = webpack_bundle_tag "test" if Rails.env.test? + = webpack_bundle_tag 'peek' if peek_enabled? - if content_for?(:page_specific_javascripts) = yield :page_specific_javascripts diff --git a/app/views/layouts/_mailer.html.haml b/app/views/layouts/_mailer.html.haml new file mode 100644 index 00000000000..983ed22a506 --- /dev/null +++ b/app/views/layouts/_mailer.html.haml @@ -0,0 +1,74 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +%html{ lang: "en" } + %head + %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/ + %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/ + %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/ + %title= message.subject + :css + /* CLIENT-SPECIFIC STYLES */ + body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } + table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; } + img { -ms-interpolation-mode: bicubic; } + + /* iOS BLUE LINKS */ + a[x-apple-data-detectors] { + color: inherit !important; + text-decoration: none !important; + font-size: inherit !important; + font-family: inherit !important; + font-weight: inherit !important; + line-height: inherit !important; + } + + /* ANDROID MARGIN HACK */ + body { margin:0 !important; } + div[style*="margin: 16px 0"] { margin:0 !important; } + + @media only screen and (max-width: 639px) { + body, #body { + min-width: 320px !important; + } + table.wrapper { + width: 100% !important; + min-width: 320px !important; + } + table.wrapper > tbody > tr > td { + border-left: 0 !important; + border-right: 0 !important; + border-radius: 0 !important; + padding-left: 10px !important; + padding-right: 10px !important; + } + } + %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" } + %tbody + %tr.line + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" } + %tr.header + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + = header_logo + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } + %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" } + %tbody + = yield + + %tr.footer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %img{ alt: "GitLab", height: "33", src: image_url('mailers/gitlab_footer_logo.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/ + %div + %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications + · + %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help + %div + You're receiving this email because of your account on + = succeed "." do + %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host + + = yield :additional_footer diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 03688e9ff21..2b07273a0a8 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -3,6 +3,7 @@ = render "layouts/head" %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } = render "layouts/init_auto_complete" if @gfm_form + = render 'peek/bar' = render "layouts/header/default", title: header_title = render 'layouts/page', sidebar: sidebar, nav: nav diff --git a/app/views/layouts/devise_mailer.html.haml b/app/views/layouts/devise_mailer.html.haml deleted file mode 100644 index e1e1f9ae516..00000000000 --- a/app/views/layouts/devise_mailer.html.haml +++ /dev/null @@ -1,34 +0,0 @@ -!!! 5 -%html - %head - %meta{ content: 'text/html; charset=UTF-8', 'http-equiv'=> 'Content-Type' } - = stylesheet_link_tag 'mailers/devise' - - %body - %table#wrapper - %tr - %td - %table#header - %td{ valign: "top" } - = image_tag('mailers/gitlab_header_logo.png', id: 'logo', alt: 'GitLab Wordmark') - - %table#body - %tr - %td#body-container - = yield - - - if Gitlab.com? - %table#footer - %tr - %td#tanuki - = image_tag('mailers/gitlab_tanuki_2x.png', alt: 'GitLab Logo') - %tr - %td#tagline - Everyone can contribute - %tr - %td#social - = link_to 'Blog', 'https://about.gitlab.com/blog/' - = link_to 'Twitter', 'https://twitter.com/gitlab' - = link_to 'Facebook', 'https://www.facebook.com/gitlab/' - = link_to 'YouTube', 'https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg' - = link_to 'LinkedIn', 'https://www.linkedin.com/company/gitlab-com' diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 9db98451f1d..249253f4906 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -36,10 +36,7 @@ %li = link_to admin_root_path, title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('wrench fw') - - if current_user.can_create_project? - %li - = link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('plus fw') + = render 'layouts/header/new_dropdown' - if Gitlab::Sherlock.enabled? %li = link_to sherlock_transactions_path, title: 'Sherlock Transactions', @@ -74,12 +71,12 @@ @#{current_user.username} %li.divider %li - = link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username } + = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username } %li - = link_to "Settings", profile_path, aria: { label: "Settings" } + = link_to "Settings", profile_path %li.divider %li - = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link", aria: { label: "Sign out" } + = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link" - else %li %div diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml new file mode 100644 index 00000000000..c7302414386 --- /dev/null +++ b/app/views/layouts/header/_new_dropdown.haml @@ -0,0 +1,45 @@ +%li.header-new.dropdown + = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do + = icon('plus fw') + = icon('caret-down') + .dropdown-menu-nav.dropdown-menu-align-right + %ul + - if @group + - create_group_project = can?(current_user, :create_projects, @group) + - create_group_subgroup = can?(current_user, :create_subgroup, @group) + - if create_group_project || create_group_subgroup + %li.dropdown-bold-header This group + - if create_group_project + %li.header-new-group-project + = link_to 'New project', new_project_path(namespace_id: @group.id) + - if create_group_subgroup + %li + = link_to 'New subgroup', new_group_path(parent_id: @group.id) + %li.divider + %li.dropdown-bold-header GitLab + + - if @project && @project.persisted? + - create_project_issue = can?(current_user, :create_issue, @project) + - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) + - create_project_snippet = can?(current_user, :create_project_snippet, @project) + - if create_project_issue || merge_project || create_project_snippet + %li.dropdown-bold-header This project + - if create_project_issue + %li + = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project) + - if merge_project + %li + = link_to 'New merge request', new_namespace_project_merge_request_path(merge_project.namespace, merge_project) + - if create_project_snippet + %li.header-new-project-snippet + = link_to 'New snippet', new_namespace_project_snippet_path(@project.namespace, @project) + %li.divider + %li.dropdown-bold-header GitLab + - if current_user.can_create_project? + %li + = link_to 'New project', new_project_path + - if current_user.can_create_group? + %li + = link_to 'New group', new_group_path + %li + = link_to 'New snippet', new_snippet_path diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml index 53268cc22f8..28dcbce7183 100644 --- a/app/views/layouts/mailer.html.haml +++ b/app/views/layouts/mailer.html.haml @@ -1,72 +1 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -%html{ lang: "en" } - %head - %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/ - %meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/ - %meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/ - %title= message.subject - :css - /* CLIENT-SPECIFIC STYLES */ - body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } - table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; } - img { -ms-interpolation-mode: bicubic; } - - /* iOS BLUE LINKS */ - a[x-apple-data-detectors] { - color: inherit !important; - text-decoration: none !important; - font-size: inherit !important; - font-family: inherit !important; - font-weight: inherit !important; - line-height: inherit !important; - } - - /* ANDROID MARGIN HACK */ - body { margin:0 !important; } - div[style*="margin: 16px 0"] { margin:0 !important; } - - @media only screen and (max-width: 639px) { - body, #body { - min-width: 320px !important; - } - table.wrapper { - width: 100% !important; - min-width: 320px !important; - } - table.wrapper > tbody > tr > td { - border-left: 0 !important; - border-right: 0 !important; - border-radius: 0 !important; - padding-left: 10px !important; - padding-right: 10px !important; - } - } - %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } - %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" } - %tbody - %tr.line - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" } - %tr.header - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } - = header_logo - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } - %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } - %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" } - %tbody - = yield - - %tr.footer - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } - %img{ alt: "GitLab", height: "33", src: image_url('mailers/gitlab_footer_logo.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/ - %div - %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications - · - %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help - %div - You're receiving this email because of your account on - = succeed "." do - %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host += render 'layouts/mailer' diff --git a/app/views/layouts/mailer/devise.html.haml b/app/views/layouts/mailer/devise.html.haml new file mode 100644 index 00000000000..054b2c2fa26 --- /dev/null +++ b/app/views/layouts/mailer/devise.html.haml @@ -0,0 +1,21 @@ +- if Gitlab.com? + - content_for :additional_footer do + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:13px;line-height:1.6;color:#5c5c5c;" } + %div + Everyone can contribute + %div + = link_to 'Blog', 'https://about.gitlab.com/blog/', style: "color:#3777b0;text-decoration:none;" + · + = link_to 'Twitter', 'https://twitter.com/gitlab', style: "color:#3777b0;text-decoration:none;" + · + = link_to 'Facebook', 'https://www.facebook.com/gitlab/', style: "color:#3777b0;text-decoration:none;" + · + = link_to 'YouTube', 'https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg', style: "color:#3777b0;text-decoration:none;" + · + = link_to 'LinkedIn', 'https://www.linkedin.com/company/gitlab-com', style: "color:#3777b0;text-decoration:none;" + += render layout: 'layouts/mailer' do + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;" } + = yield diff --git a/app/views/layouts/snippets.html.haml b/app/views/layouts/snippets.html.haml index 98b75cea03f..57971205e0e 100644 --- a/app/views/layouts/snippets.html.haml +++ b/app/views/layouts/snippets.html.haml @@ -1,9 +1,8 @@ - header_title "Snippets", snippets_path - content_for :page_specific_javascripts do - - if @snippet&.persisted? && current_user + - if @snippet && current_user :javascript - window.uploads_path = "#{upload_path('personal_snippet', @snippet)}"; - window.preview_markdown_path = "#{preview_markdown_snippet_path(@snippet)}"; + window.uploads_path = "#{upload_path('personal_snippet', id: @snippet.id)}"; = render template: "layouts/application" diff --git a/app/views/layouts/xml.atom.builder b/app/views/layouts/xml.atom.builder new file mode 100644 index 00000000000..4ee09cb87a1 --- /dev/null +++ b/app/views/layouts/xml.atom.builder @@ -0,0 +1,4 @@ +xml.instruct! +xml.feed 'xmlns' => 'http://www.w3.org/2005/Atom', 'xmlns:media' => 'http://search.yahoo.com/mrss/' do + xml << yield +end diff --git a/app/views/peek/views/_mysql2.html.haml b/app/views/peek/views/_mysql2.html.haml new file mode 100644 index 00000000000..ac811a10ef5 --- /dev/null +++ b/app/views/peek/views/_mysql2.html.haml @@ -0,0 +1,4 @@ +- local_assigns.fetch(:view) + += render 'peek/views/sql', view: view +mysql diff --git a/app/views/peek/views/_pg.html.haml b/app/views/peek/views/_pg.html.haml new file mode 100644 index 00000000000..ee94c2f3274 --- /dev/null +++ b/app/views/peek/views/_pg.html.haml @@ -0,0 +1,4 @@ +- local_assigns.fetch(:view) + += render 'peek/views/sql', view: view +pg diff --git a/app/views/peek/views/_sql.html.haml b/app/views/peek/views/_sql.html.haml new file mode 100644 index 00000000000..16fc010f66f --- /dev/null +++ b/app/views/peek/views/_sql.html.haml @@ -0,0 +1,13 @@ +%strong + %a#peek-show-queries{ href: '#' } + %span{ data: { defer_to: "#{view.defer_key}-duration" } }... + \/ + %span{ data: { defer_to: "#{view.defer_key}-calls" } }... +#modal-peek-pg-queries.modal{ tabindex: -1 } + .modal-dialog + #modal-peek-pg-queries-content.modal-content + .modal-header + %a.close{ href: "#", "data-dismiss" => "modal" } × + %h4 + SQL queries + .modal-body{ data: { defer_to: "#{view.defer_key}-queries" } }... diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 4a1438aa68e..fcfd350f0da 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -49,10 +49,10 @@ .form-group = f.label :email, class: "label-light" - - if @user.ldap_user? && @user.ldap_email? + - if @user.external_email? = f.text_field :email, class: "form-control", required: true, readonly: true %span.help-block.light - Your email address was automatically set based on the LDAP server. + Your email address was automatically set based on your #{email_provider_label} account. - else - if @user.temp_oauth_email? = f.text_field :email, class: "form-control", required: true, value: nil diff --git a/app/views/projects/_find_file_link.html.haml b/app/views/projects/_find_file_link.html.haml index 3feb11645a0..c748ccf65e6 100644 --- a/app/views/projects/_find_file_link.html.haml +++ b/app/views/projects/_find_file_link.html.haml @@ -1,3 +1,3 @@ = link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn btn-grouped shortcuts-find-file', rel: 'nofollow' do = icon('search') - %span Find file + %span= _('Find file') diff --git a/app/views/projects/_head.html.haml b/app/views/projects/_head.html.haml index db08b77c8e0..dba84838a52 100644 --- a/app/views/projects/_head.html.haml +++ b/app/views/projects/_head.html.haml @@ -4,17 +4,14 @@ .nav-links.sub-nav.scrolling-tabs %ul{ class: container_class } = nav_link(path: 'projects#show') do - = link_to project_path(@project), title: 'Project home', class: 'shortcuts-project' do - %span - Home + = link_to project_path(@project), title: _('Project home'), class: 'shortcuts-project' do + %span= _('Home') = nav_link(path: 'projects#activity') do - = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do - %span - Activity + = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do + %span= _('Activity') - if can?(current_user, :read_cycle_analytics, @project) = nav_link(path: 'cycle_analytics#show') do - = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics', class: 'shortcuts-project-cycle-analytics' do - %span - Cycle Analytics + = link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do + %span= _('Cycle Analytics') diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 9a9fca78df3..873b3045ea9 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -14,7 +14,7 @@ - if forked_from_project = @project.forked_from_project %p - Forked from + #{ s_('ForkedFromProjectPath|Forked from') } = link_to project_path(forked_from_project) do = forked_from_project.namespace.try(:name) diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml index e8b1940af2d..f1ef50d2de2 100644 --- a/app/views/projects/_last_push.html.haml +++ b/app/views/projects/_last_push.html.haml @@ -15,4 +15,4 @@ .pull-right = link_to new_mr_path_from_push_event(event), title: "New merge request", class: "btn btn-info btn-sm" do - Create merge request + #{ _('Create merge request') } diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index d0698285f84..07445434cf3 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -9,12 +9,6 @@ %li %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 } Preview - - - if defined?(@issue) && @issue.confidential? - %li.confidential-issue-warning - = icon('warning') - %span This is a confidential issue. Your comment will not be visible to the public. - %li.pull-right .toolbar-group = markdown_toolbar_button({ icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" }) diff --git a/app/views/projects/blame/_age_map_legend.html.haml b/app/views/projects/blame/_age_map_legend.html.haml new file mode 100644 index 00000000000..533dc20ffb3 --- /dev/null +++ b/app/views/projects/blame/_age_map_legend.html.haml @@ -0,0 +1,12 @@ +%span.left-label Newer +%span.legend-box.legend-box-0 +%span.legend-box.legend-box-1 +%span.legend-box.legend-box-2 +%span.legend-box.legend-box-3 +%span.legend-box.legend-box-4 +%span.legend-box.legend-box-5 +%span.legend-box.legend-box-6 +%span.legend-box.legend-box-7 +%span.legend-box.legend-box-8 +%span.legend-box.legend-box-9 +%span.right-label Older diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index a6ee2b2f7b8..ce937ee1842 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -1,4 +1,5 @@ - @no_container = true +- project_duration = age_map_duration(@blame_groups, @project) - page_title "Annotate", @blob.path, @ref = render "projects/commits/head" @@ -8,15 +9,16 @@ .file-holder = render "projects/blob/header", blob: @blob, blame: true - + .file-blame-legend + = render 'age_map_legend' .table-responsive.file-content.blame.code.js-syntax-highlight %table - current_line = 1 - @blame_groups.each do |blame_group| %tr - %td.blame-commit + - commit = blame_group[:commit] + %td.blame-commit{ class: age_map_class(commit.committed_date, project_duration) } .commit - - commit = blame_group[:commit] = author_avatar(commit, size: 36) .commit-row-title %strong diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml index 7f470b890ba..40978583e8b 100644 --- a/app/views/projects/blob/_new_dir.html.haml +++ b/app/views/projects/blob/_new_dir.html.haml @@ -3,18 +3,18 @@ .modal-content .modal-header %a.close{ href: "#", "data-dismiss" => "modal" } × - %h3.page-title Create New Directory + %h3.page-title= _('Create New Directory') .modal-body = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-quick-submit js-requires-input' do .form-group - = label_tag :dir_name, 'Directory name', class: 'control-label' + = label_tag :dir_name, _('Directory name'), class: 'control-label' .col-sm-10 = text_field_tag :dir_name, params[:dir_name], required: true, class: 'form-control' - = render 'shared/new_commit_form', placeholder: "Add new directory" + = render 'shared/new_commit_form', placeholder: _("Add new directory") .form-actions - = submit_tag "Create directory", class: 'btn btn-create' + = submit_tag _("Create directory"), class: 'btn btn-create' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" - unless can?(current_user, :push_code, @project) diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml index db6662a95ac..c8ca0406213 100644 --- a/app/views/projects/blob/_remove.html.haml +++ b/app/views/projects/blob/_remove.html.haml @@ -6,7 +6,7 @@ %h3.page-title Delete #{@blob.name} .modal-body - = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-replace-blob-form js-quick-submit js-requires-input' do + = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-delete-blob-form js-quick-submit js-requires-input' do = render 'shared/new_commit_form', placeholder: "Delete #{@blob.name}" .form-group @@ -15,4 +15,4 @@ = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" :javascript - new NewCommitForm($('.js-replace-blob-form')) + new NewCommitForm($('.js-delete-blob-form')) diff --git a/app/views/projects/blob/_viewer.html.haml b/app/views/projects/blob/_viewer.html.haml index 4252f27d007..013f1c267c8 100644 --- a/app/views/projects/blob/_viewer.html.haml +++ b/app/views/projects/blob/_viewer.html.haml @@ -1,13 +1,19 @@ - hidden = local_assigns.fetch(:hidden, false) - render_error = viewer.render_error -- load_async = local_assigns.fetch(:load_async, viewer.load_async?) +- load_async = local_assigns.fetch(:load_async, viewer.load_async? && render_error.nil?) - viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_async .blob-viewer{ data: { type: viewer.type, url: viewer_url }, class: ('hidden' if hidden) } - - if load_async - = render viewer.loading_partial_path, viewer: viewer - - elsif render_error + - if render_error = render 'projects/blob/render_error', viewer: viewer + - elsif load_async + = render viewer.loading_partial_path, viewer: viewer - else - viewer.prepare! + + -# In the rare case where the first kilobyte of the file looks like text, + -# but the file turns out to actually be binary after loading all data, + -# we fall back on the binary Download viewer. + - viewer = BlobViewer::Download.new(viewer.blob) if viewer.binary_detected_after_load? + = render viewer.partial_path, viewer: viewer diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index efec69662f3..6684ecfce81 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -26,6 +26,7 @@ ":disabled" => "disabled", ":issue-link-base" => "issueLinkBase", ":root-path" => "rootPath", + ":board-id" => "boardId", ":key" => "_uid" } = render "projects/boards/components/sidebar" %board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'), diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml index bc5c727bf0d..539ee087b14 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/projects/boards/components/_board.html.haml @@ -1,22 +1,25 @@ -.board{ ":class" => '{ "is-draggable": !list.preset }', +.board{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded }', ":data-id" => "list.id" } .board-inner - %header.board-header{ ":class" => '{ "has-border": list.label }', ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" } + %header.board-header{ ":class" => '{ "has-border": list.label && list.label.color }', ":style" => "{ borderTopColor: (list.label && list.label.color ? list.label.color : null) }", "@click" => "toggleExpanded($event)" } %h3.board-title.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset) }' } + %i.fa.fa-fw.board-title-expandable-toggle{ "v-if": "list.isExpandable", + ":class": "{ \"fa-caret-down\": list.isExpanded, \"fa-caret-right\": !list.isExpanded && list.position === -1, \"fa-caret-left\": !list.isExpanded && list.position !== -1 }", + "aria-hidden": "true" } %span.has-tooltip{ ":title" => '(list.label ? list.label.description : "")', data: { container: "body", placement: "bottom" } } {{ list.title }} - .board-issue-count-holder.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' } - %span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' } + .issue-count-badge.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' } + %span.issue-count-badge-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' } {{ list.issuesSize }} - if can?(current_user, :admin_issue, @project) - %button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button", + %button.issue-count-badge-add-button.btn.btn-small.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button", "@click" => "showNewIssueForm", "v-if" => 'list.type !== "closed"', "aria-label" => "New issue", "title" => "New issue", data: { placement: "top", container: "body" } } - = icon("plus") + = icon("plus", class: "js-no-trigger-collapse") - if can?(current_user, :admin_list, @project) %board-delete{ "inline-template" => true, ":list" => "list", diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index e2bddee0d13..3cf91bf07f7 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -2,29 +2,29 @@ - if !project.empty_repo? && can?(current_user, :download_code, project) .project-action-button.dropdown.inline> - %button.btn.has-tooltip{ title: 'Download', 'data-toggle' => 'dropdown', 'aria-label' => 'Download' } + %button.btn.has-tooltip{ title: 'Download', 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download') } = icon('download') = icon("caret-down") - %span.sr-only - Select Archive Format + %span.sr-only= _('Select Archive Format') %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } - %li.dropdown-header Source code + %li.dropdown-header + #{ _('Source code') } %li = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow', download: '' do %i.fa.fa-download - %span Download zip + %span= _('Download zip') %li = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow', download: '' do %i.fa.fa-download - %span Download tar.gz + %span= _('Download tar.gz') %li = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow', download: '' do %i.fa.fa-download - %span Download tar.bz2 + %span= _('Download tar.bz2') %li = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow', download: '' do %i.fa.fa-download - %span Download tar + %span= _('Download tar') - if pipeline - artifacts = pipeline.builds.latest.with_artifacts @@ -39,4 +39,5 @@ %li = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow', download: '' do %i.fa.fa-download - %span Download '#{job.name}' + %span + #{ s_('DownloadArtifacts|Download') } '#{job.name}' diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 76a2e720b68..312c349da3a 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -12,19 +12,19 @@ %li = link_to new_namespace_project_issue_path(@project.namespace, @project) do = icon('exclamation-circle fw') - New issue + #{ _('New issue') } - if merge_project %li = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project) do = icon('tasks fw') - New merge request + #{ _('New merge request') } - if can_create_snippet %li = link_to new_namespace_project_snippet_path(@project.namespace, @project) do = icon('file-text-o fw') - New snippet + #{ _('New snippet') } - if can_create_issue || merge_project || can_create_snippet %li.divider @@ -33,20 +33,20 @@ %li = link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do = icon('file fw') - New file + #{ _('New file') } %li = link_to new_namespace_project_branch_path(@project.namespace, @project) do = icon('code-fork fw') - New branch + #{ _('New branch') } %li = link_to new_namespace_project_tag_path(@project.namespace, @project) do = icon('tags fw') - New tag + #{ _('New tag') } - elsif current_user && current_user.already_forked?(@project) %li = link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do = icon('file fw') - New file + #{ _('New file') } - elsif can?(current_user, :fork_project, @project) %li - continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master'), @@ -56,4 +56,4 @@ continue: continue_params) = link_to fork_path, method: :post do = icon('file fw') - New file + #{ _('New file') } diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 0935ca7fa44..7a08bb37494 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -1,14 +1,14 @@ - unless @project.empty_repo? - if current_user && can?(current_user, :fork_project, @project) - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn has-tooltip' do + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: _('Go to your fork'), class: 'btn has-tooltip' do = custom_icon('icon_fork') - %span Fork + %span= s_('GoToYourFork|Fork') - else = link_to new_namespace_project_fork_path(@project.namespace, @project), class: 'btn' do = custom_icon('icon_fork') - %span Fork + %span= s_('CreateNewFork|Fork') .count-with-arrow %span.arrow - = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'count' do + = link_to namespace_project_forks_path(@project.namespace, @project), title: n_('Forks', @project.forks_count), class: 'count' do = @project.forks_count diff --git a/app/views/projects/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml index a5a9e4d0621..de2d61d4aa3 100644 --- a/app/views/projects/buttons/_koding.html.haml +++ b/app/views/projects/buttons/_koding.html.haml @@ -1,3 +1,3 @@ - if koding_enabled? && current_user && @repository.koding_yml && can_push_branch?(@project, @project.default_branch) = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank', rel: 'noopener noreferrer' do - Run in IDE (Koding) + _('Run in IDE (Koding)') diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index d57eb2cbfbc..58413e2fc52 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -2,19 +2,19 @@ = link_to toggle_star_namespace_project_path(@project.namespace, @project), { class: 'btn star-btn toggle-star', method: :post, remote: true } do - if current_user.starred?(@project) = icon('star') - %span.starred Unstar + %span.starred= _('Unstar') - else = icon('star-o') - %span Star + %span= s_('StarProject|Star') .count-with-arrow %span.arrow %span.count.star-count = @project.star_count - else - = link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: 'You must sign in to star a project' do + = link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: _('You must sign in to star a project') do = icon('star') - Star + #{ s_('StarProject|Star') } .count-with-arrow %span.arrow %span.count diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index b5f67cae341..281d823da52 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -18,14 +18,13 @@ = label_tag 'start_branch', branch_label, class: 'control-label' .col-sm-10 = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch' - = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } }) + = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } }) - if can?(current_user, :push_code, @project) - .js-create-merge-request-container - .checkbox - = label_tag do - = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: nil - Start a <strong>new merge request</strong> with these changes + .checkbox + = label_tag do + = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: nil + Start a <strong>new merge request</strong> with these changes - else = hidden_field_tag 'create_merge_request', 1, id: nil .form-actions @@ -35,6 +34,3 @@ - unless can?(current_user, :push_code, @project) .inline.prepend-left-10 = commit_in_fork_help - -:javascript - new NewCommitForm($('.js-#{type}-form'), 'start_branch') diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 0aef5822f81..aab50310234 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -72,8 +72,8 @@ Pipeline = link_to "##{last_pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, last_pipeline.id) = ci_label_for_status(last_pipeline.status) - - if last_pipeline.stages.any? - with #{"stage".pluralize(last_pipeline.stages.count)} + - if last_pipeline.stages_count.nonzero? + with #{"stage".pluralize(last_pipeline.stages_count)} .mr-widget-pipeline-graph = render 'shared/mini_pipeline_graph', pipeline: last_pipeline, klass: 'js-commit-pipeline-graph' in diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 3350a0ec152..7a03c3561af 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -31,12 +31,12 @@ = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author)) .commiter = commit_author_link(commit, avatar: false, size: 24) - committed + #{ _('committed') } #{time_ago_with_tooltip(commit.committed_date)} .commit-actions.flex-row.hidden-xs - if commit.status(ref) = render_commit_status(commit, ref: ref) = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-sha btn btn-transparent" - = clipboard_button(text: commit.id, title: "Copy commit SHA to clipboard") + = clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard")) = link_to_browse_code(project, commit) diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index dd6797f10c0..ebeaab863bc 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -5,32 +5,32 @@ %ul{ class: (container_class) } = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do = link_to project_files_path(@project) do - Files + #{ _('Files') } = nav_link(controller: [:commit, :commits]) do = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do - Commits + #{ _('Commits') } = nav_link(html_options: {class: branches_tab_class}) do = link_to namespace_project_branches_path(@project.namespace, @project) do - Branches + #{ _('Branches') } = nav_link(controller: [:tags, :releases]) do = link_to namespace_project_tags_path(@project.namespace, @project) do - Tags + #{ _('Tags') } = nav_link(path: 'graphs#show') do = link_to namespace_project_graph_path(@project.namespace, @project, current_ref) do - Contributors + #{ _('Contributors') } = nav_link(controller: %w(network)) do = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do - Graph + #{ s_('ProjectNetworkGraph|Graph') } = nav_link(controller: :compare) do = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do - Compare + #{ _('Compare') } = nav_link(path: 'graphs#charts') do = link_to charts_namespace_project_graph_path(@project.namespace, @project, current_ref) do - Charts + #{ _('Charts') } diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder index 2f0b6e39800..9cf792e1721 100644 --- a/app/views/projects/commits/show.atom.builder +++ b/app/views/projects/commits/show.atom.builder @@ -1,10 +1,7 @@ -xml.instruct! -xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do - xml.title "#{@project.name}:#{@ref} commits" - xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), rel: "self", type: "application/atom+xml" - xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref), rel: "alternate", type: "text/html" - xml.id namespace_project_commits_url(@project.namespace, @project, @ref) - xml.updated @commits.first.committed_date.xmlschema if @commits.any? +xml.title "#{@project.name}:#{@ref} commits" +xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), rel: "self", type: "application/atom+xml" +xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref), rel: "alternate", type: "text/html" +xml.id namespace_project_commits_url(@project.namespace, @project, @ref) +xml.updated @commits.first.committed_date.xmlschema if @commits.any? - xml << render(@commits) if @commits.any? -end +xml << render(@commits) if @commits.any? diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 74255167352..7000b289f75 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -2,7 +2,6 @@ - page_title "Cycle Analytics" - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('common_vue') - = page_specific_javascript_bundle_tag('locale') = page_specific_javascript_bundle_tag('cycle_analytics') = render "projects/head" diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml deleted file mode 100644 index ec8fc4c9ee8..00000000000 --- a/app/views/projects/deploy_keys/_deploy_key.html.haml +++ /dev/null @@ -1,30 +0,0 @@ -%li - .pull-left.append-right-10.hidden-xs - = icon "key", class: "key-icon" - .deploy-key-content.key-list-item-info - %strong.title - = deploy_key.title - .description - = deploy_key.fingerprint - - if deploy_key.can_push? - .write-access-allowed - Write access allowed - .deploy-key-content.prepend-left-default.deploy-key-projects - - deploy_key.projects.each do |project| - - if can?(current_user, :read_project, project) - = link_to namespace_project_path(project.namespace, project), class: "label deploy-project-label" do - = project.name_with_namespace - .deploy-key-content - %span.key-created-at - created #{time_ago_with_tooltip(deploy_key.created_at)} - .visible-xs-block.visible-sm-block - - if @deploy_keys.key_available?(deploy_key) - = link_to enable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: "btn btn-sm prepend-left-10", method: :put do - Enable - - else - - if deploy_key.destroyed_when_orphaned? && deploy_key.almost_orphaned? - = link_to disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), data: { confirm: "You are going to remove deploy key. Are you sure?" }, method: :put, class: "btn btn-warning btn-sm prepend-left-10" do - Remove - - else - = link_to disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: "btn btn-warning btn-sm prepend-left-10", method: :put do - Disable diff --git a/app/views/projects/deploy_keys/edit.html.haml b/app/views/projects/deploy_keys/edit.html.haml new file mode 100644 index 00000000000..37219f8d7ae --- /dev/null +++ b/app/views/projects/deploy_keys/edit.html.haml @@ -0,0 +1,10 @@ +- page_title 'Edit Deploy Key' +%h3.page-title Edit Deploy Key +%hr + +%div + = form_for [@project.namespace.becomes(Namespace), @project, @deploy_key], html: { class: 'form-horizontal js-requires-input' } do |f| + = render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key } + .form-actions + = f.submit 'Save changes', class: 'btn-save btn' + = link_to 'Cancel', namespace_project_settings_repository_path(@project.namespace, @project), class: 'btn btn-cancel' diff --git a/app/views/projects/deploy_keys/new.html.haml b/app/views/projects/deploy_keys/new.html.haml deleted file mode 100644 index 01fab3008a7..00000000000 --- a/app/views/projects/deploy_keys/new.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -- page_title "New Deploy Key" -%h3.page-title New Deploy Key -%hr - -= render 'form' diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index c96616a0be4..e2baaa625ae 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -6,7 +6,7 @@ %button.dropdown.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' } = custom_icon('icon_play') = icon('caret-down') - %ul.dropdown-menu + %ul.dropdown-menu.dropdown-menu-align-right - actions.each do |action| - next unless can?(current_user, :update_build, action) %li diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml index 465ddaf713a..4502c397d29 100644 --- a/app/views/projects/deployments/_commit.html.haml +++ b/app/views/projects/deployments/_commit.html.haml @@ -1,16 +1,17 @@ -.branch-commit - - if deployment.ref - %span.icon-container - = deployment.tag? ? icon('tag') : icon('code-fork') - = link_to deployment.ref, project_ref_path(@project, deployment.ref), class: "ref-name" - .icon-container.commit-icon - = custom_icon("icon_commit") - = link_to deployment.short_sha, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-sha" +.table-mobile-content + .branch-commit + - if deployment.ref + %span.icon-container + = deployment.tag? ? icon('tag') : icon('code-fork') + = link_to deployment.ref, project_ref_path(@project, deployment.ref), class: "ref-name" + .icon-container.commit-icon + = custom_icon("icon_commit") + = link_to deployment.short_sha, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-sha" - %p.commit-title.flex-truncate-parent - %span.flex-truncate-child - - if commit_title = deployment.commit_title - = author_avatar(deployment.commit, size: 20) - = link_to_gfm commit_title, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-row-message" - - else - Cant find HEAD commit for this branch + %p.commit-title.flex-truncate-parent + %span.flex-truncate-child + - if commit_title = deployment.commit_title + = author_avatar(deployment.commit, size: 20) + = link_to_gfm commit_title, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-row-message" + - else + Cant find HEAD commit for this branch diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index d6822106266..9b2ec9ae41c 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -1,22 +1,26 @@ -.gl-responsive-table-row.deployment +.gl-responsive-table-row.deployment{ role: 'row' } .table-section.section-10{ role: 'gridcell' } - %strong ##{deployment.iid} + .table-mobile-header{ role: 'rowheader' } ID + %strong.table-mobile-content ##{deployment.iid} .table-section.section-40{ role: 'gridcell' } + .table-mobile-header{ role: 'rowheader' } Commit = render 'projects/deployments/commit', deployment: deployment .table-section.section-15.build-column{ role: 'gridcell' } + .table-mobile-header{ role: 'rowheader' } Job - if deployment.deployable - = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do + = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link table-mobile-content' do #{deployment.deployable.name} (##{deployment.deployable.id}) - if deployment.user by = user_avatar(user: deployment.user, size: 20) .table-section.section-15{ role: 'gridcell' } - #{time_ago_with_tooltip(deployment.created_at)} + .table-mobile-header{ role: 'rowheader' } Created + %span.table-mobile-content= time_ago_with_tooltip(deployment.created_at) - .table-section.section-20.environments-actions.table-button-footer{ role: 'gridcell' } - .btn-group.environment-action-buttons + .table-section.section-20.table-button-footer{ role: 'gridcell' } + .btn-group.table-action-button = render 'projects/deployments/actions', deployment: deployment = render 'projects/deployments/rollback', deployment: deployment diff --git a/app/views/projects/diffs/_collapsed.html.haml b/app/views/projects/diffs/_collapsed.html.haml new file mode 100644 index 00000000000..8772bd4705f --- /dev/null +++ b/app/views/projects/diffs/_collapsed.html.haml @@ -0,0 +1,5 @@ +- diff_file = viewer.diff_file +- url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier)) +.nothing-here-block.diff-collapsed{ data: { diff_for_path: url } } + This diff is collapsed. + %a.click-to-expand Click to expand it. diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml index 59844bc00cd..68f74f702ea 100644 --- a/app/views/projects/diffs/_content.html.haml +++ b/app/views/projects/diffs/_content.html.haml @@ -1,27 +1,2 @@ -- blob = diff_file.blob - .diff-content - - if diff_file.too_large? - .nothing-here-block This diff could not be displayed because it is too large. - - elsif blob.truncated? - .nothing-here-block The file could not be displayed because it is too large. - - elsif blob.readable_text? - - if !diff_file.repository.diffable?(blob) - .nothing-here-block This diff was suppressed by a .gitattributes entry. - - elsif diff_file.collapsed? - - url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier)) - .nothing-here-block.diff-collapsed{ data: { diff_for_path: url } } - This diff is collapsed. - %a.click-to-expand - Click to expand it. - - elsif diff_file.diff_lines.length > 0 - = render "projects/diffs/viewers/text", diff_file: diff_file - - else - - if diff_file.mode_changed? - .nothing-here-block File mode changed - - elsif diff_file.renamed_file? - .nothing-here-block File moved - - elsif blob.image? - = render "projects/diffs/viewers/image", diff_file: diff_file - - else - .nothing-here-block No preview for this file type + = render 'projects/diffs/viewer', viewer: diff_file.rich_viewer || diff_file.simple_viewer diff --git a/app/views/projects/diffs/_render_error.html.haml b/app/views/projects/diffs/_render_error.html.haml new file mode 100644 index 00000000000..47a9ac3ee6b --- /dev/null +++ b/app/views/projects/diffs/_render_error.html.haml @@ -0,0 +1,6 @@ +.nothing-here-block + This #{viewer.switcher_title} could not be displayed because #{diff_render_error_reason(viewer)}. + + You can + = diff_render_error_options(viewer).to_sentence(two_words_connector: ' or ', last_word_connector: ', or ').html_safe + instead. diff --git a/app/views/projects/diffs/_viewer.html.haml b/app/views/projects/diffs/_viewer.html.haml new file mode 100644 index 00000000000..5c4d1760871 --- /dev/null +++ b/app/views/projects/diffs/_viewer.html.haml @@ -0,0 +1,16 @@ +- hidden = local_assigns.fetch(:hidden, false) + +.diff-viewer{ data: { type: viewer.type }, class: ('hidden' if hidden) } + - if viewer.render_error + = render 'projects/diffs/render_error', viewer: viewer + - elsif viewer.collapsed? + = render 'projects/diffs/collapsed', viewer: viewer + - else + - viewer.prepare! + + -# In the rare case where the first kilobyte of the file looks like text, + -# but the file turns out to actually be binary after loading all data, + -# we fall back on the binary No Preview viewer. + - viewer = DiffViewer::NoPreview.new(viewer.diff_file) if viewer.binary_detected_after_load? + + = render viewer.partial_path, viewer: viewer diff --git a/app/views/projects/diffs/viewers/_added.html.haml b/app/views/projects/diffs/viewers/_added.html.haml new file mode 100644 index 00000000000..8004fe16688 --- /dev/null +++ b/app/views/projects/diffs/viewers/_added.html.haml @@ -0,0 +1,2 @@ +.nothing-here-block + File added diff --git a/app/views/projects/diffs/viewers/_deleted.html.haml b/app/views/projects/diffs/viewers/_deleted.html.haml new file mode 100644 index 00000000000..0ac7b4ca8f6 --- /dev/null +++ b/app/views/projects/diffs/viewers/_deleted.html.haml @@ -0,0 +1,2 @@ +.nothing-here-block + File deleted diff --git a/app/views/projects/diffs/viewers/_image.html.haml b/app/views/projects/diffs/viewers/_image.html.haml index ea75373581e..19d08181223 100644 --- a/app/views/projects/diffs/viewers/_image.html.haml +++ b/app/views/projects/diffs/viewers/_image.html.haml @@ -1,3 +1,4 @@ +- diff_file = viewer.diff_file - blob = diff_file.blob - old_blob = diff_file.old_blob - blob_raw_path = diff_file_blob_raw_path(diff_file) diff --git a/app/views/projects/diffs/viewers/_mode_changed.html.haml b/app/views/projects/diffs/viewers/_mode_changed.html.haml new file mode 100644 index 00000000000..69bc96bbdad --- /dev/null +++ b/app/views/projects/diffs/viewers/_mode_changed.html.haml @@ -0,0 +1,3 @@ +- diff_file = viewer.diff_file +.nothing-here-block + File mode changed from #{diff_file.a_mode} to #{diff_file.b_mode} diff --git a/app/views/projects/diffs/viewers/_no_preview.html.haml b/app/views/projects/diffs/viewers/_no_preview.html.haml new file mode 100644 index 00000000000..befe070af2b --- /dev/null +++ b/app/views/projects/diffs/viewers/_no_preview.html.haml @@ -0,0 +1,2 @@ +.nothing-here-block + No preview for this file type diff --git a/app/views/projects/diffs/viewers/_not_diffable.html.haml b/app/views/projects/diffs/viewers/_not_diffable.html.haml new file mode 100644 index 00000000000..b2c677ec59c --- /dev/null +++ b/app/views/projects/diffs/viewers/_not_diffable.html.haml @@ -0,0 +1,2 @@ +.nothing-here-block + This diff was suppressed by a .gitattributes entry. diff --git a/app/views/projects/diffs/viewers/_renamed.html.haml b/app/views/projects/diffs/viewers/_renamed.html.haml new file mode 100644 index 00000000000..ef05ee38d8d --- /dev/null +++ b/app/views/projects/diffs/viewers/_renamed.html.haml @@ -0,0 +1,2 @@ +.nothing-here-block + File moved diff --git a/app/views/projects/diffs/viewers/_text.html.haml b/app/views/projects/diffs/viewers/_text.html.haml index e4b89671724..509e68598c9 100644 --- a/app/views/projects/diffs/viewers/_text.html.haml +++ b/app/views/projects/diffs/viewers/_text.html.haml @@ -1,5 +1,5 @@ +- diff_file = viewer.diff_file - blob = diff_file.blob -- blob.load_all_data!(diff_file.repository) - total_lines = blob.lines.size - total_lines -= 1 if total_lines > 0 && blob.lines.last.blank? - if diff_view == :parallel diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index f9068d11542..23aa4c29e69 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -28,12 +28,12 @@ = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success" - else .table-holder - .ci-table.environments + .ci-table.environments{ role: 'grid' } .gl-responsive-table-row.table-row-header{ role: 'row' } - .table-section.section-10{ role: 'rollheader' } ID - .table-section.section-40{ role: 'rollheader' } Commit - .table-section.section-15{ role: 'rollheader' } Job - .table-section.section-15{ role: 'rollheader' } Created + .table-section.section-10{ role: 'columnheader' } ID + .table-section.section-40{ role: 'columnheader' } Commit + .table-section.section-15{ role: 'columnheader' } Job + .table-section.section-15{ role: 'columnheader' } Created = render @deployments diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml index be0462f91cd..8a409541fe5 100644 --- a/app/views/projects/find_file/show.html.haml +++ b/app/views/projects/find_file/show.html.haml @@ -10,7 +10,7 @@ = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do = @project.path %li.file-finder - %input#file_find.form-control.file-finder-input{ type: "text", placeholder: 'Find by path', autocomplete: 'off' } + %input#file_find.form-control.file-finder-input{ type: "text", placeholder: _('Find by path'), autocomplete: 'off' } .tree-content-holder .table-holder diff --git a/app/views/projects/group_links/_index.html.haml b/app/views/projects/group_links/_index.html.haml deleted file mode 100644 index debb0214d06..00000000000 --- a/app/views/projects/group_links/_index.html.haml +++ /dev/null @@ -1,53 +0,0 @@ -- page_title "Groups" -.row.prepend-top-default - .col-lg-3.settings-sidebar - %h4.prepend-top-0 - Share project with other groups - %p - Projects can be stored in only one group at once. However you can share a project with other groups here. - .col-lg-9 - = form_tag namespace_project_group_links_path(@project.namespace, @project), class: 'js-requires-input', method: :post do - .form-group - = label_tag :link_group_id, "Select a group to share with", class: "label-light" - = groups_select_tag(:link_group_id, data: { skip_groups: @skip_groups }, required: true) - .form-group - = label_tag :link_group_access, "Max access level", class: "label-light" - .select-wrapper - = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control" - = icon('caret-down') - .form-group - = label_tag :expires_at, 'Access expiration date', class: 'label-light' - .clearable-input - = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date-groups', placeholder: 'Select access expiration date', id: 'expires_at_groups' - %i.clear-icon.js-clear-input - .help-block - On this date, all members in the group will automatically lose access to this project. - = submit_tag "Share", class: "btn btn-create" - .col-lg-9.col-lg-offset-3 - %hr - .col-lg-9.col-lg-offset-3.append-bottom-default.enabled-groups - %h5.prepend-top-0 - Groups you share with (#{@group_links.size}) - - if @group_links.present? - %ul.well-list - - @group_links.each do |group_link| - - group = group_link.group - %li - .pull-left.append-right-10.hidden-xs - = icon("folder-open-o", class: "settings-list-icon") - .pull-left - = link_to group do - = group.full_name - %br - up to #{group_link.human_access} - - if group_link.expires? - · - %span{ class: ('text-warning' if group_link.expires_soon?) } - expires in #{distance_of_time_in_words_to_now(group_link.expires_at)} - .pull-right - = link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: "btn btn-transparent" do - %span.sr-only disable sharing - = icon("trash") - - else - .settings-message.text-center - There are no groups with access to your project, add one in the form above diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder index 4feec09bb5d..61346884346 100644 --- a/app/views/projects/issues/index.atom.builder +++ b/app/views/projects/issues/index.atom.builder @@ -1,10 +1,7 @@ -xml.instruct! -xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do - xml.title "#{@project.name} issues" - xml.link href: url_for(params), rel: "self", type: "application/atom+xml" - xml.link href: namespace_project_issues_url(@project.namespace, @project), rel: "alternate", type: "text/html" - xml.id namespace_project_issues_url(@project.namespace, @project) - xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? +xml.title "#{@project.name} issues" +xml.link href: url_for(params), rel: "self", type: "application/atom+xml" +xml.link href: namespace_project_issues_url(@project.namespace, @project), rel: "alternate", type: "text/html" +xml.id namespace_project_issues_url(@project.namespace, @project) +xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? - xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any? -end +xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any? diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index d909b0bfbbd..5f92d020eef 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -5,6 +5,13 @@ - can_update_issue = can?(current_user, :update_issue, @issue) - can_report_spam = @issue.submittable_as_spam_by?(current_user) +- if defined?(@issue) && @issue.confidential? + .confidential-issue-warning{ data: { spy: 'affix' } } + %span.confidential-issue-text + #{confidential_icon(@issue)} This issue is confidential. + %a{ href: help_page_path('user/project/issues/confidential_issues'), target: '_blank' } + What are confidential issues? + .clearfix.detail-page-header .issuable-header .issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) } @@ -19,7 +26,6 @@ = icon('angle-double-left') .issuable-meta - = confidential_icon(@issue) = issuable_meta(@issue, @project, "Issue") .issuable-actions diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml index f700b5c9455..93e8a4e385c 100644 --- a/app/views/projects/jobs/_sidebar.html.haml +++ b/app/views/projects/jobs/_sidebar.html.haml @@ -1,19 +1,15 @@ - builds = @build.pipeline.builds.to_a %aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } } - .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default - Job - %strong ##{@build.id} - %a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" } - = icon('angle-double-right') - - if @build.coverage - .block.coverage - .title - Test coverage - %p.build-detail-row - #{@build.coverage}% - .blocks-container + .block + %strong + = @build.name + %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-build-toggle{ href: "#", 'aria-label': 'Toggle Sidebar', role: 'button' } + = icon('angle-double-right') + + #js-details-block-vue + - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?) .block{ class: ("block-first" if !@build.coverage) } .title @@ -40,37 +36,6 @@ = link_to browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do Browse - .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) } - .title - Job details - - if can?(current_user, :update_build, @build) && @build.retryable? - = link_to "Retry job", retry_namespace_project_job_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: - = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request), class: 'bold' - - if @build.duration - %p.build-detail-row - %span.build-light-text Duration: - = time_interval_in_words(@build.duration) - - if @build.finished_at - %p.build-detail-row - %span.build-light-text Finished: - #{time_ago_with_tooltip(@build.finished_at)} - - if @build.erased_at - %p.build-detail-row - %span.build-light-text Erased: - #{time_ago_with_tooltip(@build.erased_at)} - %p.build-detail-row - %span.build-light-text Runner: - - if @build.runner && current_user && current_user.admin - = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id) - - elsif @build.runner - \##{@build.runner.id} - .btn-group.btn-group-justified{ role: :group } - - if @build.active? - = link_to "Cancel", cancel_namespace_project_job_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post - - if @build.trigger_request .build-widget %h4.title @@ -87,31 +52,35 @@ - @build.trigger_request.variables.each do |key, value| .hide.js-build - .js-build-variable= key - .js-build-value= value + .js-build-variable.trigger-build-variable= key + .js-build-value.trigger-build-value= value .block - .title - Commit title + %p + Commit + = link_to @build.pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, @build.pipeline.sha), class: 'commit-sha link-commit' + = clipboard_button(text: @build.pipeline.short_sha, title: "Copy commit SHA to clipboard") + - if @build.merge_request + in + = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request), class: 'link-commit' + %p.build-light-text.append-bottom-0 #{@build.pipeline.git_commit_title} - - if @build.tags.any? - .block - .title - Tags - - @build.tag_list.each do |tag| - %span.label.label-primary - = tag - - if @build.pipeline.stages_count > 1 .dropdown.build-dropdown - .title Stage + .title + %span{ class: "ci-status-icon-#{@build.pipeline.status}" } + = ci_icon_for_status(@build.pipeline.status) + Pipeline + = link_to "##{@build.pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @build.pipeline), class: 'link-commit' + from + = link_to "#{@build.pipeline.ref}", namespace_project_branch_path(@project.namespace, @project, @build.pipeline.ref), class: 'link-commit' %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span.stage-selection More = icon('chevron-down') %ul.dropdown-menu - - @build.pipeline.stages.each do |stage| + - @build.pipeline.legacy_stages.each do |stage| %li %a.stage-item= stage.name diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index 0d10dfcef70..c73bae0a2c9 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -3,9 +3,8 @@ = render "projects/pipelines/head" %div{ class: container_class } - .build-page - = render "header" - + .build-page.js-build-page + #js-build-header-vue - if @build.stuck? - unless @build.any_runners_online? .bs-callout.bs-callout-warning.js-build-stuck @@ -47,52 +46,52 @@ - if environment.try(:last_deployment) and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')} - .prepend-top-default.js-build-erased - - if @build.erased? + - if @build.erased? + .prepend-top-default.js-build-erased .erased.alert.alert-warning - if @build.erased_by_user? Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)} - else Job has been erased #{time_ago_with_tooltip(@build.erased_at)} - .prepend-top-default - .build-trace-container#build-trace - .top-bar.sticky - .js-truncated-info.truncated-info.hidden< - Showing last - %span.js-truncated-info-size.truncated-info-size>< - KiB of log - - %a.js-raw-link.raw-link{ href: raw_namespace_project_job_path(@project.namespace, @project, @build) }>< Complete Raw - .controllers - - if @build.has_trace? - = link_to raw_namespace_project_job_path(@project.namespace, @project, @build), - title: 'Open raw trace', - data: { placement: 'top', container: 'body' }, - class: 'js-raw-link-controller has-tooltip' do - = icon('download') - - - if can?(current_user, :update_build, @project) && @build.erasable? - = link_to erase_namespace_project_job_path(@project.namespace, @project, @build), - method: :post, - data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' }, - title: 'Erase Build', - class: 'has-tooltip js-erase-link' do - = icon('trash') + .build-trace-container#build-trace + .top-bar.sticky + .js-truncated-info.truncated-info.hidden< + Showing last + %span.js-truncated-info-size.truncated-info-size>< + KiB of log - + %a.js-raw-link.raw-link{ href: raw_namespace_project_job_path(@project.namespace, @project, @build) }>< Complete Raw + .controllers + - if @build.has_trace? + = link_to raw_namespace_project_job_path(@project.namespace, @project, @build), + title: 'Show complete raw', + data: { placement: 'top', container: 'body' }, + class: 'js-raw-link-controller has-tooltip controllers-buttons' do + = icon('file-text-o') - %button.js-scroll-up.btn-scroll.btn-transparent.btn-blank.has-tooltip{ type: 'button', - disabled: true, - title: 'Scroll Up', - data: { placement: 'top', container: 'body'} } + - if can?(current_user, :update_build, @project) && @build.erasable? + = link_to erase_namespace_project_job_path(@project.namespace, @project, @build), + method: :post, + data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' }, + title: 'Erase job log', + class: 'has-tooltip js-erase-link controllers-buttons' do + = icon('trash') + .has-tooltip.controllers-buttons{ title: 'Scroll to top', data: { placement: 'top', container: 'body'} } + %button.js-scroll-up.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true } = custom_icon('scroll_up') - %button.js-scroll-down.btn-scroll.btn-transparent.btn-blank.has-tooltip{ type: 'button', - disabled: true, - title: 'Scroll Down', - data: { placement: 'top', container: 'body'} } + .has-tooltip.controllers-buttons{ title: 'Scroll to bottom', data: { placement: 'top', container: 'body'} } + %button.js-scroll-down.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true } = custom_icon('scroll_down') - .bash.sticky.js-scroll-container - %code.js-build-output - .build-loader-animation.js-build-refresh + .bash.sticky.js-scroll-container + %code.js-build-output + .build-loader-animation.js-build-refresh = render "sidebar" .js-build-options{ data: javascript_build_options } + +#js-job-details-vue{ data: { endpoint: namespace_project_job_path(@project.namespace, @project, @build, format: :json) } } + +- content_for :page_specific_javascripts do + = webpack_bundle_tag('common_vue') + = webpack_bundle_tag('job_details') diff --git a/app/views/projects/no_repo.html.haml b/app/views/projects/no_repo.html.haml index 720957e8336..1cf286ddc40 100644 --- a/app/views/projects/no_repo.html.haml +++ b/app/views/projects/no_repo.html.haml @@ -1,22 +1,22 @@ %h2 %i.fa.fa-warning - No repository + #{ _('No repository') } %p.slead - The repository for this project does not exist. + #{ _('The repository for this project does not exist.') } %br - This means you can not push code until you create an empty repository or import existing one. + #{ _('This means you can not push code until you create an empty repository or import existing one.') } %hr .no-repo-actions = link_to namespace_project_repository_path(@project.namespace, @project), method: :post, class: 'btn btn-primary' do - Create empty bare repository + #{ _('Create empty bare repository') } %strong.prepend-left-10.append-right-10 or = link_to new_namespace_project_import_path(@project.namespace, @project), class: 'btn' do - Import repository + #{ _('Import repository') } - if can? current_user, :remove_project, @project .prepend-top-20 - = link_to 'Remove project', project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" + = link_to _('Remove project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml index 3e79dbec70c..9c42be4e0ff 100644 --- a/app/views/projects/notes/_actions.html.haml +++ b/app/views/projects/notes/_actions.html.haml @@ -37,8 +37,4 @@ %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') - - if note_editable - = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip' do - = icon('pencil', class: 'link-highlight') - = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger has-tooltip' do - = icon('trash-o', class: 'danger-highlight') + = render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml new file mode 100644 index 00000000000..e0d45054854 --- /dev/null +++ b/app/views/projects/notes/_more_actions_dropdown.html.haml @@ -0,0 +1,14 @@ +.dropdown.more-actions + = button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn btn-transparent', data: { toggle: 'dropdown', container: 'body' } do + = icon('ellipsis-v', class: 'icon') + %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left + %li + = button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent' + %li.divider + %li + = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do + Report as abuse + - if note_editable + %li + = link_to note_url(note), method: :delete, data: { confirm: 'Are you sure you want to delete this comment?' }, remote: true, class: 'js-note-delete' do + %span.text-danger Delete comment diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml index bbed10039af..e8dedf26206 100644 --- a/app/views/projects/pipeline_schedules/_form.html.haml +++ b/app/views/projects/pipeline_schedules/_form.html.haml @@ -6,28 +6,28 @@ = form_errors(@schedule) .form-group .col-md-9 - = f.label :description, 'Description', class: 'label-light' - = f.text_field :description, class: 'form-control', required: true, autofocus: true, placeholder: 'Provide a short description for this pipeline' + = f.label :description, _('Description'), class: 'label-light' + = f.text_field :description, class: 'form-control', required: true, autofocus: true, placeholder: _('PipelineSchedules|Provide a short description for this pipeline') .form-group .col-md-9 - = f.label :cron, 'Interval Pattern', class: 'label-light' + = f.label :cron, _('Interval Pattern'), class: 'label-light' #interval-pattern-input{ data: { initial_interval: @schedule.cron } } .form-group .col-md-9 - = f.label :cron_timezone, 'Cron Timezone', class: 'label-light' - = dropdown_tag("Select a timezone", options: { toggle_class: 'btn js-timezone-dropdown', title: "Select a timezone", filter: true, placeholder: "Filter", data: { data: timezone_data } } ) + = f.label :cron_timezone, _('Cron Timezone'), class: 'label-light' + = dropdown_tag(_("Select a timezone"), options: { toggle_class: 'btn js-timezone-dropdown', title: _("Select a timezone"), filter: true, placeholder: _("Filter"), data: { data: timezone_data } } ) = f.text_field :cron_timezone, value: @schedule.cron_timezone, id: 'schedule_cron_timezone', class: 'hidden', name: 'schedule[cron_timezone]', required: true .form-group .col-md-9 - = f.label :ref, 'Target Branch', class: 'label-light' - = dropdown_tag("Select target branch", options: { toggle_class: 'btn js-target-branch-dropdown git-revision-dropdown-toggle', dropdown_class: 'git-revision-dropdown', title: "Select target branch", filter: true, placeholder: "Filter", data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } ) + = f.label :ref, _('Target Branch'), class: 'label-light' + = dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: _("Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } ) = f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true .form-group .col-md-9 - = f.label :active, 'Activated', class: 'label-light' + = f.label :active, _('PipelineSchedules|Activated'), class: 'label-light' %div = f.check_box :active, required: false, value: @schedule.active? Active .footer-block.row-content-block - = f.submit 'Save pipeline schedule', class: 'btn btn-create', tabindex: 3 - = link_to 'Cancel', pipeline_schedules_path(@project), class: 'btn btn-cancel' + = f.submit _('Save pipeline schedule'), class: 'btn btn-create', tabindex: 3 + = link_to _('Cancel'), pipeline_schedules_path(@project), class: 'btn btn-cancel' diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml index 7bde839e26f..2d3344a4aaf 100644 --- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml +++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml @@ -13,12 +13,12 @@ = ci_icon_for_status(pipeline_schedule.last_pipeline.status) %span ##{pipeline_schedule.last_pipeline.id} - else - None + = _("PipelineSchedules|None") %td.next-run-cell - if pipeline_schedule.active? = time_ago_with_tooltip(pipeline_schedule.real_next_run) - else - Inactive + = _("PipelineSchedules|Inactive") %td - if pipeline_schedule.owner = image_tag avatar_icon(pipeline_schedule.owner, 20), class: "avatar s20" @@ -27,11 +27,11 @@ %td .pull-right.btn-group - if can?(current_user, :update_pipeline_schedule, @project) && !pipeline_schedule.owned_by?(current_user) - = link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: 'Take Ownership', class: 'btn' do - Take ownership + = link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('PipelineSchedules|Take ownership'), class: 'btn' do + = s_('PipelineSchedules|Take ownership') - if can?(current_user, :update_pipeline_schedule, pipeline_schedule) - = link_to edit_pipeline_schedule_path(pipeline_schedule), title: 'Edit', class: 'btn' do + = link_to edit_pipeline_schedule_path(pipeline_schedule), title: _('Edit'), class: 'btn' do = icon('pencil') - if can?(current_user, :admin_pipeline_schedule, pipeline_schedule) - = link_to pipeline_schedule_path(pipeline_schedule), title: 'Delete', method: :delete, class: 'btn btn-remove', data: { confirm: "Are you sure you want to cancel this pipeline?" } do + = link_to pipeline_schedule_path(pipeline_schedule), title: _('Delete'), method: :delete, class: 'btn btn-remove', data: { confirm: _("Are you sure you want to delete this pipeline schedule?") } do = icon('trash') diff --git a/app/views/projects/pipeline_schedules/_table.html.haml b/app/views/projects/pipeline_schedules/_table.html.haml index 25c7604eb24..d0c7ea77263 100644 --- a/app/views/projects/pipeline_schedules/_table.html.haml +++ b/app/views/projects/pipeline_schedules/_table.html.haml @@ -2,11 +2,11 @@ %table.table.ci-table %thead %tr - %th Description - %th Target - %th Last Pipeline - %th Next Run - %th Owner + %th= _("Description") + %th= s_("PipelineSchedules|Target") + %th= _("Last Pipeline") + %th= s_("PipelineSchedules|Next Run") + %th= _("Owner") %th = render partial: "pipeline_schedule", collection: @schedules diff --git a/app/views/projects/pipeline_schedules/_tabs.html.haml b/app/views/projects/pipeline_schedules/_tabs.html.haml index 2a1fb16876a..7fcb624a9dd 100644 --- a/app/views/projects/pipeline_schedules/_tabs.html.haml +++ b/app/views/projects/pipeline_schedules/_tabs.html.haml @@ -1,18 +1,18 @@ %ul.nav-links %li{ class: active_when(scope.nil?) }> = link_to schedule_path_proc.call(nil) do - All + = s_("PipelineSchedules|All") %span.badge.js-totalbuilds-count = number_with_delimiter(all_schedules.count(:id)) %li{ class: active_when(scope == 'active') }> = link_to schedule_path_proc.call('active') do - Active + = s_("PipelineSchedules|Active") %span.badge = number_with_delimiter(all_schedules.active.count(:id)) %li{ class: active_when(scope == 'inactive') }> = link_to schedule_path_proc.call('inactive') do - Inactive + = s_("PipelineSchedules|Inactive") %span.badge = number_with_delimiter(all_schedules.inactive.count(:id)) diff --git a/app/views/projects/pipeline_schedules/edit.html.haml b/app/views/projects/pipeline_schedules/edit.html.haml index e16fe0b7a98..9b2a7b5821d 100644 --- a/app/views/projects/pipeline_schedules/edit.html.haml +++ b/app/views/projects/pipeline_schedules/edit.html.haml @@ -1,7 +1,7 @@ -- page_title "Edit", @schedule.description, "Pipeline Schedule" +- page_title _("Edit"), @schedule.description, _("Pipeline Schedule") %h3.page-title - Edit Pipeline Schedule #{@schedule.id} + = _("Edit Pipeline Schedule %{id}") % { id: @schedule.id } %hr = render "form" diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml index 6751efaaf2f..4a96ee652d2 100644 --- a/app/views/projects/pipeline_schedules/index.html.haml +++ b/app/views/projects/pipeline_schedules/index.html.haml @@ -3,7 +3,7 @@ = webpack_bundle_tag 'schedules_index' - @no_container = true -- page_title "Pipeline Schedules" +- page_title _("Pipeline Schedules") = render "projects/pipelines/head" %div{ class: container_class } @@ -21,4 +21,4 @@ = render partial: "table" - else .light-well - .nothing-here-block No schedules + .nothing-here-block= _("No schedules") diff --git a/app/views/projects/pipeline_schedules/new.html.haml b/app/views/projects/pipeline_schedules/new.html.haml index b89e170ad3c..87390d4dd02 100644 --- a/app/views/projects/pipeline_schedules/new.html.haml +++ b/app/views/projects/pipeline_schedules/new.html.haml @@ -1,7 +1,7 @@ -- page_title "New Pipeline Schedule" +- page_title _("New Pipeline Schedule") %h3.page-title - Schedule a new pipeline + = _("Schedule a new pipeline") %hr = render "form" diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index a33da149c62..d2f0cb0806f 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -10,7 +10,7 @@ Pipelines - if project_nav_tab? :builds - = nav_link(controller: [:builds, :artifacts]) do + = nav_link(controller: [:jobs, :artifacts]) do = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do %span Jobs diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 01cf2cc80e5..85550e8fd32 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -42,7 +42,7 @@ %th %th Coverage %th - = render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage + = render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage - if failed_builds.present? #js-tab-failures.build-failures.tab-pane - failed_builds.each_with_index do |build, index| diff --git a/app/views/projects/project_members/_index.html.haml b/app/views/projects/project_members/_index.html.haml index d080b6c83d4..cfae371e169 100644 --- a/app/views/projects/project_members/_index.html.haml +++ b/app/views/projects/project_members/_index.html.haml @@ -1,11 +1,12 @@ .row.prepend-top-default .col-lg-3.settings-sidebar %h4.prepend-top-0 - Members + Project members - if can?(current_user, :admin_project_member, @project) %p - Add a new member to + You can add a new member to %strong= @project.name + or share it with another group. - else %p Members can be added by project @@ -13,9 +14,20 @@ or %i Owners .col-lg-9 - .light.prepend-top-default + .light - if can?(current_user, :admin_project_member, @project) - = render "projects/project_members/new_project_member" + %ul.nav-links.project-member-tabs{ role: 'tablist' } + %li.active{ role: 'presentation' } + %a{ href: '#add-member-pane', id: 'add-member-tab', data: { toggle: 'tab' }, role: 'tab' } Add member + - if @project.allowed_to_share_with_group? + %li{ role: 'presentation' } + %a{ href: '#share-with-group-pane', id: 'share-with-group-tab', data: { toggle: 'tab' }, role: 'tab' } Share with group + + .tab-content.project-member-tab-content + .tab-pane.active{ id: 'add-member-pane', role: 'tabpanel' } + = render 'projects/project_members/new_project_member', tab_title: 'Add member' + .tab-pane{ id: 'share-with-group-pane', role: 'tabpanel' } + = render 'projects/project_members/new_shared_group', tab_title: 'Share with group' = render 'shared/members/requests', membership_source: @project, requesters: @requesters .clearfix diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index 2b1c23f7dda..247c4bdbe2d 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -1,18 +1,19 @@ -= form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'users-project-form' } do |f| - .form-group - = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true, placeholder: "Search for members to update or invite") - .help-block.append-bottom-10 - Search for members by name, username, or email, or invite new ones using their email address. - .form-group - = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select" - .help-block.append-bottom-10 - = link_to "Read more", help_page_path("user/permissions"), class: "vlink" - about role permissions - .form-group - .clearable-input - = 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 member(s) will automatically lose access to this project. - = f.submit "Add to project", class: "btn btn-create" - = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default", title: "Import members from another project" +.row + .col-sm-12 + = form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'users-project-form' } do |f| + .form-group + = label_tag :user_ids, "Select members to invite", class: "label-light" + = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true, placeholder: "Search for members to update or invite") + .form-group + = label_tag :access_level, "Choose a role permission", class: "label-light" + = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select" + .help-block.append-bottom-10 + = link_to "Read more", help_page_path("user/permissions"), class: "vlink" + about role permissions + .form-group + .clearable-input + = label_tag :expires_at, 'Access expiration date', class: 'label-light' + = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' + %i.clear-icon.js-clear-input + = f.submit "Add to project", class: "btn btn-create" + = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default", title: "Import members from another project" diff --git a/app/views/projects/project_members/_new_shared_group.html.haml b/app/views/projects/project_members/_new_shared_group.html.haml new file mode 100644 index 00000000000..b7cc8dd7062 --- /dev/null +++ b/app/views/projects/project_members/_new_shared_group.html.haml @@ -0,0 +1,20 @@ +.row + .col-sm-12 + = form_tag namespace_project_group_links_path(@project.namespace, @project), class: 'js-requires-input', method: :post do + .form-group + = label_tag :link_group_id, "Select a group to share with", class: "label-light" + = groups_select_tag(:link_group_id, data: { skip_groups: @skip_groups }, class: "input-clamp", required: true) + .form-group + = label_tag :link_group_access, "Max access level", class: "label-light" + .select-wrapper + = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control" + = icon('caret-down') + .help-block.append-bottom-10 + = link_to "Read more", help_page_path("user/permissions"), class: "vlink" + about role permissions + .form-group + = label_tag :expires_at, 'Access expiration date', class: 'label-light' + .clearable-input + = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date-groups', placeholder: 'Expiration date', id: 'expires_at_groups' + %i.clear-icon.js-clear-input + = submit_tag "Share", class: "btn btn-create" diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml index 20e1ad68244..343807b87cd 100644 --- a/app/views/projects/settings/members/show.html.haml +++ b/app/views/projects/settings/members/show.html.haml @@ -2,6 +2,3 @@ = render "projects/settings/head" = render "projects/project_members/index" -- if can?(current_user, :admin_project, @project) - - if @project.allowed_to_share_with_group? - = render "projects/group_links/index" diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder index 5c7f2e315f0..ed34f5c0520 100644 --- a/app/views/projects/show.atom.builder +++ b/app/views/projects/show.atom.builder @@ -1,10 +1,7 @@ -xml.instruct! -xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do - xml.title "#{@project.name} activity" - xml.link href: namespace_project_url(@project.namespace, @project, rss_url_options), rel: "self", type: "application/atom+xml" - xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html" - xml.id namespace_project_url(@project.namespace, @project) - xml.updated @events[0].updated_at.xmlschema if @events[0] +xml.title "#{@project.name} activity" +xml.link href: namespace_project_url(@project.namespace, @project, rss_url_options), rel: "self", type: "application/atom+xml" +xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html" +xml.id namespace_project_url(@project.namespace, @project) +xml.updated @events[0].updated_at.xmlschema if @events[0] - xml << render(@events) if @events.any? -end +xml << render(@events) if @events.any? diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 1ca464696ed..7447197ed89 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -17,24 +17,24 @@ %ul.nav %li = link_to project_files_path(@project) do - Files (#{storage_counter(@project.statistics.total_repository_size)}) + #{_('Files')} (#{storage_counter(@project.statistics.total_repository_size)}) %li = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do - #{'Commit'.pluralize(@project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)}) - %li + #{n_('Commit', 'Commits', @project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)}) + %l = link_to namespace_project_branches_path(@project.namespace, @project) do - #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) + #{n_('Branch', 'Branches', @repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) %li = link_to namespace_project_tags_path(@project.namespace, @project) do - #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)}) + #{n_('Tag', 'Tags', @repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)}) - if default_project_view != 'readme' && @repository.readme %li - = link_to 'Readme', readme_path(@project) + = link_to _('Readme'), readme_path(@project) - if @repository.changelog %li - = link_to 'Changelog', changelog_path(@project) + = link_to _('Changelog'), changelog_path(@project) - if @repository.license_blob %li @@ -42,43 +42,43 @@ - if @repository.contribution_guide %li - = link_to 'Contribution guide', contribution_guide_path(@project) + = link_to _('Contribution guide'), contribution_guide_path(@project) - if @repository.gitlab_ci_yml %li - = link_to 'CI configuration', ci_configuration_path(@project) + = link_to _('CI configuration'), ci_configuration_path(@project) - if current_user && can_push_branch?(@project, @project.default_branch) - unless @repository.changelog %li.missing = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do - Add Changelog + #{ _('Add Changelog') } - unless @repository.license_blob %li.missing = link_to add_special_file_path(@project, file_name: 'LICENSE') do - Add License + #{ _('Add License') } - unless @repository.contribution_guide %li.missing = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do - Add Contribution guide + #{ _('Add Contribution guide') } - unless @repository.gitlab_ci_yml %li.missing = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do - Set up CI + #{ _('Set up CI') } - if koding_enabled? && @repository.koding_yml.blank? %li.missing - = link_to 'Set up Koding', add_koding_stack_path(@project) + = link_to _('Set up Koding'), add_koding_stack_path(@project) - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present? %li.missing = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', branch_name: 'auto-deploy', context: 'autodeploy') do - Set up auto deploy + #{ _('Set up auto deploy') } %div{ class: container_class } - if @project.archived? .text-warning.center.prepend-top-20 %p = icon("exclamation-triangle fw") - Archived project! Repository is read-only + #{ _('Archived project! Repository is read-only') } - view_path = default_project_view diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index 2e34803b143..7854e1305db 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -3,10 +3,10 @@ %table.table#tree-slider{ class: "table_#{@hex_path} tree-table" } %thead %tr - %th Name + %th= s_('ProjectFileTree|Name') %th.hidden-xs - .pull-left Last commit - %th.text-right Last Update + .pull-left= _('Last commit') + %th.text-right= _('Last Update') - if @path.present? %tr.tree-item %td.tree-item-file-name @@ -20,7 +20,7 @@ = render "projects/tree/readme", readme: tree.readme - if can_edit_tree? - = render 'projects/blob/upload', title: 'Upload New File', placeholder: 'Upload new file', button_title: 'Upload file', form_path: namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post + = render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post = render 'projects/blob/new_dir' :javascript diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index e4d9e24f56e..abde2a48587 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -1,7 +1,7 @@ .tree-controls = render 'projects/find_file_link' - = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'btn btn-grouped' + = link_to s_('Commits|History'), namespace_project_commits_path(@project.namespace, @project, @id), class: 'btn btn-grouped' = render 'projects/buttons/download', project: @project, ref: @ref @@ -19,7 +19,7 @@ - if current_user %li - if !on_top_of_branch? - %span.btn.add-to-tree.disabled.has-tooltip{ title: "You can only add files when you are on a branch", data: { container: 'body' } } + %span.btn.add-to-tree.disabled.has-tooltip{ title: _("You can only add files when you are on a branch"), data: { container: 'body' } } = icon('plus') - else %span.dropdown @@ -30,15 +30,15 @@ %li = link_to namespace_project_new_blob_path(@project.namespace, @project, @id) do = icon('pencil fw') - New file + #{ _('New file') } %li = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do = icon('file fw') - Upload file + #{ _('Upload file') } %li = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do = icon('folder fw') - New directory + #{ _('New directory') } - elsif can?(current_user, :fork_project, @project) %li - continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @id), @@ -48,7 +48,7 @@ continue: continue_params) = link_to fork_path, method: :post do = icon('pencil fw') - New file + #{ _('New file') } %li - continue_params = { to: request.fullpath, notice: edit_in_new_fork_notice + " Try to upload a file again.", @@ -57,7 +57,7 @@ continue: continue_params) = link_to fork_path, method: :post do = icon('file fw') - Upload file + #{ _('Upload file') } %li - continue_params = { to: request.fullpath, notice: edit_in_new_fork_notice + " Try to create a new directory again.", @@ -66,14 +66,14 @@ continue: continue_params) = link_to fork_path, method: :post do = icon('folder fw') - New directory + #{ _('New directory') } %li.divider %li = link_to new_namespace_project_branch_path(@project.namespace, @project) do = icon('code-fork fw') - New branch + #{ _('New branch') } %li = link_to new_namespace_project_tag_path(@project.namespace, @project) do = icon('tags fw') - New tag + #{ _('New tag') } diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index f7e410e27b8..96a08f9f8be 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -1,6 +1,6 @@ - @no_container = true -- page_title @path.presence || "Files", @ref +- page_title @path.presence || _("Files"), @ref = content_for :meta_tags do = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits") = render "projects/commits/head" diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 6cb7c1e9c4d..c10b3004bc3 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -5,13 +5,13 @@ = f.hidden_field :title, value: @page.title .form-group - = f.label :format, class: 'control-label' - .col-sm-10 + .col-sm-12= f.label :format, class: 'control-label-full-width' + .col-sm-12 = f.select :format, options_for_select(ProjectWiki::MARKUPS, {selected: @page.format}), {}, class: "form-control" .form-group - = f.label :content, class: 'control-label' - .col-sm-10 + .col-sm-12= f.label :content, class: 'control-label-full-width' + .col-sm-12 = render layout: 'projects/md_preview', locals: { url: namespace_project_wiki_preview_markdown_path(@project.namespace, @project, @page.slug) } do = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: 'Write your content or drag files here...' = render 'shared/notes/hints' @@ -29,8 +29,8 @@ = link_to 'documentation', help_page_path("user/markdown", anchor: "wiki-specific-markdown") .form-group - = f.label :commit_message, class: 'control-label' - .col-sm-10= f.text_field :message, class: 'form-control', rows: 18, value: commit_message + .col-sm-12= f.label :commit_message, class: 'control-label-full-width' + .col-sm-12= f.text_field :message, class: 'form-control', rows: 18, value: commit_message .form-actions - if @page && @page.persisted? diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index ba47574563d..1e553940593 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -1,21 +1,18 @@ -- @no_container = true - -%div{ class: container_class } - #modal-new-wiki.modal - .modal-dialog - .modal-content - .modal-header - %a.close{ href: "#", "data-dismiss" => "modal" } × - %h3.page-title New Wiki Page - .modal-body - %form.new-wiki-page - .form-group - = label_tag :new_wiki_path do - %span Page slug - = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project), autofocus: true - %span.new-wiki-page-slug-tip - = icon('lightbulb-o') - Tip: You can specify the full path for the new file. - We will automatically create any missing directories. - .form-actions - = button_tag 'Create page', class: 'build-new-wiki btn btn-create' +#modal-new-wiki.modal + .modal-dialog + .modal-content + .modal-header + %a.close{ href: "#", "data-dismiss" => "modal" } × + %h3.page-title New Wiki Page + .modal-body + %form.new-wiki-page + .form-group + = label_tag :new_wiki_path do + %span Page slug + = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project), autofocus: true + %span.new-wiki-page-slug-tip + = icon('lightbulb-o') + Tip: You can specify the full path for the new file. + We will automatically create any missing directories. + .form-actions + = button_tag 'Create page', class: 'build-new-wiki btn btn-create' diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index b995d08cd02..fbe192a40ec 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -1,35 +1,34 @@ -- @no_container = true +- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout - page_title "Edit", @page.title.capitalize, "Wiki" -%div{ class: container_class } - .wiki-page-header.has-sidebar-toggle - %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } - = icon('angle-double-left') +.wiki-page-header.has-sidebar-toggle + %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } + = icon('angle-double-left') - .nav-text - %h2.wiki-page-title + .nav-text + %h2.wiki-page-title + - if @page.persisted? + = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) + - else + = @page.title.capitalize + %span.light + · - if @page.persisted? - = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) + Edit Page - else - = @page.title.capitalize - %span.light - · - - if @page.persisted? - Edit Page - - else - Create Page + Create Page - .nav-controls - - if can?(current_user, :create_wiki, @project) - = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do - New page - - if @page.persisted? - = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do - Page history - - if can?(current_user, :admin_wiki, @project) - = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger" do - Delete + .nav-controls + - if can?(current_user, :create_wiki, @project) + = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do + New page + - if @page.persisted? + = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do + Page history + - if can?(current_user, :admin_wiki, @project) + = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger" do + Delete - = render 'form' += render 'form' = render 'sidebar' diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index 68862206248..e64dd6085fe 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -1,43 +1,42 @@ -- @no_container = true +- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout - page_title "Git Access", "Wiki" -%div{ class: container_class } - .wiki-page-header.has-sidebar-toggle - %button.btn.btn-default.visible-xs.visible-sm.pull-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } - = icon('angle-double-left') +.wiki-page-header.has-sidebar-toggle + %button.btn.btn-default.visible-xs.visible-sm.pull-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } + = icon('angle-double-left') - .git-access-header - Clone repository - %strong= @project_wiki.path_with_namespace + .git-access-header + Clone repository + %strong= @project_wiki.path_with_namespace - = render "shared/clone_panel", project: @project_wiki + = render "shared/clone_panel", project: @project_wiki - .wiki-git-access - %h3 Install Gollum - %pre.dark - :preserve - gem install gollum - %p - It is recommended to install - %code github-markdown - so that GFM features render locally: - %pre.dark - :preserve - gem install github-markdown +.wiki-git-access + %h3 Install Gollum + %pre.dark + :preserve + gem install gollum + %p + It is recommended to install + %code github-markdown + so that GFM features render locally: + %pre.dark + :preserve + gem install github-markdown - %h3 Clone your wiki - %pre.dark - :preserve - git clone #{ content_tag(:span, h(default_url_to_repo(@project_wiki)), class: 'clone')} - cd #{h @project_wiki.path} + %h3 Clone your wiki + %pre.dark + :preserve + git clone #{ content_tag(:span, h(default_url_to_repo(@project_wiki)), class: 'clone')} + cd #{h @project_wiki.path} - %h3 Start Gollum and edit locally - %pre.dark - :preserve - gollum - == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin - >> Thin web server (v1.5.0 codename Knife) - >> Maximum connections set to 1024 - >> Listening on 0.0.0.0:4567, CTRL+C to stop + %h3 Start Gollum and edit locally + %pre.dark + :preserve + gollum + == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin + >> Thin web server (v1.5.0 codename Knife) + >> Maximum connections set to 1024 + >> Listening on 0.0.0.0:4567, CTRL+C to stop = render 'sidebar' diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index dd7213622c1..0e47e2a5fa3 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -1,42 +1,41 @@ - page_title "History", @page.title.capitalize, "Wiki" -%div{ class: container_class } - .wiki-page-header.has-sidebar-toggle - %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } - = icon('angle-double-left') +.wiki-page-header.has-sidebar-toggle + %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } + = icon('angle-double-left') - .nav-text - %h2.wiki-page-title - = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) - %span.light - · - History + .nav-text + %h2.wiki-page-title + = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) + %span.light + · + History - .table-holder - %table.table - %thead +.table-holder + %table.table + %thead + %tr + %th Page version + %th Author + %th Commit Message + %th Last updated + %th Format + %tbody + - @page.versions.each_with_index do |version, index| + - commit = version %tr - %th Page version - %th Author - %th Commit Message - %th Last updated - %th Format - %tbody - - @page.versions.each_with_index do |version, index| - - commit = version - %tr - %td - = link_to project_wiki_path_with_version(@project, @page, - commit.id, index == 0) do - = truncate_sha(commit.id) - %td - = commit.author.name - %td - = commit.message - %td - #{time_ago_with_tooltip(version.authored_date)} - %td - %strong - = @page.page.wiki.page(@page.page.name, commit.id).try(:format) + %td + = link_to project_wiki_path_with_version(@project, @page, + commit.id, index == 0) do + = truncate_sha(commit.id) + %td + = commit.author.name + %td + = commit.message + %td + #{time_ago_with_tooltip(version.authored_date)} + %td + %strong + = @page.page.wiki.page(@page.page.name, commit.id).try(:format) = render 'sidebar' diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index c00967546aa..f003ff6b63f 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -1,32 +1,31 @@ -- @no_container = true +- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout - page_title @page.title.capitalize, "Wiki" -%div{ class: container_class } - .wiki-page-header.has-sidebar-toggle - %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } - = icon('angle-double-left') +.wiki-page-header.has-sidebar-toggle + %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } + = icon('angle-double-left') - .wiki-breadcrumb - %span= breadcrumb(@page.slug) + .wiki-breadcrumb + %span= breadcrumb(@page.slug) - .nav-text - %h2.wiki-page-title= @page.title.capitalize - %span.wiki-last-edit-by - Last edited by - %strong - #{@page.commit.author.name} - #{time_ago_with_tooltip(@page.commit.authored_date)} + .nav-text + %h2.wiki-page-title= @page.title.capitalize + %span.wiki-last-edit-by + Last edited by + %strong + #{@page.commit.author.name} + #{time_ago_with_tooltip(@page.commit.authored_date)} - .nav-controls - = render 'main_links' + .nav-controls + = render 'main_links' - - if @page.historical? - .warning_message - This is an old version of this page. - You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}. +- if @page.historical? + .warning_message + This is an old version of this page. + You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}. - .wiki-holder.prepend-top-default.append-bottom-default - .wiki - = render_wiki_content(@page) +.wiki-holder.prepend-top-default.append-bottom-default + .wiki + = render_wiki_content(@page) = render 'sidebar' diff --git a/app/views/shared/_branch_switcher.html.haml b/app/views/shared/_branch_switcher.html.haml deleted file mode 100644 index 69e3f3042a9..00000000000 --- a/app/views/shared/_branch_switcher.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -- dropdown_toggle_text = @branch_name || tree_edit_branch -= hidden_field_tag 'branch_name', dropdown_toggle_text - -.dropdown - = dropdown_toggle dropdown_toggle_text, { toggle: 'dropdown', selected: dropdown_toggle_text, field_name: 'branch_name', form_id: '.js-edit-blob-form', refs_url: namespace_project_branches_path(@project.namespace, @project) }, { toggle_class: 'js-project-branches-dropdown js-target-branch' } - .dropdown-menu.dropdown-menu-selectable.dropdown-menu-paging.dropdown-menu-branches - = render partial: 'shared/projects/blob/branch_page_default' - = render partial: 'shared/projects/blob/branch_page_create' diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 0992a65f7cd..75704eda361 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -19,7 +19,7 @@ = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: 'Project clone URL' } .input-group-btn - = clipboard_button(target: '#project_clone', title: "Copy URL to clipboard") + = clipboard_button(target: '#project_clone', title: _("Copy URL to clipboard"), class: "btn-default btn-clipboard") :javascript $('ul.clone-options-dropdown a').on('click',function(e){ diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index bd994cdad01..c185e9b73ee 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -64,7 +64,7 @@ %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) + - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, 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') diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml index 07970ad9cba..aa93572bf94 100644 --- a/app/views/shared/_mini_pipeline_graph.html.haml +++ b/app/views/shared/_mini_pipeline_graph.html.haml @@ -1,5 +1,5 @@ .stage-cell - - pipeline.stages.each do |stage| + - pipeline.legacy_stages.each do |stage| - if stage.status - detailed_status = stage.detailed_status(current_user) - icon_status = "#{detailed_status.icon}_borderless" diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml index 0b37fe3013b..25a56f84ec5 100644 --- a/app/views/shared/_new_commit_form.html.haml +++ b/app/views/shared/_new_commit_form.html.haml @@ -7,7 +7,7 @@ .form-group.branch = label_tag 'branch_name', 'Target branch', class: 'control-label' .col-sm-10 - = render 'shared/branch_switcher' + = text_field_tag 'branch_name', @branch_name || tree_edit_branch, required: true, class: "form-control js-branch-name ref-name" .js-create-merge-request-container .checkbox diff --git a/app/views/shared/_no_password.html.haml b/app/views/shared/_no_password.html.haml index ed6fc76c61e..b561e6dc248 100644 --- a/app/views/shared/_no_password.html.haml +++ b/app/views/shared/_no_password.html.haml @@ -1,8 +1,10 @@ - if cookies[:hide_no_password_message].blank? && !current_user.hide_no_password && current_user.require_password? .no-password-message.alert.alert-warning - You won't be able to pull or push project code via #{gitlab_config.protocol.upcase} until you #{link_to 'set a password', edit_profile_password_path} on your account + - set_password_link = link_to s_('SetPasswordToCloneLink|set a password'), edit_profile_password_path + - translation_params = { protocol: gitlab_config.protocol.upcase, set_password_link: set_password_link } + - set_password_message = _("You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account") % translation_params .alert-link-group - = link_to "Don't show again", profile_path(user: {hide_no_password: true}), method: :put + = link_to _("Don't show again"), profile_path(user: {hide_no_password: true}), method: :put | - = link_to 'Remind later', '#', class: 'hide-no-password-message' + = link_to _('Remind later'), '#', class: 'hide-no-password-message' diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml index d663fa13d10..e7815e28017 100644 --- a/app/views/shared/_no_ssh.html.haml +++ b/app/views/shared/_no_ssh.html.haml @@ -1,8 +1,9 @@ - if cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key? .no-ssh-key-message.alert.alert-warning - You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', profile_keys_path, class: 'alert-link'} to your profile - + - add_ssh_key_link = link_to s_('MissingSSHKeyWarningLink|add an SSH key'), profile_keys_path, class: 'alert-link' + - ssh_message = _("You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile") % { add_ssh_key_link: add_ssh_key_link } + #{ ssh_message.html_safe } .alert-link-group - = link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'alert-link' + = link_to _("Don't show again"), profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'alert-link' | - = link_to 'Remind later', '#', class: 'hide-no-ssh-message alert-link' + = link_to _('Remind later'), '#', class: 'hide-no-ssh-message alert-link' diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index 2029eb5824a..d52bb6b4dd7 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -6,9 +6,9 @@ - @options && @options.each do |key, value| = hidden_field_tag key, value, id: nil .dropdown - = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project), field_name: 'ref', submit_form_on_click: true }, { toggle_class: "js-project-refs-dropdown git-revision-dropdown-toggle" } + = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project), field_name: 'ref', submit_form_on_click: true }, { toggle_class: "js-project-refs-dropdown" } .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } - = dropdown_title "Switch branch/tag" - = dropdown_filter "Search branches and tags" + = dropdown_title _("Switch branch/tag") + = dropdown_filter _("Search branches and tags") = dropdown_content = dropdown_loading diff --git a/app/views/shared/deploy_keys/_form.html.haml b/app/views/shared/deploy_keys/_form.html.haml new file mode 100644 index 00000000000..e6075c3ae3a --- /dev/null +++ b/app/views/shared/deploy_keys/_form.html.haml @@ -0,0 +1,30 @@ +- form = local_assigns.fetch(:form) +- deploy_key = local_assigns.fetch(:deploy_key) + += form_errors(deploy_key) + +.form-group + = form.label :title, class: 'control-label' + .col-sm-10= form.text_field :title, class: 'form-control' + +.form-group + - if deploy_key.new_record? + = form.label :key, class: 'control-label' + .col-sm-10 + %p.light + Paste a machine public key here. Read more about how to generate it + = link_to 'here', help_page_path('ssh/README') + = form.text_area :key, class: 'form-control thin_area', rows: 5 + - else + = form.label :fingerprint, class: 'control-label' + .col-sm-10 + = form.text_field :fingerprint, class: 'form-control', readonly: 'readonly' + +.form-group + .control-label + .col-sm-10 + = form.label :can_push do + = form.check_box :can_push + %strong Write access allowed + %p.light.append-bottom-0 + Allow this key to push to repository as well? (Default only allows pull access.) diff --git a/app/views/shared/issuable/form/_description.html.haml b/app/views/shared/form_elements/_description.html.haml index 7ef0ae96be2..307d4919224 100644 --- a/app/views/shared/issuable/form/_description.html.haml +++ b/app/views/shared/form_elements/_description.html.haml @@ -1,10 +1,11 @@ - project = local_assigns.fetch(:project) -- issuable = local_assigns.fetch(:issuable) +- model = local_assigns.fetch(:model) + - form = local_assigns.fetch(:form) -- supports_slash_commands = issuable.new_record? +- supports_slash_commands = model.new_record? - if supports_slash_commands - - preview_url = preview_markdown_path(project, slash_commands_target_type: issuable.class.name) + - preview_url = preview_markdown_path(project, slash_commands_target_type: model.class.name) - else - preview_url = preview_markdown_path(project) diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml index 37589b634fa..760370a6984 100644 --- a/app/views/shared/groups/_dropdown.html.haml +++ b/app/views/shared/groups/_dropdown.html.haml @@ -1,10 +1,10 @@ -.dropdown.inline +.dropdown.inline.js-group-filter-dropdown-wrap %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' } - %span.light - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_recently_created + %span.dropdown-label + - if @sort.present? + = sort_options_hash[@sort] + - else + = sort_title_recently_created = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 7748351b333..c016aa2abcd 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -17,7 +17,7 @@ = render 'shared/issuable/form/template_selector', issuable: issuable = render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?) -= render 'shared/issuable/form/description', issuable: issuable, form: form, project: project += render 'shared/form_elements/description', model: issuable, form: form, project: project - if issuable.respond_to?(:confidential) .form-group diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index ad995cbe962..cf7ba52d840 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -1,25 +1,24 @@ - type = local_assigns.fetch(:type, :issues) - page_context_word = type.to_s.humanize(capitalize: false) - issuables = @issues || @merge_requests +- closed_title = 'Filter by issues that are currently closed.' %ul.nav-links.issues-state-filters %li{ class: active_when(params[:state] == 'opened') }> - = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened." do + %button.btn.btn-link{ id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", type: 'button', data: { state: 'opened' } } #{issuables_state_counter_text(type, :opened)} - if type == :merge_requests %li{ class: active_when(params[:state] == 'merged') }> - = link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.' do + %button.btn.btn-link{ id: 'state-merged', title: 'Filter by merge requests that are currently merged.', type: 'button', data: { state: 'merged' } } #{issuables_state_counter_text(type, :merged)} - %li{ class: active_when(params[:state] == 'closed') }> - = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.' do - #{issuables_state_counter_text(type, :closed)} - - else - %li{ class: active_when(params[:state] == 'closed') }> - = link_to page_filter_path(state: 'closed', label: true), id: 'state-all', title: 'Filter by issues that are currently closed.' do - #{issuables_state_counter_text(type, :closed)} + - closed_title = 'Filter by merge requests that are currently closed and unmerged.' + + %li{ class: active_when(params[:state] == 'closed') }> + %button.btn.btn-link{ id: 'state-closed', title: closed_title, type: 'button', data: { state: 'closed' } } + #{issuables_state_counter_text(type, :closed)} %li{ class: active_when(params[:state] == 'all') }> - = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}." do + %button.btn.btn-link{ id: 'state-all', title: "Show all #{page_context_word}.", type: 'button', data: { state: 'all' } } #{issuables_state_counter_text(type, :all)} diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml index 271150ed318..bfa91629e1e 100644 --- a/app/views/shared/issuable/form/_merge_params.html.haml +++ b/app/views/shared/issuable/form/_merge_params.html.haml @@ -3,7 +3,8 @@ - return unless issuable.is_a?(MergeRequest) - return if issuable.closed_without_fork? --# This check is duplicated below, to avoid conflicts with EE. +-# This check is duplicated below to avoid CE -> EE merge conflicts. +-# This comment and the following line should only exist in CE. - return unless issuable.can_remove_source_branch?(current_user) .form-group diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml index fb795ad1c72..d97fdf179d7 100644 --- a/app/views/shared/members/_access_request_buttons.html.haml +++ b/app/views/shared/members/_access_request_buttons.html.haml @@ -2,16 +2,17 @@ .project-action-button.inline - if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id)) - = link_to "Leave #{model_name}", polymorphic_path([:leave, source, :members]), + - link_text = source.is_a?(Group) ? _('Leave group') : _('Leave project') + = link_to link_text, polymorphic_path([:leave, source, :members]), method: :delete, data: { confirm: leave_confirmation_message(source) }, class: 'btn' - elsif requester = source.requesters.find_by(user_id: current_user.id) - = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]), + = link_to _('Withdraw Access Request'), polymorphic_path([:leave, source, :members]), method: :delete, data: { confirm: remove_member_message(requester) }, class: 'btn' - elsif source.request_access_enabled && can?(current_user, :request_access, source) - = link_to 'Request Access', polymorphic_path([:request_access, source, :members]), + = link_to _('Request Access'), polymorphic_path([:request_access, source, :members]), method: :post, class: 'btn' diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml index 183ed34fba1..752932e6045 100644 --- a/app/views/shared/notifications/_custom_notifications.html.haml +++ b/app/views/shared/notifications/_custom_notifications.html.haml @@ -5,7 +5,7 @@ %button.close{ type: "button", "aria-label": "close", data: { dismiss: "modal" } } %span{ "aria-hidden": "true" } } × %h4#custom-notifications-title.modal-title - Custom notification events + #{ _('Custom notification events') } .modal-body .container-fluid @@ -13,12 +13,11 @@ = hidden_setting_source_input(notification_setting) .row .col-lg-4 - %h4.prepend-top-0 - Notification events + %h4.prepend-top-0= _('Notification events') %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: help_page_path('workflow/notifications'), target: "_blank" } notification emails + - notification_link = link_to _('notification emails'), help_page_path('workflow/notifications'), target: '_blank' + - paragraph = _('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 %{notification_link}.') % { notification_link: notification_link.html_safe } + #{ paragraph.html_safe } .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/projects/blob/_branch_page_create.html.haml b/app/views/shared/projects/blob/_branch_page_create.html.haml deleted file mode 100644 index c279a0d8846..00000000000 --- a/app/views/shared/projects/blob/_branch_page_create.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -.dropdown-page-two.dropdown-new-branch - = dropdown_title('Create new branch', back: true) - = dropdown_content do - %input#new_branch_name.default-dropdown-input.append-bottom-10{ type: "text", placeholder: "Name new branch" } - %button.btn.btn-primary.pull-left.js-new-branch-btn{ type: "button" } - Create - %button.btn.btn-default.pull-right.js-cancel-branch-btn{ type: "button" } - Cancel diff --git a/app/views/shared/projects/blob/_branch_page_default.html.haml b/app/views/shared/projects/blob/_branch_page_default.html.haml deleted file mode 100644 index 9bf78d10878..00000000000 --- a/app/views/shared/projects/blob/_branch_page_default.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -.dropdown-page-one - = dropdown_title "Select branch" - = dropdown_filter "Search branches" - = dropdown_content - = dropdown_loading - = dropdown_footer do - %ul.dropdown-footer-list - %li - %a.create-new-branch.dropdown-toggle-page{ href: "#" } - Create new branch diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 0296597b294..8549cb91b03 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -3,7 +3,7 @@ = page_specific_javascript_bundle_tag('snippet') .snippet-form-holder - = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input js-quick-submit" } do |f| + = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input js-quick-submit common-note-form" } do |f| = form_errors(@snippet) .form-group @@ -11,6 +11,8 @@ .col-sm-10 = f.text_field :title, class: 'form-control', required: true, autofocus: true + = render 'shared/form_elements/description', model: @snippet, project: @project, form: f + = render 'shared/visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet .file-editor @@ -23,6 +25,9 @@ .file-content.code %pre#editor= @snippet.content = f.hidden_field :content, class: 'snippet-file-content' + - if params[:files] + - params[:files].each_with_index do |file, index| + = hidden_field_tag "files[]", file, id: "files_#{index}" .form-actions - if @snippet.new_record? diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 501c09d71d5..813d8d69d8d 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -22,3 +22,9 @@ - if @snippet.updated_at != @snippet.created_at = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago', exclude_author: true) + - if @snippet.description.present? + .description + .wiki + = markdown_field(@snippet, :description) + %textarea.hidden.js-task-list-field + = @snippet.description diff --git a/app/views/snippets/notes/_actions.html.haml b/app/views/snippets/notes/_actions.html.haml index e8119642ab8..098a88c48c5 100644 --- a/app/views/snippets/notes/_actions.html.haml +++ b/app/views/snippets/notes/_actions.html.haml @@ -6,8 +6,5 @@ %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') - - if note_editable - = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip' do - = icon('pencil', class: 'link-highlight') - = link_to snippet_note_path(note.noteable, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger has-tooltip' do - = icon('trash-o', class: 'danger-highlight') + + = render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder index 6c85e5f9fbd..e95814875f1 100644 --- a/app/views/users/show.atom.builder +++ b/app/views/users/show.atom.builder @@ -1,10 +1,7 @@ -xml.instruct! -xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do - xml.title "#{@user.name} activity" - xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml" - xml.link href: user_url(@user), rel: "alternate", type: "text/html" - xml.id user_url(@user) - xml.updated @events[0].updated_at.xmlschema if @events[0] +xml.title "#{@user.name} activity" +xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml" +xml.link href: user_url(@user), rel: "alternate", type: "text/html" +xml.id user_url(@user) +xml.updated @events[0].updated_at.xmlschema if @events[0] - xml << render(@events) if @events.any? -end +xml << render(@events) if @events.any? diff --git a/app/workers/background_migration_worker.rb b/app/workers/background_migration_worker.rb new file mode 100644 index 00000000000..e85e221d353 --- /dev/null +++ b/app/workers/background_migration_worker.rb @@ -0,0 +1,23 @@ +class BackgroundMigrationWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + # Schedules a number of jobs in bulk + # + # The `jobs` argument should be an Array of Arrays, each sub-array must be in + # the form: + # + # [migration-class, [arg1, arg2, ...]] + def self.perform_bulk(*jobs) + Sidekiq::Client.push_bulk('class' => self, + 'queue' => sidekiq_options['queue'], + 'args' => jobs) + end + + # Performs the background migration. + # + # See Gitlab::BackgroundMigration.perform for more information. + def perform(class_name, arguments = []) + Gitlab::BackgroundMigration.perform(class_name, arguments) + end +end diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index c29571d3c62..89286595ca6 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -17,14 +17,15 @@ class PostReceive post_received = Gitlab::GitPostReceive.new(project, identifier, changes) if is_wiki - # Nothing defined here yet. + process_wiki_changes(post_received) else process_project_changes(post_received) - process_repository_update(post_received) end end - def process_repository_update(post_received) + private + + def process_project_changes(post_received) changes = [] refs = Set.new @@ -36,32 +37,27 @@ class PostReceive return false end - changes << Gitlab::DataBuilder::Repository.single_change(oldrev, newrev, ref) - refs << ref - end - - hook_data = Gitlab::DataBuilder::Repository.update(post_received.project, @user, changes, refs.to_a) - SystemHooksService.new.execute_hooks(hook_data, :repository_update_hooks) - end - - def process_project_changes(post_received) - post_received.changes_refs do |oldrev, newrev, ref| - @user ||= post_received.identify(newrev) - - unless @user - log("Triggered hook for non-existing user \"#{post_received.identifier}\"") - return false - end - if Gitlab::Git.tag_ref?(ref) GitTagPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute elsif Gitlab::Git.branch_ref?(ref) GitPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute end + + changes << Gitlab::DataBuilder::Repository.single_change(oldrev, newrev, ref) + refs << ref end + + after_project_changes_hooks(post_received, @user, refs.to_a, changes) end - private + def after_project_changes_hooks(post_received, user, refs, changes) + hook_data = Gitlab::DataBuilder::Repository.update(post_received.project, user, changes, refs) + SystemHooksService.new.execute_hooks(hook_data, :repository_update_hooks) + end + + def process_wiki_changes(post_received) + # Nothing defined here yet. + end # To maintain backwards compatibility, we accept both gl_repository or # repository paths as project identifiers. Our plan is to migrate to diff --git a/changelogs/unreleased/12200-add-french-translation.yml b/changelogs/unreleased/12200-add-french-translation.yml new file mode 100644 index 00000000000..f31d982e0b9 --- /dev/null +++ b/changelogs/unreleased/12200-add-french-translation.yml @@ -0,0 +1,4 @@ +--- +title: "Adding French translations" +merge_request: 12200 +author : Erwan "Dremor" Georget diff --git a/changelogs/unreleased/12614-fix-long-message.yml b/changelogs/unreleased/12614-fix-long-message.yml new file mode 100644 index 00000000000..94f8127c3c1 --- /dev/null +++ b/changelogs/unreleased/12614-fix-long-message.yml @@ -0,0 +1,4 @@ +--- +title: Fix long urls in the title of commit +merge_request: 10938 +author: Alexander Randa diff --git a/changelogs/unreleased/12910-snippets-description.yml b/changelogs/unreleased/12910-snippets-description.yml new file mode 100644 index 00000000000..ac3d754fee1 --- /dev/null +++ b/changelogs/unreleased/12910-snippets-description.yml @@ -0,0 +1,4 @@ +--- +title: Support descriptions for snippets +merge_request: +author: diff --git a/changelogs/unreleased/13336-multiple-broadcast-messages.yml b/changelogs/unreleased/13336-multiple-broadcast-messages.yml new file mode 100644 index 00000000000..7dc73e1c6ea --- /dev/null +++ b/changelogs/unreleased/13336-multiple-broadcast-messages.yml @@ -0,0 +1,4 @@ +--- +title: Display all current broadcast messages, not just the last one +merge_request: 11113 +author: rickettm diff --git a/changelogs/unreleased/23603-add-extra-functionality-for-the-top-right-button.yml b/changelogs/unreleased/23603-add-extra-functionality-for-the-top-right-button.yml new file mode 100644 index 00000000000..77f8e31e16e --- /dev/null +++ b/changelogs/unreleased/23603-add-extra-functionality-for-the-top-right-button.yml @@ -0,0 +1,4 @@ +--- +title: Add extra context-sensitive functionality for the top right menu button +merge_request: 11632 +author: diff --git a/changelogs/unreleased/23998-blame-age-map.yml b/changelogs/unreleased/23998-blame-age-map.yml new file mode 100644 index 00000000000..26a38f0939c --- /dev/null +++ b/changelogs/unreleased/23998-blame-age-map.yml @@ -0,0 +1,4 @@ +--- +title: Add blame view age mapping +merge_request: 7198 +author: Jeff Stubler diff --git a/changelogs/unreleased/25426-group-dashboard-ui.yml b/changelogs/unreleased/25426-group-dashboard-ui.yml new file mode 100644 index 00000000000..cc2bf62d07b --- /dev/null +++ b/changelogs/unreleased/25426-group-dashboard-ui.yml @@ -0,0 +1,4 @@ +--- +title: Update Dashboard Groups UI with better support for subgroups +merge_request: +author: diff --git a/changelogs/unreleased/27586-center-dropdown.yml b/changelogs/unreleased/27586-center-dropdown.yml new file mode 100644 index 00000000000..4935f7504f7 --- /dev/null +++ b/changelogs/unreleased/27586-center-dropdown.yml @@ -0,0 +1,4 @@ +--- +title: Center dropdown for mini graph +merge_request: +author: diff --git a/changelogs/unreleased/28607-forking-and-configuring-project-via-api-works-very-unreliable.yml b/changelogs/unreleased/28607-forking-and-configuring-project-via-api-works-very-unreliable.yml new file mode 100644 index 00000000000..9cf8d745f92 --- /dev/null +++ b/changelogs/unreleased/28607-forking-and-configuring-project-via-api-works-very-unreliable.yml @@ -0,0 +1,4 @@ +--- +title: Confirm Project forking behaviour via the API +merge_request: +author: diff --git a/changelogs/unreleased/29010-perf-bar.yml b/changelogs/unreleased/29010-perf-bar.yml new file mode 100644 index 00000000000..f4167e5562f --- /dev/null +++ b/changelogs/unreleased/29010-perf-bar.yml @@ -0,0 +1,4 @@ +--- +title: Add an optional performance bar to view performance metrics for the current page +merge_request: 11439 +author: diff --git a/changelogs/unreleased/29118-add-prometheus-instrumenting-to-gitlab-webapp.yml b/changelogs/unreleased/29118-add-prometheus-instrumenting-to-gitlab-webapp.yml new file mode 100644 index 00000000000..99c55f128e3 --- /dev/null +++ b/changelogs/unreleased/29118-add-prometheus-instrumenting-to-gitlab-webapp.yml @@ -0,0 +1,4 @@ +--- +title: Add prometheus based metrics collection to gitlab webapp +merge_request: +author: diff --git a/changelogs/unreleased/31397-job-detail-real-time.yml b/changelogs/unreleased/31397-job-detail-real-time.yml new file mode 100644 index 00000000000..90487a1e75a --- /dev/null +++ b/changelogs/unreleased/31397-job-detail-real-time.yml @@ -0,0 +1,4 @@ +--- +title: Adds realtime feature to job show view header and sidebar info. Updates UX. +merge_request: +author: diff --git a/changelogs/unreleased/31415-responsive-pipelines-table-2.yml b/changelogs/unreleased/31415-responsive-pipelines-table-2.yml new file mode 100644 index 00000000000..59402b85871 --- /dev/null +++ b/changelogs/unreleased/31415-responsive-pipelines-table-2.yml @@ -0,0 +1,4 @@ +--- +title: Create responsive mobile view for pipelines table +merge_request: +author: diff --git a/changelogs/unreleased/31556-ci-coverage-paralel-rspec.yml b/changelogs/unreleased/31556-ci-coverage-paralel-rspec.yml deleted file mode 100644 index 4137050a077..00000000000 --- a/changelogs/unreleased/31556-ci-coverage-paralel-rspec.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix the last coverage in trace log should be extracted -merge_request: 11128 -author: dosuken123 diff --git a/changelogs/unreleased/3191-deploy-keys-update.yml b/changelogs/unreleased/3191-deploy-keys-update.yml new file mode 100644 index 00000000000..4100163e94f --- /dev/null +++ b/changelogs/unreleased/3191-deploy-keys-update.yml @@ -0,0 +1,4 @@ +--- +title: Implement ability to update deploy keys +merge_request: 10383 +author: Alexander Randa diff --git a/changelogs/unreleased/32054-rails-should-use-timestamptz-database-type-for-postgresql.yml b/changelogs/unreleased/32054-rails-should-use-timestamptz-database-type-for-postgresql.yml new file mode 100644 index 00000000000..7fc9e0a4f0e --- /dev/null +++ b/changelogs/unreleased/32054-rails-should-use-timestamptz-database-type-for-postgresql.yml @@ -0,0 +1,4 @@ +--- +title: Add database helpers 'add_timestamps_with_timezone' and 'timestamps_with_timezone' +merge_request: 11229 +author: @blackst0ne diff --git a/changelogs/unreleased/32470-pag-links.yml b/changelogs/unreleased/32470-pag-links.yml new file mode 100644 index 00000000000..d0fd284f3ee --- /dev/null +++ b/changelogs/unreleased/32470-pag-links.yml @@ -0,0 +1,4 @@ +--- +title: more visual contrast in pagination widget +merge_request: +author: diff --git a/changelogs/unreleased/32517-disable-hover-state.yml b/changelogs/unreleased/32517-disable-hover-state.yml new file mode 100644 index 00000000000..31b02778963 --- /dev/null +++ b/changelogs/unreleased/32517-disable-hover-state.yml @@ -0,0 +1,5 @@ +--- +title: Removes hover style for nodes that are either links or buttons in the pipeline + graph +merge_request: +author: diff --git a/changelogs/unreleased/32715-fix-note-padding.yml b/changelogs/unreleased/32715-fix-note-padding.yml deleted file mode 100644 index 867ed7eb171..00000000000 --- a/changelogs/unreleased/32715-fix-note-padding.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make all notes use equal padding -merge_request: -author: diff --git a/changelogs/unreleased/32720-emoji-spacing.yml b/changelogs/unreleased/32720-emoji-spacing.yml new file mode 100644 index 00000000000..da3df0f9093 --- /dev/null +++ b/changelogs/unreleased/32720-emoji-spacing.yml @@ -0,0 +1,4 @@ +--- +title: Create equal padding for emoji +merge_request: +author: diff --git a/changelogs/unreleased/32790-pipeline_schedules-pages-throwing-error-500.yml b/changelogs/unreleased/32790-pipeline_schedules-pages-throwing-error-500.yml deleted file mode 100644 index a58f3a7429e..00000000000 --- a/changelogs/unreleased/32790-pipeline_schedules-pages-throwing-error-500.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix pipeline_schedules pages throwing error 500 -merge_request: 11706 -author: dosuken123 diff --git a/changelogs/unreleased/32834-task-note-only.yml b/changelogs/unreleased/32834-task-note-only.yml new file mode 100644 index 00000000000..c9ea61ec4ec --- /dev/null +++ b/changelogs/unreleased/32834-task-note-only.yml @@ -0,0 +1,4 @@ +--- +title: Prevent description change notes when toggling tasks +merge_request: 12057 +author: Jared Deckard <jared.deckard@gmail.com> diff --git a/changelogs/unreleased/32955-special-keywords.yml b/changelogs/unreleased/32955-special-keywords.yml new file mode 100644 index 00000000000..0f9939ced8c --- /dev/null +++ b/changelogs/unreleased/32955-special-keywords.yml @@ -0,0 +1,4 @@ +--- +title: Add all pipeline sources as special keywords to 'only' and 'except' +merge_request: 11844 +author: Filip Krakowski diff --git a/changelogs/unreleased/33003-avatar-in-project-api.yml b/changelogs/unreleased/33003-avatar-in-project-api.yml new file mode 100644 index 00000000000..41d796ebb32 --- /dev/null +++ b/changelogs/unreleased/33003-avatar-in-project-api.yml @@ -0,0 +1,4 @@ +--- +title: Accept image for avatar in project API +merge_request: 11988 +author: Ivan Chernov diff --git a/changelogs/unreleased/33048-markdown-rendering-of-md-files-has-ceased-to-display-latex-equations.yml b/changelogs/unreleased/33048-markdown-rendering-of-md-files-has-ceased-to-display-latex-equations.yml deleted file mode 100644 index 5648e013e75..00000000000 --- a/changelogs/unreleased/33048-markdown-rendering-of-md-files-has-ceased-to-display-latex-equations.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix math rendering on blob pages -merge_request: -author: diff --git a/changelogs/unreleased/33132-change-icon-color.yml b/changelogs/unreleased/33132-change-icon-color.yml new file mode 100644 index 00000000000..c0e148f985b --- /dev/null +++ b/changelogs/unreleased/33132-change-icon-color.yml @@ -0,0 +1,4 @@ +--- +title: Render CI statuses with warnings in orange +merge_request: +author: diff --git a/changelogs/unreleased/33208-singup-active-state-underline.yml b/changelogs/unreleased/33208-singup-active-state-underline.yml new file mode 100644 index 00000000000..cddb43214ea --- /dev/null +++ b/changelogs/unreleased/33208-singup-active-state-underline.yml @@ -0,0 +1,4 @@ +--- +title: Fixes "sign in / Register" active state underline misalignment +merge_request: 11890 +author: Frank Sierra diff --git a/changelogs/unreleased/33308-use-pre-wrap-for-commit-messages.yml b/changelogs/unreleased/33308-use-pre-wrap-for-commit-messages.yml new file mode 100644 index 00000000000..43e8f242947 --- /dev/null +++ b/changelogs/unreleased/33308-use-pre-wrap-for-commit-messages.yml @@ -0,0 +1,4 @@ +--- +title: Use pre-wrap for commit messages to keep lists indented +merge_request: +author: diff --git a/changelogs/unreleased/33334-portuguese_brazil_translation_of_cycle_analytics_page.yml b/changelogs/unreleased/33334-portuguese_brazil_translation_of_cycle_analytics_page.yml new file mode 100644 index 00000000000..a0e0458da16 --- /dev/null +++ b/changelogs/unreleased/33334-portuguese_brazil_translation_of_cycle_analytics_page.yml @@ -0,0 +1,4 @@ +--- +title: Add Portuguese Brazil of Cycle Analytics Page to I18N +merge_request: 11920 +author:Huang Tao diff --git a/changelogs/unreleased/33381-display-issue-state-in-mr-widget-issue-links.yml b/changelogs/unreleased/33381-display-issue-state-in-mr-widget-issue-links.yml new file mode 100644 index 00000000000..4a7b02fec94 --- /dev/null +++ b/changelogs/unreleased/33381-display-issue-state-in-mr-widget-issue-links.yml @@ -0,0 +1,4 @@ +--- +title: Display issue state in issue links section of merge request widget +merge_request: 12021 +author: diff --git a/changelogs/unreleased/33383-bulgarian_translation_of_cycle_analytics_page.yml b/changelogs/unreleased/33383-bulgarian_translation_of_cycle_analytics_page.yml new file mode 100644 index 00000000000..71bd5505be7 --- /dev/null +++ b/changelogs/unreleased/33383-bulgarian_translation_of_cycle_analytics_page.yml @@ -0,0 +1,4 @@ +--- +title: add bulgarian translation of cycle analytics page to I18N +merge_request: 11958 +author: Lyubomir Vasilev diff --git a/changelogs/unreleased/33445-document-delete-merge-branches-won-t-touch-protected-branches-docs.yml b/changelogs/unreleased/33445-document-delete-merge-branches-won-t-touch-protected-branches-docs.yml new file mode 100644 index 00000000000..385f18e2560 --- /dev/null +++ b/changelogs/unreleased/33445-document-delete-merge-branches-won-t-touch-protected-branches-docs.yml @@ -0,0 +1,4 @@ +--- +title: Document the Delete Merged Branches functionality +merge_request: +author: diff --git a/changelogs/unreleased/allow-reporters-to-promote-group-labels.yml b/changelogs/unreleased/allow-reporters-to-promote-group-labels.yml new file mode 100644 index 00000000000..2364ce6d068 --- /dev/null +++ b/changelogs/unreleased/allow-reporters-to-promote-group-labels.yml @@ -0,0 +1,4 @@ +--- +title: Allow reporters to promote project labels to group labels +merge_request: +author: diff --git a/changelogs/unreleased/artifacts-keyboard-shortcuts.yml b/changelogs/unreleased/artifacts-keyboard-shortcuts.yml new file mode 100644 index 00000000000..69569504c4f --- /dev/null +++ b/changelogs/unreleased/artifacts-keyboard-shortcuts.yml @@ -0,0 +1,4 @@ +--- +title: Enabled keyboard shortcuts on artifacts pages +merge_request: +author: diff --git a/changelogs/unreleased/auto-search-when-state-changed.yml b/changelogs/unreleased/auto-search-when-state-changed.yml new file mode 100644 index 00000000000..2723beb8600 --- /dev/null +++ b/changelogs/unreleased/auto-search-when-state-changed.yml @@ -0,0 +1,4 @@ +--- +title: Perform filtered search when state tab is changed +merge_request: +author: diff --git a/changelogs/unreleased/bvl-translate-project-pages.yml b/changelogs/unreleased/bvl-translate-project-pages.yml new file mode 100644 index 00000000000..fb90aba08b4 --- /dev/null +++ b/changelogs/unreleased/bvl-translate-project-pages.yml @@ -0,0 +1,4 @@ +--- +title: Translate backend for Project & Repository pages +merge_request: 11183 +author: diff --git a/changelogs/unreleased/ce-31853-projects-shared-groups.yml b/changelogs/unreleased/ce-31853-projects-shared-groups.yml new file mode 100644 index 00000000000..ffa3aed682d --- /dev/null +++ b/changelogs/unreleased/ce-31853-projects-shared-groups.yml @@ -0,0 +1,4 @@ +--- +title: Remove duplication for sharing projects with groups in project settings +merge_request: +author: diff --git a/changelogs/unreleased/counters_cache_invalidation.yml b/changelogs/unreleased/counters_cache_invalidation.yml deleted file mode 100644 index 1e78765ec10..00000000000 --- a/changelogs/unreleased/counters_cache_invalidation.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Invalidate cache for issue and MR counters more granularly -merge_request: -author: diff --git a/changelogs/unreleased/dashboard-milestone-tabs-loading-async.yml b/changelogs/unreleased/dashboard-milestone-tabs-loading-async.yml new file mode 100644 index 00000000000..357a623e0e8 --- /dev/null +++ b/changelogs/unreleased/dashboard-milestone-tabs-loading-async.yml @@ -0,0 +1,4 @@ +--- +title: Fixed dashboard milestone tabs not loading +merge_request: +author: diff --git a/changelogs/unreleased/disable-blocked-manual-actions.yml b/changelogs/unreleased/disable-blocked-manual-actions.yml new file mode 100644 index 00000000000..a640f61a7dd --- /dev/null +++ b/changelogs/unreleased/disable-blocked-manual-actions.yml @@ -0,0 +1,4 @@ +--- +title: disable blocked manual actions +merge_request: +author: diff --git a/changelogs/unreleased/dm-blob-binaryness-change.yml b/changelogs/unreleased/dm-blob-binaryness-change.yml new file mode 100644 index 00000000000..f3e3af26f12 --- /dev/null +++ b/changelogs/unreleased/dm-blob-binaryness-change.yml @@ -0,0 +1,5 @@ +--- +title: Detect if file that appears to be text in the first 1024 bytes is actually + binary afer loading all data +merge_request: +author: diff --git a/changelogs/unreleased/dm-diff-viewers.yml b/changelogs/unreleased/dm-diff-viewers.yml new file mode 100644 index 00000000000..e5b1352c8f1 --- /dev/null +++ b/changelogs/unreleased/dm-diff-viewers.yml @@ -0,0 +1,4 @@ +--- +title: Implement diff viewers +merge_request: +author: diff --git a/changelogs/unreleased/dm-fix-parser-cache.yml b/changelogs/unreleased/dm-fix-parser-cache.yml new file mode 100644 index 00000000000..31c163b7272 --- /dev/null +++ b/changelogs/unreleased/dm-fix-parser-cache.yml @@ -0,0 +1,4 @@ +--- +title: Don't return nil for missing objects from parser cache +merge_request: +author: diff --git a/changelogs/unreleased/dm-mail-room-check-without-omnibus.yml b/changelogs/unreleased/dm-mail-room-check-without-omnibus.yml new file mode 100644 index 00000000000..7fd252e9b8b --- /dev/null +++ b/changelogs/unreleased/dm-mail-room-check-without-omnibus.yml @@ -0,0 +1,4 @@ +--- +title: Don't check if MailRoom is running on Omnibus +merge_request: +author: diff --git a/changelogs/unreleased/dm-revert-mr-8427.yml b/changelogs/unreleased/dm-revert-mr-8427.yml new file mode 100644 index 00000000000..a91cff2e9cd --- /dev/null +++ b/changelogs/unreleased/dm-revert-mr-8427.yml @@ -0,0 +1,4 @@ +--- +title: Revert 'New file from interface on existing branch' +merge_request: +author: diff --git a/changelogs/unreleased/dm-target-branch-slash-command-desc.yml b/changelogs/unreleased/dm-target-branch-slash-command-desc.yml new file mode 100644 index 00000000000..768ddf0416e --- /dev/null +++ b/changelogs/unreleased/dm-target-branch-slash-command-desc.yml @@ -0,0 +1,4 @@ +--- +title: Update /target_branch slash command description to be more consistent +merge_request: +author: diff --git a/changelogs/unreleased/expand-backlog-closed-lists-issue-boards.yml b/changelogs/unreleased/expand-backlog-closed-lists-issue-boards.yml new file mode 100644 index 00000000000..4796f8e918b --- /dev/null +++ b/changelogs/unreleased/expand-backlog-closed-lists-issue-boards.yml @@ -0,0 +1,4 @@ +--- +title: Expand/collapse backlog & closed lists in issue boards +merge_request: +author: diff --git a/changelogs/unreleased/feature-add-support-for-services-configuration.yml b/changelogs/unreleased/feature-add-support-for-services-configuration.yml new file mode 100644 index 00000000000..88a3eacd774 --- /dev/null +++ b/changelogs/unreleased/feature-add-support-for-services-configuration.yml @@ -0,0 +1,4 @@ +--- +title: Add support for image and services configuration in .gitlab-ci.yml +merge_request: 8578 +author: diff --git a/changelogs/unreleased/feature-gb-persist-pipeline-stages.yml b/changelogs/unreleased/feature-gb-persist-pipeline-stages.yml new file mode 100644 index 00000000000..1404b342359 --- /dev/null +++ b/changelogs/unreleased/feature-gb-persist-pipeline-stages.yml @@ -0,0 +1,4 @@ +--- +title: Persist pipeline stages in the database +merge_request: 11790 +author: diff --git a/changelogs/unreleased/feature-unify-email-layouts.yml b/changelogs/unreleased/feature-unify-email-layouts.yml new file mode 100644 index 00000000000..7a2e3f20b6b --- /dev/null +++ b/changelogs/unreleased/feature-unify-email-layouts.yml @@ -0,0 +1,4 @@ +--- +title: Update the devise mail templates to match the design of the pipeline emails +merge_request: 10483 +author: Alexis Reigel diff --git a/changelogs/unreleased/fix-gb-use-merge-ability-for-protected-manual-actions.yml b/changelogs/unreleased/fix-gb-use-merge-ability-for-protected-manual-actions.yml deleted file mode 100644 index 43c18502cd6..00000000000 --- a/changelogs/unreleased/fix-gb-use-merge-ability-for-protected-manual-actions.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Respect merge, instead of push, permissions for protected actions -merge_request: 11648 -author: diff --git a/changelogs/unreleased/fix-github-clone-wiki.yml b/changelogs/unreleased/fix-github-clone-wiki.yml new file mode 100644 index 00000000000..eadd90e1390 --- /dev/null +++ b/changelogs/unreleased/fix-github-clone-wiki.yml @@ -0,0 +1,4 @@ +--- +title: Github - Fix token interpolation when cloning wiki repository +merge_request: +author: diff --git a/changelogs/unreleased/fix-support-for-external-ci-services.yml b/changelogs/unreleased/fix-support-for-external-ci-services.yml new file mode 100644 index 00000000000..eecb4519259 --- /dev/null +++ b/changelogs/unreleased/fix-support-for-external-ci-services.yml @@ -0,0 +1,4 @@ +--- +title: Fix support for external CI services +merge_request: 11176 +author: diff --git a/changelogs/unreleased/fix-terminals-support-for-kubernetes-service.yml b/changelogs/unreleased/fix-terminals-support-for-kubernetes-service.yml deleted file mode 100644 index fb91da9510c..00000000000 --- a/changelogs/unreleased/fix-terminals-support-for-kubernetes-service.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix terminals support for Kubernetes Service -merge_request: -author: diff --git a/changelogs/unreleased/fix-u2f-for-opera.yml b/changelogs/unreleased/fix-u2f-for-opera.yml new file mode 100644 index 00000000000..0eafb8eff9a --- /dev/null +++ b/changelogs/unreleased/fix-u2f-for-opera.yml @@ -0,0 +1,4 @@ +--- +title: Fix FIDO U2F for Opera browser +merge_request: 12082 +author: Jakub Kramarz and Jonas Kalderstam diff --git a/changelogs/unreleased/fix_docs_commits_multiple_files.yml b/changelogs/unreleased/fix_docs_commits_multiple_files.yml new file mode 100644 index 00000000000..36567354b28 --- /dev/null +++ b/changelogs/unreleased/fix_docs_commits_multiple_files.yml @@ -0,0 +1,5 @@ +--- +title: Documentation bugfix of invalid JSON payload example of Create a commit with + multiple files and actions +merge_request: 12117 +author: @blackst0ne diff --git a/changelogs/unreleased/fixed-confidential-issue-bar.yml b/changelogs/unreleased/fixed-confidential-issue-bar.yml new file mode 100644 index 00000000000..6a41590d0af --- /dev/null +++ b/changelogs/unreleased/fixed-confidential-issue-bar.yml @@ -0,0 +1,4 @@ +--- +title: Make confidential issues more obviously confidential +merge_request: +author: diff --git a/changelogs/unreleased/help-landing-page-customizations.yml b/changelogs/unreleased/help-landing-page-customizations.yml new file mode 100644 index 00000000000..58cab751ded --- /dev/null +++ b/changelogs/unreleased/help-landing-page-customizations.yml @@ -0,0 +1,4 @@ +--- +title: Help landing page customizations +merge_request: 11878 +author: Robin Bobbitt diff --git a/changelogs/unreleased/instrument-merge-request-diff-load-commits.yml b/changelogs/unreleased/instrument-merge-request-diff-load-commits.yml new file mode 100644 index 00000000000..916b182a48b --- /dev/null +++ b/changelogs/unreleased/instrument-merge-request-diff-load-commits.yml @@ -0,0 +1,4 @@ +--- +title: Instrument MergeRequestDiff#load_commits +merge_request: +author: diff --git a/changelogs/unreleased/issuable-sidebar-edit-button-field-focus.yml b/changelogs/unreleased/issuable-sidebar-edit-button-field-focus.yml new file mode 100644 index 00000000000..05d52fcad0f --- /dev/null +++ b/changelogs/unreleased/issuable-sidebar-edit-button-field-focus.yml @@ -0,0 +1,4 @@ +--- +title: Fixed dropdown filter input not focusing after transition +merge_request: +author: diff --git a/changelogs/unreleased/issue_27166_2.yml b/changelogs/unreleased/issue_27166_2.yml new file mode 100644 index 00000000000..9b9906e03dd --- /dev/null +++ b/changelogs/unreleased/issue_27166_2.yml @@ -0,0 +1,4 @@ +--- +title: Avoid repeated queries for pipeline builds on merge requests +merge_request: +author: diff --git a/changelogs/unreleased/karma-headless-chrome.yml b/changelogs/unreleased/karma-headless-chrome.yml new file mode 100644 index 00000000000..af3e9b3b0f9 --- /dev/null +++ b/changelogs/unreleased/karma-headless-chrome.yml @@ -0,0 +1,4 @@ +--- +title: Replace PhantomJS with headless Chrome for karma test suite +merge_request: 12036 +author: diff --git a/changelogs/unreleased/pat-msg-on-auth-failure.yml b/changelogs/unreleased/pat-msg-on-auth-failure.yml new file mode 100644 index 00000000000..c1b1528bb7a --- /dev/null +++ b/changelogs/unreleased/pat-msg-on-auth-failure.yml @@ -0,0 +1,4 @@ +--- +title: Instruct user to use personal access token for Git over HTTP +merge_request: 11986 +author: Robin Bobbitt diff --git a/changelogs/unreleased/sh-bump-oauth2-gem.yml b/changelogs/unreleased/sh-bump-oauth2-gem.yml new file mode 100644 index 00000000000..b894a64968b --- /dev/null +++ b/changelogs/unreleased/sh-bump-oauth2-gem.yml @@ -0,0 +1,4 @@ +--- +title: Bump Faraday and dependent OAuth2 gem version to support no_proxy variable +merge_request: +author: diff --git a/changelogs/unreleased/sh-fix-lfs-from-moving-across-filesystems.yml b/changelogs/unreleased/sh-fix-lfs-from-moving-across-filesystems.yml deleted file mode 100644 index 161bce45601..00000000000 --- a/changelogs/unreleased/sh-fix-lfs-from-moving-across-filesystems.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix LFS timeouts when trying to save large files -merge_request: -author: diff --git a/changelogs/unreleased/sh-fix-refactor-uploader-work-dir.yml b/changelogs/unreleased/sh-fix-refactor-uploader-work-dir.yml new file mode 100644 index 00000000000..255608bd89a --- /dev/null +++ b/changelogs/unreleased/sh-fix-refactor-uploader-work-dir.yml @@ -0,0 +1,4 @@ +--- +title: Set artifact working directory to be in the destination store to prevent unnecessary I/O +merge_request: +author: diff --git a/changelogs/unreleased/sh-fix-submodules-trailing-spaces.yml b/changelogs/unreleased/sh-fix-submodules-trailing-spaces.yml deleted file mode 100644 index d633995d467..00000000000 --- a/changelogs/unreleased/sh-fix-submodules-trailing-spaces.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Strip trailing whitespaces in submodule URLs -merge_request: -author: diff --git a/changelogs/unreleased/speed-up-graphs.yml b/changelogs/unreleased/speed-up-graphs.yml new file mode 100644 index 00000000000..7cb155af6fd --- /dev/null +++ b/changelogs/unreleased/speed-up-graphs.yml @@ -0,0 +1,4 @@ +--- +title: Speed up used languages calculation on charts page +merge_request: +author: diff --git a/changelogs/unreleased/sync-email-from-omniauth.yml b/changelogs/unreleased/sync-email-from-omniauth.yml new file mode 100644 index 00000000000..ed14a95a5f1 --- /dev/null +++ b/changelogs/unreleased/sync-email-from-omniauth.yml @@ -0,0 +1,4 @@ +--- +title: Sync email address from specified omniauth provider +merge_request: 11268 +author: Robin Bobbitt diff --git a/changelogs/unreleased/tc-link-to-commit-on-help-page.yml b/changelogs/unreleased/tc-link-to-commit-on-help-page.yml new file mode 100644 index 00000000000..3d11ba43d1f --- /dev/null +++ b/changelogs/unreleased/tc-link-to-commit-on-help-page.yml @@ -0,0 +1,4 @@ +--- +title: Make the revision on the `/help` page clickable +merge_request: 12016 +author: diff --git a/changelogs/unreleased/zj-commit-status-sortable-name.yml b/changelogs/unreleased/zj-commit-status-sortable-name.yml new file mode 100644 index 00000000000..1be9ac6380f --- /dev/null +++ b/changelogs/unreleased/zj-commit-status-sortable-name.yml @@ -0,0 +1,4 @@ +--- +title: Handle nameless legacy jobs +merge_request: +author: diff --git a/changelogs/unreleased/zj-drop-fk-if-exists.yml b/changelogs/unreleased/zj-drop-fk-if-exists.yml deleted file mode 100644 index 237ba936de9..00000000000 --- a/changelogs/unreleased/zj-drop-fk-if-exists.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove foreigh key on ci_trigger_schedules only if it exists -merge_request: -author: diff --git a/changelogs/unreleased/zj-fix-pipeline-etag.yml b/changelogs/unreleased/zj-fix-pipeline-etag.yml deleted file mode 100644 index 03ebef8c575..00000000000 --- a/changelogs/unreleased/zj-fix-pipeline-etag.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix issue where real time pipelines were not cached -merge_request: 11615 -author: diff --git a/changelogs/unreleased/zj-i18n-pipeline-schedules.yml b/changelogs/unreleased/zj-i18n-pipeline-schedules.yml new file mode 100644 index 00000000000..51c82a16359 --- /dev/null +++ b/changelogs/unreleased/zj-i18n-pipeline-schedules.yml @@ -0,0 +1,4 @@ +--- +title: Allow translation of Pipeline Schedules +merge_request: +author: diff --git a/changelogs/unreleased/zj-prom-pipeline-count.yml b/changelogs/unreleased/zj-prom-pipeline-count.yml new file mode 100644 index 00000000000..191e4f2f949 --- /dev/null +++ b/changelogs/unreleased/zj-prom-pipeline-count.yml @@ -0,0 +1,4 @@ +--- +title: Add prometheus metrics on pipeline creation +merge_request: +author: diff --git a/changelogs/unreleased/zj-raise-etag-route-regex-miss.yml b/changelogs/unreleased/zj-raise-etag-route-regex-miss.yml new file mode 100644 index 00000000000..57a5f4e44c0 --- /dev/null +++ b/changelogs/unreleased/zj-raise-etag-route-regex-miss.yml @@ -0,0 +1,4 @@ +--- +title: Fix etag route not being a match for environments +merge_request: +author: diff --git a/changelogs/unreleased/zj-read-registry-pat.yml b/changelogs/unreleased/zj-read-registry-pat.yml new file mode 100644 index 00000000000..d36159bbdf5 --- /dev/null +++ b/changelogs/unreleased/zj-read-registry-pat.yml @@ -0,0 +1,4 @@ +--- +title: Allow pulling of container images using personal access tokens +merge_request: 11845 +author: diff --git a/config/application.rb b/config/application.rb index b0533759252..8bbecf3ed0f 100644 --- a/config/application.rb +++ b/config/application.rb @@ -105,6 +105,7 @@ module Gitlab config.assets.precompile << "katex.css" config.assets.precompile << "katex.js" config.assets.precompile << "xterm/xterm.css" + config.assets.precompile << "peek.css" config.assets.precompile << "lib/ace.js" config.assets.precompile << "vendor/assets/fonts/*" config.assets.precompile << "test.css" diff --git a/config/boot.rb b/config/boot.rb index f2830ae3166..db5ab918021 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -4,3 +4,18 @@ require 'rubygems' ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) + +# set default directory for multiproces metrics gathering +ENV['prometheus_multiproc_dir'] ||= 'tmp/prometheus_multiproc_dir' + +# Default Bootsnap configuration from https://github.com/Shopify/bootsnap#usage +require 'bootsnap' +Bootsnap.setup( + cache_dir: 'tmp/cache', + development_mode: ENV['RAILS_ENV'] == 'development', + load_path_cache: true, + autoload_paths_cache: true, + disable_trace: false, + compile_cache_iseq: true, + compile_cache_yaml: true +) diff --git a/config/environments/production.rb b/config/environments/production.rb index 82a19085b1d..c5cbfcf64cf 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -50,7 +50,7 @@ Rails.application.configure do # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) # Enable serving of images, stylesheets, and JavaScripts from an asset server - # config.action_controller.asset_host = "http://assets.example.com" + config.action_controller.asset_host = ENV['GITLAB_CDN_HOST'] if ENV['GITLAB_CDN_HOST'].present? # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) # config.assets.precompile += %w( search.js ) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index d2aeb66ebf6..0b33783869b 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -337,6 +337,10 @@ production: &base # showing GitLab's sign-in page (default: show the GitLab sign-in page) # auto_sign_in_with_provider: saml + # Sync user's email address from the specified Omniauth provider every time the user logs + # in (default: nil). And consequently make this field read-only. + # sync_email_from_provider: cas3 + # CAUTION! # This allows users to login without having a user account first. Define the allowed providers # using an array, e.g. ["saml", "twitter"], or as true/false to allow all providers or none. diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 45ea2040d23..8ddf8e4d2e4 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -156,6 +156,7 @@ Settings.omniauth['external_providers'] = [] if Settings.omniauth['external_prov Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block_auto_created_users'].nil? Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil? Settings.omniauth['auto_link_saml_user'] = false if Settings.omniauth['auto_link_saml_user'].nil? +Settings.omniauth['sync_email_from_provider'] ||= nil Settings.omniauth['providers'] ||= [] Settings.omniauth['cas3'] ||= Settingslogic.new({}) diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb index 5e0eefdb154..508b886d6a0 100644 --- a/config/initializers/8_metrics.rb +++ b/config/initializers/8_metrics.rb @@ -113,6 +113,9 @@ def instrument_classes(instrumentation) # This is a Rails scope so we have to instrument it manually. instrumentation.instrument_method(Project, :visible_to_user) + + # Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/30224#note_32306159 + instrumentation.instrument_instance_method(MergeRequestDiff, :load_commits) end # rubocop:enable Metrics/AbcSize diff --git a/config/initializers/active_record_data_types.rb b/config/initializers/active_record_data_types.rb new file mode 100644 index 00000000000..beb97c6fce0 --- /dev/null +++ b/config/initializers/active_record_data_types.rb @@ -0,0 +1,24 @@ +# ActiveRecord custom data type for storing datetimes with timezone information. +# See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11229 + +if Gitlab::Database.postgresql? + require 'active_record/connection_adapters/postgresql_adapter' + + module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter + NATIVE_DATABASE_TYPES.merge!(datetime_with_timezone: { name: 'timestamptz' }) + end + end + end +elsif Gitlab::Database.mysql? + require 'active_record/connection_adapters/mysql2_adapter' + + module ActiveRecord + module ConnectionAdapters + class AbstractMysqlAdapter + NATIVE_DATABASE_TYPES.merge!(datetime_with_timezone: { name: 'timestamp' }) + end + end + end +end diff --git a/config/initializers/active_record_table_definition.rb b/config/initializers/active_record_table_definition.rb new file mode 100644 index 00000000000..4f59e35f4da --- /dev/null +++ b/config/initializers/active_record_table_definition.rb @@ -0,0 +1,24 @@ +# ActiveRecord custom method definitions with timezone information. +# See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11229 + +require 'active_record/connection_adapters/abstract/schema_definitions' + +# Appends columns `created_at` and `updated_at` to a table. +# +# It is used in table creation like: +# create_table 'users' do |t| +# t.timestamps_with_timezone +# end +module ActiveRecord + module ConnectionAdapters + class TableDefinition + def timestamps_with_timezone(**options) + options[:null] = false if options[:null].nil? + + [:created_at, :updated_at].each do |column_name| + column(column_name, :datetime_with_timezone, options) + end + end + end + end +end diff --git a/config/initializers/peek.rb b/config/initializers/peek.rb new file mode 100644 index 00000000000..65432caac2a --- /dev/null +++ b/config/initializers/peek.rb @@ -0,0 +1,32 @@ +Rails.application.config.peek.adapter = :redis, { client: ::Redis.new(Gitlab::Redis.params) } + +Peek.into Peek::Views::Host +Peek.into Peek::Views::PerformanceBar +if Gitlab::Database.mysql? + require 'peek-mysql2' + PEEK_DB_CLIENT = ::Mysql2::Client + PEEK_DB_VIEW = Peek::Views::Mysql2 +else + require 'peek-pg' + PEEK_DB_CLIENT = ::PG::Connection + PEEK_DB_VIEW = Peek::Views::PG +end +Peek.into PEEK_DB_VIEW +Peek.into Peek::Views::Redis +Peek.into Peek::Views::Sidekiq +Peek.into Peek::Views::Rblineprof +Peek.into Peek::Views::GC + +# rubocop:disable Style/ClassAndModuleCamelCase +class PEEK_DB_CLIENT + class << self + attr_accessor :query_details + end + self.query_details = Concurrent::Array.new +end + +PEEK_DB_VIEW.prepend ::Gitlab::PerformanceBar::PeekQueryTracker + +class Peek::Views::PerformanceBar::ProcessUtilization + prepend ::Gitlab::PerformanceBar::PeekPerformanceBarWithRackBody +end diff --git a/config/initializers/rugged_use_gitlab_git_attributes.rb b/config/initializers/rugged_use_gitlab_git_attributes.rb new file mode 100644 index 00000000000..7d652799786 --- /dev/null +++ b/config/initializers/rugged_use_gitlab_git_attributes.rb @@ -0,0 +1,25 @@ +# We don't want to ever call Rugged::Repository#fetch_attributes, because it has +# a lot of I/O overhead: +# <https://gitlab.com/gitlab-org/gitlab_git/commit/340e111e040ae847b614d35b4d3173ec48329015> +# +# While we don't do this from within the GitLab source itself, the Linguist gem +# has a dependency on Rugged and uses the gitattributes file when calculating +# repository-wide language statistics: +# <https://github.com/github/linguist/blob/v4.7.0/lib/linguist/lazy_blob.rb#L33-L36> +# +# The options passed by Linguist are those assumed by Gitlab::Git::Attributes +# anyway, and there is no great efficiency gain from just fetching the listed +# attributes with our implementation, so we ignore the additional arguments. +# +module Rugged + class Repository + module UseGitlabGitAttributes + def fetch_attributes(name, *) + @attributes ||= Gitlab::Git::Attributes.new(path) + @attributes.attributes(name) + end + end + + prepend UseGitlabGitAttributes + end +end diff --git a/config/karma.config.js b/config/karma.config.js index 40c58e7771d..978850e5d70 100644 --- a/config/karma.config.js +++ b/config/karma.config.js @@ -21,7 +21,18 @@ module.exports = function(config) { var karmaConfig = { basePath: ROOT_PATH, - browsers: ['PhantomJS'], + browsers: ['ChromeHeadlessCustom'], + customLaunchers: { + ChromeHeadlessCustom: { + base: 'ChromeHeadless', + displayName: 'Chrome', + flags: [ + // chrome cannot run in sandboxed mode inside a docker container unless it is run with + // escalated kernel privileges (e.g. docker run --cap-add=CAP_SYS_ADMIN) + '--no-sandbox', + ], + } + }, frameworks: ['jasmine'], files: [ { pattern: 'spec/javascripts/test_bundle.js', watched: false }, @@ -45,5 +56,14 @@ module.exports = function(config) { }; } + if (process.env.DEBUG) { + karmaConfig.logLevel = config.LOG_DEBUG; + process.env.CHROME_LOG_FILE = process.env.CHROME_LOG_FILE || 'chrome_debug.log'; + } + + if (process.env.CHROME_LOG_FILE) { + karmaConfig.customLaunchers.ChromeHeadlessCustom.flags.push('--enable-logging', '--v=1'); + } + config.set(karmaConfig); }; diff --git a/config/locales/de.yml b/config/locales/de.yml index 533663a2704..38c3711c6c7 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -62,6 +62,43 @@ de: - :month - :year datetime: + # used in a custom scope that has been created to fix https://gitlab.com/gitlab-org/gitlab-ce/issues/32747 + time_ago_in_words: + half_a_minute: vor einer halben Minute + less_than_x_seconds: + one: vor weniger als einer Sekunde + other: "vor weniger als %{count} Sekunden" + x_seconds: + one: vor einer Sekunde + other: "vor %{count} Sekunden" + less_than_x_minutes: + one: vor weniger als einer Minute + other: vor weniger als %{count} Minuten + x_minutes: + one: vor einer Minute + other: "vor %{count} Minuten" + about_x_hours: + one: vor etwa einer Stunde + other: "vor etwa %{count} Stunden" + x_days: + one: vor einem Tag + other: "vor %{count} Tagen" + about_x_months: + one: vor etwa einem Monat + other: "vor etwa %{count} Monaten" + x_months: + one: vor einem Monat + other: "vor %{count} Monaten" + about_x_years: + one: vor etwa einem Jahr + other: "vor etwa %{count} Jahren" + over_x_years: + one: vor mehr als einem Jahr + other: "vor mehr als %{count} Jahren" + almost_x_years: + one: vor fast einem Jahr + other: "vor fast %{count} Jahren" + # Used in distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words() distance_in_words: about_x_hours: one: etwa eine Stunde diff --git a/config/locales/es.yml b/config/locales/es.yml index 0f9dc39535d..d71c6eb5047 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -61,6 +61,7 @@ es: - :month - :year datetime: + # used in a custom scope that has been created to fix https://gitlab.com/gitlab-org/gitlab-ce/issues/32747 time_ago_in_words: half_a_minute: "hace medio minuto" less_than_x_seconds: @@ -96,6 +97,7 @@ es: almost_x_years: one: "hace casi 1 año" other: "hace casi %{count} años" + # Used in distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words() distance_in_words: about_x_hours: one: alrededor de 1 hora diff --git a/config/routes.rb b/config/routes.rb index 846054e6917..4fd6cb5d439 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -38,10 +38,11 @@ Rails.application.routes.draw do # Health check get 'health_check(/:checks)' => 'health_check#index', as: :health_check - scope path: '-', controller: 'health' do - get :liveness - get :readiness - get :metrics + scope path: '-' do + get 'liveness' => 'health#liveness' + get 'readiness' => 'health#readiness' + resources :metrics, only: [:index] + mount Peek::Railtie => '/peek' end # Koding route diff --git a/config/routes/admin.rb b/config/routes/admin.rb index c7b639b7b3c..5427bab93ce 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -48,7 +48,7 @@ namespace :admin do end end - resources :deploy_keys, only: [:index, :new, :create, :destroy] + resources :deploy_keys, only: [:index, :new, :create, :edit, :update, :destroy] resources :hooks, only: [:index, :create, :edit, :update, :destroy] do member do diff --git a/config/routes/dashboard.rb b/config/routes/dashboard.rb index 8e380a0b0ac..d2437285cdf 100644 --- a/config/routes/dashboard.rb +++ b/config/routes/dashboard.rb @@ -4,7 +4,13 @@ resource :dashboard, controller: 'dashboard', only: [] do get :activity scope module: :dashboard do - resources :milestones, only: [:index, :show] + resources :milestones, only: [:index, :show] do + member do + get :merge_requests + get :participants + get :labels + end + end resources :labels, only: [:index] resources :groups, only: [:index] diff --git a/config/routes/project.rb b/config/routes/project.rb index 10ed32a3a33..19e18c733b1 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -77,7 +77,7 @@ constraints(ProjectUrlConstrainer.new) do get :active_metrics end - resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do + resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create, :edit, :update] do member do put :enable put :disable diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb index dae83734fe6..0a4ebac3ca3 100644 --- a/config/routes/snippets.rb +++ b/config/routes/snippets.rb @@ -2,6 +2,9 @@ resources :snippets, concerns: :awardable do member do get :raw post :mark_as_spam + end + + collection do post :preview_markdown end diff --git a/config/routes/uploads.rb b/config/routes/uploads.rb index b315186b178..a49e244af1a 100644 --- a/config/routes/uploads.rb +++ b/config/routes/uploads.rb @@ -1,6 +1,6 @@ scope path: :uploads do # Note attachments and User/Group/Project avatars - get ":model/:mounted_as/:id/:filename", + get "system/:model/:mounted_as/:id/:filename", to: "uploads#show", constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ } @@ -9,8 +9,13 @@ scope path: :uploads do to: 'uploads#show', constraints: { model: /personal_snippet/, id: /\d+/, filename: /[^\/]+/ } + # show temporary uploads + get 'temp/:secret/:filename', + to: 'uploads#show', + constraints: { filename: /[^\/]+/ } + # Appearance - get ":model/:mounted_as/:id/:filename", + get "system/:model/:mounted_as/:id/:filename", to: "uploads#show", constraints: { model: /appearance/, mounted_as: /logo|header_logo/, filename: /.+/ } @@ -20,7 +25,7 @@ scope path: :uploads do constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /[^\/]+/ } # create uploads for models, snippets (notes) available for now - post ':model/:id/', + post ':model', to: 'uploads#create', constraints: { model: /personal_snippet/, id: /\d+/ }, as: 'upload' diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 93df2d6f5ff..1d9e69a2408 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -54,3 +54,4 @@ - [system_hook_push, 1] - [update_user_activity, 1] - [propagate_service_template, 1] + - [background_migration, 1] diff --git a/config/webpack.config.js b/config/webpack.config.js index a49f02d7ae8..04a25edcdb2 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -18,6 +18,15 @@ var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false'; var WEBPACK_REPORT = process.env.WEBPACK_REPORT; var NO_COMPRESSION = process.env.NO_COMPRESSION; +// optional dependency `node-zopfli` is unavailable on CentOS 6 +var ZOPFLI_AVAILABLE; +try { + require.resolve('node-zopfli'); + ZOPFLI_AVAILABLE = true; +} catch(err) { + ZOPFLI_AVAILABLE = false; +} + var config = { // because sqljs requires fs. node: { @@ -40,9 +49,11 @@ var config = { filtered_search: './filtered_search/filtered_search_bundle.js', graphs: './graphs/graphs_bundle.js', group: './group.js', + groups: './groups/index.js', groups_list: './groups_list.js', issue_show: './issue_show/index.js', integrations: './integrations', + job_details: './jobs/job_details_bundle.js', locale: './locale/index.js', main: './main.js', merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js', @@ -50,7 +61,7 @@ var config = { network: './network/network_bundle.js', notebook_viewer: './blob/notebook_viewer.js', pdf_viewer: './blob/pdf_viewer.js', - pipelines: './pipelines/index.js', + pipelines: './pipelines/pipelines_bundle.js', pipelines_details: './pipelines/pipeline_details_bundle.js', profile: './profile/profile_bundle.js', prometheus_metrics: './prometheus_metrics', @@ -68,6 +79,7 @@ var config = { raven: './raven/index.js', vue_merge_request_widget: './vue_merge_request_widget/index.js', test: './test.js', + peek: './peek.js', }, output: { @@ -156,7 +168,9 @@ var config = { 'environments', 'environments_folder', 'filtered_search', + 'groups', 'issue_show', + 'job_details', 'merge_conflicts', 'notebook_viewer', 'pdf_viewer', @@ -184,15 +198,7 @@ var config = { // create cacheable common library bundles new webpack.optimize.CommonsChunkPlugin({ - names: ['main', 'common', 'runtime'], - }), - - // locale common library - new webpack.optimize.CommonsChunkPlugin({ - name: 'locale', - chunks: [ - 'cycle_analytics', - ], + names: ['main', 'locale', 'common', 'runtime'], }), ], @@ -231,7 +237,7 @@ if (IS_PRODUCTION) { config.plugins.push( new CompressionPlugin({ asset: '[path].gz[query]', - algorithm: 'zopfli', + algorithm: ZOPFLI_AVAILABLE ? 'zopfli' : 'gzip', }) ); } diff --git a/db/fixtures/production/010_settings.rb b/db/fixtures/production/010_settings.rb index 5522f31629a..7626cdb0b9c 100644 --- a/db/fixtures/production/010_settings.rb +++ b/db/fixtures/production/010_settings.rb @@ -1,16 +1,26 @@ -if ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN'].present? - settings = ApplicationSetting.current || ApplicationSetting.create_from_defaults - settings.set_runners_registration_token(ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN']) - +def save(settings, topic) if settings.save - puts "Saved Runner Registration Token".color(:green) + puts "Saved #{topic}".color(:green) else - puts "Could not save Runner Registration Token".color(:red) + puts "Could not save #{topic}".color(:red) puts settings.errors.full_messages.map do |message| puts "--> #{message}".color(:red) end puts - exit 1 + exit(1) end end + +if ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN'].present? + settings = Gitlab::CurrentSettings.current_application_settings + settings.set_runners_registration_token(ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN']) + save(settings, 'Runner Registration Token') +end + +if ENV['GITLAB_PROMETHEUS_METRICS_ENABLED'].present? + settings = Gitlab::CurrentSettings.current_application_settings + value = Gitlab::Utils.to_boolean(ENV['GITLAB_PROMETHEUS_METRICS_ENABLED']) || false + settings.prometheus_metrics_enabled = value + save(settings, 'Prometheus metrics enabled flag') +end diff --git a/db/migrate/20160314114439_add_requested_at_to_members.rb b/db/migrate/20160314114439_add_requested_at_to_members.rb index 273819d4cd8..76c8b8a1a24 100644 --- a/db/migrate/20160314114439_add_requested_at_to_members.rb +++ b/db/migrate/20160314114439_add_requested_at_to_members.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Datetime class AddRequestedAtToMembers < ActiveRecord::Migration def change add_column :members, :requested_at, :datetime diff --git a/db/migrate/20160415062917_create_personal_access_tokens.rb b/db/migrate/20160415062917_create_personal_access_tokens.rb index ce0b33f32bd..c7b49870bf7 100644 --- a/db/migrate/20160415062917_create_personal_access_tokens.rb +++ b/db/migrate/20160415062917_create_personal_access_tokens.rb @@ -1,3 +1,5 @@ +# rubocop:disable Migration/Datetime +# rubocop:disable Migration/Timestamps class CreatePersonalAccessTokens < ActiveRecord::Migration def change create_table :personal_access_tokens do |t| diff --git a/db/migrate/20160610204157_add_deployments.rb b/db/migrate/20160610204157_add_deployments.rb index cb144ea8a6d..0e7e6e747a3 100644 --- a/db/migrate/20160610204157_add_deployments.rb +++ b/db/migrate/20160610204157_add_deployments.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable Migration/Datetime class AddDeployments < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160610204158_add_environments.rb b/db/migrate/20160610204158_add_environments.rb index e1c71d173c4..699cee2b246 100644 --- a/db/migrate/20160610204158_add_environments.rb +++ b/db/migrate/20160610204158_add_environments.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable Migration/Datetime class AddEnvironments < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160705054938_add_protected_branches_push_access.rb b/db/migrate/20160705054938_add_protected_branches_push_access.rb index f27295524e1..97aaaf9d2c8 100644 --- a/db/migrate/20160705054938_add_protected_branches_push_access.rb +++ b/db/migrate/20160705054938_add_protected_branches_push_access.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable Migration/Timestamps class AddProtectedBranchesPushAccess < ActiveRecord::Migration DOWNTIME = false diff --git a/db/migrate/20160705054952_add_protected_branches_merge_access.rb b/db/migrate/20160705054952_add_protected_branches_merge_access.rb index 32adfa266cd..51a52a5ac17 100644 --- a/db/migrate/20160705054952_add_protected_branches_merge_access.rb +++ b/db/migrate/20160705054952_add_protected_branches_merge_access.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable Migration/Timestamps class AddProtectedBranchesMergeAccess < ActiveRecord::Migration DOWNTIME = false diff --git a/db/migrate/20160724205507_add_resolved_to_notes.rb b/db/migrate/20160724205507_add_resolved_to_notes.rb index b8ebcdbd156..3aca272a3f7 100644 --- a/db/migrate/20160724205507_add_resolved_to_notes.rb +++ b/db/migrate/20160724205507_add_resolved_to_notes.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Datetime class AddResolvedToNotes < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160727163552_create_user_agent_details.rb b/db/migrate/20160727163552_create_user_agent_details.rb index ed4ccfedc0a..3eb36f8464f 100644 --- a/db/migrate/20160727163552_create_user_agent_details.rb +++ b/db/migrate/20160727163552_create_user_agent_details.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Timestamps class CreateUserAgentDetails < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160727191041_create_boards.rb b/db/migrate/20160727191041_create_boards.rb index 56afbd4e030..9ec8df1b8e8 100644 --- a/db/migrate/20160727191041_create_boards.rb +++ b/db/migrate/20160727191041_create_boards.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Timestamps class CreateBoards < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160727193336_create_lists.rb b/db/migrate/20160727193336_create_lists.rb index 61d501215f2..3fd95dc8cfc 100644 --- a/db/migrate/20160727193336_create_lists.rb +++ b/db/migrate/20160727193336_create_lists.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Timestamps class CreateLists < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb b/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb index 30d98a0124e..404c253e18b 100644 --- a/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb +++ b/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Datetime # rubocop:disable RemoveIndex class AddDeletedAtToNamespaces < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160824124900_add_table_issue_metrics.rb b/db/migrate/20160824124900_add_table_issue_metrics.rb index e9bb79b3c62..30d35ef1db2 100644 --- a/db/migrate/20160824124900_add_table_issue_metrics.rb +++ b/db/migrate/20160824124900_add_table_issue_metrics.rb @@ -1,6 +1,8 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable Migration/Datetime +# rubocop:disable Migration/Timestamps class AddTableIssueMetrics < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160825052008_add_table_merge_request_metrics.rb b/db/migrate/20160825052008_add_table_merge_request_metrics.rb index e01cc5038b9..56b39634dfd 100644 --- a/db/migrate/20160825052008_add_table_merge_request_metrics.rb +++ b/db/migrate/20160825052008_add_table_merge_request_metrics.rb @@ -1,6 +1,8 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable Migration/Datetime +# rubocop:disable Migration/Timestamps class AddTableMergeRequestMetrics < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160831214002_create_project_features.rb b/db/migrate/20160831214002_create_project_features.rb index 343953826f0..7ac6c8ec654 100644 --- a/db/migrate/20160831214002_create_project_features.rb +++ b/db/migrate/20160831214002_create_project_features.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Timestamps class CreateProjectFeatures < ActiveRecord::Migration DOWNTIME = false diff --git a/db/migrate/20160915042921_create_merge_requests_closing_issues.rb b/db/migrate/20160915042921_create_merge_requests_closing_issues.rb index 94874a853da..10c5604bb5c 100644 --- a/db/migrate/20160915042921_create_merge_requests_closing_issues.rb +++ b/db/migrate/20160915042921_create_merge_requests_closing_issues.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable Migration/Timestamps class CreateMergeRequestsClosingIssues < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161014173530_create_label_priorities.rb b/db/migrate/20161014173530_create_label_priorities.rb index 2c22841c28a..28937c81e02 100644 --- a/db/migrate/20161014173530_create_label_priorities.rb +++ b/db/migrate/20161014173530_create_label_priorities.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Timestamps class CreateLabelPriorities < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161113184239_create_user_chat_names_table.rb b/db/migrate/20161113184239_create_user_chat_names_table.rb index 97b597654f7..62ccb599f2e 100644 --- a/db/migrate/20161113184239_create_user_chat_names_table.rb +++ b/db/migrate/20161113184239_create_user_chat_names_table.rb @@ -1,3 +1,5 @@ +# rubocop:disable Migration/Datetime +# rubocop:disable Migration/Timestamps class CreateUserChatNamesTable < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161124111402_add_routes_table.rb b/db/migrate/20161124111402_add_routes_table.rb index a02e046a18e..f5241d906d1 100644 --- a/db/migrate/20161124111402_add_routes_table.rb +++ b/db/migrate/20161124111402_add_routes_table.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable Migration/Timestamps class AddRoutesTable < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161221152132_add_last_used_at_to_key.rb b/db/migrate/20161221152132_add_last_used_at_to_key.rb index fb2b15817de..86dc7870247 100644 --- a/db/migrate/20161221152132_add_last_used_at_to_key.rb +++ b/db/migrate/20161221152132_add_last_used_at_to_key.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Datetime class AddLastUsedAtToKey < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161223034646_create_timelogs_ce.rb b/db/migrate/20161223034646_create_timelogs_ce.rb index 66d9cd823fb..1e894cc9161 100644 --- a/db/migrate/20161223034646_create_timelogs_ce.rb +++ b/db/migrate/20161223034646_create_timelogs_ce.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Timestamps class CreateTimelogsCe < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161228124936_change_expires_at_to_date_in_personal_access_tokens.rb b/db/migrate/20161228124936_change_expires_at_to_date_in_personal_access_tokens.rb index af1bac897cc..16f7cc487ce 100644 --- a/db/migrate/20161228124936_change_expires_at_to_date_in_personal_access_tokens.rb +++ b/db/migrate/20161228124936_change_expires_at_to_date_in_personal_access_tokens.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable Migration/Datetime class ChangeExpiresAtToDateInPersonalAccessTokens < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170120131253_create_chat_teams.rb b/db/migrate/20170120131253_create_chat_teams.rb index 7995d383986..52208821911 100644 --- a/db/migrate/20170120131253_create_chat_teams.rb +++ b/db/migrate/20170120131253_create_chat_teams.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Timestamps class CreateChatTeams < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170130221926_create_uploads.rb b/db/migrate/20170130221926_create_uploads.rb index 6f06c5dd840..4d9fa0bb692 100644 --- a/db/migrate/20170130221926_create_uploads.rb +++ b/db/migrate/20170130221926_create_uploads.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Datetime class CreateUploads < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170222143317_drop_ci_projects.rb b/db/migrate/20170222143317_drop_ci_projects.rb index 4db8658f36f..9973e53501c 100644 --- a/db/migrate/20170222143317_drop_ci_projects.rb +++ b/db/migrate/20170222143317_drop_ci_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Datetime class DropCiProjects < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb index 69dd15b8b4e..1a77d5934a3 100644 --- a/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb +++ b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Datetime class RemoveUnusedCiTablesAndColumns < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170309173138_create_protected_tags.rb b/db/migrate/20170309173138_create_protected_tags.rb index 796f3c90344..4684c9964c4 100644 --- a/db/migrate/20170309173138_create_protected_tags.rb +++ b/db/migrate/20170309173138_create_protected_tags.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Timestamps class CreateProtectedTags < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170314082049_create_system_note_metadata.rb b/db/migrate/20170314082049_create_system_note_metadata.rb index dd1e6cf8172..fee47e96053 100644 --- a/db/migrate/20170314082049_create_system_note_metadata.rb +++ b/db/migrate/20170314082049_create_system_note_metadata.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Timestamps class CreateSystemNoteMetadata < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170315194013_add_closed_at_to_issues.rb b/db/migrate/20170315194013_add_closed_at_to_issues.rb index 1326118cc8d..34a1bd7ca8c 100644 --- a/db/migrate/20170315194013_add_closed_at_to_issues.rb +++ b/db/migrate/20170315194013_add_closed_at_to_issues.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Datetime class AddClosedAtToIssues < ActiveRecord::Migration DOWNTIME = false diff --git a/db/migrate/20170316163800_rename_system_namespaces.rb b/db/migrate/20170316163800_rename_system_namespaces.rb new file mode 100644 index 00000000000..b5408fbf112 --- /dev/null +++ b/db/migrate/20170316163800_rename_system_namespaces.rb @@ -0,0 +1,231 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. +class RenameSystemNamespaces < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + include Gitlab::ShellAdapter + disable_ddl_transaction! + + class User < ActiveRecord::Base + self.table_name = 'users' + end + + class Namespace < ActiveRecord::Base + self.table_name = 'namespaces' + belongs_to :parent, class_name: 'RenameSystemNamespaces::Namespace' + has_one :route, as: :source + has_many :children, class_name: 'RenameSystemNamespaces::Namespace', foreign_key: :parent_id + belongs_to :owner, class_name: 'RenameSystemNamespaces::User' + + # Overridden to have the correct `source_type` for the `route` relation + def self.name + 'Namespace' + end + + def full_path + if route && route.path.present? + @full_path ||= route.path + else + update_route if persisted? + + build_full_path + end + end + + def build_full_path + if parent && path + parent.full_path + '/' + path + else + path + end + end + + def update_route + prepare_route + route.save + end + + def prepare_route + route || build_route(source: self) + route.path = build_full_path + route.name = build_full_name + @full_path = nil + @full_name = nil + end + + def build_full_name + if parent && name + parent.human_name + ' / ' + name + else + name + end + end + + def human_name + owner&.name + end + end + + class Route < ActiveRecord::Base + self.table_name = 'routes' + belongs_to :source, polymorphic: true + end + + class Project < ActiveRecord::Base + self.table_name = 'projects' + + def repository_storage_path + Gitlab.config.repositories.storages[repository_storage]['path'] + end + end + + DOWNTIME = false + + def up + return unless system_namespace + + old_path = system_namespace.path + old_full_path = system_namespace.full_path + # Only remove the last occurrence of the path name to get the parent namespace path + namespace_path = remove_last_occurrence(old_full_path, old_path) + new_path = rename_path(namespace_path, old_path) + new_full_path = join_namespace_path(namespace_path, new_path) + + Namespace.where(id: system_namespace).update_all(path: new_path) # skips callbacks & validations + + replace_statement = replace_sql(Route.arel_table[:path], old_full_path, new_full_path) + route_matches = [old_full_path, "#{old_full_path}/%"] + + update_column_in_batches(:routes, :path, replace_statement) do |table, query| + query.where(Route.arel_table[:path].matches_any(route_matches)) + end + + clear_cache_for_namespace(system_namespace) + + # tasks here are based on `Namespace#move_dir` + move_repositories(system_namespace, old_full_path, new_full_path) + move_namespace_folders(uploads_dir, old_full_path, new_full_path) if file_storage? + move_namespace_folders(pages_dir, old_full_path, new_full_path) + end + + def down + # nothing to do + end + + def remove_last_occurrence(string, pattern) + string.reverse.sub(pattern.reverse, "").reverse + end + + def move_namespace_folders(directory, old_relative_path, new_relative_path) + old_path = File.join(directory, old_relative_path) + return unless File.directory?(old_path) + + new_path = File.join(directory, new_relative_path) + FileUtils.mv(old_path, new_path) + end + + def move_repositories(namespace, old_full_path, new_full_path) + repo_paths_for_namespace(namespace).each do |repository_storage_path| + # Ensure old directory exists before moving it + gitlab_shell.add_namespace(repository_storage_path, old_full_path) + + unless gitlab_shell.mv_namespace(repository_storage_path, old_full_path, new_full_path) + say "Exception moving path #{repository_storage_path} from #{old_full_path} to #{new_full_path}" + end + end + end + + def rename_path(namespace_path, path_was) + counter = 0 + path = "#{path_was}#{counter}" + + while route_exists?(join_namespace_path(namespace_path, path)) + counter += 1 + path = "#{path_was}#{counter}" + end + + path + end + + def route_exists?(full_path) + Route.where(Route.arel_table[:path].matches(full_path)).any? + end + + def join_namespace_path(namespace_path, path) + if namespace_path.present? + File.join(namespace_path, path) + else + path + end + end + + def system_namespace + @system_namespace ||= Namespace.where(parent_id: nil). + where(arel_table[:path].matches(system_namespace_path)). + first + end + + def system_namespace_path + "system" + end + + def clear_cache_for_namespace(namespace) + project_ids = projects_for_namespace(namespace).pluck(:id) + + update_column_in_batches(:projects, :description_html, nil) do |table, query| + query.where(table[:id].in(project_ids)) + end + + update_column_in_batches(:issues, :description_html, nil) do |table, query| + query.where(table[:project_id].in(project_ids)) + end + + update_column_in_batches(:merge_requests, :description_html, nil) do |table, query| + query.where(table[:target_project_id].in(project_ids)) + end + + update_column_in_batches(:notes, :note_html, nil) do |table, query| + query.where(table[:project_id].in(project_ids)) + end + + update_column_in_batches(:milestones, :description_html, nil) do |table, query| + query.where(table[:project_id].in(project_ids)) + end + end + + def projects_for_namespace(namespace) + namespace_ids = child_ids_for_parent(namespace, ids: [namespace.id]) + namespace_or_children = Project.arel_table[:namespace_id].in(namespace_ids) + Project.unscoped.where(namespace_or_children) + end + + # This won't scale to huge trees, but it should do for a handful of namespaces + # called `system`. + def child_ids_for_parent(namespace, ids: []) + namespace.children.each do |child| + ids << child.id + child_ids_for_parent(child, ids: ids) if child.children.any? + end + ids + end + + def repo_paths_for_namespace(namespace) + projects_for_namespace(namespace).distinct. + select(:repository_storage).map(&:repository_storage_path) + end + + def uploads_dir + File.join(Rails.root, "public", "uploads") + end + + def pages_dir + Settings.pages.path + end + + def file_storage? + CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File + end + + def arel_table + Namespace.arel_table + end +end diff --git a/db/migrate/20170316163845_move_uploads_to_system_dir.rb b/db/migrate/20170316163845_move_uploads_to_system_dir.rb new file mode 100644 index 00000000000..564ee10b5ab --- /dev/null +++ b/db/migrate/20170316163845_move_uploads_to_system_dir.rb @@ -0,0 +1,59 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MoveUploadsToSystemDir < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + DOWNTIME = false + DIRECTORIES_TO_MOVE = %w(user project note group appearance).freeze + + def up + return unless file_storage? + + FileUtils.mkdir_p(new_upload_dir) + + DIRECTORIES_TO_MOVE.each do |dir| + source = File.join(old_upload_dir, dir) + destination = File.join(new_upload_dir, dir) + next unless File.directory?(source) + next if File.directory?(destination) + + say "Moving #{source} -> #{destination}" + FileUtils.mv(source, destination) + FileUtils.ln_s(destination, source) + end + end + + def down + return unless file_storage? + return unless File.directory?(new_upload_dir) + + DIRECTORIES_TO_MOVE.each do |dir| + source = File.join(new_upload_dir, dir) + destination = File.join(old_upload_dir, dir) + next unless File.directory?(source) + next if File.directory?(destination) && !File.symlink?(destination) + + say "Moving #{source} -> #{destination}" + FileUtils.rm(destination) if File.symlink?(destination) + FileUtils.mv(source, destination) + end + end + + def file_storage? + CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File + end + + def base_directory + Rails.root + end + + def old_upload_dir + File.join(base_directory, "public", "uploads") + end + + def new_upload_dir + File.join(base_directory, "public", "uploads", "system") + end +end diff --git a/db/migrate/20170322013926_create_container_repository.rb b/db/migrate/20170322013926_create_container_repository.rb index 91540bc88bd..242f7b8d17d 100644 --- a/db/migrate/20170322013926_create_container_repository.rb +++ b/db/migrate/20170322013926_create_container_repository.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Timestamps class CreateContainerRepository < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170329095907_create_ci_trigger_schedules.rb b/db/migrate/20170329095907_create_ci_trigger_schedules.rb index cfcfa27ebb5..06a2010db23 100644 --- a/db/migrate/20170329095907_create_ci_trigger_schedules.rb +++ b/db/migrate/20170329095907_create_ci_trigger_schedules.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Datetime class CreateCiTriggerSchedules < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170425112128_create_pipeline_schedules_table.rb b/db/migrate/20170425112128_create_pipeline_schedules_table.rb index 3612a796ae8..57df47f5f42 100644 --- a/db/migrate/20170425112128_create_pipeline_schedules_table.rb +++ b/db/migrate/20170425112128_create_pipeline_schedules_table.rb @@ -1,3 +1,5 @@ +# rubocop:disable Migration/Datetime +# rubocop:disable Migration/Timestamps class CreatePipelineSchedulesTable < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170427215854_create_redirect_routes.rb b/db/migrate/20170427215854_create_redirect_routes.rb index 2bf086b3e30..6db508e5db4 100644 --- a/db/migrate/20170427215854_create_redirect_routes.rb +++ b/db/migrate/20170427215854_create_redirect_routes.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Timestamps class CreateRedirectRoutes < ActiveRecord::Migration # Set this constant to true if this migration requires downtime. DOWNTIME = false diff --git a/db/migrate/20170503004125_add_last_repository_updated_at_to_projects.rb b/db/migrate/20170503004125_add_last_repository_updated_at_to_projects.rb index 00c685cf342..2ea49f62742 100644 --- a/db/migrate/20170503004125_add_last_repository_updated_at_to_projects.rb +++ b/db/migrate/20170503004125_add_last_repository_updated_at_to_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Datetime class AddLastRepositoryUpdatedAtToProjects < ActiveRecord::Migration DOWNTIME = false diff --git a/db/migrate/20170503114228_add_description_to_snippets.rb b/db/migrate/20170503114228_add_description_to_snippets.rb new file mode 100644 index 00000000000..3fc960b2da5 --- /dev/null +++ b/db/migrate/20170503114228_add_description_to_snippets.rb @@ -0,0 +1,12 @@ +class AddDescriptionToSnippets < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def change + add_column :snippets, :description, :text + add_column :snippets, :description_html, :text + end +end diff --git a/db/migrate/20170519102115_add_prometheus_settings_to_metrics_settings.rb b/db/migrate/20170519102115_add_prometheus_settings_to_metrics_settings.rb new file mode 100644 index 00000000000..6ec2ed712b9 --- /dev/null +++ b/db/migrate/20170519102115_add_prometheus_settings_to_metrics_settings.rb @@ -0,0 +1,16 @@ +class AddPrometheusSettingsToMetricsSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_column_with_default(:application_settings, :prometheus_metrics_enabled, :boolean, + default: false, allow_null: false) + end + + def down + remove_column(:application_settings, :prometheus_metrics_enabled) + end +end diff --git a/db/migrate/20170523121229_create_conversational_development_index_metrics.rb b/db/migrate/20170523121229_create_conversational_development_index_metrics.rb index 9f9ec526055..7026a867ae1 100644 --- a/db/migrate/20170523121229_create_conversational_development_index_metrics.rb +++ b/db/migrate/20170523121229_create_conversational_development_index_metrics.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Timestamps class CreateConversationalDevelopmentIndexMetrics < ActiveRecord::Migration DOWNTIME = false diff --git a/db/migrate/20170525132202_create_pipeline_stages.rb b/db/migrate/20170525132202_create_pipeline_stages.rb new file mode 100644 index 00000000000..825993aa41e --- /dev/null +++ b/db/migrate/20170525132202_create_pipeline_stages.rb @@ -0,0 +1,26 @@ +# rubocop:disable Migration/Timestamps +class CreatePipelineStages < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + create_table :ci_stages do |t| + t.integer :project_id + t.integer :pipeline_id + t.timestamps null: true + t.string :name + end + + add_concurrent_foreign_key :ci_stages, :projects, column: :project_id, on_delete: :cascade + add_concurrent_foreign_key :ci_stages, :ci_pipelines, column: :pipeline_id, on_delete: :cascade + add_concurrent_index :ci_stages, :project_id + add_concurrent_index :ci_stages, :pipeline_id + end + + def down + drop_table :ci_stages + end +end diff --git a/db/migrate/20170526185602_add_stage_id_to_ci_builds.rb b/db/migrate/20170526185602_add_stage_id_to_ci_builds.rb new file mode 100644 index 00000000000..d5675d5828b --- /dev/null +++ b/db/migrate/20170526185602_add_stage_id_to_ci_builds.rb @@ -0,0 +1,21 @@ +class AddStageIdToCiBuilds < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column :ci_builds, :stage_id, :integer + + add_concurrent_foreign_key :ci_builds, :ci_stages, column: :stage_id, on_delete: :cascade + add_concurrent_index :ci_builds, :stage_id + end + + def down + remove_foreign_key :ci_builds, column: :stage_id + remove_concurrent_index :ci_builds, :stage_id + + remove_column :ci_builds, :stage_id, :integer + end +end diff --git a/db/migrate/20170531202042_rename_users_ldap_email_to_external_email.rb b/db/migrate/20170531202042_rename_users_ldap_email_to_external_email.rb new file mode 100644 index 00000000000..470c3b8166c --- /dev/null +++ b/db/migrate/20170531202042_rename_users_ldap_email_to_external_email.rb @@ -0,0 +1,15 @@ +class RenameUsersLdapEmailToExternalEmail < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + rename_column_concurrently :users, :ldap_email, :external_email + end + + def down + cleanup_concurrent_column_rename :users, :external_email, :ldap_email + end +end diff --git a/db/migrate/20170602154736_add_help_page_hide_commercial_content_to_application_settings.rb b/db/migrate/20170602154736_add_help_page_hide_commercial_content_to_application_settings.rb new file mode 100644 index 00000000000..5e8b667b86d --- /dev/null +++ b/db/migrate/20170602154736_add_help_page_hide_commercial_content_to_application_settings.rb @@ -0,0 +1,9 @@ +class AddHelpPageHideCommercialContentToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :application_settings, :help_page_hide_commercial_content, :boolean, default: false + end +end diff --git a/db/migrate/20170602154813_add_help_page_support_url_to_application_settings.rb b/db/migrate/20170602154813_add_help_page_support_url_to_application_settings.rb new file mode 100644 index 00000000000..138fe9b2a37 --- /dev/null +++ b/db/migrate/20170602154813_add_help_page_support_url_to_application_settings.rb @@ -0,0 +1,9 @@ +class AddHelpPageSupportUrlToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :application_settings, :help_page_support_url, :string + end +end diff --git a/db/migrate/20170603200744_add_email_provider_to_users.rb b/db/migrate/20170603200744_add_email_provider_to_users.rb new file mode 100644 index 00000000000..ed90af9aadc --- /dev/null +++ b/db/migrate/20170603200744_add_email_provider_to_users.rb @@ -0,0 +1,9 @@ +class AddEmailProviderToUsers < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :users, :email_provider, :string + end +end diff --git a/db/migrate/20170606154216_add_notification_setting_columns.rb b/db/migrate/20170606154216_add_notification_setting_columns.rb new file mode 100644 index 00000000000..0a9b5da6583 --- /dev/null +++ b/db/migrate/20170606154216_add_notification_setting_columns.rb @@ -0,0 +1,26 @@ +class AddNotificationSettingColumns < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + COLUMNS = [ + :new_note, + :new_issue, + :reopen_issue, + :close_issue, + :reassign_issue, + :new_merge_request, + :reopen_merge_request, + :close_merge_request, + :reassign_merge_request, + :merge_merge_request, + :failed_pipeline, + :success_pipeline + ] + + def change + COLUMNS.each do |column| + add_column(:notification_settings, column, :boolean) + end + end +end diff --git a/db/post_migrate/20170317162059_update_upload_paths_to_system.rb b/db/post_migrate/20170317162059_update_upload_paths_to_system.rb new file mode 100644 index 00000000000..9a77b0bbdfb --- /dev/null +++ b/db/post_migrate/20170317162059_update_upload_paths_to_system.rb @@ -0,0 +1,55 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class UpdateUploadPathsToSystem < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + AFFECTED_MODELS = %w(User Project Note Namespace Appearance) + + def up + update_column_in_batches(:uploads, :path, replace_sql(arel_table[:path], base_directory, new_upload_dir)) do |_table, query| + query.where(uploads_to_switch_to_new_path) + end + end + + def down + update_column_in_batches(:uploads, :path, replace_sql(arel_table[:path], new_upload_dir, base_directory)) do |_table, query| + query.where(uploads_to_switch_to_old_path) + end + end + + # "SELECT \"uploads\".* FROM \"uploads\" WHERE \"uploads\".\"model_type\" IN ('User', 'Project', 'Note', 'Namespace', 'Appearance') AND (\"uploads\".\"path\" ILIKE 'uploads/%' AND NOT (\"uploads\".\"path\" ILIKE 'uploads/system/%'))" + def uploads_to_switch_to_new_path + affected_uploads.and(starting_with_base_directory).and(starting_with_new_upload_directory.not) + end + + # "SELECT \"uploads\".* FROM \"uploads\" WHERE \"uploads\".\"model_type\" IN ('User', 'Project', 'Note', 'Namespace', 'Appearance') AND (\"uploads\".\"path\" ILIKE 'uploads/%' AND \"uploads\".\"path\" ILIKE 'uploads/system/%')" + def uploads_to_switch_to_old_path + affected_uploads.and(starting_with_new_upload_directory) + end + + def starting_with_base_directory + arel_table[:path].matches("#{base_directory}/%") + end + + def starting_with_new_upload_directory + arel_table[:path].matches("#{new_upload_dir}/%") + end + + def affected_uploads + arel_table[:model_type].in(AFFECTED_MODELS) + end + + def base_directory + "uploads" + end + + def new_upload_dir + File.join(base_directory, "system") + end + + def arel_table + Arel::Table.new(:uploads) + end +end diff --git a/db/post_migrate/20170406111121_clean_upload_symlinks.rb b/db/post_migrate/20170406111121_clean_upload_symlinks.rb new file mode 100644 index 00000000000..3ac9a6c10bc --- /dev/null +++ b/db/post_migrate/20170406111121_clean_upload_symlinks.rb @@ -0,0 +1,52 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CleanUploadSymlinks < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + DOWNTIME = false + DIRECTORIES_TO_MOVE = %w(user project note group appeareance) + + def up + return unless file_storage? + + DIRECTORIES_TO_MOVE.each do |dir| + symlink_location = File.join(old_upload_dir, dir) + next unless File.symlink?(symlink_location) + say "removing symlink: #{symlink_location}" + FileUtils.rm(symlink_location) + end + end + + def down + return unless file_storage? + + DIRECTORIES_TO_MOVE.each do |dir| + symlink = File.join(old_upload_dir, dir) + destination = File.join(new_upload_dir, dir) + + next if File.directory?(symlink) + next unless File.directory?(destination) + + say "Creating symlink #{symlink} -> #{destination}" + FileUtils.ln_s(destination, symlink) + end + end + + def file_storage? + CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File + end + + def base_directory + Rails.root + end + + def old_upload_dir + File.join(base_directory, "public", "uploads") + end + + def new_upload_dir + File.join(base_directory, "public", "uploads", "system") + end +end diff --git a/db/post_migrate/20170425130047_drop_ci_trigger_schedules_table.rb b/db/post_migrate/20170425130047_drop_ci_trigger_schedules_table.rb index 24750c58ef0..159b533eaaa 100644 --- a/db/post_migrate/20170425130047_drop_ci_trigger_schedules_table.rb +++ b/db/post_migrate/20170425130047_drop_ci_trigger_schedules_table.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Datetime class DropCiTriggerSchedulesTable < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/post_migrate/20170526185842_migrate_pipeline_stages.rb b/db/post_migrate/20170526185842_migrate_pipeline_stages.rb new file mode 100644 index 00000000000..afd4db183c2 --- /dev/null +++ b/db/post_migrate/20170526185842_migrate_pipeline_stages.rb @@ -0,0 +1,22 @@ +class MigratePipelineStages < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + disable_statement_timeout + + execute <<-SQL.strip_heredoc + INSERT INTO ci_stages (project_id, pipeline_id, name) + SELECT project_id, commit_id, stage FROM ci_builds + WHERE stage IS NOT NULL + AND stage_id IS NULL + AND EXISTS (SELECT 1 FROM projects WHERE projects.id = ci_builds.project_id) + AND EXISTS (SELECT 1 FROM ci_pipelines WHERE ci_pipelines.id = ci_builds.commit_id) + GROUP BY project_id, commit_id, stage + ORDER BY MAX(stage_idx) + SQL + end +end diff --git a/db/post_migrate/20170526185858_create_index_in_pipeline_stages.rb b/db/post_migrate/20170526185858_create_index_in_pipeline_stages.rb new file mode 100644 index 00000000000..ec9ff33b6b7 --- /dev/null +++ b/db/post_migrate/20170526185858_create_index_in_pipeline_stages.rb @@ -0,0 +1,15 @@ +class CreateIndexInPipelineStages < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index(:ci_stages, [:pipeline_id, :name]) + end + + def down + remove_concurrent_index(:ci_stages, [:pipeline_id, :name]) + end +end diff --git a/db/post_migrate/20170526185921_migrate_build_stage_reference.rb b/db/post_migrate/20170526185921_migrate_build_stage_reference.rb new file mode 100644 index 00000000000..797e106cae4 --- /dev/null +++ b/db/post_migrate/20170526185921_migrate_build_stage_reference.rb @@ -0,0 +1,25 @@ +class MigrateBuildStageReference < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + disable_statement_timeout + + stage_id = Arel.sql <<-SQL.strip_heredoc + (SELECT id FROM ci_stages + WHERE ci_stages.pipeline_id = ci_builds.commit_id + AND ci_stages.name = ci_builds.stage) + SQL + + update_column_in_batches(:ci_builds, :stage_id, stage_id) do |table, query| + query.where(table[:stage_id].eq(nil)) + end + end + + def down + disable_statement_timeout + + update_column_in_batches(:ci_builds, :stage_id, nil) + end +end diff --git a/db/post_migrate/20170531203055_cleanup_users_ldap_email_rename.rb b/db/post_migrate/20170531203055_cleanup_users_ldap_email_rename.rb new file mode 100644 index 00000000000..15edb402b86 --- /dev/null +++ b/db/post_migrate/20170531203055_cleanup_users_ldap_email_rename.rb @@ -0,0 +1,15 @@ +class CleanupUsersLdapEmailRename < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + cleanup_concurrent_column_rename :users, :ldap_email, :external_email + end + + def down + rename_column_concurrently :users, :external_email, :ldap_email + end +end diff --git a/db/post_migrate/20170606202615_move_appearance_to_system_dir.rb b/db/post_migrate/20170606202615_move_appearance_to_system_dir.rb new file mode 100644 index 00000000000..561de59ec69 --- /dev/null +++ b/db/post_migrate/20170606202615_move_appearance_to_system_dir.rb @@ -0,0 +1,57 @@ +class MoveAppearanceToSystemDir < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + DOWNTIME = false + DIRECTORY_TO_MOVE = 'appearance'.freeze + + def up + source = File.join(old_upload_dir, DIRECTORY_TO_MOVE) + destination = File.join(new_upload_dir, DIRECTORY_TO_MOVE) + + move_directory(source, destination) + end + + def down + source = File.join(new_upload_dir, DIRECTORY_TO_MOVE) + destination = File.join(old_upload_dir, DIRECTORY_TO_MOVE) + + move_directory(source, destination) + end + + def move_directory(source, destination) + unless file_storage? + say 'Not using file storage, skipping' + return + end + + unless File.directory?(source) + say "#{source} did not exist, skipping" + return + end + + if File.directory?(destination) + say "#{destination} already existed, skipping" + return + end + + say "Moving #{source} -> #{destination}" + FileUtils.mv(source, destination) + end + + def file_storage? + CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File + end + + def base_directory + Rails.root + end + + def old_upload_dir + File.join(base_directory, "public", "uploads") + end + + def new_upload_dir + File.join(base_directory, "public", "uploads", "system") + end +end diff --git a/db/post_migrate/20170607121233_convert_custom_notification_settings_to_columns.rb b/db/post_migrate/20170607121233_convert_custom_notification_settings_to_columns.rb new file mode 100644 index 00000000000..9abda6a1d73 --- /dev/null +++ b/db/post_migrate/20170607121233_convert_custom_notification_settings_to_columns.rb @@ -0,0 +1,55 @@ +class ConvertCustomNotificationSettingsToColumns < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + class NotificationSetting < ActiveRecord::Base + self.table_name = 'notification_settings' + + store :events, coder: JSON + end + + EMAIL_EVENTS = [ + :new_note, + :new_issue, + :reopen_issue, + :close_issue, + :reassign_issue, + :new_merge_request, + :reopen_merge_request, + :close_merge_request, + :reassign_merge_request, + :merge_merge_request, + :failed_pipeline, + :success_pipeline + ] + + # We only need to migrate (up or down) rows where at least one of these + # settings is set. + def up + NotificationSetting.where("events LIKE '%true%'").find_each do |notification_setting| + EMAIL_EVENTS.each do |event| + notification_setting[event] = notification_setting.events[event] + end + + notification_setting[:events] = nil + notification_setting.save! + end + end + + def down + NotificationSetting.where(EMAIL_EVENTS.join(' OR ')).find_each do |notification_setting| + events = {} + + EMAIL_EVENTS.each do |event| + events[event] = !!notification_setting.public_send(event) + notification_setting[event] = nil + end + + notification_setting[:events] = events + notification_setting.save! + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 0496ce2ced3..956ca2278f4 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: 20170525174156) do +ActiveRecord::Schema.define(version: 20170607121233) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -123,6 +123,9 @@ ActiveRecord::Schema.define(version: 20170525174156) do t.integer "cached_markdown_version" t.boolean "clientside_sentry_enabled", default: false, null: false t.string "clientside_sentry_dsn" + t.boolean "prometheus_metrics_enabled", default: false, null: false + t.boolean "help_page_hide_commercial_content", default: false + t.string "help_page_support_url" end create_table "audit_events", force: :cascade do |t| @@ -233,6 +236,7 @@ ActiveRecord::Schema.define(version: 20170525174156) do t.string "coverage_regex" t.integer "auto_canceled_by_id" t.boolean "retried" + t.integer "stage_id" end add_index "ci_builds", ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id", using: :btree @@ -242,6 +246,7 @@ ActiveRecord::Schema.define(version: 20170525174156) do add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree + add_index "ci_builds", ["stage_id"], name: "index_ci_builds_on_stage_id", using: :btree add_index "ci_builds", ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree add_index "ci_builds", ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree @@ -326,6 +331,18 @@ ActiveRecord::Schema.define(version: 20170525174156) do add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree + create_table "ci_stages", force: :cascade do |t| + t.integer "project_id" + t.integer "pipeline_id" + t.datetime "created_at" + t.datetime "updated_at" + t.string "name" + end + + add_index "ci_stages", ["pipeline_id", "name"], name: "index_ci_stages_on_pipeline_id_and_name", using: :btree + add_index "ci_stages", ["pipeline_id"], name: "index_ci_stages_on_pipeline_id", using: :btree + add_index "ci_stages", ["project_id"], name: "index_ci_stages_on_project_id", using: :btree + create_table "ci_trigger_requests", force: :cascade do |t| t.integer "trigger_id", null: false t.text "variables" @@ -861,6 +878,18 @@ ActiveRecord::Schema.define(version: 20170525174156) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.text "events" + t.boolean "new_note" + t.boolean "new_issue" + t.boolean "reopen_issue" + t.boolean "close_issue" + t.boolean "reassign_issue" + t.boolean "new_merge_request" + t.boolean "reopen_merge_request" + t.boolean "close_merge_request" + t.boolean "reassign_merge_request" + t.boolean "merge_merge_request" + t.boolean "failed_pipeline" + t.boolean "success_pipeline" end add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree @@ -1198,6 +1227,8 @@ ActiveRecord::Schema.define(version: 20170525174156) do t.text "title_html" t.text "content_html" t.integer "cached_markdown_version" + t.text "description" + t.text "description_html" end add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree @@ -1398,7 +1429,6 @@ ActiveRecord::Schema.define(version: 20170525174156) do t.boolean "hide_project_limit", default: false t.string "unlock_token" t.datetime "otp_grace_period_started_at" - t.boolean "ldap_email", default: false, null: false t.boolean "external", default: false t.string "incoming_email_token" t.string "organization" @@ -1409,6 +1439,8 @@ ActiveRecord::Schema.define(version: 20170525174156) do t.boolean "notified_of_own_activity" t.string "preferred_language" t.string "rss_token" + t.boolean "external_email", default: false, null: false + t.string "email_provider" end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree @@ -1481,10 +1513,13 @@ ActiveRecord::Schema.define(version: 20170525174156) do add_foreign_key "boards", "projects" add_foreign_key "chat_teams", "namespaces", on_delete: :cascade add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify + add_foreign_key "ci_builds", "ci_stages", column: "stage_id", name: "fk_3a9eaa254d", on_delete: :cascade add_foreign_key "ci_pipeline_schedules", "projects", name: "fk_8ead60fcc4", on_delete: :cascade add_foreign_key "ci_pipeline_schedules", "users", column: "owner_id", name: "fk_9ea99f58d2", on_delete: :nullify add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify add_foreign_key "ci_pipelines", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_262d4c2d19", on_delete: :nullify + add_foreign_key "ci_stages", "ci_pipelines", column: "pipeline_id", name: "fk_fb57e6cc56", on_delete: :cascade + add_foreign_key "ci_stages", "projects", name: "fk_2360681d1d", on_delete: :cascade add_foreign_key "ci_trigger_requests", "ci_triggers", column: "trigger_id", name: "fk_b8ec8b7245", on_delete: :cascade add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade add_foreign_key "ci_variables", "projects", name: "fk_ada5eb64b3", on_delete: :cascade diff --git a/doc/README.md b/doc/README.md index 9f12eed1471..775fffab2dd 100644 --- a/doc/README.md +++ b/doc/README.md @@ -59,6 +59,7 @@ Manage files and branches from the UI (user interface): - Branches - [Create a branch](user/project/repository/web_editor.md#create-a-new-branch) - [Protected branches](user/project/protected_branches.md#protected-branches) + - [Delete merged branches](user/project/repository/branches/index.md#delete-merged-branches) ### Issues and Merge Requests (MRs) diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md index b6676026d06..9bcd13a52f7 100644 --- a/doc/administration/environment_variables.md +++ b/doc/administration/environment_variables.md @@ -13,6 +13,7 @@ override certain values. Variable | Type | Description -------- | ---- | ----------- +`GITLAB_CDN_HOST` | string | Sets the hostname for a CDN to serve static assets (e.g. `mycdnsubdomain.fictional-cdn.com`) `GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation `GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`) `RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test` @@ -58,6 +59,9 @@ to the naming scheme `GITLAB_#{name in 1_settings.rb in upper case}`. ## Omnibus configuration +To set environment variables, follow [these +instructions](https://docs.gitlab.com/omnibus/settings/environment-variables.html). + It's possible to preconfigure the GitLab docker image by adding the environment variable `GITLAB_OMNIBUS_CONFIG` to the `docker run` command. For more information see the ['preconfigure-docker-container' section in the Omnibus documentation](http://docs.gitlab.com/omnibus/docker/#preconfigure-docker-container). diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md index 7b0610ae414..5599435564e 100644 --- a/doc/administration/job_artifacts.md +++ b/doc/administration/job_artifacts.md @@ -82,6 +82,42 @@ _The artifacts are stored by default in 1. Save the file and [restart GitLab][] for the changes to take effect. +## Expiring artifacts + +If an expiry date is used for the artifacts, they are marked for deletion +right after that date passes. Artifacts are cleaned up by the +`expire_build_artifacts_worker` cron job which is run by Sidekiq every hour at +50 minutes (`50 * * * *`). + +To change the default schedule on which the artifacts are expired, follow the +steps below. + +--- + +**In Omnibus installations:** + +1. Edit `/etc/gitlab/gitlab.rb` and comment out or add the following line + + ```ruby + gitlab_rails['expire_build_artifacts_worker_cron'] = "50 * * * *" + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +--- + +**In installations from source:** + +1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following + lines: + + ```yaml + expire_build_artifacts_worker: + cron: "50 * * * *" + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. + ## Set the maximum file size of the artifacts Provided the artifacts are enabled, you can change the maximum file size of the diff --git a/doc/administration/raketasks/github_import.md b/doc/administration/raketasks/github_import.md index affb4d17861..04c70c3644e 100644 --- a/doc/administration/raketasks/github_import.md +++ b/doc/administration/raketasks/github_import.md @@ -3,7 +3,7 @@ >**Note:** > > - [Introduced][ce-10308] in GitLab 9.1. -> - You need a personal access token in order to retrieve and import GitHub +> - You need a personal access token in order to retrieve and import GitHub > projects. You can get it from: https://github.com/settings/tokens > - You also need to pass an username as the second argument to the rake task > which will become the owner of the project. @@ -19,7 +19,7 @@ bundle exec rake import:github[access_token,root,foo/bar] RAILS_ENV=production ``` In this case, `access_token` is your GitHub personal access token, `root` -is your GitLab username, and `foo/bar` is the new GitLab namespace/project that +is your GitLab username, and `foo/bar` is the new GitLab namespace/project that will get created from your GitHub project. Subgroups are also possible: `foo/foo/bar`. diff --git a/doc/api/README.md b/doc/api/README.md index e1d4009dedc..4f189c16673 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -55,19 +55,35 @@ following locations: - [V3 to V4](v3_to_v4.md) - [Version](version.md) -### Internal CI API - The following documentation is for the [internal CI API](ci/README.md): - [Builds](ci/builds.md) - [Runners](ci/runners.md) +## Road to GraphQL + +Going forward, we will start on moving to +[GraphQL](http://graphql.org/learn/best-practices/) and deprecate the use of +controller-specific endpoints. GraphQL has a number of benefits: + +1. We avoid having to maintain two different APIs. +2. Callers of the API can request only what they need. +3. It is versioned by default. + +It will co-exist with the current v4 REST API. If we have a v5 API, this should +be a compatibility layer on top of GraphQL. + ## Authentication -Most API requests require authentication via a session cookie or token. For those cases where it is not required, this will be mentioned in the documentation -for each individual endpoint. For example, the [`/projects/:id` endpoint](projects.md). -There are three types of tokens available: private tokens, OAuth 2 tokens, and personal -access tokens. +Most API requests require authentication via a session cookie or token. For +those cases where it is not required, this will be mentioned in the documentation +for each individual endpoint. For example, the [`/projects/:id` endpoint](projects.md). + +There are three types of access tokens available: + +1. [OAuth2 tokens](#oauth2-tokens) +1. [Private tokens](#private-tokens) +1. [Personal access tokens](#personal-access-tokens) If authentication information is invalid or omitted, an error message will be returned with status code `401`: @@ -78,20 +94,13 @@ returned with status code `401`: } ``` -### Session Cookie +### Session cookie When signing in to GitLab as an ordinary user, a `_gitlab_session` cookie is set. The API will use this cookie for authentication if it is present, but using the API to generate a new session cookie is currently not supported. -### Private Tokens - -You need to pass a `private_token` parameter via query string or header. If passed as a -header, the header name must be `PRIVATE-TOKEN` (uppercase and with a dash instead of -an underscore). You can find or reset your private token in your account page -(`/profile/account`). - -### OAuth 2 Tokens +### OAuth2 tokens You can use an OAuth 2 token to authenticate with the API by passing it either in the `access_token` parameter or in the `Authorization` header. @@ -104,30 +113,31 @@ curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api Read more about [GitLab as an OAuth2 client](oauth2.md). -### Personal Access Tokens +### Private tokens -> [Introduced][ce-3749] in GitLab 8.8. +Private tokens provide full access to the GitLab API. Anyone with access to +them can interact with GitLab as if they were you. You can find or reset your +private token in your account page (`/profile/account`). -You can create as many personal access tokens as you like from your GitLab -profile (`/profile/personal_access_tokens`); perhaps one for each application -that needs access to the GitLab API. +For examples of usage, [read the basic usage section](#basic-usage). -Once you have your token, pass it to the API using either the `private_token` -parameter or the `PRIVATE-TOKEN` header. +### Personal access tokens -> [Introduced][ce-5951] in GitLab 8.15. +Instead of using your private token which grants full access to your account, +personal access tokens could be a better fit because of their granular +permissions. -Personal Access Tokens can be created with one or more scopes that allow various actions -that a given token can perform. Although there are only two scopes available at the -moment – `read_user` and `api` – the groundwork has been laid to add more scopes easily. +Once you have your token, pass it to the API using either the `private_token` +parameter or the `PRIVATE-TOKEN` header. For examples of usage, +[read the basic usage section](#basic-usage). -At any time you can revoke any personal access token by just clicking **Revoke**. +[Read more about personal access tokens.][pat] ### Impersonation tokens > [Introduced][ce-9099] in GitLab 9.0. Needs admin permissions. -Impersonation tokens are a type of [Personal Access Token](#personal-access-tokens) +Impersonation tokens are a type of [personal access token][pat] that can only be created by an admin for a specific user. They are a better alternative to using the user's password/private token @@ -136,9 +146,11 @@ or private token, since the password/token can change over time. Impersonation tokens are a great fit if you want to build applications or tools which authenticate with the API as a specific user. -For more information about the usage please refer to the +For more information, refer to the [users API](users.md#retrieve-user-impersonation-tokens) docs. +For examples of usage, [read the basic usage section](#basic-usage). + ### Sudo > Needs admin permissions. @@ -191,11 +203,16 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23 curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https://gitlab.example.com/api/v4/projects" ``` -## Basic Usage +## Basic usage API requests should be prefixed with `api` and the API version. The API version is defined in [`lib/api.rb`][lib-api-url]. +For endpoints that require [authentication](#authentication), you need to pass +a `private_token` parameter via query string or header. If passed as a header, +the header name must be `PRIVATE-TOKEN` (uppercase and with a dash instead of +an underscore). + Example of a valid API request: ``` @@ -208,6 +225,12 @@ Example of a valid API request using cURL and authentication via header: curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects" ``` +Example of a valid API request using cURL and authentication via a query string: + +```shell +curl "https://gitlab.example.com/api/v4/projects?private_token=9koXpg98eAheJpvBs5tK" +``` + The API uses JSON to serialize data. You don't need to specify `.json` at the end of an API URL. @@ -423,3 +446,4 @@ programming languages. Visit the [GitLab website] for a complete list. [ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749 [ce-5951]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951 [ce-9099]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9099 +[pat]: ../user/profile/personal_access_tokens.md diff --git a/doc/api/branches.md b/doc/api/branches.md index 325d0ea4ce3..dfaa7d6fab7 100644 --- a/doc/api/branches.md +++ b/doc/api/branches.md @@ -251,6 +251,8 @@ curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gi Will delete all branches that are merged into the project's default branch. +Protected branches will not be deleted as part of this operation. + ``` DELETE /projects/:id/repository/merged_branches ``` diff --git a/doc/api/commits.md b/doc/api/commits.md index 9cb58dd3ae9..c91f9ecbdaf 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -97,7 +97,7 @@ PAYLOAD=$(cat << 'JSON' }, { "action": "delete", - "file_path": "foo/bar2", + "file_path": "foo/bar2" }, { "action": "move", diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index 46fe64d382e..07cb64cb373 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -134,4 +134,4 @@ access_token = client.password.get_token('user@example.com', 'secret') puts access_token.token ``` -[personal access tokens]: ./README.md#personal-access-tokens
\ No newline at end of file +[personal access tokens]: ../user/profile/personal_access_tokens.md diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md index ff379473961..92491de4daa 100644 --- a/doc/api/project_snippets.md +++ b/doc/api/project_snippets.md @@ -43,6 +43,7 @@ Parameters: "id": 1, "title": "test", "file_name": "add.rb", + "description": "Ruby test snippet", "author": { "id": 1, "username": "john_smith", @@ -70,6 +71,7 @@ Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user - `title` (required) - The title of a snippet - `file_name` (required) - The name of a snippet file +- `description` (optional) - The description of a snippet - `code` (required) - The content of a snippet - `visibility` (required) - The snippet's visibility @@ -87,6 +89,7 @@ Parameters: - `snippet_id` (required) - The ID of a project's snippet - `title` (optional) - The title of a snippet - `file_name` (optional) - The name of a snippet file +- `description` (optional) - The description of a snippet - `code` (optional) - The content of a snippet - `visibility` (optional) - The snippet's visibility diff --git a/doc/api/projects.md b/doc/api/projects.md index 0debdcfae89..58f18105e21 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -2,10 +2,10 @@ ### Project visibility level -Project in GitLab has be either private, internal or public. -You can determine it by `visibility` field in project. +Project in GitLab can be either private, internal or public. +This is determined by the `visibility` field in the project. -Constants for project visibility levels are next: +Values for the project visibility level are: * `private`: Project access must be granted explicitly for each user. @@ -18,7 +18,7 @@ Constants for project visibility levels are next: ## List projects -Get a list of visible projects for authenticated user. When being accessed without authentication, all public projects are returned. +Get a list of visible projects for authenticated user. When accessed without authentication, only public projects are returned. ``` GET /projects @@ -343,6 +343,7 @@ Parameters: | `lfs_enabled` | boolean | no | Enable LFS | | `request_access_enabled` | boolean | no | Allow users to request member access | | `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project | +| `avatar` | mixed | no | Image file for avatar of the project | ### Create project for user @@ -377,6 +378,7 @@ Parameters: | `lfs_enabled` | boolean | no | Enable LFS | | `request_access_enabled` | boolean | no | Allow users to request member access | | `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project | +| `avatar` | mixed | no | Image file for avatar of the project | ### Edit project @@ -410,11 +412,14 @@ Parameters: | `lfs_enabled` | boolean | no | Enable LFS | | `request_access_enabled` | boolean | no | Allow users to request member access | | `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project | +| `avatar` | mixed | no | Image file for avatar of the project | ### Fork project Forks a project into the user namespace of the authenticated user or the one provided. +The forking operation for a project is asynchronous and is completed in a background job. The request will return immediately. To determine whether the fork of the project has completed, query the `import_status` for the new project. + ``` POST /projects/:id/fork ``` diff --git a/doc/api/session.md b/doc/api/session.md index 7dd504b67c5..f79eac11689 100644 --- a/doc/api/session.md +++ b/doc/api/session.md @@ -1,11 +1,9 @@ # Session API -## Deprecation Notice - -1. Starting in GitLab 8.11, this feature has been *disabled* for users with two-factor authentication turned on. -2. These users can access the API using [personal access tokens] instead. - ---- +>**Deprecation notice:** +Starting in GitLab 8.11, this feature has been **disabled** for users with +[two-factor authentication][2fa] turned on. These users can access the API +using [personal access tokens] instead. You can login with both GitLab and LDAP credentials in order to obtain the private token. @@ -52,4 +50,5 @@ Example response: } ``` -[personal access tokens]: ./README.md#personal-access-tokens +[2fa]: ../user/profile/account/two_factor_authentication.md +[personal access tokens]: ../user/profile/personal_access_tokens.md diff --git a/doc/api/snippets.md b/doc/api/snippets.md index fb8cf97896c..efaab712367 100644 --- a/doc/api/snippets.md +++ b/doc/api/snippets.md @@ -48,6 +48,7 @@ Example response: "id": 1, "title": "test", "file_name": "add.rb", + "description": "Ruby test snippet", "author": { "id": 1, "username": "john_smith", @@ -73,16 +74,17 @@ POST /snippets Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `title` | String | yes | The title of a snippet | -| `file_name` | String | yes | The name of a snippet file | -| `content` | String | yes | The content of a snippet | -| `visibility` | String | yes | The snippet's visibility | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `title` | String | yes | The title of a snippet | +| `file_name` | String | yes | The name of a snippet file | +| `content` | String | yes | The content of a snippet | +| `description` | String | no | The description of a snippet | +| `visibility` | String | no | The snippet's visibility | ``` bash -curl --request POST --data '{"title": "This is a snippet", "content": "Hello world", "file_name": "test.txt", "visibility": "internal" }' --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/snippets +curl --request POST --data '{"title": "This is a snippet", "content": "Hello world", "description": "Hello World snippet", "file_name": "test.txt", "visibility": "internal" }' --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/snippets ``` Example response: @@ -92,6 +94,7 @@ Example response: "id": 1, "title": "This is a snippet", "file_name": "test.txt", + "description": "Hello World snippet", "author": { "id": 1, "username": "john_smith", @@ -117,13 +120,14 @@ PUT /snippets/:id Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | Integer | yes | The ID of a snippet | -| `title` | String | no | The title of a snippet | -| `file_name` | String | no | The name of a snippet file | -| `content` | String | no | The content of a snippet | -| `visibility` | String | no | The snippet's visibility | +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | Integer | yes | The ID of a snippet | +| `title` | String | no | The title of a snippet | +| `file_name` | String | no | The name of a snippet file | +| `description` | String | no | The description of a snippet | +| `content` | String | no | The content of a snippet | +| `visibility` | String | no | The snippet's visibility | ``` bash @@ -137,6 +141,7 @@ Example response: "id": 1, "title": "test", "file_name": "add.rb", + "description": "description of snippet", "author": { "id": 1, "username": "john_smith", diff --git a/doc/api/users.md b/doc/api/users.md index f4167ba2605..91ce4f6dac3 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -804,7 +804,7 @@ Example response: It creates a new impersonation token. Note that only administrators can do this. You are only able to create impersonation tokens to impersonate the user and perform -both API calls and Git reads and writes. The user will not see these tokens in his profile +both API calls and Git reads and writes. The user will not see these tokens in their profile settings page. ``` diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index 408d46a756c..f7c2a0ef0ca 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -282,9 +282,9 @@ which can be avoided if a different driver is used, for example `overlay`. > **Notes:** - This feature requires GitLab 8.8 and GitLab Runner 1.2. -- Starting from GitLab 8.12, if you have 2FA enabled in your account, you need - to pass a personal access token instead of your password in order to login to - GitLab's Container Registry. +- Starting from GitLab 8.12, if you have [2FA] enabled in your account, you need + to pass a [personal access token][pat] instead of your password in order to + login to GitLab's Container Registry. Once you've built a Docker image, you can push it up to the built-in [GitLab Container Registry](../../user/project/container_registry.md). For example, @@ -409,3 +409,5 @@ Some things you should be aware of when using the Container Registry: [docker-in-docker]: https://blog.docker.com/2013/09/docker-can-now-run-within-docker/ [docker-cap]: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities +[2fa]: ../../user/profile/account/two_factor_authentication.md +[pat]: ../../user/profile/personal_access_tokens.md diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 96834e15bb9..be4dea55c20 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -140,21 +140,58 @@ that runner. ## Define an image from a private Docker registry -Starting with GitLab Runner 0.6.0, you are able to define images located to -private registries that could also require authentication. - -All you have to do is be explicit on the image definition in `.gitlab-ci.yml`. - -```yaml -image: my.registry.tld:5000/namespace/image:tag -``` - -In the example above, GitLab Runner will look at `my.registry.tld:5000` for the -image `namespace/image:tag`. - -If the repository is private you need to authenticate your GitLab Runner in the -registry. Learn how to do that on -[GitLab Runner's documentation][runner-priv-reg]. +> **Notes:** +- This feature requires GitLab Runner **1.8** or higher +- For GitLab Runner versions **>= 0.6, <1.8** there was a partial + support for using private registries, which required manual configuration + of credentials on runner's host. We recommend to upgrade your Runner to + at least version **1.8** if you want to use private registries. +- If the repository is private you need to authenticate your GitLab Runner in the + registry. Learn more about how [GitLab Runner works in this case][runner-priv-reg]. + +As an example, let's assume that you want to use the `registry.example.com/private/image:latest` +image which is private and requires you to login into a private container registry. +To configure access for `registry.example.com`, follow these steps: + +1. Do a `docker login` on your computer: + + ```bash + docker login registry.example.com --username my_username --password my_password + ``` + +1. Copy the content of `~/.docker/config.json` +1. Create a [secret variable] `DOCKER_AUTH_CONFIG` with the content of the + Docker configuration file as the value: + + ```json + { + "auths": { + "registry.example.com": { + "auth": "bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ=" + } + } + } + ``` + +1. Do a `docker logout` on your computer if you don't need access to the + registry from it: + + ```bash + docker logout registry.example.com + ``` + +1. You can now use any private image from `registry.example.com` defined in + `image` and/or `services` in your [`.gitlab-ci.yml` file][yaml-priv-reg]: + + ```yaml + image: my.registry.tld:5000/namespace/image:tag + ``` + + In the example above, GitLab Runner will look at `my.registry.tld:5000` for the + image `namespace/image:tag`. + +You can add configuration for as many registries as you want, adding more +registries to the `"auths"` hash as described above. ## Accessing the services @@ -173,6 +210,18 @@ When the job is run, `tutum/wordpress` will be started and you will have access to it from your build container under the hostnames `tutum-wordpress` (requires GitLab Runner v1.1.0 or newer) and `tutum__wordpress`. +When using a private registry, the image name also includes a hostname and port +of the registry. + +```yaml +services: +- docker.example.com:5000/wordpress:latest +``` + +The service hostname will also include the registry hostname. Service will be +available under hostnames `docker.example.com-wordpress` (requires GitLab Runner v1.1.0 or newer) +and `docker.example.com__wordpress`. + *Note: hostname with underscores is not RFC valid and may cause problems in 3rd party applications.* The alias hostnames for the service are made from the image name following these @@ -283,4 +332,5 @@ creation. [tutum/wordpress]: https://hub.docker.com/r/tutum/wordpress/ [postgres-hub]: https://hub.docker.com/r/_/postgres/ [mysql-hub]: https://hub.docker.com/r/_/mysql/ -[runner-priv-reg]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry +[runner-priv-reg]: http://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-container-registry +[secret variable]: ../variables/README.md#secret-variables diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md index a047e809788..5659a8c2a2a 100644 --- a/doc/ci/examples/code_climate.md +++ b/doc/ci/examples/code_climate.md @@ -27,7 +27,7 @@ download and analyze the report artifact in JSON format. For GitLab [Enterprise Edition Starter][ee] users, this information can be automatically extracted and shown right in the merge request widget. [Learn more on code quality -diffs in merge requests](http://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.md). +diffs in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html). [cli]: https://github.com/codeclimate/codeclimate [dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index 1bd1ee93ac5..76d746155eb 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -1,124 +1,168 @@ # Runners -In GitLab CI, Runners run your [yaml](../yaml/README.md). -A Runner is an isolated (virtual) machine that picks up jobs -through the coordinator API of GitLab CI. +In GitLab CI, Runners run the code defined in [`.gitlab-ci.yml`](../yaml/README.md). +They are isolated (virtual) machines that pick up jobs through the coordinator +API of GitLab CI. A Runner can be specific to a certain project or serve any project in GitLab CI. A Runner that serves all projects is called a shared Runner. -Ideally, GitLab Runner should not be installed on the same machine as GitLab. +Ideally, the GitLab Runner should not be installed on the same machine as GitLab. Read the [requirements documentation](../../install/requirements.md#gitlab-runner) for more information. -## Shared vs. Specific Runners - -A Runner that is specific only runs for the specified project. A shared Runner -can run jobs for every project that has enabled the option **Allow shared Runners**. - -**Shared Runners** are useful for jobs that have similar requirements, -between multiple projects. Rather than having multiple Runners idling for -many projects, you can have a single or a small number of Runners that handle -multiple projects. This makes it easier to maintain and update Runners. - -**Specific Runners** are useful for jobs that have special requirements or for -projects with a specific demand. If a job has certain requirements, you can set -up the specific Runner with this in mind, while not having to do this for all -Runners. For example, if you want to deploy a certain project, you can setup -a specific Runner to have the right credentials for this. - -Projects with high demand of CI activity can also benefit from using specific Runners. -By having dedicated Runners you are guaranteed that the Runner is not being held -up by another project's jobs. +## Shared vs specific Runners + +After [installing the Runner][install], you can either register it as shared or +specific. You can only register a shared Runner if you have admin access to +the GitLab instance. The main differences between a shared and a specific Runner +are: + +- **Shared Runners** are useful for jobs that have similar requirements, + between multiple projects. Rather than having multiple Runners idling for + many projects, you can have a single or a small number of Runners that handle + multiple projects. This makes it easier to maintain and update them. + Shared Runners process jobs using a [fair usage queue](#how-shared-runners-pick-jobs). + In contrast to specific Runners that use a FIFO queue, this prevents + cases where projects create hundreds of jobs which can lead to eating all + available shared Runners resources. +- **Specific Runners** are useful for jobs that have special requirements or for + projects with a specific demand. If a job has certain requirements, you can set + up the specific Runner with this in mind, while not having to do this for all + Runners. For example, if you want to deploy a certain project, you can setup + a specific Runner to have the right credentials for this. The [usage of tags](#using-tags) + may be useful in this case. Specific Runners process jobs using a [FIFO] queue. + +A Runner that is specific only runs for the specified project(s). A shared Runner +can run jobs for every project that has enabled the option **Allow shared Runners** +under **Settings ➔ Pipelines**. + +Projects with high demand of CI activity can also benefit from using specific +Runners. By having dedicated Runners you are guaranteed that the Runner is not +being held up by another project's jobs. You can set up a specific Runner to be used by multiple projects. The difference with a shared Runner is that you have to enable each project explicitly for the Runner to be able to run its jobs. Specific Runners do not get shared with forked projects automatically. -A fork does copy the CI settings (jobs, allow shared, etc) of the cloned repository. - -# Creating and Registering a Runner - -There are several ways to create a Runner. Only after creation, upon -registration its status as Shared or Specific is determined. - -[See the documentation for](https://docs.gitlab.com/runner/install) -the different methods of installing a Runner instance. +A fork does copy the CI settings (jobs, allow shared, etc) of the cloned +repository. -After installing the Runner, you can either register it as `Shared` or as `Specific`. -You can only register a Shared Runner if you have admin access to the GitLab instance. +## Registering a shared Runner -## Registering a Shared Runner +You can only register a shared Runner if you are an admin of the GitLab instance. -You can only register a shared Runner if you are an admin on the linked -GitLab instance. +1. Grab the shared-Runner token on the `admin/runners` page -Grab the shared-Runner token on the `admin/runners` page of your GitLab CI -instance. + ![Shared Runners admin area](img/shared_runners_admin.png) -![shared token](shared_runner.png) +1. [Register the Runner][register] -Now simply register the Runner as any Runner: +Shared Runners are enabled by default as of GitLab 8.2, but can be disabled +with the **Disable shared Runners** button which is present under each project's +**Settings ➔ Pipelines** page. Previous versions of GitLab defaulted shared +Runners to disabled. -``` -sudo gitlab-ci-multi-runner register -``` - -Shared Runners are enabled by default as of GitLab 8.2, but can be disabled with the -`DISABLE SHARED RUNNERS` button. Previous versions of GitLab defaulted shared Runners to -disabled. - -## Registering a Specific Runner +## Registering a specific Runner Registering a specific can be done in two ways: 1. Creating a Runner with the project registration token 1. Converting a shared Runner into a specific Runner (one-way, admin only) -There are several ways to create a Runner instance. The steps below only -concern registering the Runner on GitLab CI. - -### Registering a Specific Runner with a Project Registration token +### Registering a specific Runner with a project registration token To create a specific Runner without having admin rights to the GitLab instance, -visit the project you want to make the Runner work for in GitLab CI. +visit the project you want to make the Runner work for in GitLab: -Click on the Runner tab and use the registration token you find there to -setup a specific Runner for this project. +1. Go to **Settings ➔ Pipelines** to obtain the token +1. [Register the Runner][register] -![project Runners in GitLab CI](project_specific.png) +### Making an existing shared Runner specific -To register the Runner, run the command below and follow instructions: +If you are an admin on your GitLab instance, you can turn any shared Runner into +a specific one, but not the other way around. Keep in mind that this is a one +way transition. -``` -sudo gitlab-ci-multi-runner register -``` +1. Go to the Runners in the admin area **Overview ➔ Runners** (`/admin/runners`) + and find your Runner +1. Enable any projects under **Restrict projects for this Runner** to be used + with the Runner -### Lock a specific Runner from being enabled for other projects +From now on, the shared Runner will be specific to those projects. + +## Locking a specific Runner from being enabled for other projects You can configure a Runner to assign it exclusively to a project. When a Runner is locked this way, it can no longer be enabled for other projects. -This setting is available on each Runner in *Project Settings* > *Runners*. +This setting can be enabled the first time you [register a Runner][register] and +can be changed afterwards under each Runner's settings. + +To lock/unlock a Runner: + +1. Visit your project's **Settings ➔ Pipelines** +1. Find the Runner you wish to lock/unlock and make sure it's enabled +1. Click the pencil button +1. Check the **Lock to current projects** option +1. Click **Save changes** for the changes to take effect -### Making an existing Shared Runner Specific +## How shared Runners pick jobs -If you are an admin on your GitLab instance, -you can make any shared Runner a specific Runner, _but you can not -make a specific Runner a shared Runner_. +Shared Runners abide to a process queue we call fair usage. The fair usage +algorithm tries to assign jobs to shared Runners from projects that have the +lowest number of jobs currently running on shared Runners. -To make a shared Runner specific, go to the Runner page (`/admin/runners`) -and find your Runner. Add any projects on the left to make this Runner -run exclusively for these projects, therefore making it a specific Runner. +**Example 1** -![making a shared Runner specific](shared_to_specific_admin.png) +We have following jobs in queue: -## Using Shared Runners Effectively +- Job 1 for Project 1 +- Job 2 for Project 1 +- Job 3 for Project 1 +- Job 4 for Project 2 +- Job 5 for Project 2 +- Job 6 for Project 3 + +With the fair usage algorithm jobs are assigned in following order: + +1. Job 1 is chosen first, because it has the lowest job number from projects with no running jobs (i.e. all projects) +1. Job 4 is next, because 4 is now the lowest job number from projects with no running jobs (Project 1 has a job running) +1. Job 6 is next, because 6 is now the lowest job number from projects with no running jobs (Projects 1 and 2 have jobs running) +1. Job 2 is next, because, of projects with the lowest number of jobs running (each has 1), it is the lowest job number +1. Job 5 is next, because Project 1 now has 2 jobs running, and between Projects 2 and 3, Job 5 is the lowest remaining job number +1. Lastly we choose Job 3... because it's the only job left + +--- + +**Example 2** + +We have following jobs in queue: + +- Job 1 for project 1 +- Job 2 for project 1 +- Job 3 for project 1 +- Job 4 for project 2 +- Job 5 for project 2 +- Job 6 for project 3 + +With the fair usage algorithm jobs are assigned in following order: + +1. Job 1 is chosen first, because it has the lowest job number from projects with no running jobs (i.e. all projects) +1. We finish job 1 +1. Job 2 is next, because, having finished Job 1, all projects have 0 jobs running again, and 2 is the lowest available job number +1. Job 4 is next, because with Project 1 running a job, 4 is the lowest number from projects running no jobs (Projects 2 and 3) +1. We finish job 4 +1. Job 5 is next, because having finished Job 4, Project 2 has no jobs running again +1. Job 6 is next, because Project 3 is the only project left with no running jobs +1. Lastly we choose Job 3... because, again, it's the only job left (who says 1 is the loneliest number?) + +## Using shared Runners effectively If you are planning to use shared Runners, there are several things you should keep in mind. -### Use Tags +### Using tags You must setup a Runner to be able to run all the different types of jobs that it may encounter on the projects it's shared over. This would be @@ -130,17 +174,27 @@ shared Runners will only run the jobs they are equipped to run. For instance, at GitLab we have Runners tagged with "rails" if they contain the appropriate dependencies to run Rails test suites. -### Prevent Runner with tags from picking jobs without tags +### Preventing Runners with tags from picking jobs without tags You can configure a Runner to prevent it from picking jobs with tags when -the Runner does not have tags assigned. This setting is available on each -Runner in *Project Settings* > *Runners*. +the Runner does not have tags assigned. This setting can be enabled the first +time you [register a Runner][register] and can be changed afterwards under +each Runner's settings. + +To make a Runner pick tagged/untagged jobs: + +1. Visit your project's **Settings ➔ Pipelines** +1. Find the Runner you wish and make sure it's enabled +1. Click the pencil button +1. Check the **Run untagged jobs** option +1. Click **Save changes** for the changes to take effect ### Be careful with sensitive information If you can run a job on a Runner, you can get access to any code it runs and get the token of the Runner. With shared Runners, this means that anyone -that runs jobs on the Runner, can access anyone else's code that runs on the Runner. +that runs jobs on the Runner, can access anyone else's code that runs on the +Runner. In addition, because you can get access to the Runner token, it is possible to create a clone of a Runner and submit false jobs, for example. @@ -160,3 +214,7 @@ project. Mentioned briefly earlier, but the following things of Runners can be exploited. We're always looking for contributions that can mitigate these [Security Considerations](https://docs.gitlab.com/runner/security/). + +[install]: http://docs.gitlab.com/runner/install/ +[fifo]: https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics) +[register]: http://docs.gitlab.com/runner/register/ diff --git a/doc/ci/runners/img/shared_runners_admin.png b/doc/ci/runners/img/shared_runners_admin.png Binary files differnew file mode 100644 index 00000000000..e049b339b36 --- /dev/null +++ b/doc/ci/runners/img/shared_runners_admin.png diff --git a/doc/ci/runners/project_specific.png b/doc/ci/runners/project_specific.png Binary files differdeleted file mode 100644 index c812defa67b..00000000000 --- a/doc/ci/runners/project_specific.png +++ /dev/null diff --git a/doc/ci/runners/shared_runner.png b/doc/ci/runners/shared_runner.png Binary files differdeleted file mode 100644 index 31574a17764..00000000000 --- a/doc/ci/runners/shared_runner.png +++ /dev/null diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index cb646827fb4..7ec7136d8c6 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -4,15 +4,22 @@ - [Introduced][ci-229] in GitLab CE 7.14. - GitLab 8.12 has a completely redesigned job permissions system. Read all about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#job-triggers). -- GitLab 9.0 introduced a trigger ownership to solve permission problems. -Triggers can be used to force a rebuild of a specific `ref` (branch or tag) -with an API call. +Triggers can be used to force a pipeline rerun of a specific `ref` (branch or +tag) with an API call. -## Add a trigger +## Authentication tokens + +The following methods of authentication are supported. + +### Trigger token + +A unique trigger token can be obtained when [adding a new trigger](#adding-a-new-trigger). + +## Adding a new trigger You can add a new trigger by going to your project's -**Settings ➔ Pipelines ➔ Triggers**. The **Add trigger** button will +**Settings ➔ Pipelines** under **Triggers**. The **Add trigger** button will create a new token which you can then use to trigger a rerun of this particular project's pipeline. @@ -22,7 +29,10 @@ overview of the time the triggers were last used. ![Triggers page overview](img/triggers_page.png) -## Take ownership +## Taking ownership of a trigger + +> **Note**: +GitLab 9.0 introduced a trigger ownership to solve permission problems. Each created trigger when run will impersonate their associated user including their access to projects and their project permissions. @@ -30,26 +40,20 @@ their access to projects and their project permissions. You can take ownership of existing triggers by clicking *Take ownership*. From now on the trigger will be run as you. -## Legacy triggers - -Old triggers, created before 9.0 will be marked as Legacy. Triggers with -the legacy label do not have an associated user and only have access -to the current project. - -Legacy trigger are considered deprecated and will be removed -with one of the future versions of GitLab. - -## Revoke a trigger +## Revoking a trigger You can revoke a trigger any time by going at your project's -**Settings > Triggers** and hitting the **Revoke** button. The action is -irreversible. +**Settings ➔ Pipelines** under **Triggers** and hitting the **Revoke** button. +The action is irreversible. -## Trigger a pipeline +## Triggering a pipeline -> **Note**: -Valid refs are only the branches and tags. If you pass a commit SHA as a ref, -it will not trigger a job. +> **Notes**: +- Valid refs are only the branches and tags. If you pass a commit SHA as a ref, + it will not trigger a job. +- If your project is public, passing the token in plain text is probably not the + wisest idea, so you might want to use a + [secret variable](../variables/README.md#secret-variables) for that purpose. To trigger a job you need to send a `POST` request to GitLab's API endpoint: @@ -57,11 +61,11 @@ To trigger a job you need to send a `POST` request to GitLab's API endpoint: POST /projects/:id/trigger/pipeline ``` -The required parameters are the trigger's `token` and the Git `ref` on which -the trigger will be performed. Valid refs are the branch and the tag. The `:id` -of a project can be found by [querying the API](../../api/projects.md) -or by visiting the **Pipelines** settings page which provides -self-explanatory examples. +The required parameters are the [trigger's `token`](#authentication-tokens) +and the Git `ref` on which the trigger will be performed. Valid refs are the +branch and the tag. The `:id` of a project can be found by +[querying the API](../../api/projects.md) or by visiting the **Pipelines** +settings page which provides self-explanatory examples. When a rerun of a pipeline is triggered, the information is exposed in GitLab's UI under the **Jobs** page and the jobs are marked as triggered 'by API'. @@ -78,46 +82,7 @@ below. --- -See the [Examples](#examples) section for more details on how to actually -trigger a rebuild. - -## Trigger a pipeline from webhook - -> Introduced in GitLab 8.14. - -To trigger a job from webhook of another project you need to add the following -webhook url for Push and Tag push events: - -``` -https://gitlab.example.com/api/v4/projects/:id/ref/:ref/trigger/pipeline?token=TOKEN -``` - -> **Note**: -- `ref` should be passed as part of url in order to take precedence over `ref` - from webhook body that designates the branchref that fired the trigger in the source repository. -- `ref` should be url encoded if contains slashes. - -## Pass job variables to a trigger - -You can pass any number of arbitrary variables in the trigger API call and they -will be available in GitLab CI so that they can be used in your `.gitlab-ci.yml` -file. The parameter is of the form: - -``` -variables[key]=value -``` - -This information is also exposed in the UI. - -![Job variables in UI](img/trigger_variables.png) - ---- - -See the [Examples](#examples) section below for more details. - -## Examples - -Using cURL you can trigger a rebuild with minimal effort, for example: +By using cURL you can trigger a pipeline rerun with minimal effort, for example: ```bash curl --request POST \ @@ -135,8 +100,6 @@ curl --request POST \ "https://gitlab.example.com/api/v4/projects/9/trigger/pipeline?token=TOKEN&ref=master" ``` -### Triggering a pipeline within `.gitlab-ci.yml` - You can also benefit by using triggers in your `.gitlab-ci.yml`. Let's say that you have two projects, A and B, and you want to trigger a rebuild on the `master` branch of project B whenever a tag on project A is created. This is the job you @@ -156,14 +119,37 @@ Now, whenever a new tag is pushed on project A, the job will run and the `stage: deploy` ensures that this job will run only after all jobs with `stage: test` complete successfully. -_**Note:** If your project is public, passing the token in plain text is -probably not the wisest idea, so you might want to use a -[secure variable](../variables/README.md#user-defined-variables-secure-variables) -for that purpose._ +## Triggering a pipeline from a webhook -### Making use of trigger variables +> **Notes**: +- Introduced in GitLab 8.14. +- `ref` should be passed as part of the URL in order to take precedence over + `ref` from the webhook body that designates the branch ref that fired the + trigger in the source repository. +- `ref` should be URL-encoded if it contains slashes. -Using trigger variables can be proven useful for a variety of reasons. +To trigger a job from a webhook of another project you need to add the following +webhook URL for Push and Tag events (change the project ID, ref and token): + +``` +https://gitlab.example.com/api/v4/projects/9/ref/master/trigger/pipeline?token=TOKEN +``` + +## Making use of trigger variables + +You can pass any number of arbitrary variables in the trigger API call and they +will be available in GitLab CI so that they can be used in your `.gitlab-ci.yml` +file. The parameter is of the form: + +``` +variables[key]=value +``` + +This information is also exposed in the UI. + +![Job variables in UI](img/trigger_variables.png) + +Using trigger variables can be proven useful for a variety of reasons: * Identifiable jobs. Since the variable is exposed in the UI you can know why the rebuild was triggered if you pass a variable that explains the @@ -208,15 +194,7 @@ curl --request POST \ https://gitlab.example.com/api/v4/projects/9/trigger/pipeline ``` -### Using a webhook to trigger a pipeline - -You can add the following webhook to another project in order to trigger a job: - -``` -https://gitlab.example.com/api/v4/projects/9/ref/master/trigger/pipeline?token=TOKEN&variables[UPLOAD_TO_S3]=true -``` - -### Using cron to trigger nightly pipelines +## Using cron to trigger nightly pipelines >**Note:** The following behavior can also be achieved through GitLab's UI with @@ -230,4 +208,18 @@ branch of project with ID `9` every night at `00:30`: 30 0 * * * curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/pipeline ``` +## Legacy triggers + +Old triggers, created before GitLab 9.0 will be marked as legacy. + +Triggers with the legacy label do not have an associated user and only have +access to the current project. They are considered deprecated and will be +removed with one of the future versions of GitLab. You are advised to +[take ownership](#taking-ownership) of any legacy triggers. + +[ee-2017]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2017 [ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229 +[ee]: https://about.gitlab.com/gitlab-ee/ +[variables]: ../variables/README.md +[predef]: ../variables/README.md#predefined-variables-environment-variables +[registry]: ../../user/project/container_registry.md diff --git a/doc/ci/triggers/img/triggers_page.png b/doc/ci/triggers/img/triggers_page.png Binary files differindex eafd8519a23..7dc8f91cf7e 100644 --- a/doc/ci/triggers/img/triggers_page.png +++ b/doc/ci/triggers/img/triggers_page.png diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 76ba78ea7be..d1f9881e51b 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -431,3 +431,4 @@ export CI_REGISTRY_PASSWORD="longalfanumstring" [protected branches]: ../../user/project/protected_branches.md [protected tags]: ../../user/project/protected_tags.md [shellexecutors]: https://docs.gitlab.com/runner/executors/ +[eep]: https://about.gitlab.com/gitlab-ee/ "Available only in GitLab Enterprise Edition Premium" diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index fc813694ff2..8a0662db6fd 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -393,7 +393,8 @@ There are a few rules that apply to the usage of refs policy: * `only` and `except` are inclusive. If both `only` and `except` are defined in a job specification, the ref is filtered by `only` and `except`. * `only` and `except` allow the use of regular expressions. -* `only` and `except` allow the use of special keywords: `branches`, `tags`, and `triggers`. +* `only` and `except` allow the use of special keywords: +`api`, `branches`, `external`, `tags`, `pushes`, `schedules`, `triggers`, and `web` * `only` and `except` allow to specify a repository path to filter jobs for forks. @@ -411,7 +412,7 @@ job: ``` In this example, `job` will run only for refs that are tagged, or if a build is -explicitly requested via an API trigger. +explicitly requested via an API trigger or a [Pipeline Schedule](../../user/project/pipelines/schedules.md). ```yaml job: @@ -419,6 +420,7 @@ job: only: - tags - triggers + - schedules ``` The repository path can be used to have jobs executed only for the parent diff --git a/doc/development/README.md b/doc/development/README.md index af4131c4a8f..9496a87d84d 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -51,6 +51,9 @@ - [Post Deployment Migrations](post_deployment_migrations.md) - [Foreign Keys & Associations](foreign_keys.md) - [Serializing Data](serializing_data.md) +- [Polymorphic Associations](polymorphic_associations.md) +- [Single Table Inheritance](single_table_inheritance.md) +- [Background Migrations](background_migrations.md) ## i18n diff --git a/doc/development/background_migrations.md b/doc/development/background_migrations.md new file mode 100644 index 00000000000..0239e6b3163 --- /dev/null +++ b/doc/development/background_migrations.md @@ -0,0 +1,205 @@ +# Background Migrations + +Background migrations can be used to perform data migrations that would +otherwise take a very long time (hours, days, years, etc) to complete. For +example, you can use background migrations to migrate data so that instead of +storing data in a single JSON column the data is stored in a separate table. + +## When To Use Background Migrations + +In the vast majority of cases you will want to use a regular Rails migration +instead. Background migrations should _only_ be used when migrating _data_ in +tables that have so many rows this process would take hours when performed in a +regular Rails migration. + +Background migrations _may not_ be used to perform schema migrations, they +should only be used for data migrations. + +Some examples where background migrations can be useful: + +* Migrating events from one table to multiple separate tables. +* Populating one column based on JSON stored in another column. +* Migrating data that depends on the output of exernal services (e.g. an API). + +## Isolation + +Background migrations must be isolated and can not use application code (e.g. +models defined in `app/models`). Since these migrations can take a long time to +run it's possible for new versions to be deployed while they are still running. + +It's also possible for different migrations to be executed at the same time. +This means that different background migrations should not migrate data in a +way that would cause conflicts. + +## How It Works + +Background migrations are simple classes that define a `perform` method. A +Sidekiq worker will then execute such a class, passing any arguments to it. All +migration classes must be defined in the namespace +`Gitlab::BackgroundMigration`, the files should be placed in the directory +`lib/gitlab/background_migration/`. + +## Scheduling + +Scheduling a migration can be done in either a regular migration or a +post-deployment migration. To do so, simply use the following code while +replacing the class name and arguments with whatever values are necessary for +your migration: + +```ruby +BackgroundMigrationWorker.perform_async('BackgroundMigrationClassName', [arg1, arg2, ...]) +``` + +Usually it's better to schedule jobs in bulk, for this you can use +`BackgroundMigrationWorker.perform_bulk`: + +```ruby +BackgroundMigrationWorker.perform_bulk( + ['BackgroundMigrationClassName', [1]], + ['BackgroundMigrationClassName', [2]], + ... +) +``` + +You'll also need to make sure that newly created data is either migrated, or +saved in both the old and new version upon creation. For complex and time +consuming migrations it's best to schedule a background job using an +`after_create` hook so this doesn't affect response timings. The same applies to +updates. Removals in turn can be handled by simply defining foreign keys with +cascading deletes. + +## Cleaning Up + +Because background migrations can take a long time you can't immediately clean +things up after scheduling them. For example, you can't drop a column that's +used in the migration process as this would cause jobs to fail. This means that +you'll need to add a separate _post deployment_ migration in a future release +that finishes any remaining jobs before cleaning things up (e.g. removing a +column). + +As an example, say you want to migrate the data from column `foo` (containing a +big JSON blob) to column `bar` (containing a string). The process for this would +roughly be as follows: + +1. Release A: + 1. Create a migration class that perform the migration for a row with a given ID. + 1. Deploy the code for this release, this should include some code that will + schedule jobs for newly created data (e.g. using an `after_create` hook). + 1. Schedule jobs for all existing rows in a post-deployment migration. It's + possible some newly created rows may be scheduled twice so your migration + should take care of this. +1. Release B: + 1. Deploy code so that the application starts using the new column and stops + scheduling jobs for newly created data. + 1. In a post-deployment migration you'll need to ensure no jobs remain. To do + so you can use `Gitlab::BackgroundMigration.steal` to process any remaining + jobs before continueing. + 1. Remove the old column. + +## Example + +To explain all this, let's use the following example: the table `services` has a +field called `properties` which is stored in JSON. For all rows you want to +extract the `url` key from this JSON object and store it in the `services.url` +column. There are millions of services and parsing JSON is slow, thus you can't +do this in a regular migration. + +To do this using a background migration we'll start with defining our migration +class: + +```ruby +class Gitlab::BackgroundMigration::ExtractServicesUrl + class Service < ActiveRecord::Base + self.table_name = 'services' + end + + def perform(service_id) + # A row may be removed between scheduling and starting of a job, thus we + # need to make sure the data is still present before doing any work. + service = Service.select(:properties).find_by(id: service_id) + + return unless service + + begin + json = JSON.load(service.properties) + rescue JSON::ParserError + # If the JSON is invalid we don't want to keep the job around forever, + # instead we'll just leave the "url" field to whatever the default value + # is. + return + end + + service.update(url: json['url']) if json['url'] + end +end +``` + +Next we'll need to adjust our code so we schedule the above migration for newly +created and updated services. We can do this using something along the lines of +the following: + +```ruby +class Service < ActiveRecord::Base + after_commit :schedule_service_migration, on: :update + after_commit :schedule_service_migration, on: :create + + def schedule_service_migration + BackgroundMigrationWorker.perform_async('ExtractServicesUrl', [id]) + end +end +``` + +We're using `after_commit` here to ensure the Sidekiq job is not scheduled +before the transaction completes as doing so can lead to race conditions where +the changes are not yet visible to the worker. + +Next we'll need a post-deployment migration that schedules the migration for +existing data. Since we're dealing with a lot of rows we'll schedule jobs in +batches instead of doing this one by one: + +```ruby +class ScheduleExtractServicesUrl < ActiveRecord::Migration + disable_ddl_transaction! + + class Service < ActiveRecord::Base + self.table_name = 'services' + end + + def up + Service.select(:id).in_batches do |relation| + jobs = relation.pluck(:id).map do |id| + ['ExtractServicesUrl', [id]] + end + + BackgroundMigrationWorker.perform_bulk(jobs) + end + end + + def down + end +end +``` + +Once deployed our application will continue using the data as before but at the +same time will ensure that both existing and new data is migrated. + +In the next release we can remove the `after_commit` hooks and related code. We +will also need to add a post-deployment migration that consumes any remaining +jobs. Such a migration would look like this: + +```ruby +class ConsumeRemainingExtractServicesUrlJobs < ActiveRecord::Migration + disable_ddl_transaction! + + def up + Gitlab::BackgroundMigration.steal('ExtractServicesUrl') + end + + def down + end +end +``` + +This migration will then process any jobs for the ExtractServicesUrl migration +and continue once all jobs have been processed. Once done you can safely remove +the `services.properties` column. diff --git a/doc/development/fe_guide/testing.md b/doc/development/fe_guide/testing.md index 0ef9fc61a61..867c83f1e72 100644 --- a/doc/development/fe_guide/testing.md +++ b/doc/development/fe_guide/testing.md @@ -7,7 +7,7 @@ feature tests with Capybara for e2e (end-to-end) integration testing. Unit and feature tests need to be written for all new features. Most of the time, you should use rspec for your feature tests. There are cases where the behaviour you are testing is not worth the time spent running the full application, -for example, if you are testing styling, animation or small actions that don't involve the backend, +for example, if you are testing styling, animation, edge cases or small actions that don't involve the backend, you should write an integration test using Jasmine. ![Testing priority triangle](img/testing_triangle.png) diff --git a/doc/development/i18n_guide.md b/doc/development/i18n_guide.md index bfb0779fbfa..756535e28bc 100644 --- a/doc/development/i18n_guide.md +++ b/doc/development/i18n_guide.md @@ -127,6 +127,14 @@ New translations will be added with their default content and will be marked fuzzy. To use the translation, look for the `#, fuzzy` mention in `gitlab.edit.po` and remove it. +We need to make sure we remove the `fuzzy` translations before generating the +`locale/**/gitlab.po` file. When they aren't removed, the resulting `.po` will +be treated as a binary file which could overwrite translations that were merged +before the new translations. + +When we are just preparing a page to be translated, but not actually adding any +translations. There's no need to generate `.po` files. + Translations that aren't used in the source code anymore will be marked with `~#`; these can be removed to keep our translation files clutter-free. diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 77ba2a5fd87..161d2544169 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -122,7 +122,7 @@ limit can vary from installation to installation. As a result it's recommended you do not use more than 32 threads in a single migration. Usually 4-8 threads should be more than enough. -## Removing indices +## Removing indexes When removing an index make sure to use the method `remove_concurrent_index` instead of the regular `remove_index` method. The `remove_concurrent_index` method @@ -142,7 +142,7 @@ class MyMigration < ActiveRecord::Migration end ``` -## Adding indices +## Adding indexes If you need to add a unique index please keep in mind there is the possibility of existing duplicates being present in the database. This means that should @@ -222,6 +222,41 @@ add_column_with_default(:projects, :foo, :integer, default: 10, limit: 8) add_column(:projects, :foo, :integer, default: 10, limit: 8) ``` +## Timestamp column type + +By default, Rails uses the `timestamp` data type that stores timestamp data without timezone information. +The `timestamp` data type is used by calling either the `add_timestamps` or the `timestamps` method. +Also Rails converts the `:datetime` data type to the `timestamp` one. + +Example: + +```ruby +# timestamps +create_table :users do |t| + t.timestamps +end + +# add_timestamps +def up + add_timestamps :users +end + +# :datetime +def up + add_column :users, :last_sign_in, :datetime +end +``` + +Instead of using these methods one should use the following methods to store timestamps with timezones: + +* `add_timestamps_with_timezone` +* `timestamps_with_timezone` + +This ensures all timestamps have a time zone specified. This in turn means existing timestamps won't +suddenly use a different timezone when the system's timezone changes. It also makes it very clear which +timezone was used in the first place. + + ## Testing Make sure that your migration works with MySQL and PostgreSQL with data. An diff --git a/doc/development/polymorphic_associations.md b/doc/development/polymorphic_associations.md new file mode 100644 index 00000000000..d63b9fb115f --- /dev/null +++ b/doc/development/polymorphic_associations.md @@ -0,0 +1,146 @@ +# Polymorphic Associations + +**Summary:** always use separate tables instead of polymorphic associations. + +Rails makes it possible to define so called "polymorphic associations". This +usually works by adding two columns to a table: a target type column, and a +target id. For example, at the time of writing we have such a setup for +`members` with the following columns: + +* `source_type`: a string defining the model to use, can be either `Project` or + `Namespace`. +* `source_id`: the ID of the row to retrieve based on `source_type`. For + example, when `source_type` is `Project` then `source_id` will contain a + project ID. + +While such a setup may appear to be useful, it comes with many drawbacks; enough +that you should avoid this at all costs. + +## Space Wasted + +Because this setup relies on string values to determine the model to use it will +end up wasting a lot of space. For example, for `Project` and `Namespace` the +maximum size is 9 bytes, plus 1 extra byte for every string when using +PostgreSQL. While this may only be 10 bytes per row, given enough tables and +rows using such a setup we can end up wasting quite a bit of disk space and +memory (for any indexes). + +## Indexes + +Because our associations are broken up into two columns this may result in +requiring composite indexes for queries to be performed efficiently. While +composite indexes are not wrong at all, they can be tricky to set up as the +ordering of columns in these indexes is important to ensure optimal performance. + +## Consistency + +One really big problem with polymorphic associations is being unable to enforce +data consistency on the database level using foreign keys. For consistency to be +enforced on the database level one would have to write their own foreign key +logic to support polymorphic associations. + +Enforcing consistency on the database level is absolutely crucial for +maintaining a healthy environment, and thus is another reason to avoid +polymorphic associations. + +## Query Overhead + +When using polymorphic associations you always need to filter using both +columns. For example, you may end up writing a query like this: + +```sql +SELECT * +FROM members +WHERE source_type = 'Project' +AND source_id = 13083; +``` + +Here PostgreSQL can perform the query quite efficiently if both columns are +indexed, but as the query gets more complex it may not be able to use these +indexes efficiently. + +## Mixed Responsibilities + +Similar to functions and classes a table should have a single responsibility: +storing data with a certain set of pre-defined columns. When using polymorphic +associations you are instead storing different types of data (possibly with +different columns set) in the same table. + +## The Solution + +Fortunately there is a very simple solution to these problems: simply use a +separate table for every type you would otherwise store in the same table. Using +a separate table allows you to use everything a database may provide to ensure +consistency and query data efficiently, without any additional application logic +being necessary. + +Let's say you have a `members` table storing both approved and pending members, +for both projects and groups, and the pending state is determined by the column +`requested_at` being set or not. Schema wise such a setup can lead to various +columns only being set for certain rows, wasting space. It's also possible that +certain indexes will only be set for certain rows, again wasting space. Finally, +querying such a table requires less than ideal queries. For example: + +```sql +SELECT * +FROM members +WHERE requested_at IS NULL +AND source_type = 'GroupMember' +AND source_id = 4 +``` + +Instead such a table should be broken up into separate tables. For example, you +may end up with 4 tables in this case: + +* project_members +* group_members +* pending_project_members +* pending_group_members + +This makes querying data trivial. For example, to get the members of a group +you'd run: + +```sql +SELECT * +FROM group_members +WHERE group_id = 4 +``` + +To get all the pending members of a group in turn you'd run: + +```sql +SELECT * +FROM pending_group_members +WHERE group_id = 4 +``` + +If you want to get both you can use a UNION, though you need to be explicit +about what columns you want to SELECT as otherwise the result set will use the +columns of the first query. For example: + +```sql +SELECT id, 'Group' AS target_type, group_id AS target_id +FROM group_members + +UNION ALL + +SELECT id, 'Project' AS target_type, project_id AS target_id +FROM project_members +``` + +The above example is perhaps a bit silly, but it shows that there's nothing +stopping you from merging the data together and presenting it on the same page. +Selecting columns explicitly can also speed up queries as the database has to do +less work to get the data (compared to selecting all columns, even ones you're +not using). + +Our schema also becomes easier. No longer do we need to both store and index the +`source_type` column, we can define foreign keys easily, and we don't need to +filter rows using the `IS NULL` condition. + +To summarize: using separate tables allows us to use foreign keys effectively, +create indexes only where necessary, conserve space, query data more +efficiently, and scale these tables more easily (e.g. by storing them on +separate disks). A nice side effect of this is that code can also become easier +as you won't end up with a single model having to handle different kinds of +data. diff --git a/doc/development/single_table_inheritance.md b/doc/development/single_table_inheritance.md new file mode 100644 index 00000000000..27c3c4f3199 --- /dev/null +++ b/doc/development/single_table_inheritance.md @@ -0,0 +1,18 @@ +# Single Table Inheritance + +**Summary:** don't use Single Table Inheritance (STI), use separate tables +instead. + +Rails makes it possible to have multiple models stored in the same table and map +these rows to the correct models using a `type` column. This can be used to for +example store two different types of SSH keys in the same table. + +While tempting to use one should avoid this at all costs for the same reasons as +outlined in the document ["Polymorphic Associations"](polymorphic_associations.md). + +## Solution + +The solution is very simple: just use a separate table for every type you'd +otherwise store in the same table. For example, instead of having a `keys` table +with `type` set to either `Key` or `DeployKey` you'd have two separate tables: +`keys` and `deploy_keys`. diff --git a/doc/development/testing.md b/doc/development/testing.md index 6d8b846d27f..cf3ea2ccfc2 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -25,7 +25,7 @@ records should use stubs/doubles as much as possible. | --------- | ---------- | -------------- | ----- | | `app/finders/` | `spec/finders/` | RSpec | | | `app/helpers/` | `spec/helpers/` | RSpec | | -| `app/db/{post_,}migrate/` | `spec/migrations/` | RSpec | | +| `app/db/{post_,}migrate/` | `spec/migrations/` | RSpec | More details at [`spec/migrations/README.md`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md). | | `app/policies/` | `spec/policies/` | RSpec | | | `app/presenters/` | `spec/presenters/` | RSpec | | | `app/routing/` | `spec/routing/` | RSpec | | diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md index 88c56a1d17c..5ea08869a9b 100644 --- a/doc/install/kubernetes/index.md +++ b/doc/install/kubernetes/index.md @@ -1,7 +1,7 @@ # Installing GitLab on Kubernetes > Officially supported cloud providers are Google Container Service and Azure Container Service. -> Officially supported schedulers are Kubernetes and Terraform. +> Officially supported schedulers are Kubernetes, Terraform and Tectonic. The easiest method to deploy GitLab in [Kubernetes](https://kubernetes.io/) is to take advantage of the official GitLab Helm charts. [Helm] is a package diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 5338ccb9d3a..197a92905c8 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -129,7 +129,8 @@ We _highly_ recommend the use of PostgreSQL instead of MySQL/MariaDB as not all features of GitLab may work with MySQL/MariaDB. For example, MySQL does not have the right features to support nested groups in an efficient manner; see <https://gitlab.com/gitlab-org/gitlab-ce/issues/30472> for more information -about this. Existing users using GitLab with MySQL/MariaDB are advised to +about this. GitLab Geo also does [not support MySQL](https://docs.gitlab.com/ee/gitlab-geo/database.html#mysql-replication). +Existing users using GitLab with MySQL/MariaDB are advised to migrate to PostgreSQL instead. The server running the database should have _at least_ 5-10 GB of storage diff --git a/doc/integration/google.md b/doc/integration/google.md index 1e7ad90c5a8..d5b523e6dc0 100644 --- a/doc/integration/google.md +++ b/doc/integration/google.md @@ -72,6 +72,21 @@ To enable the Google OAuth2 OmniAuth provider you must register your application 1. Change 'YOUR_APP_SECRET' to the client secret from the Google Developer page from step 10. +1. Make sure that you configure GitLab to use an FQDN as Google will not accept raw IP addresses. + + For Omnibus packages: + + ```ruby + external_url 'https://gitlab.example.com' + ``` + + For installations from source: + + ```yaml + gitlab: + host: https://gitlab.example.com + ``` + 1. Save the configuration file. 1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md index 591d1524061..9544de41b9a 100644 --- a/doc/university/glossary/README.md +++ b/doc/university/glossary/README.md @@ -1,4 +1,3 @@ - ## What is the Glossary This contains a simplified list and definitions of some of the terms that you will encounter in your day to day activities when working with GitLab. @@ -10,7 +9,7 @@ User authentication by combination of 2 different steps during login. This allow ### Access Levels -Process of selective restriction to create, view, modify or delete a resource based on a set of assigned permissions. See [GitLab's Permission Guidelines](../../permissions/permissions.md +Process of selective restriction to create, view, modify or delete a resource based on a set of assigned permissions. See [GitLab's Permission Guidelines](../../user/permissions.md) ### Active Directory (AD) diff --git a/doc/update/9.1-to-9.2.md b/doc/update/9.1-to-9.2.md index 19db6e5763e..e7d97fde14e 100644 --- a/doc/update/9.1-to-9.2.md +++ b/doc/update/9.1-to-9.2.md @@ -110,8 +110,8 @@ sudo -u git -H bin/compile ### 7. Update gitlab-workhorse Install and compile gitlab-workhorse. This requires -[Go 1.5](https://golang.org/dl) which should already be on your system from -GitLab 8.1. GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/). +[Go 1.8](https://golang.org/dl). Go (at least 1.5) should already be on your system from +GitLab 8.1 and shall be upgraded if necessary. Please note that starting in Gitlab 9.3, only Go 1.8.3 and above will be supported. GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/). If you are not using Linux you may have to run `gmake` instead of `make` below. diff --git a/doc/update/9.3-to-9.4.md b/doc/update/9.3-to-9.4.md new file mode 100644 index 00000000000..a712ce5a8b1 --- /dev/null +++ b/doc/update/9.3-to-9.4.md @@ -0,0 +1,317 @@ +# From 9.3 to 9.4 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + +```bash +sudo service gitlab stop +``` + +### 2. Backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Update Ruby + +NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be +sure to upgrade your interpreter if necessary. + +You can check which version you are running with `ruby -v`. + +Download and compile Ruby: + +```bash +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 +cd ruby-2.3.3 +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. Update Node + +GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and +it has a minimum requirement of node v4.3.0. + +You can check which version you are running with `node -v`. If you are running +a version older than `v4.3.0` you will need to update to a newer version. You +can find instructions to install from community maintained packages or compile +from source at the nodejs.org website. + +<https://nodejs.org/en/download/> + + +Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage +JavaScript dependencies. + +```bash +curl --location https://yarnpkg.com/install.sh | bash - +``` + +More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). + +### 5. Update Go + +NOTE: GitLab 9.4 and higher only supports Go 1.8.3 and dropped support for Go 1.5.x through 1.7.x. Be +sure to upgrade your installation if necessary + +You can check which version you are running with `go version`. + +Download and install Go: + +```bash +# Remove former Go installation folder +sudo rm -rf /usr/local/go + +curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz +echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ + sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.8.3.linux-amd64.tar.gz +``` + +### 6. Get latest code + +```bash +cd /home/git/gitlab + +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 9-4-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 9-4-stable-ee +``` + +### 5. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) +sudo -u git -H bin/compile +``` + +### 6. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.8](https://golang.org/dl) which should already be on your system from +GitLab 8.1. GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/). +If you are not using Linux you may have to run `gmake` instead of +`make` below. + +```bash +cd /home/git/gitlab-workhorse + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION) +sudo -u git -H make +``` + +### 7. Update Gitaly + +If you have not yet set up Gitaly then follow [Gitaly section of the installation +guide](../install/installation.md#install-gitaly). + +#### Check Gitaly configuration + +Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly +configuration file may contain syntax errors. The block name +`[[storages]]`, which may occur more than once in your `config.toml` +file, should be `[[storage]]` instead. + +```shell +cd /home/git/gitaly +sudo -u git -H editor config.toml +``` + +#### Compile Gitaly + +```shell +cd /home/git/gitaly +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION) +sudo -u git -H make +``` + +### 10. Update configuration files + +#### New configuration options for `gitlab.yml` + +There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +cd /home/git/gitlab + +git diff origin/9-3-stable:config/gitlab.yml.example origin/9-4-stable:config/gitlab.yml.example +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +cd /home/git/gitlab + +# For HTTPS configurations +git diff origin/9-3-stable:lib/support/nginx/gitlab-ssl origin/9-4-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/9-3-stable:lib/support/nginx/gitlab origin/9-4-stable:lib/support/nginx/gitlab +``` + +If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx +configuration as GitLab application no longer handles setting it. + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-4-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-3-stable/config/initializers/smtp_settings.rb.sample#L13 + +#### Init script + +There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`: + +```sh +cd /home/git/gitlab + +git diff origin/9-3-stable:lib/support/init.d/gitlab.default.example origin/9-4-stable:lib/support/init.d/gitlab.default.example +``` + +Ensure you're still up-to-date with the latest init script changes: + +```bash +cd /home/git/gitlab + +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +For Ubuntu 16.04.1 LTS: + +```bash +sudo systemctl daemon-reload +``` + +### 11. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Update node dependencies and recompile assets +sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production + +# Clean up cache +sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production +``` + +**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). + +### 12. Start application + +```bash +sudo service gitlab start +sudo service nginx restart +``` + +### 13. Check application status + +Check if GitLab and its environment are configured correctly: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` + +To make sure you didn't miss anything run a more thorough check: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (9.3) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 9.2 to 9.3](9.2-to-9.3.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. + +[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-4-stable/config/gitlab.yml.example +[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-4-stable/lib/support/init.d/gitlab.default.example diff --git a/doc/user/admin_area/monitoring/convdev.md b/doc/user/admin_area/monitoring/convdev.md new file mode 100644 index 00000000000..3d93c7557a4 --- /dev/null +++ b/doc/user/admin_area/monitoring/convdev.md @@ -0,0 +1,29 @@ +# Conversational Development Index + +> [Introduced][ce-30469] in GitLab 9.3. + +Conversational Development Index (ConvDev) gives you an overview of your entire +instance's feature usage, from idea to production. It looks at your usage in the +past 30 days, averaged over the number of active users in that time period. It also +provides a lead score per feature, which is calculated based on GitLab's analysis +of top performing instances, based on [usage ping data][ping] that GitLab has +collected. Your score is compared to the lead score, expressed as a percentage. +The overall index score is an average over all your feature scores. + +![ConvDev index](img/convdev_index.png) + +The page also provides helpful links to articles and GitLab docs, to help you +improve your scores. + +Your GitLab instance's usage ping must be activated in order to use this feature. +Usage ping data is aggregated on GitLab's servers for analysis. Your usage +information is **not sent** to any other GitLab instances. + +If you have just started using GitLab, it may take a few weeks for data to be +collected before this feature is available. + +This feature is accessible only to a system admin, at +**Admin area > Monitoring > ConvDev Index**. + +[ce-30469]: https://gitlab.com/gitlab-org/gitlab-ce/issues/30469 +[ping]: ../settings/usage_statistics.md#usage-ping diff --git a/doc/user/admin_area/monitoring/img/convdev_index.png b/doc/user/admin_area/monitoring/img/convdev_index.png Binary files differnew file mode 100644 index 00000000000..4e47ff2228d --- /dev/null +++ b/doc/user/admin_area/monitoring/img/convdev_index.png diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md index f3745d0efa7..d874688cc29 100644 --- a/doc/user/admin_area/settings/usage_statistics.md +++ b/doc/user/admin_area/settings/usage_statistics.md @@ -3,7 +3,8 @@ GitLab Inc. will periodically collect information about your instance in order to perform various actions. -All statistics are opt-out, you can disable them from the admin panel. +All statistics are opt-out, you can enable/disable them from the admin panel +under **Admin area > Settings > Usage statistics**. ## Version check diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index fb69d934ae1..590c3f862fb 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -125,23 +125,14 @@ applications and U2F devices. ## Personal access tokens When 2FA is enabled, you can no longer use your normal account password to -authenticate with Git over HTTPS on the command line, you must use a personal -access token instead. - -1. Log in to your GitLab account. -1. Go to your **Profile Settings**. -1. Go to **Access Tokens**. -1. Choose a name and expiry date for the token. -1. Click on **Create Personal Access Token**. -1. Save the personal access token somewhere safe. - -When using Git over HTTPS on the command line, enter the personal access token -into the password field. +authenticate with Git over HTTPS on the command line or when using +[GitLab's API][api], you must use a [personal access token][pat] instead. ## Recovery options To disable two-factor authentication on your account (for example, if you have lost your code generation device) you can: + * [Use a saved recovery code](#use-a-saved-recovery-code) * [Generate new recovery codes using SSH](#generate-new-recovery-codes-using-ssh) * [Ask a GitLab administrator to disable two-factor authentication on your account](#ask-a-gitlab-administrator-to-disable-two-factor-authentication-on-your-account) @@ -154,8 +145,9 @@ codes. If you saved these codes, you can use one of them to sign in. To use a recovery code, enter your username/email and password on the GitLab sign-in page. When prompted for a two-factor code, enter the recovery code. -> **Note:** Once you use a recovery code, you cannot re-use it. You can still - use the other recovery codes you saved. +>**Note:** +Once you use a recovery code, you cannot re-use it. You can still use the other +recovery codes you saved. ### Generate new recovery codes using SSH @@ -190,11 +182,14 @@ a new set of recovery codes with SSH. two-factor code. Then, visit your Profile Settings and add a new device so you do not lose access to your account again. ``` -3. Go to the GitLab sign-in page and enter your username/email and password. When prompted for a two-factor code, enter one of the recovery codes obtained -from the command-line output. -> **Note:** After signing in, visit your **Profile Settings -> Account** immediately to set up two-factor authentication with a new - device. +3. Go to the GitLab sign-in page and enter your username/email and password. + When prompted for a two-factor code, enter one of the recovery codes obtained + from the command-line output. + +>**Note:** +After signing in, visit your **Profile settings > Account** immediately to set +up two-factor authentication with a new device. ### Ask a GitLab administrator to disable two-factor authentication on your account @@ -206,23 +201,23 @@ Sign in and re-enable two-factor authentication as soon as possible. ## Note to GitLab administrators - You need to take special care to that 2FA keeps working after -[restoring a GitLab backup](../../../raketasks/backup_restore.md). - + [restoring a GitLab backup](../../../raketasks/backup_restore.md). - To ensure 2FA authorizes correctly with TOTP server, you may want to ensure -your GitLab server's time is synchronized via a service like NTP. Otherwise, -you may have cases where authorization always fails because of time differences. - -[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en -[FreeOTP]: https://freeotp.github.io/ -[YubiKey]: https://www.yubico.com/products/yubikey-hardware/ - + your GitLab server's time is synchronized via a service like NTP. Otherwise, + you may have cases where authorization always fails because of time differences. - The GitLab U2F implementation does _not_ work when the GitLab instance is accessed from -multiple hostnames, or FQDNs. Each U2F registration is linked to the _current hostname_ at -the time of registration, and cannot be used for other hostnames/FQDNs. + multiple hostnames, or FQDNs. Each U2F registration is linked to the _current hostname_ at + the time of registration, and cannot be used for other hostnames/FQDNs. For example, if a user is trying to access a GitLab instance from `first.host.xyz` and `second.host.xyz`: - The user logs in via `first.host.xyz` and registers their U2F key. - The user logs out and attempts to log in via `first.host.xyz` - U2F authentication suceeds. - - The user logs out and attempts to log in via `second.host.xyz` - U2F authentication fails, because + - The user logs out and attempts to log in via `second.host.xyz` - U2F authentication fails, because the U2F key has only been registered on `first.host.xyz`. + +[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en +[FreeOTP]: https://freeotp.github.io/ +[YubiKey]: https://www.yubico.com/products/yubikey-hardware/ +[api]: ../../../api/README.md +[pat]: ../personal_access_tokens.md diff --git a/doc/user/profile/img/personal_access_tokens.png b/doc/user/profile/img/personal_access_tokens.png Binary files differnew file mode 100644 index 00000000000..6aa63dbe342 --- /dev/null +++ b/doc/user/profile/img/personal_access_tokens.png diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md new file mode 100644 index 00000000000..9488ce1ef30 --- /dev/null +++ b/doc/user/profile/personal_access_tokens.md @@ -0,0 +1,57 @@ +# Personal access tokens + +> [Introduced][ce-3749] in GitLab 8.8. + +Personal access tokens are useful if you need access to the [GitLab API][api]. +Instead of using your private token which grants full access to your account, +personal access tokens could be a better fit because of their +[granular permissions](#limiting-scopes-of-a-personal-access-token). + +You can also use them to authenticate against Git over HTTP. They are the only +accepted method of authentication when you have +[Two-Factor Authentication (2FA)][2fa] enabled. + +Once you have your token, [pass it to the API][usage] using either the +`private_token` parameter or the `PRIVATE-TOKEN` header. + +## Creating a personal access token + +You can create as many personal access tokens as you like from your GitLab +profile. + +1. Log in to your GitLab account. +1. Go to your **Profile settings**. +1. Go to **Access tokens**. +1. Choose a name and optionally an expiry date for the token. +1. Choose the [desired scopes](#limiting-scopes-of-a-personal-access-token). +1. Click on **Create personal access token**. +1. Save the personal access token somewhere safe. Once you leave or refresh + the page, you won't be able to access it again. + +![Personal access tokens page](img/personal_access_tokens.png) + +## Revoking a personal access token + +At any time, you can revoke any personal access token by just clicking the +respective **Revoke** button under the 'Active personal access tokens' area. + +## Limiting scopes of a personal access token + +Personal access tokens can be created with one or more scopes that allow various +actions that a given token can perform. The available scopes are depicted in +the following table. + +| Scope | Description | +| ----- | ----------- | +|`read_user` | Allows access to the read-only endpoints under `/users`. Essentially, any of the `GET` requests in the [Users API][users] are allowed ([introduced][ce-5951] in GitLab 8.15). | +| `api` | Grants complete access to the API (read/write) ([introduced][ce-5951] in GitLab 8.15). Required for accessing Git repositories over HTTP when 2FA is enabled. | +| `read_registry` | Allows to read [container registry] images if a project is private and authorization is required ([introduced][ce-11845] in GitLab 9.3). | + +[2fa]: ../account/two_factor_authentication.md +[api]: ../../api/README.md +[ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749 +[ce-5951]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951 +[ce-11845]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845 +[container registry]: ../project/container_registry.md +[users]: ../../api/users.md +[usage]: ../../api/README.md#basic-usage diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index 3cbb0b5196d..629d69d8aea 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -8,8 +8,8 @@ Registry across your GitLab instance, visit the [administrator documentation](../../administration/container_registry.md). - Starting from GitLab 8.12, if you have 2FA enabled in your account, you need - to pass a personal access token instead of your password in order to login to - GitLab's Container Registry. + to pass a [personal access token][pat] instead of your password in order to + login to GitLab's Container Registry. - Multiple level image names support was added in GitLab 9.1 With the Docker Container Registry integrated into GitLab, every project can @@ -39,6 +39,14 @@ You can read more about Docker Registry at https://docs.docker.com/registry/intr ## Build and push images +>**Notes:** +- Moving or renaming existing container registry repositories is not supported +once you have pushed images because the images are signed, and the +signature includes the repository name. +- To move or rename a repository with a container registry you will have to +delete all existing images. + + If you visit the **Registry** link under your project's menu, you can see the explicit instructions to login to the Container Registry using your GitLab credentials. @@ -104,12 +112,13 @@ Make sure that your GitLab Runner is configured to allow building Docker images following the [Using Docker Build](../../ci/docker/using_docker_build.md) and [Using the GitLab Container Registry documentation](../../ci/docker/using_docker_build.md#using-the-gitlab-container-registry). -## Limitations +## Using with private projects + +> [Introduced][ce-11845] in GitLab 9.3. -In order to use a container image from your private project as an `image:` in -your `.gitlab-ci.yml`, you have to follow the -[Using a private Docker Registry][private-docker] -documentation. This workflow will be simplified in the future. +If a project is private, credentials will need to be provided for authorization. +The preferred way to do this, is by using [personal access tokens][pat]. +The minimal scope needed is `read_registry`. ## Troubleshooting the GitLab Container Registry @@ -254,5 +263,6 @@ The solution: check the [IAM permissions again](https://docs.docker.com/registry Once the right permissions were set, the error will go away. [ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040 +[ce-11845]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845 [docker-docs]: https://docs.docker.com/engine/userguide/intro/ -[private-docker]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-container-registry +[pat]: ../profile/personal_access_tokens.md diff --git a/doc/user/project/img/protected_branches_delete.png b/doc/user/project/img/protected_branches_delete.png Binary files differnew file mode 100644 index 00000000000..cfdfe6c6c29 --- /dev/null +++ b/doc/user/project/img/protected_branches_delete.png diff --git a/doc/user/project/integrations/img/jira_service_page.png b/doc/user/project/integrations/img/jira_service_page.png Binary files differindex c74351b57b8..e69376f74c4 100644 --- a/doc/user/project/integrations/img/jira_service_page.png +++ b/doc/user/project/integrations/img/jira_service_page.png diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md index a048260b033..cf03f2a9033 100644 --- a/doc/user/project/integrations/jira.md +++ b/doc/user/project/integrations/jira.md @@ -76,7 +76,7 @@ We have split this stage in steps so it is easier to follow. ![JIRA add user to group](img/jira_add_user_to_group.png) ---- + --- The JIRA configuration is over. Write down the new JIRA username and its password as they will be needed when configuring GitLab in the next section. @@ -98,14 +98,14 @@ in the table below. | Field | Description | | ----- | ----------- | | `Web URL` | The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., `https://jira.example.com`. | -| `JIRA API URL` | The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. | -| `Project key` | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. | +| `JIRA API URL` | The base URL to the JIRA instance API. E.g., `https://jira-api.example.com`. This is optional. If not entered, the Web URL value be used. | +| `Project key` | Put a JIRA project key (in uppercase), e.g. `MARS` in this field. This is only for testing the configuration settings. JIRA integration in GitLab works with _all_ JIRA projects in your JIRA instance. This field will be removed in a future release. | | `Username` | The user name created in [configuring JIRA step](#configuring-jira). | | `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | | `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** | After saving the configuration, your GitLab project will be able to interact -with the linked JIRA project. +with all JIRA projects in your JIRA instance. ![JIRA service page](img/jira_service_page.png) diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 5aa8337b75d..ebea7062ecb 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -31,10 +31,11 @@ Below is a table of the definitions used for GitLab's Issue Board. | **Card** | Every card represents an issue and it is shown under the list for which it has a label. The information you can see on a card consists of the issue number, the issue title, the assignee and the labels associated with it. You can drag cards around from one list to another. You can re-order cards within a list. | There are two types of lists, the ones you create based on your labels, and -one default: +two defaults: - Label list: a list based on a label. It shows all opened issues with that label. -- **Done** (default): shows all closed issues. Always appears on the very right. +- **Backlog** (default): shows all open issues that does not belong to one of lists. Always appears on the very left. +- **Closed** (default): shows all closed issues. Always appears on the very right. ![GitLab Issue Board](img/issue_board.png) diff --git a/doc/user/project/issues/confidential_issues.md b/doc/user/project/issues/confidential_issues.md index 1760b182114..208be7d0ed5 100644 --- a/doc/user/project/issues/confidential_issues.md +++ b/doc/user/project/issues/confidential_issues.md @@ -43,9 +43,8 @@ next to the issues that are marked as confidential. --- -Likewise, while inside the issue, you can see the eye-slash icon right next to -the issue number, but there is also an indicator in the comment area that the -issue you are commenting on is confidential. +While inside the issue, you can see a persistent dark banner at the top of the +screen. ![Confidential issue page](img/confidential_issues_issue_page.png) diff --git a/doc/user/project/issues/img/confidential_issues_issue_page.png b/doc/user/project/issues/img/confidential_issues_issue_page.png Binary files differindex f04ec8ff32b..91f7cc8d3ca 100755 --- a/doc/user/project/issues/img/confidential_issues_issue_page.png +++ b/doc/user/project/issues/img/confidential_issues_issue_page.png diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md index e9512497d6c..271adee7da1 100644 --- a/doc/user/project/new_ci_build_permissions_model.md +++ b/doc/user/project/new_ci_build_permissions_model.md @@ -212,9 +212,9 @@ Container Registries for private projects. access token created explicitly for this purpose). This issue is resolved with latest changes in GitLab Runner 1.8 which receives GitLab credentials with build data. -- Starting with GitLab 8.12, if you have 2FA enabled in your account, you need - to pass a personal access token instead of your password in order to login to - GitLab's Container Registry. +- Starting from GitLab 8.12, if you have [2FA] enabled in your account, you need + to pass a [personal access token][pat] instead of your password in order to + login to GitLab's Container Registry. Your jobs can access all container images that you would normally have access to. The only implication is that you can push to the Container Registry of the @@ -239,3 +239,5 @@ test: [update-docs]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update [workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse [jobenv]: ../../ci/variables/README.md#predefined-variables-environment-variables +[2fa]: ../profile/account/two_factor_authentication.md +[pat]: ../profile/personal_access_tokens.md diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md index 151ee4728ad..e853bfff444 100644 --- a/doc/user/project/pipelines/job_artifacts.md +++ b/doc/user/project/pipelines/job_artifacts.md @@ -12,7 +12,7 @@ to GitLab using GitLab Runner version 1.0 and up. It will not be possible to browse old artifacts already uploaded to GitLab. >- This is the user documentation. For the administration guide see - [administration/job_artifacts.md](../../../administration/job_artifacts.md). + [administration/job_artifacts](../../../administration/job_artifacts.md). Artifacts is a list of files and directories which are attached to a job after it completes successfully. This feature is enabled by default in all @@ -29,25 +29,31 @@ pdf: artifacts: paths: - mycv.pdf + expire_in: 1 week ``` A job named `pdf` calls the `xelatex` command in order to build a pdf file from the latex source file `mycv.tex`. We then define the `artifacts` paths which in turn are defined with the `paths` keyword. All paths to files and directories -are relative to the repository that was cloned during the build. +are relative to the repository that was cloned during the build. These uploaded +artifacts will be kept in GitLab for 1 week as defined by the `expire_in` +definition. You have the option to keep the artifacts from expiring via the +[web interface](#browsing-job-artifacts). If you don't define an expiry date, +the artifacts will be kept forever. -For more examples on artifacts, follow the artifacts reference in -[`.gitlab-ci.yml` documentation](../../../ci/yaml/README.md#artifacts). +For more examples on artifacts, follow the [artifacts reference in +`.gitlab-ci.yml`](../../../ci/yaml/README.md#artifacts). ## Browsing job artifacts >**Note:** -With GitLab 9.2, PDFs, images, videos and other formats can be previewed directly -in the job artifacts browser without the need to download them. +With GitLab 9.2, PDFs, images, videos and other formats can be previewed +directly in the job artifacts browser without the need to download them. -After a job finishes, if you visit the job's specific page, you can see -that there are two buttons. One is for downloading the artifacts archive and -the other for browsing its contents. +After a job finishes, if you visit the job's specific page, there are three +buttons. You can download the artifacts archive or browse its contents, whereas +the **Keep** button appears only if you have set an [expiry date] to the +artifacts in case you changed your mind and want to keep them. ![Job artifacts browser button](img/job_artifacts_browser_button.png) @@ -103,7 +109,7 @@ https://example.com/<namespace>/<project>/builds/artifacts/<ref>/download?job=<j To download a single file from the artifacts use the following URL: ``` -https://example.com/<namespace>/<project>/builds/artifacts/<ref>/file/<path_to_file>?job=<job_name> +https://example.com/<namespace>/<project>/builds/artifacts/<ref>/raw/<path_to_file>?job=<job_name> ``` For example, to download the latest artifacts of the job named `coverage` of @@ -118,7 +124,7 @@ To download the file `coverage/index.html` from the same artifacts use the following URL: ``` -https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/file/coverage/index.html?job=coverage +https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/raw/coverage/index.html?job=coverage ``` There is also a URL to browse the latest job artifacts: @@ -145,3 +151,5 @@ information in the UI. ![Latest artifacts button](img/job_latest_artifacts_browser.png) + +[expiry date]: ../../../ci/yaml/README.md#artifacts-expire_in diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md index d19d184f9b0..17cc21238ff 100644 --- a/doc/user/project/pipelines/schedules.md +++ b/doc/user/project/pipelines/schedules.md @@ -31,6 +31,26 @@ is installed on. ![Schedules list](img/pipeline_schedules_list.png) +## Using only and except + +To configure that a job can be executed only when the pipeline has been +scheduled (or the opposite), you can use +[only and except](../../../ci/yaml/README.md#only-and-except) configuration keywords. + +``` +job:on-schedule: + only: + - schedules + script: + - make world + +job: + except: + - schedules + script: + - make build +``` + ## Taking ownership Pipelines are executed as a user, who owns a schedule. This influences what diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md index 7650020b37e..0570d9f471f 100644 --- a/doc/user/project/protected_branches.md +++ b/doc/user/project/protected_branches.md @@ -94,8 +94,33 @@ all matching branches: ![Protected branch matches](img/protected_branches_matches.png) +## Deleting a protected branch + +> [Introduced][ce-21393] in GitLab 9.3. + +From time to time, it may be required to delete or clean up branches that are +protected. + +User with [Master permissions][perm] and up can manually delete protected +branches via GitLab's web interface: + +1. Visit **Repository > Branches** +1. Click on the delete icon next to the branch you wish to delete +1. In order to prevent accidental deletion, an additional confirmation is + required + + ![Delete protected branches](img/protected_branches_delete.png) + +Deleting a protected branch is only allowed via the web interface, not via Git. +This means that you can't accidentally delete a protected branch from your +command line or a Git client application. + ## Changelog +**9.2** + +- Allow deletion of protected branches via the web interface [gitlab-org/gitlab-ce#21393][ce-21393] + **8.11** - Allow creating protected branches that can't be pushed to [gitlab-org/gitlab-ce!5081][ce-5081] @@ -110,4 +135,6 @@ all matching branches: [ce-4665]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4665 "Allow specifying protected branches using wildcards" [ce-4892]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4892 "Allow developers to merge into a protected branch without having push access" [ce-5081]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5081 "Allow creating protected branches that can't be pushed to" +[ce-21393]: https://gitlab.com/gitlab-org/gitlab-ce/issues/21393 [ee-restrict]: http://docs.gitlab.com/ee/user/project/protected_branches.html#restricting-push-and-merge-access-to-certain-users +[perm]: ../permissions.md diff --git a/doc/user/project/repository/branches/img/delete_merged_branches.png b/doc/user/project/repository/branches/img/delete_merged_branches.png Binary files differnew file mode 100644 index 00000000000..1856a624f74 --- /dev/null +++ b/doc/user/project/repository/branches/img/delete_merged_branches.png diff --git a/doc/user/project/repository/branches/index.md b/doc/user/project/repository/branches/index.md new file mode 100644 index 00000000000..1948627ee79 --- /dev/null +++ b/doc/user/project/repository/branches/index.md @@ -0,0 +1,17 @@ +# Branches + +## Delete merged branches + +> [Introduced][ce-6449] in GitLab 8.14. + +![Delete merged branches](img/delete_merged_branches.png) + +This feature allows merged branches to be deleted in bulk. Only branches that +have been merged and [are not protected][protected] will be deleted as part of +this operation. + +It's particularly useful to clean up old branches that were not deleting +automatically when a merge request was merged. + +[ce-6449]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6449 "Add button to delete all merged branches" +[protected]: ../../protected_branches.md diff --git a/doc/workflow/groups.md b/doc/workflow/groups.md index 1cb3c940f00..1645e7e8d65 100644 --- a/doc/workflow/groups.md +++ b/doc/workflow/groups.md @@ -23,9 +23,10 @@ You can use the 'New project' button to add a project to the new group. ## Transferring an existing project into a group -You can transfer an existing project into a group you own from the project settings page. The option to transfer a project is only available if you are the Owner of the project. +You can transfer an existing project into a group you have at least Master access in from the project settings page. +The option to transfer a project is only available if you are the Owner of the project. First scroll down to the 'Dangerous settings' and click 'Show them to me'. -Now you can pick any of the groups you manage as the new namespace for the group. +Now you can pick any of the groups you have at least Master access in as the new namespace for the group. ![Transfer a project to a new namespace](groups/transfer_project.png) diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md index c5b7488be69..87416008e98 100644 --- a/doc/workflow/shortcuts.md +++ b/doc/workflow/shortcuts.md @@ -6,7 +6,10 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?' | Keyboard Shortcut | Description | | ----------------- | ----------- | +| <kbd>n</kbd> | Main navigation | | <kbd>s</kbd> | Focus search | +| <kbd>f</kbd> | Focus filter | +| <kbd>p b</kbd> | Show/hide the Performance Bar | | <kbd>?</kbd> | Show/hide this dialog | | <kbd>⌘</kbd> + <kbd>shift</kbd> + <kbd>p</kbd> | Toggle markdown preview | | <kbd>↑</kbd> | Edit last comment (when focused on an empty textarea) | diff --git a/features/project/builds/permissions.feature b/features/project/builds/permissions.feature index 3c7f72335d9..db15968db06 100644 --- a/features/project/builds/permissions.feature +++ b/features/project/builds/permissions.feature @@ -27,6 +27,7 @@ Feature: Project Builds Permissions When I visit project builds page Then page status code should be 404 + @javascript Scenario: I try to visit build details of internal project with access to builds Given The project is internal And public access for builds is enabled diff --git a/features/project/builds/summary.feature b/features/project/builds/summary.feature index 550ebccf0d7..3bf15b0cf87 100644 --- a/features/project/builds/summary.feature +++ b/features/project/builds/summary.feature @@ -6,16 +6,19 @@ Feature: Project Builds Summary And project has coverage enabled And project has a recent build + @javascript Scenario: I browse build details page When I visit recent build details page Then I see details of a build And I see build trace + @javascript Scenario: I browse project builds page When I visit project builds page Then I see coverage Then I see button to CI Lint + @javascript Scenario: I erase a build Given recent build is successful And recent build has a build trace diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index 1b00d8a32a0..4f905674d8c 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -12,11 +12,13 @@ Feature: Project Issues Given I should see "Release 0.4" in issues And I should not see "Release 0.3" in issues + @javascript Scenario: I should see closed issues Given I click link "Closed" Then I should see "Release 0.3" in issues And I should not see "Release 0.4" in issues + @javascript Scenario: I should see all issues Given I click link "All" Then I should see "Release 0.3" in issues diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index a8c528d3d6f..0ebeded7fc5 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -38,11 +38,13 @@ Feature: Project Merge Requests When I visit merge request page "Bug NS-08" Then I should see the diverged commits count + @javascript Scenario: I should see rejected merge requests Given I click link "Closed" Then I should see "Feature NS-03" in merge requests And I should not see "Bug NS-04" in merge requests + @javascript Scenario: I should see all merge requests Given I click link "All" Then I should see "Feature NS-03" in merge requests diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb index 4fb16d3bb57..530fd6f7bdb 100644 --- a/features/steps/dashboard/new_project.rb +++ b/features/steps/dashboard/new_project.rb @@ -4,7 +4,13 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps include SharedProject step 'I click "New project" link' do - page.within('.content') do + page.within '#content-body' do + click_link "New project" + end + end + + step 'I click "New project" in top right menu' do + page.within '.header-content' do click_link "New project" end end diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 83d8abbab1f..25bb374b868 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -81,7 +81,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps step 'I should see new group "Owned" avatar' do expect(owned_group.avatar).to be_instance_of AvatarUploader - expect(owned_group.avatar.url).to eq "/uploads/group/avatar/#{Group.find_by(name: "Owned").id}/banana_sample.gif" + expect(owned_group.avatar.url).to eq "/uploads/system/group/avatar/#{Group.find_by(name: "Owned").id}/banana_sample.gif" end step 'I should see the "Remove avatar" button' do diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index 24cfbaad7fe..254c26bb6af 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -36,7 +36,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps step 'I should see new avatar' do expect(@user.avatar).to be_instance_of AvatarUploader - expect(@user.avatar.url).to eq "/uploads/user/avatar/#{@user.id}/banana_sample.gif" + expect(@user.avatar.url).to eq "/uploads/system/user/avatar/#{@user.id}/banana_sample.gif" end step 'I should see the "Remove avatar" button' do diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb index 229e5d7cdf4..20a5c873ecd 100644 --- a/features/steps/project/builds/summary.rb +++ b/features/steps/project/builds/summary.rb @@ -13,7 +13,7 @@ class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps step 'I see button to CI Lint' do page.within('.nav-controls') do ci_lint_tool_link = page.find_link('CI lint') - expect(ci_lint_tool_link[:href]).to eq ci_lint_path + expect(ci_lint_tool_link[:href]).to end_with(ci_lint_path) end end diff --git a/features/steps/project/create.rb b/features/steps/project/create.rb index 5f5f806df36..28be9c6df5b 100644 --- a/features/steps/project/create.rb +++ b/features/steps/project/create.rb @@ -5,7 +5,9 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps step 'fill project form with valid data' do fill_in 'project_path', with: 'Empty' - click_button "Create project" + page.within '#content-body' do + click_button "Create project" + end end step 'I should see project page' do diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index 14932491daa..35df403a85f 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -42,7 +42,9 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end step 'I click link "New merge request"' do - page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request') + page.within '#content-body' do + page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request') + end end step 'I should see the new merge request page for my namespace' do diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index 25514eb9ef2..2d9d3efd9d4 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -17,7 +17,9 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I click link "New Merge Request"' do - page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request') + page.within '#content-body' do + page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request') + end end step 'I should see merge request "Merge Request On Forked Project"' do diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb index dfd0bc13305..2324edda975 100644 --- a/features/steps/project/issues/award_emoji.rb +++ b/features/steps/project/issues/award_emoji.rb @@ -34,8 +34,8 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps page.within '.awards' do expect do page.find('.js-emoji-btn.active').click - sleep 0.3 - end.to change{ page.all(".award-control.js-emoji-btn").size }.from(3).to(2) + wait_for_requests + end.to change { page.all(".award-control.js-emoji-btn").size }.from(3).to(2) end end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index 637e6568267..e4a559d8ff5 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -28,7 +28,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I click link "Closed"' do - find('.issues-state-filters a', text: "Closed").click + find('.issues-state-filters [data-state="closed"] span', text: 'Closed').click end step 'I click button "Unsubscribe"' do @@ -44,7 +44,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I click link "All"' do - click_link "All" + find('.issues-state-filters [data-state="all"] span', text: 'All').click # Waits for load expect(find('.issues-state-filters > .active')).to have_content 'All' end @@ -62,7 +62,9 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I click link "New issue"' do - page.has_link?('New Issue') ? click_link('New Issue') : click_link('New issue') + page.within '#content-body' do + page.has_link?('New Issue') ? click_link('New Issue') : click_link('New issue') + end end step 'I click "author" dropdown' do diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 54b6352c952..69f5d0f8410 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -14,7 +14,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I click link "New Merge Request"' do - page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request') + page.within '#content-body' do + page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request') + end end step 'I click link "Bug NS-04"' do @@ -26,7 +28,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I click link "All"' do - click_link "All" + find('.issues-state-filters [data-state="all"] span', text: 'All').click # Waits for load expect(find('.issues-state-filters > .active')).to have_content 'All' end @@ -36,9 +38,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I click link "Closed"' do - page.within('.issues-state-filters') do - click_link "Closed" - end + find('.issues-state-filters [data-state="closed"] span', text: 'Closed').click end step 'I should see merge request "Wiki Feature"' do @@ -299,6 +299,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I change the comment "Line is wrong" to "Typo, please fix" on diff' do page.within('.diff-file:nth-of-type(5) .note') do + find('.more-actions').click + find('.more-actions .dropdown-menu li', match: :first) + find('.js-note-edit').click page.within('.current-note-edit-form', visible: true) do @@ -324,6 +327,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I delete the comment "Line is wrong" on diff' do page.within('.diff-file:nth-of-type(5) .note') do + find('.more-actions').click + find('.more-actions .dropdown-menu li', match: :first) + find('.js-note-delete').click end end diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index de32c9afcca..7d34331db46 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -38,7 +38,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps step 'I should see new project avatar' do expect(@project.avatar).to be_instance_of AvatarUploader url = @project.avatar.url - expect(url).to eq "/uploads/project/avatar/#{@project.id}/banana_sample.gif" + expect(url).to eq "/uploads/system/project/avatar/#{@project.id}/banana_sample.gif" end step 'I should see the "Remove avatar" button' do diff --git a/features/steps/project/project_group_links.rb b/features/steps/project/project_group_links.rb index 739a85e5fa4..5280a38ce81 100644 --- a/features/steps/project/project_group_links.rb +++ b/features/steps/project/project_group_links.rb @@ -5,18 +5,19 @@ class Spinach::Features::ProjectGroupLinks < Spinach::FeatureSteps include Select2Helper step 'I should see project already shared with group "Ops"' do - page.within '.enabled-groups' do + page.within '.project-members-groups' do expect(page).to have_content "Ops" end end step 'I should see project is not shared with group "Market"' do - page.within '.enabled-groups' do + page.within '.project-members-groups' do expect(page).not_to have_content "Market" end end step 'I select group "Market" for share' do + click_link 'Share with group' group = Group.find_by(path: 'market') select2(group.id, from: "#link_group_id") select "Master", from: 'link_group_access' @@ -24,7 +25,7 @@ class Spinach::Features::ProjectGroupLinks < Spinach::FeatureSteps end step 'I should see project is shared with group "Market"' do - page.within '.enabled-groups' do + page.within '.project-members-groups' do expect(page).to have_content "Market" end end diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb index e3f5e9e3ef3..dd49701a3d9 100644 --- a/features/steps/project/snippets.rb +++ b/features/steps/project/snippets.rb @@ -23,7 +23,9 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps end step 'I click link "New snippet"' do - first(:link, "New snippet").click + page.within '#content-body' do + first(:link, "New snippet").click + end end step 'I click link "Snippet one"' do diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index d099d7af167..80aa3a047a0 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -89,10 +89,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I fill the new branch name' do - first('button.js-target-branch', visible: true).click - find('.create-new-branch', visible: true).click - find('#new_branch_name', visible: true).set('new_branch_name') - find('.js-new-branch-btn', visible: true).click + fill_in :branch_name, with: 'new_branch_name', visible: true end step 'I fill the new file name with an illegal name' do diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb index 0fee158d590..cf31e61437e 100644 --- a/features/steps/project/source/markdown_render.rb +++ b/features/steps/project/source/markdown_render.rb @@ -90,6 +90,8 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps click_link "api" end + wait_for_requests + page.within '.tree-table' do click_link "README.md" end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index 44eb8f321dd..80187b83fee 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -8,7 +8,12 @@ module SharedNote step 'I delete a comment' do page.within('.main-notes-list') do - find('.note').hover + note = find('.note') + note.hover + + note.find('.more-actions').click + note.find('.more-actions .dropdown-menu li', match: :first) + find(".js-note-delete").click end end @@ -139,8 +144,13 @@ module SharedNote step 'I edit the last comment with a +1' do page.within(".main-notes-list") do - find(".note").hover - find('.js-note-edit').click + note = find('.note') + note.hover + + note.find('.more-actions').click + note.find('.more-actions .dropdown-menu li', match: :first) + + note.find('.js-note-edit').click end page.within(".current-note-edit-form") do diff --git a/features/support/capybara.rb b/features/support/capybara.rb index 6da8aaac6cb..f4691647d4b 100644 --- a/features/support/capybara.rb +++ b/features/support/capybara.rb @@ -11,8 +11,10 @@ Capybara.register_driver :poltergeist do |app| js_errors: true, timeout: timeout, window_size: [1366, 768], + url_whitelist: %w[localhost 127.0.0.1], + url_blacklist: %w[.mp4 .png .gif .avi .bmp .jpg .jpeg], phantomjs_options: [ - '--load-images=no' + '--load-images=yes' ] ) end diff --git a/lib/api/api.rb b/lib/api/api.rb index 88f91c07194..d767af36e8e 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -45,6 +45,7 @@ module API end before { allow_access_with_scope :api } + before { header['X-Frame-Options'] = 'SAMEORIGIN' } before { Gitlab::I18n.locale = current_user&.preferred_language } after { Gitlab::I18n.use_default_locale } diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 8a54f7f3f05..7cdee8aced7 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -76,6 +76,27 @@ module API end end + desc 'Update an existing deploy key for a project' do + success Entities::SSHKey + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + optional :title, type: String, desc: 'The name of the deploy key' + optional :can_push, type: Boolean, desc: "Can deploy key push to the project's repository" + at_least_one_of :title, :can_push + end + put ":id/deploy_keys/:key_id" do + key = user_project.deploy_keys.find(params.delete(:key_id)) + + authorize!(:update_deploy_key, key) + + if key.update_attributes(declared_params(include_missing: false)) + present key, with: Entities::SSHKey + else + render_validation_error!(key) + end + end + desc 'Enable a deploy key for a project' do detail 'This feature was added in GitLab 8.11' success Entities::SSHKey diff --git a/lib/api/entities.rb b/lib/api/entities.rb index ded5c65e303..412443a2405 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -226,7 +226,7 @@ module API end class ProjectSnippet < Grape::Entity - expose :id, :title, :file_name + expose :id, :title, :file_name, :description expose :author, using: Entities::UserBasic expose :updated_at, :created_at @@ -236,7 +236,7 @@ module API end class PersonalSnippet < Grape::Entity - expose :id, :title, :file_name + expose :id, :title, :file_name, :description expose :author, using: Entities::UserBasic expose :updated_at, :created_at @@ -603,6 +603,9 @@ module API expose :plantuml_url expose :terminal_max_session_time expose :polling_interval_multiplier + expose :help_page_hide_commercial_content + expose :help_page_text + expose :help_page_support_url end class Release < Grape::Entity @@ -804,7 +807,11 @@ module API end class Image < Grape::Entity - expose :name + expose :name, :entrypoint + end + + class Service < Image + expose :alias, :command end class Artifacts < Grape::Entity @@ -848,7 +855,7 @@ module API expose :variables expose :steps, using: Step expose :image, using: Image - expose :services, using: Image + expose :services, using: Service expose :artifacts, using: Artifacts expose :cache, using: Cache expose :credentials, using: Credentials diff --git a/lib/api/files.rb b/lib/api/files.rb index 25b0968a271..521287ee2b4 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -25,7 +25,7 @@ module API @blob = @repo.blob_at(@commit.sha, params[:file_path]) not_found!('File') unless @blob - @blob.load_all_data!(@repo) + @blob.load_all_data! end def commit_response(attrs) diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 38631953014..ecd6d672cf7 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -86,8 +86,16 @@ module API } end + get "/broadcast_messages" do + if messages = BroadcastMessage.current + present messages, with: Entities::BroadcastMessage + else + [] + end + end + get "/broadcast_message" do - if message = BroadcastMessage.current + if message = BroadcastMessage.current.last present message, with: Entities::BroadcastMessage else {} diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index 98bc9c28527..64efe82a937 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -49,6 +49,7 @@ module API requires :title, type: String, desc: 'The title of the snippet' requires :file_name, type: String, desc: 'The file name of the snippet' requires :code, type: String, desc: 'The content of the snippet' + optional :description, type: String, desc: 'The description of a snippet' requires :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the snippet' @@ -77,6 +78,7 @@ module API optional :title, type: String, desc: 'The title of the snippet' optional :file_name, type: String, desc: 'The file name of the snippet' optional :code, type: String, desc: 'The content of the snippet' + optional :description, type: String, desc: 'The description of a snippet' optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the snippet' diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 56046742e08..50d34e8a738 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -22,6 +22,7 @@ module API optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed' optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved' optional :tag_list, type: Array[String], desc: 'The list of tags for a project' + optional :avatar, type: File, desc: 'Avatar image for project' end params :optional_params do diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 82f513c984e..d598f9a62a2 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -39,7 +39,9 @@ module API :email_author_in_body, :enabled_git_access_protocol, :gravatar_enabled, + :help_page_hide_commercial_content, :help_page_text, + :help_page_support_url, :home_page_url, :housekeeping_enabled, :html_emails_enabled, @@ -101,7 +103,9 @@ module API optional :home_page_url, type: String, desc: 'We will redirect non-logged in users to this page' optional :after_sign_out_path, type: String, desc: 'We will redirect users to this page after they sign out' optional :sign_in_text, type: String, desc: 'The sign in text of the GitLab application' + optional :help_page_hide_commercial_content, type: Boolean, desc: 'Hide marketing-related entries from help' optional :help_page_text, type: String, desc: 'Custom text displayed on the help page' + optional :help_page_support_url, type: String, desc: 'Alternate support URL for help page' optional :shared_runners_enabled, type: Boolean, desc: 'Enable shared runners for new projects' given shared_runners_enabled: ->(val) { val } do requires :shared_runners_text, type: String, desc: 'Shared runners text ' @@ -110,6 +114,7 @@ module API optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts" optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB' optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' + optional :prometheus_metrics_enabled, type: Boolean, desc: 'Enable Prometheus metrics' optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics' given metrics_enabled: ->(val) { val } do requires :metrics_host, type: String, desc: 'The InfluxDB host' diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index 53f5953a8fb..c630c24c339 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -58,6 +58,7 @@ module API requires :title, type: String, desc: 'The title of a snippet' requires :file_name, type: String, desc: 'The name of a snippet file' requires :content, type: String, desc: 'The content of a snippet' + optional :description, type: String, desc: 'The description of a snippet' optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, default: 'internal', @@ -85,6 +86,7 @@ module API optional :title, type: String, desc: 'The title of a snippet' optional :file_name, type: String, desc: 'The name of a snippet file' optional :content, type: String, desc: 'The content of a snippet' + optional :description, type: String, desc: 'The description of a snippet' optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the snippet' diff --git a/lib/api/v3/files.rb b/lib/api/v3/files.rb index c76acc86504..7b4b3448b6d 100644 --- a/lib/api/v3/files.rb +++ b/lib/api/v3/files.rb @@ -56,7 +56,7 @@ module API blob = repo.blob_at(commit.sha, params[:file_path]) not_found!('File') unless blob - blob.load_all_data!(repo) + blob.load_all_data! status(200) { diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 6b29600a751..a1685c77916 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -7,15 +7,15 @@ module Backup prepare Project.find_each(batch_size: 1000) do |project| - $progress.print " * #{project.path_with_namespace} ... " + progress.print " * #{project.path_with_namespace} ... " path_to_project_repo = path_to_repo(project) path_to_project_bundle = path_to_bundle(project) # Create namespace dir if missing FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.full_path)) if project.namespace - if project.empty_repo? - $progress.puts "[SKIPPED]".color(:cyan) + if empty_repo?(project) + progress.puts "[SKIPPED]".color(:cyan) else in_path(path_to_project_repo) do |dir| FileUtils.mkdir_p(path_to_tars(project)) @@ -23,10 +23,7 @@ module Backup output, status = Gitlab::Popen.popen(cmd) unless status.zero? - puts "[FAILED]".color(:red) - puts "failed: #{cmd.join(' ')}" - puts output - abort 'Backup failed' + progress_warn(project, cmd.join(' '), output) end end @@ -34,12 +31,9 @@ module Backup output, status = Gitlab::Popen.popen(cmd) if status.zero? - $progress.puts "[DONE]".color(:green) + progress.puts "[DONE]".color(:green) else - puts "[FAILED]".color(:red) - puts "failed: #{cmd.join(' ')}" - puts output - abort 'Backup failed' + progress_warn(project, cmd.join(' '), output) end end @@ -48,19 +42,16 @@ module Backup path_to_wiki_bundle = path_to_bundle(wiki) if File.exist?(path_to_wiki_repo) - $progress.print " * #{wiki.path_with_namespace} ... " - if wiki.repository.empty? - $progress.puts " [SKIPPED]".color(:cyan) + progress.print " * #{wiki.path_with_namespace} ... " + if empty_repo?(wiki) + progress.puts " [SKIPPED]".color(:cyan) else cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_wiki_repo} bundle create #{path_to_wiki_bundle} --all) output, status = Gitlab::Popen.popen(cmd) if status.zero? - $progress.puts " [DONE]".color(:green) + progress.puts " [DONE]".color(:green) else - puts " [FAILED]".color(:red) - puts "failed: #{cmd.join(' ')}" - puts output - abort 'Backup failed' + progress_warn(wiki, cmd.join(' '), output) end end end @@ -80,7 +71,7 @@ module Backup end Project.find_each(batch_size: 1000) do |project| - $progress.print " * #{project.path_with_namespace} ... " + progress.print " * #{project.path_with_namespace} ... " path_to_project_repo = path_to_repo(project) path_to_project_bundle = path_to_bundle(project) @@ -94,12 +85,9 @@ module Backup output, status = Gitlab::Popen.popen(cmd) if status.zero? - $progress.puts "[DONE]".color(:green) + progress.puts "[DONE]".color(:green) else - puts "[FAILED]".color(:red) - puts "failed: #{cmd.join(' ')}" - puts output - abort 'Restore failed' + progress_warn(project, cmd.join(' '), output) end in_path(path_to_tars(project)) do |dir| @@ -107,10 +95,7 @@ module Backup output, status = Gitlab::Popen.popen(cmd) unless status.zero? - puts "[FAILED]".color(:red) - puts "failed: #{cmd.join(' ')}" - puts output - abort 'Restore failed' + progress_warn(project, cmd.join(' '), output) end end @@ -119,7 +104,7 @@ module Backup path_to_wiki_bundle = path_to_bundle(wiki) if File.exist?(path_to_wiki_bundle) - $progress.print " * #{wiki.path_with_namespace} ... " + progress.print " * #{wiki.path_with_namespace} ... " # If a wiki bundle exists, first remove the empty repo # that was initialized with ProjectWiki.new() and then @@ -129,22 +114,19 @@ module Backup output, status = Gitlab::Popen.popen(cmd) if status.zero? - $progress.puts " [DONE]".color(:green) + progress.puts " [DONE]".color(:green) else - puts " [FAILED]".color(:red) - puts "failed: #{cmd.join(' ')}" - puts output - abort 'Restore failed' + progress_warn(project, cmd.join(' '), output) end end end - $progress.print 'Put GitLab hooks in repositories dirs'.color(:yellow) + progress.print 'Put GitLab hooks in repositories dirs'.color(:yellow) cmd = %W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args output, status = Gitlab::Popen.popen(cmd) if status.zero? - $progress.puts " [DONE]".color(:green) + progress.puts " [DONE]".color(:green) else puts " [FAILED]".color(:red) puts "failed: #{cmd}" @@ -201,8 +183,25 @@ module Backup private + def progress_warn(project, cmd, output) + progress.puts "[WARNING] Executing #{cmd}".color(:orange) + progress.puts "Ignoring error on #{project.path_with_namespace} - #{output}".color(:orange) + end + + def empty_repo?(project_or_wiki) + project_or_wiki.repository.empty_repo? + rescue => e + progress.puts "Ignoring repository error and continuing backing up project: #{project_or_wiki.path_with_namespace} - #{e.message}".color(:orange) + + false + end + def repository_storage_paths_args Gitlab.config.repositories.storages.values.map { |rs| rs['path'] } end + + def progress + $progress + end end end diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb index d99a3bfa625..279fca8d043 100644 --- a/lib/banzai/reference_parser/base_parser.rb +++ b/lib/banzai/reference_parser/base_parser.rb @@ -62,7 +62,7 @@ module Banzai nodes.select do |node| if node.has_attribute?(project_attr) - can_read_reference?(user, projects[node]) + can_read_reference?(user, projects[node], node) else true end @@ -171,7 +171,7 @@ module Banzai collection.where(id: to_query).each { |row| cache[row.id] = row } end - cache.values_at(*ids) + cache.values_at(*ids).compact else collection.where(id: ids) end @@ -231,7 +231,7 @@ module Banzai # see reference comments. # Override this method on subclasses # to check if user can read resource - def can_read_reference?(user, ref_project) + def can_read_reference?(user, ref_project, node) raise NotImplementedError end diff --git a/lib/banzai/reference_parser/commit_parser.rb b/lib/banzai/reference_parser/commit_parser.rb index 8c54a041cb8..30dc87248b4 100644 --- a/lib/banzai/reference_parser/commit_parser.rb +++ b/lib/banzai/reference_parser/commit_parser.rb @@ -32,7 +32,7 @@ module Banzai private - def can_read_reference?(user, ref_project) + def can_read_reference?(user, ref_project, node) can?(user, :download_code, ref_project) end end diff --git a/lib/banzai/reference_parser/commit_range_parser.rb b/lib/banzai/reference_parser/commit_range_parser.rb index 0878b6afba3..a50e6f8ef8f 100644 --- a/lib/banzai/reference_parser/commit_range_parser.rb +++ b/lib/banzai/reference_parser/commit_range_parser.rb @@ -36,7 +36,7 @@ module Banzai private - def can_read_reference?(user, ref_project) + def can_read_reference?(user, ref_project, node) can?(user, :download_code, ref_project) end end diff --git a/lib/banzai/reference_parser/external_issue_parser.rb b/lib/banzai/reference_parser/external_issue_parser.rb index 6e7b7669578..6307c1b571a 100644 --- a/lib/banzai/reference_parser/external_issue_parser.rb +++ b/lib/banzai/reference_parser/external_issue_parser.rb @@ -23,7 +23,7 @@ module Banzai private - def can_read_reference?(user, ref_project) + def can_read_reference?(user, ref_project, node) can?(user, :read_issue, ref_project) end end diff --git a/lib/banzai/reference_parser/label_parser.rb b/lib/banzai/reference_parser/label_parser.rb index aa76c64ac5f..30e2a012f09 100644 --- a/lib/banzai/reference_parser/label_parser.rb +++ b/lib/banzai/reference_parser/label_parser.rb @@ -9,7 +9,7 @@ module Banzai private - def can_read_reference?(user, ref_project) + def can_read_reference?(user, ref_project, node) can?(user, :read_label, ref_project) end end diff --git a/lib/banzai/reference_parser/merge_request_parser.rb b/lib/banzai/reference_parser/merge_request_parser.rb index 8b0662749fd..75cbc7fdac4 100644 --- a/lib/banzai/reference_parser/merge_request_parser.rb +++ b/lib/banzai/reference_parser/merge_request_parser.rb @@ -40,6 +40,10 @@ module Banzai self.class.data_attribute ) end + + def can_read_reference?(user, ref_project, node) + can?(user, :read_merge_request, ref_project) + end end end end diff --git a/lib/banzai/reference_parser/milestone_parser.rb b/lib/banzai/reference_parser/milestone_parser.rb index d3968d6b229..68675abe22a 100644 --- a/lib/banzai/reference_parser/milestone_parser.rb +++ b/lib/banzai/reference_parser/milestone_parser.rb @@ -9,7 +9,7 @@ module Banzai private - def can_read_reference?(user, ref_project) + def can_read_reference?(user, ref_project, node) can?(user, :read_milestone, ref_project) end end diff --git a/lib/banzai/reference_parser/snippet_parser.rb b/lib/banzai/reference_parser/snippet_parser.rb index 63b592137bb..3ade168b566 100644 --- a/lib/banzai/reference_parser/snippet_parser.rb +++ b/lib/banzai/reference_parser/snippet_parser.rb @@ -9,8 +9,8 @@ module Banzai private - def can_read_reference?(user, ref_project) - can?(user, :read_project_snippet, ref_project) + def can_read_reference?(user, ref_project, node) + can?(user, :read_project_snippet, referenced_by([node]).first) end end end diff --git a/lib/banzai/reference_parser/user_parser.rb b/lib/banzai/reference_parser/user_parser.rb index 09b66cbd8fb..3efbd2fd631 100644 --- a/lib/banzai/reference_parser/user_parser.rb +++ b/lib/banzai/reference_parser/user_parser.rb @@ -103,7 +103,7 @@ module Banzai flat_map { |p| p.team.members.to_a } end - def can_read_reference?(user, ref_project) + def can_read_reference?(user, ref_project, node) can?(user, :read_project, ref_project) end end diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb index 792ff628b09..6b82b2b4f13 100644 --- a/lib/ci/api/entities.rb +++ b/lib/ci/api/entities.rb @@ -45,7 +45,21 @@ module Ci expose :artifacts_expire_at, if: ->(build, _) { build.artifacts? } expose :options do |model| - model.options + # This part ensures that output of old API is still the same after adding support + # for extended docker configuration options, used by new API + # + # I'm leaving this here, not in the model, because it should be removed at the same time + # when old API will be removed (planned for August 2017). + model.options.dup.tap do |options| + options[:image] = options[:image][:name] if options[:image].is_a?(Hash) + options[:services].map! do |service| + if service.is_a?(Hash) + service[:name] + else + service + end + end + end end expose :timeout do |model| diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index b06474cda7f..56ad2c77c7d 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -20,26 +20,26 @@ module Ci raise ValidationError, e.message end - def jobs_for_ref(ref, tag = false, trigger_request = nil) + def jobs_for_ref(ref, tag = false, source = nil) @jobs.select do |_, job| - process?(job[:only], job[:except], ref, tag, trigger_request) + process?(job[:only], job[:except], ref, tag, source) end end - def jobs_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil) - jobs_for_ref(ref, tag, trigger_request).select do |_, job| + def jobs_for_stage_and_ref(stage, ref, tag = false, source = nil) + jobs_for_ref(ref, tag, source).select do |_, job| job[:stage] == stage end end - def builds_for_ref(ref, tag = false, trigger_request = nil) - jobs_for_ref(ref, tag, trigger_request).map do |name, _| + def builds_for_ref(ref, tag = false, source = nil) + jobs_for_ref(ref, tag, source).map do |name, _| build_attributes(name) end end - def builds_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil) - jobs_for_stage_and_ref(stage, ref, tag, trigger_request).map do |name, _| + def builds_for_stage_and_ref(stage, ref, tag = false, source = nil) + jobs_for_stage_and_ref(stage, ref, tag, source).map do |name, _| build_attributes(name) end end @@ -50,10 +50,21 @@ module Ci end end + def stage_seeds(pipeline) + seeds = @stages.uniq.map do |stage| + builds = builds_for_stage_and_ref( + stage, pipeline.ref, pipeline.tag?, pipeline.source) + + Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any? + end + + seeds.compact + end + def build_attributes(name) job = @jobs[name.to_sym] || {} - { - stage_idx: @stages.index(job[:stage]), + + { stage_idx: @stages.index(job[:stage]), stage: job[:stage], commands: job[:commands], tag_list: job[:tags] || [], @@ -71,8 +82,7 @@ module Ci dependencies: job[:dependencies], after_script: job[:after_script], environment: job[:environment] - }.compact - } + }.compact } end def self.validation_message(content) @@ -181,30 +191,35 @@ module Ci end end - def process?(only_params, except_params, ref, tag, trigger_request) + def process?(only_params, except_params, ref, tag, source) if only_params.present? - return false unless matching?(only_params, ref, tag, trigger_request) + return false unless matching?(only_params, ref, tag, source) end if except_params.present? - return false if matching?(except_params, ref, tag, trigger_request) + return false if matching?(except_params, ref, tag, source) end true end - def matching?(patterns, ref, tag, trigger_request) + def matching?(patterns, ref, tag, source) patterns.any? do |pattern| - match_ref?(pattern, ref, tag, trigger_request) + pattern, path = pattern.split('@', 2) + matches_path?(path) && matches_pattern?(pattern, ref, tag, source) end end - def match_ref?(pattern, ref, tag, trigger_request) - pattern, path = pattern.split('@', 2) - return false if path && path != self.path + def matches_path?(path) + return true unless path + + path == self.path + end + + def matches_pattern?(pattern, ref, tag, source) return true if tag && pattern == 'tags' return true if !tag && pattern == 'branches' - return true if trigger_request.present? && pattern == 'triggers' + return true if source_to_pattern(source) == pattern if pattern.first == "/" && pattern.last == "/" Regexp.new(pattern[1...-1]) =~ ref @@ -212,5 +227,13 @@ module Ci pattern == ref end end + + def source_to_pattern(source) + if %w[api external web].include?(source) + source + else + source&.pluralize + end + end end end diff --git a/lib/github/import.rb b/lib/github/import.rb index 9c7eb965f93..b20614b3060 100644 --- a/lib/github/import.rb +++ b/lib/github/import.rb @@ -92,7 +92,7 @@ module Github end def fetch_wiki_repository - wiki_url = "https://{options.fetch(:token)}@github.com/#{repo}.wiki.git" + wiki_url = "https://#{options.fetch(:token)}@github.com/#{repo}.wiki.git" wiki_path = "#{project.path_with_namespace}.wiki" unless project.wiki.repository_exists? diff --git a/lib/gitlab.rb b/lib/gitlab.rb index c3064163e07..11f7c8b9510 100644 --- a/lib/gitlab.rb +++ b/lib/gitlab.rb @@ -1,9 +1,11 @@ require_dependency 'gitlab/git' module Gitlab + COM_URL = 'https://gitlab.com'.freeze + def self.com? # Check `staging?` as well to keep parity with gitlab.com - Gitlab.config.gitlab.url == 'https://gitlab.com' || staging? + Gitlab.config.gitlab.url == COM_URL || staging? end def self.staging? diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 099c45dcfb7..3933c3b04dd 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -2,6 +2,8 @@ module Gitlab module Auth MissingPersonalTokenError = Class.new(StandardError) + REGISTRY_SCOPES = [:read_registry].freeze + # Scopes used for GitLab API access API_SCOPES = [:api, :read_user].freeze @@ -11,8 +13,10 @@ module Gitlab # Default scopes for OAuth applications that don't define their own DEFAULT_SCOPES = [:api].freeze + AVAILABLE_SCOPES = (API_SCOPES + REGISTRY_SCOPES).freeze + # Other available scopes - OPTIONAL_SCOPES = (API_SCOPES + OPENID_SCOPES - DEFAULT_SCOPES).freeze + OPTIONAL_SCOPES = (AVAILABLE_SCOPES + OPENID_SCOPES - DEFAULT_SCOPES).freeze class << self def find_for_git_client(login, password, project:, ip:) @@ -26,14 +30,18 @@ module Gitlab build_access_token_check(login, password) || lfs_token_check(login, password) || oauth_access_token_check(login, password) || - user_with_password_for_git(login, password) || personal_access_token_check(password) || + user_with_password_for_git(login, password) || Gitlab::Auth::Result.new rate_limit!(ip, success: result.success?, login: login) Gitlab::Auth::UniqueIpsLimiter.limit_user!(result.actor) - result + return result if result.success? || current_application_settings.signin_enabled? || Gitlab::LDAP::Config.enabled? + + # If sign-in is disabled and LDAP is not configured, recommend a + # personal access token on failed auth attempts + raise Gitlab::Auth::MissingPersonalTokenError end def find_with_user_password(login, password) @@ -109,6 +117,7 @@ module Gitlab def oauth_access_token_check(login, password) if login == "oauth2" && password.present? token = Doorkeeper::AccessToken.by_token(password) + if valid_oauth_token?(token) user = User.find_by(id: token.resource_owner_id) Gitlab::Auth::Result.new(user, nil, :oauth, full_authentication_abilities) @@ -121,17 +130,23 @@ module Gitlab token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password) - if token && valid_api_token?(token) - Gitlab::Auth::Result.new(token.user, nil, :personal_token, full_authentication_abilities) + if token && valid_scoped_token?(token, AVAILABLE_SCOPES.map(&:to_s)) + Gitlab::Auth::Result.new(token.user, nil, :personal_token, abilities_for_scope(token.scopes)) end end def valid_oauth_token?(token) - token && token.accessible? && valid_api_token?(token) + token && token.accessible? && valid_scoped_token?(token, ["api"]) end - def valid_api_token?(token) - AccessTokenValidationService.new(token).include_any_scope?(['api']) + def valid_scoped_token?(token, scopes) + AccessTokenValidationService.new(token).include_any_scope?(scopes) + end + + def abilities_for_scope(scopes) + scopes.map do |scope| + self.public_send(:"#{scope}_scope_authentication_abilities") + end.flatten.uniq end def lfs_token_check(login, password) @@ -202,6 +217,16 @@ module Gitlab :create_container_image ] end + alias_method :api_scope_authentication_abilities, :full_authentication_abilities + + def read_registry_scope_authentication_abilities + [:read_container_image] + end + + # The currently used auth method doesn't allow any actions for this scope + def read_user_scope_authentication_abilities + [] + end end end end diff --git a/lib/gitlab/auth/result.rb b/lib/gitlab/auth/result.rb index 39b86c61a18..75451cf8aa9 100644 --- a/lib/gitlab/auth/result.rb +++ b/lib/gitlab/auth/result.rb @@ -15,6 +15,10 @@ module Gitlab def success? actor.present? || type == :ci end + + def failed? + !success? + end end end end diff --git a/lib/gitlab/background_migration.rb b/lib/gitlab/background_migration.rb new file mode 100644 index 00000000000..914a3b72abd --- /dev/null +++ b/lib/gitlab/background_migration.rb @@ -0,0 +1,31 @@ +module Gitlab + module BackgroundMigration + # Begins stealing jobs from the background migrations queue, blocking the + # caller until all jobs have been completed. + # + # steal_class - The name of the class for which to steal jobs. + def self.steal(steal_class) + queue = Sidekiq::Queue. + new(BackgroundMigrationWorker.sidekiq_options['queue']) + + queue.each do |job| + migration_class, migration_args = job.args + + next unless migration_class == steal_class + + perform(migration_class, migration_args) + + job.delete + end + end + + # class_name - The name of the background migration class as defined in the + # Gitlab::BackgroundMigration namespace. + # + # arguments - The arguments to pass to the background migration's "perform" + # method. + def self.perform(class_name, arguments) + const_get(class_name).new.perform(*arguments) + end + end +end diff --git a/lib/gitlab/background_migration/.gitkeep b/lib/gitlab/background_migration/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/lib/gitlab/background_migration/.gitkeep diff --git a/lib/gitlab/blame.rb b/lib/gitlab/blame.rb index d62bc50ce78..169aac79854 100644 --- a/lib/gitlab/blame.rb +++ b/lib/gitlab/blame.rb @@ -40,7 +40,7 @@ module Gitlab end def highlighted_lines - @blob.load_all_data!(repository) + @blob.load_all_data! @highlighted_lines ||= Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: repository).lines end diff --git a/lib/gitlab/ci/build/image.rb b/lib/gitlab/ci/build/image.rb index c62aeb60fa9..b88b2e36d53 100644 --- a/lib/gitlab/ci/build/image.rb +++ b/lib/gitlab/ci/build/image.rb @@ -2,7 +2,7 @@ module Gitlab module Ci module Build class Image - attr_reader :name + attr_reader :alias, :command, :entrypoint, :name class << self def from_image(job) @@ -21,7 +21,14 @@ module Gitlab end def initialize(image) - @name = image + if image.is_a?(String) + @name = image + elsif image.is_a?(Hash) + @alias = image[:alias] + @command = image[:command] + @entrypoint = image[:entrypoint] + @name = image[:name] + end end def valid? diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb index b5050257688..897dcff8012 100644 --- a/lib/gitlab/ci/config/entry/image.rb +++ b/lib/gitlab/ci/config/entry/image.rb @@ -8,8 +8,36 @@ module Gitlab class Image < Node include Validatable + ALLOWED_KEYS = %i[name entrypoint].freeze + validations do - validates :config, type: String + validates :config, hash_or_string: true + validates :config, allowed_keys: ALLOWED_KEYS + + validates :name, type: String, presence: true + validates :entrypoint, type: String, allow_nil: true + end + + def hash? + @config.is_a?(Hash) + end + + def string? + @config.is_a?(String) + end + + def name + value[:name] + end + + def entrypoint + value[:entrypoint] + end + + def value + return { name: @config } if string? + return @config if hash? + {} end end end diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb new file mode 100644 index 00000000000..b52faf48b58 --- /dev/null +++ b/lib/gitlab/ci/config/entry/service.rb @@ -0,0 +1,34 @@ +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents a configuration of Docker service. + # + class Service < Image + include Validatable + + ALLOWED_KEYS = %i[name entrypoint command alias].freeze + + validations do + validates :config, hash_or_string: true + validates :config, allowed_keys: ALLOWED_KEYS + + validates :name, type: String, presence: true + validates :entrypoint, type: String, allow_nil: true + validates :command, type: String, allow_nil: true + validates :alias, type: String, allow_nil: true + end + + def alias + value[:alias] + end + + def command + value[:command] + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/services.rb b/lib/gitlab/ci/config/entry/services.rb index 84f8ab780f5..0066894e069 100644 --- a/lib/gitlab/ci/config/entry/services.rb +++ b/lib/gitlab/ci/config/entry/services.rb @@ -9,7 +9,30 @@ module Gitlab include Validatable validations do - validates :config, array_of_strings: true + validates :config, type: Array + end + + def compose!(deps = nil) + super do + @entries = [] + @config.each do |config| + @entries << Entry::Factory.new(Entry::Service) + .value(config || {}) + .create! + end + + @entries.each do |entry| + entry.compose!(deps) + end + end + end + + def value + @entries.map(&:value) + end + + def descendants + @entries end end end diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb index bd7428b1272..b2ca3c881e4 100644 --- a/lib/gitlab/ci/config/entry/validators.rb +++ b/lib/gitlab/ci/config/entry/validators.rb @@ -44,6 +44,14 @@ module Gitlab end end + class HashOrStringValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless value.is_a?(Hash) || value.is_a?(String) + record.errors.add(attribute, 'should be a hash or a string') + end + end + end + class KeyValidator < ActiveModel::EachValidator include LegacyValidationHelpers diff --git a/lib/gitlab/ci/stage/seed.rb b/lib/gitlab/ci/stage/seed.rb new file mode 100644 index 00000000000..f81f9347b4d --- /dev/null +++ b/lib/gitlab/ci/stage/seed.rb @@ -0,0 +1,49 @@ +module Gitlab + module Ci + module Stage + class Seed + attr_reader :pipeline + delegate :project, to: :pipeline + + def initialize(pipeline, stage, jobs) + @pipeline = pipeline + @stage = { name: stage } + @jobs = jobs.to_a.dup + end + + def user=(current_user) + @jobs.map! do |attributes| + attributes.merge(user: current_user) + end + end + + def stage + @stage.merge(project: project) + end + + def builds + trigger = pipeline.trigger_requests.first + + @jobs.map do |attributes| + attributes.merge(project: project, + ref: pipeline.ref, + tag: pipeline.tag, + trigger_request: trigger) + end + end + + def create! + pipeline.stages.create!(stage).tap do |stage| + builds_attributes = builds.map do |attributes| + attributes.merge(stage_id: stage.id) + end + + pipeline.builds.create!(builds_attributes).each do |build| + yield build if block_given? + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb index 97c121ce7b9..e5fdc1f8136 100644 --- a/lib/gitlab/ci/status/canceled.rb +++ b/lib/gitlab/ci/status/canceled.rb @@ -3,11 +3,11 @@ module Gitlab module Status class Canceled < Status::Core def text - 'canceled' + s_('CiStatusText|canceled') end def label - 'canceled' + s_('CiStatusLabel|canceled') end def icon diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb index 0721bf6ec7c..d188bd286a6 100644 --- a/lib/gitlab/ci/status/created.rb +++ b/lib/gitlab/ci/status/created.rb @@ -3,11 +3,11 @@ module Gitlab module Status class Created < Status::Core def text - 'created' + s_('CiStatusText|created') end def label - 'created' + s_('CiStatusLabel|created') end def icon diff --git a/lib/gitlab/ci/status/external/common.rb b/lib/gitlab/ci/status/external/common.rb index 4969a350862..9307545b5b1 100644 --- a/lib/gitlab/ci/status/external/common.rb +++ b/lib/gitlab/ci/status/external/common.rb @@ -3,6 +3,10 @@ module Gitlab module Status module External module Common + def label + subject.description + end + def has_details? subject.target_url.present? && can?(user, :read_commit_status, subject) diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb index cb75e9383a8..38e45714c22 100644 --- a/lib/gitlab/ci/status/failed.rb +++ b/lib/gitlab/ci/status/failed.rb @@ -3,11 +3,11 @@ module Gitlab module Status class Failed < Status::Core def text - 'failed' + s_('CiStatusText|failed') end def label - 'failed' + s_('CiStatusLabel|failed') end def icon diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb index f8f6c2903ba..a4a7edadac9 100644 --- a/lib/gitlab/ci/status/manual.rb +++ b/lib/gitlab/ci/status/manual.rb @@ -3,11 +3,11 @@ module Gitlab module Status class Manual < Status::Core def text - 'manual' + s_('CiStatusText|manual') end def label - 'manual action' + s_('CiStatusLabel|manual action') end def icon diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb index f40cc1314dc..5164260b861 100644 --- a/lib/gitlab/ci/status/pending.rb +++ b/lib/gitlab/ci/status/pending.rb @@ -3,11 +3,11 @@ module Gitlab module Status class Pending < Status::Core def text - 'pending' + s_('CiStatusText|pending') end def label - 'pending' + s_('CiStatusLabel|pending') end def icon diff --git a/lib/gitlab/ci/status/pipeline/blocked.rb b/lib/gitlab/ci/status/pipeline/blocked.rb index 37dfe43fb62..bf7e484ee9b 100644 --- a/lib/gitlab/ci/status/pipeline/blocked.rb +++ b/lib/gitlab/ci/status/pipeline/blocked.rb @@ -4,11 +4,11 @@ module Gitlab module Pipeline class Blocked < Status::Extended def text - 'blocked' + s_('CiStatusText|blocked') end def label - 'waiting for manual action' + s_('CiStatusLabel|waiting for manual action') end def self.matches?(pipeline, user) diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb index 1237cd47dc8..993937e98ca 100644 --- a/lib/gitlab/ci/status/running.rb +++ b/lib/gitlab/ci/status/running.rb @@ -3,11 +3,11 @@ module Gitlab module Status class Running < Status::Core def text - 'running' + s_('CiStatus|running') end def label - 'running' + s_('CiStatus|running') end def icon diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb index 28005d91503..0c942920b02 100644 --- a/lib/gitlab/ci/status/skipped.rb +++ b/lib/gitlab/ci/status/skipped.rb @@ -3,11 +3,11 @@ module Gitlab module Status class Skipped < Status::Core def text - 'skipped' + s_('CiStatusText|skipped') end def label - 'skipped' + s_('CiStatusLabel|skipped') end def icon diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb index 88f7758a270..d7af98857b0 100644 --- a/lib/gitlab/ci/status/success.rb +++ b/lib/gitlab/ci/status/success.rb @@ -3,11 +3,11 @@ module Gitlab module Status class Success < Status::Core def text - 'passed' + s_('CiStatusText|passed') end def label - 'passed' + s_('CiStatusLabel|passed') end def icon diff --git a/lib/gitlab/ci/status/success_warning.rb b/lib/gitlab/ci/status/success_warning.rb index df6e76b0151..4d7d82e04cf 100644 --- a/lib/gitlab/ci/status/success_warning.rb +++ b/lib/gitlab/ci/status/success_warning.rb @@ -7,11 +7,11 @@ module Gitlab # class SuccessWarning < Status::Extended def text - 'passed' + s_('CiStatusText|passed') end def label - 'passed with warnings' + s_('CiStatusLabel|passed with warnings') end def icon diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index 15992b77680..060e013183f 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -28,7 +28,7 @@ module Gitlab union = Gitlab::SQL::Union.new([repo_events, issue_events, mr_events, note_events]) events = Event.find_by_sql(union.to_sql).map(&:attributes) - @activity_events = events.each_with_object(Hash.new {|h, k| h[k] = 0 }) do |event, activities| + @activity_dates = events.each_with_object(Hash.new {|h, k| h[k] = 0 }) do |event, activities| activities[event["date"]] += event["total_amount"] end end diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb index 182a30fd74d..e47fb85b5ee 100644 --- a/lib/gitlab/data_builder/pipeline.rb +++ b/lib/gitlab/data_builder/pipeline.rb @@ -22,7 +22,7 @@ module Gitlab sha: pipeline.sha, before_sha: pipeline.before_sha, status: pipeline.status, - stages: pipeline.stages_name, + stages: pipeline.stages_names, created_at: pipeline.created_at, finished_at: pipeline.finished_at, duration: pipeline.duration diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index a412bb6dbd2..cd85f961242 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -1,6 +1,39 @@ module Gitlab module Database module MigrationHelpers + # Adds `created_at` and `updated_at` columns with timezone information. + # + # This method is an improved version of Rails' built-in method `add_timestamps`. + # + # Available options are: + # default - The default value for the column. + # null - When set to `true` the column will allow NULL values. + # The default is to not allow NULL values. + def add_timestamps_with_timezone(table_name, options = {}) + options[:null] = false if options[:null].nil? + + [:created_at, :updated_at].each do |column_name| + if options[:default] && transaction_open? + raise '`add_timestamps_with_timezone` with default value cannot be run inside a transaction. ' \ + 'You can disable transactions by calling `disable_ddl_transaction!` ' \ + 'in the body of your migration class' + end + + # If default value is presented, use `add_column_with_default` method instead. + if options[:default] + add_column_with_default( + table_name, + column_name, + :datetime_with_timezone, + default: options[:default], + allow_null: options[:null] + ) + else + add_column(table_name, column_name, :datetime_with_timezone, options) + end + end + end + # Creates a new index, concurrently when supported # # On PostgreSQL this method creates an index concurrently, on MySQL this diff --git a/lib/gitlab/diff/diff_refs.rb b/lib/gitlab/diff/diff_refs.rb index 7948782aecc..371cbe04b9b 100644 --- a/lib/gitlab/diff/diff_refs.rb +++ b/lib/gitlab/diff/diff_refs.rb @@ -37,6 +37,16 @@ module Gitlab def complete? start_sha && head_sha end + + def compare_in(project) + # We're at the initial commit, so just get that as we can't compare to anything. + if Gitlab::Git.blank_ref?(start_sha) + project.commit(head_sha) + else + straight = start_sha == base_sha + CompareService.new(project, head_sha).execute(project, start_sha, straight: straight) + end + end end end end diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index 2aef7fdaa35..d2863a4da71 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -5,7 +5,20 @@ module Gitlab delegate :new_file?, :deleted_file?, :renamed_file?, :old_path, :new_path, :a_mode, :b_mode, :mode_changed?, - :submodule?, :too_large?, :collapsed?, to: :diff, prefix: false + :submodule?, :expanded?, :too_large?, :collapsed?, :line_count, to: :diff, prefix: false + + # Finding a viewer for a diff file happens based only on extension and whether the + # diff file blobs are binary or text, which means 1 diff file should only be matched by 1 viewer, + # and the order of these viewers doesn't really matter. + # + # However, when the diff file blobs are LFS pointers, we cannot know for sure whether the + # file being pointed to is binary or text. In this case, we match only on + # extension, preferring binary viewers over text ones if both exist, since the + # large files referred to in "Large File Storage" are much more likely to be + # binary than text. + RICH_VIEWERS = [ + DiffViewer::Image + ].sort_by { |v| v.binary? ? 0 : 1 }.freeze def initialize(diff, repository:, diff_refs: nil, fallback_diff_refs: nil) @diff = diff @@ -58,19 +71,19 @@ module Gitlab diff_refs&.head_sha end - def content_sha - return old_content_sha if deleted_file? - return @content_sha if defined?(@content_sha) + def new_content_sha + return if deleted_file? + return @new_content_sha if defined?(@new_content_sha) refs = diff_refs || fallback_diff_refs - @content_sha = refs&.head_sha + @new_content_sha = refs&.head_sha end - def content_commit - return @content_commit if defined?(@content_commit) + def new_content_commit + return @new_content_commit if defined?(@new_content_commit) - sha = content_sha - @content_commit = repository.commit(sha) if sha + sha = new_content_commit + @new_content_commit = repository.commit(sha) if sha end def old_content_sha @@ -88,13 +101,13 @@ module Gitlab @old_content_commit = repository.commit(sha) if sha end - def blob - return @blob if defined?(@blob) + def new_blob + return @new_blob if defined?(@new_blob) - sha = content_sha - return @blob = nil unless sha + sha = new_content_sha + return @new_blob = nil unless sha - repository.blob_at(sha, file_path) + @new_blob = repository.blob_at(sha, file_path) end def old_blob @@ -106,6 +119,18 @@ module Gitlab @old_blob = repository.blob_at(sha, old_path) end + def content_sha + new_content_sha || old_content_sha + end + + def content_commit + new_content_commit || old_content_commit + end + + def blob + new_blob || old_blob + end + attr_writer :highlighted_diff_lines # Array of Gitlab::Diff::Line objects @@ -153,6 +178,112 @@ module Gitlab def file_identifier "#{file_path}-#{new_file?}-#{deleted_file?}-#{renamed_file?}" end + + def diffable? + repository.attributes(file_path).fetch('diff') { true } + end + + def binary? + old_blob&.binary? || new_blob&.binary? + end + + def text? + !binary? + end + + def external_storage_error? + old_blob&.external_storage_error? || new_blob&.external_storage_error? + end + + def stored_externally? + old_blob&.stored_externally? || new_blob&.stored_externally? + end + + def external_storage + old_blob&.external_storage || new_blob&.external_storage + end + + def content_changed? + old_blob && new_blob && old_blob.id != new_blob.id + end + + def different_type? + old_blob && new_blob && old_blob.binary? != new_blob.binary? + end + + def size + [old_blob&.size, new_blob&.size].compact.sum + end + + def raw_size + [old_blob&.raw_size, new_blob&.raw_size].compact.sum + end + + def raw_binary? + old_blob&.raw_binary? || new_blob&.raw_binary? + end + + def raw_text? + !raw_binary? && !different_type? + end + + def simple_viewer + @simple_viewer ||= simple_viewer_class.new(self) + end + + def rich_viewer + return @rich_viewer if defined?(@rich_viewer) + + @rich_viewer = rich_viewer_class&.new(self) + end + + def rendered_as_text?(ignore_errors: true) + simple_viewer.is_a?(DiffViewer::Text) && (ignore_errors || simple_viewer.render_error.nil?) + end + + private + + def simple_viewer_class + return DiffViewer::NotDiffable unless diffable? + + if content_changed? + if raw_text? + DiffViewer::Text + else + DiffViewer::NoPreview + end + elsif new_file? + if raw_text? + DiffViewer::Text + else + DiffViewer::Added + end + elsif deleted_file? + if raw_text? + DiffViewer::Text + else + DiffViewer::Deleted + end + elsif renamed_file? + DiffViewer::Renamed + elsif mode_changed? + DiffViewer::ModeChanged + end + end + + def rich_viewer_class + viewer_class_from(RICH_VIEWERS) + end + + def viewer_class_from(classes) + return unless diffable? + return if different_type? || external_storage_error? + return unless new_file? || deleted_file? || content_changed? + + verify_binary = !stored_externally? + + classes.find { |viewer_class| viewer_class.can_render?(self, verify_binary: verify_binary) } + end end end end diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb index 9a58b500a2c..fcda1fe2233 100644 --- a/lib/gitlab/diff/file_collection/merge_request_diff.rb +++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb @@ -66,10 +66,7 @@ module Gitlab end def cacheable?(diff_file) - @merge_request_diff.present? && - diff_file.blob && - diff_file.blob.text? && - @project.repository.diffable?(diff_file.blob) + @merge_request_diff.present? && diff_file.text? && diff_file.diffable? end def cache_key diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index ed2f541977a..b669ee5b799 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -42,9 +42,9 @@ module Gitlab rich_line = if diff_line.unchanged? || diff_line.added? - new_lines[diff_line.new_pos - 1] + new_lines[diff_line.new_pos - 1]&.html_safe elsif diff_line.removed? - old_lines[diff_line.old_pos - 1] + old_lines[diff_line.old_pos - 1]&.html_safe end # Only update text if line is found. This will prevent @@ -60,13 +60,18 @@ module Gitlab end def old_lines - return unless diff_file - @old_lines ||= Gitlab::Highlight.highlight_lines(self.repository, diff_old_sha, diff_old_path) + @old_lines ||= highlighted_blob_lines(diff_file.old_blob) end def new_lines - return unless diff_file - @new_lines ||= Gitlab::Highlight.highlight_lines(self.repository, diff_new_sha, diff_new_path) + @new_lines ||= highlighted_blob_lines(diff_file.new_blob) + end + + def highlighted_blob_lines(blob) + return [] unless blob + + blob.load_all_data! + Gitlab::Highlight.highlight(blob.path, blob.data, repository: repository).lines end end end diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb index 4d96778a2b2..f80afb20f0c 100644 --- a/lib/gitlab/diff/position.rb +++ b/lib/gitlab/diff/position.rb @@ -145,23 +145,9 @@ module Gitlab private def find_diff_file(repository) - # We're at the initial commit, so just get that as we can't compare to anything. - compare = - if Gitlab::Git.blank_ref?(start_sha) - Gitlab::Git::Commit.find(repository.raw_repository, head_sha) - else - Gitlab::Git::Compare.new( - repository.raw_repository, - start_sha, - head_sha - ) - end - - diff = compare.diffs(paths: paths).first - - return unless diff + return unless diff_refs.complete? - Gitlab::Diff::File.new(diff, repository: repository, diff_refs: diff_refs) + diff_refs.compare_in(repository.project).diffs(paths: paths, expanded: true).diff_files.first end end end diff --git a/lib/gitlab/diff/position_tracer.rb b/lib/gitlab/diff/position_tracer.rb index dcabb5f7fe5..b68a1636814 100644 --- a/lib/gitlab/diff/position_tracer.rb +++ b/lib/gitlab/diff/position_tracer.rb @@ -216,7 +216,7 @@ module Gitlab def compare(start_sha, head_sha, straight: false) compare = CompareService.new(project, head_sha).execute(project, start_sha, straight: straight) - compare.diffs(paths: paths) + compare.diffs(paths: paths, expanded: true) end def position(diff_file, old_line, new_line) diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb index 38e27513281..6d326ee213a 100644 --- a/lib/gitlab/ee_compat_check.rb +++ b/lib/gitlab/ee_compat_check.rb @@ -76,13 +76,9 @@ module Gitlab step( "Generating the patch against origin/master in #{patch_path}", - %w[git format-patch origin/master --stdout] + %W[git diff --binary origin/master > #{patch_path}] ) do |output, status| - throw(:halt_check, :ko) unless status.zero? - - File.write(patch_path, output) - - throw(:halt_check, :ko) unless File.exist?(patch_path) + throw(:halt_check, :ko) unless status.zero? && File.exist?(patch_path) end end @@ -296,7 +292,7 @@ module Gitlab # In the CE repo $ git fetch origin master - $ git format-patch origin/master --stdout > #{ce_branch}.patch + $ git diff --binary origin/master > #{ce_branch}.patch # In the EE repo $ git fetch origin master diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb index 270d67dd50c..1d6f5bb5e1c 100644 --- a/lib/gitlab/etag_caching/middleware.rb +++ b/lib/gitlab/etag_caching/middleware.rb @@ -6,12 +6,13 @@ module Gitlab end def call(env) - route = Gitlab::EtagCaching::Router.match(env) + request = Rack::Request.new(env) + route = Gitlab::EtagCaching::Router.match(request.path_info) return @app.call(env) unless route track_event(:etag_caching_middleware_used, route) - etag, cached_value_present = get_etag(env) + etag, cached_value_present = get_etag(request) if_none_match = env['HTTP_IF_NONE_MATCH'] if if_none_match == etag @@ -27,8 +28,8 @@ module Gitlab private - def get_etag(env) - cache_key = env['PATH_INFO'] + def get_etag(request) + cache_key = request.path store = Gitlab::EtagCaching::Store.new current_value = store.get(cache_key) cached_value_present = current_value.present? diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb index ca49eda51fb..75167a6b088 100644 --- a/lib/gitlab/etag_caching/router.rb +++ b/lib/gitlab/etag_caching/router.rb @@ -53,8 +53,8 @@ module Gitlab ) ].freeze - def self.match(env) - ROUTES.find { |route| route.regexp.match(env['PATH_INFO']) } + def self.match(path) + ROUTES.find { |route| route.regexp.match(path) } end end end diff --git a/lib/gitlab/etag_caching/store.rb b/lib/gitlab/etag_caching/store.rb index 0039fc01c8f..072fcfc65e6 100644 --- a/lib/gitlab/etag_caching/store.rb +++ b/lib/gitlab/etag_caching/store.rb @@ -25,6 +25,8 @@ module Gitlab end def redis_key(key) + raise 'Invalid key' if !Rails.env.production? && !Gitlab::EtagCaching::Router.match(key) + "#{REDIS_NAMESPACE}#{key}" end end diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index d60e607b02b..33a7624e303 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -123,6 +123,7 @@ module Gitlab @loaded_all_data = true @data = repository.lookup(id).content @loaded_size = @data.bytesize + @binary = nil end def name diff --git a/lib/gitlab/git/compare.rb b/lib/gitlab/git/compare.rb index 696a2acd5e3..78e440395a5 100644 --- a/lib/gitlab/git/compare.rb +++ b/lib/gitlab/git/compare.rb @@ -3,7 +3,7 @@ module Gitlab class Compare attr_reader :head, :base, :straight - def initialize(repository, base, head, straight = false) + def initialize(repository, base, head, straight: false) @repository = repository @straight = straight diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb index 8926aa19925..4b689f0e94f 100644 --- a/lib/gitlab/git/diff.rb +++ b/lib/gitlab/git/diff.rb @@ -17,12 +17,31 @@ module Gitlab attr_accessor :expanded + alias_method :expanded?, :expanded + # We need this accessor because of `to_hash` and `init_from_hash` attr_accessor :too_large class << self # The maximum size of a diff to display. def size_limit + if RequestStore.active? + RequestStore['gitlab_git_diff_size_limit'] ||= find_size_limit + else + find_size_limit + end + end + + # The maximum size before a diff is collapsed. + def collapse_limit + if RequestStore.active? + RequestStore['gitlab_git_diff_collapse_limit'] ||= find_collapse_limit + else + find_collapse_limit + end + end + + def find_size_limit if Feature.enabled?('gitlab_git_diff_size_limit_increase') 200.kilobytes else @@ -30,8 +49,7 @@ module Gitlab end end - # The maximum size before a diff is collapsed. - def collapse_limit + def find_collapse_limit if Feature.enabled?('gitlab_git_diff_size_limit_increase') 100.kilobytes else diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb index 334e06a6eca..555894907cc 100644 --- a/lib/gitlab/git/diff_collection.rb +++ b/lib/gitlab/git/diff_collection.rb @@ -97,7 +97,7 @@ module Gitlab diff = Gitlab::Git::Diff.new(raw, expanded: expanded) - if !expanded && over_safe_limits?(i) + if !expanded && over_safe_limits?(i) && diff.line_count > 0 diff.collapse! end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 9d6adbdb4ac..85695d0a4df 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -962,11 +962,6 @@ module Gitlab end end - # Checks if the blob should be diffable according to its attributes - def diffable?(blob) - attributes(blob.path).fetch('diff') { blob.text? } - end - # Returns the Git attributes for the given file path. # # See `Gitlab::Git::Attributes` for more information. diff --git a/lib/gitlab/gitaly_client/util.rb b/lib/gitlab/gitaly_client/util.rb index 86d055d3533..f5a4c5493ef 100644 --- a/lib/gitlab/gitaly_client/util.rb +++ b/lib/gitlab/gitaly_client/util.rb @@ -4,7 +4,6 @@ module Gitlab class << self def repository(repository_storage, relative_path) Gitaly::Repository.new( - path: File.join(Gitlab.config.repositories.storages[repository_storage]['path'], relative_path), storage_name: repository_storage, relative_path: relative_path ) diff --git a/lib/gitlab/health_checks/prometheus_text_format.rb b/lib/gitlab/health_checks/prometheus_text_format.rb new file mode 100644 index 00000000000..b3c759b4730 --- /dev/null +++ b/lib/gitlab/health_checks/prometheus_text_format.rb @@ -0,0 +1,40 @@ +module Gitlab + module HealthChecks + class PrometheusTextFormat + def marshal(metrics) + "#{metrics_with_type_declarations(metrics).join("\n")}\n" + end + + private + + def metrics_with_type_declarations(metrics) + type_declaration_added = {} + + metrics.flat_map do |metric| + metric_lines = [] + + unless type_declaration_added.key?(metric.name) + type_declaration_added[metric.name] = true + metric_lines << metric_type_declaration(metric) + end + + metric_lines << metric_text(metric) + end + end + + def metric_type_declaration(metric) + "# TYPE #{metric.name} gauge" + end + + def metric_text(metric) + labels = metric.labels&.map { |key, value| "#{key}=\"#{value}\"" }&.join(',') || '' + + if labels.empty? + "#{metric.name} #{metric.value}" + else + "#{metric.name}{#{labels}} #{metric.value}" + end + end + end + end +end diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index 83bc230df3e..6b24da030df 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -5,14 +5,6 @@ module Gitlab highlight(blob_content, continue: false, plain: plain) end - def self.highlight_lines(repository, ref, file_name) - blob = repository.blob_at(ref, file_name) - return [] unless blob - - blob.load_all_data!(repository) - highlight(file_name, blob.data, repository: repository).lines.map!(&:html_safe) - end - attr_reader :blob_name def initialize(blob_name, blob_content, repository: nil) diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb index f7ac48f7dbd..a5ad2f952d3 100644 --- a/lib/gitlab/i18n.rb +++ b/lib/gitlab/i18n.rb @@ -6,9 +6,12 @@ module Gitlab 'en' => 'English', 'es' => 'Español', 'de' => 'Deutsch', + 'fr' => 'Français', + 'pt_BR' => 'Português(Brasil)', 'zh_CN' => '简体中文', 'zh_HK' => '繁體中文(香港)', - 'zh_TW' => '繁體中文(臺灣)' + 'zh_TW' => '繁體中文(臺灣)', + 'bg' => 'български' }.freeze def available_locales diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index d0f3cf2b514..ff2b1d08c3c 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -38,6 +38,7 @@ project_tree: - notes: - :author - :events + - :stages - :statuses - :triggers - :pipeline_schedules diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 19e23a4715f..695852526cb 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -3,6 +3,7 @@ module Gitlab class RelationFactory OVERRIDES = { snippets: :project_snippets, pipelines: 'Ci::Pipeline', + stages: 'Ci::Stage', statuses: 'commit_status', triggers: 'Ci::Trigger', pipeline_schedules: 'Ci::PipelineSchedule', diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb index 4a6091488c8..c56c1a4322f 100644 --- a/lib/gitlab/kubernetes.rb +++ b/lib/gitlab/kubernetes.rb @@ -8,13 +8,13 @@ module Gitlab ) # Filters an array of pods (as returned by the kubernetes API) by their labels - def filter_pods(pods, labels = {}) - pods.select do |pod| - metadata = pod.fetch("metadata", {}) - pod_labels = metadata.fetch("labels", nil) - next unless pod_labels + def filter_by_label(items, labels = {}) + items.select do |item| + metadata = item.fetch("metadata", {}) + item_labels = metadata.fetch("labels", nil) + next unless item_labels - labels.all? { |k, v| pod_labels[k.to_s] == v } + labels.all? { |k, v| item_labels[k.to_s] == v } end end diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index 2d5e47a6f3b..5e299e26c54 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -41,11 +41,6 @@ module Gitlab def update_user_attributes if persisted? - if auth_hash.has_email? - gl_user.skip_reconfirmation! - gl_user.email = auth_hash.email - end - # find_or_initialize_by doesn't update `gl_user.identities`, and isn't autosaved. identity = gl_user.identities.find { |identity| identity.provider == auth_hash.provider } identity ||= gl_user.identities.build(provider: auth_hash.provider) @@ -55,10 +50,6 @@ module Gitlab # For an existing identity with no change in DN, this line changes nothing. identity.extern_uid = auth_hash.uid end - - gl_user.ldap_email = auth_hash.has_email? - - gl_user end def changed? @@ -69,6 +60,10 @@ module Gitlab ldap_config.block_auto_created_users end + def sync_email_from_provider? + true + end + def allowed? Gitlab::LDAP::Access.allowed?(gl_user) end diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index cb8db2f1e9f..4779755bb22 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -1,161 +1,10 @@ module Gitlab module Metrics - extend Gitlab::CurrentSettings - - RAILS_ROOT = Rails.root.to_s - METRICS_ROOT = Rails.root.join('lib', 'gitlab', 'metrics').to_s - PATH_REGEX = /^#{RAILS_ROOT}\/?/ - - def self.settings - @settings ||= { - enabled: current_application_settings[:metrics_enabled], - pool_size: current_application_settings[:metrics_pool_size], - timeout: current_application_settings[:metrics_timeout], - method_call_threshold: current_application_settings[:metrics_method_call_threshold], - host: current_application_settings[:metrics_host], - port: current_application_settings[:metrics_port], - sample_interval: current_application_settings[:metrics_sample_interval] || 15, - packet_size: current_application_settings[:metrics_packet_size] || 1 - } - end + extend Gitlab::Metrics::InfluxDb + extend Gitlab::Metrics::Prometheus def self.enabled? - settings[:enabled] || false - end - - def self.mri? - RUBY_ENGINE == 'ruby' - end - - def self.method_call_threshold - # This is memoized since this method is called for every instrumented - # method. Loading data from an external cache on every method call slows - # things down too much. - @method_call_threshold ||= settings[:method_call_threshold] - end - - def self.pool - @pool - end - - def self.submit_metrics(metrics) - prepared = prepare_metrics(metrics) - - pool.with do |connection| - prepared.each_slice(settings[:packet_size]) do |slice| - begin - connection.write_points(slice) - rescue StandardError - end - end - end - rescue Errno::EADDRNOTAVAIL, SocketError => ex - Gitlab::EnvironmentLogger.error('Cannot resolve InfluxDB address. GitLab Performance Monitoring will not work.') - Gitlab::EnvironmentLogger.error(ex) - end - - def self.prepare_metrics(metrics) - metrics.map do |hash| - new_hash = hash.symbolize_keys - - new_hash[:tags].each do |key, value| - if value.blank? - new_hash[:tags].delete(key) - else - new_hash[:tags][key] = escape_value(value) - end - end - - new_hash - end - end - - def self.escape_value(value) - value.to_s.gsub('=', '\\=') - end - - # Measures the execution time of a block. - # - # Example: - # - # Gitlab::Metrics.measure(:find_by_username_duration) do - # User.find_by_username(some_username) - # end - # - # name - The name of the field to store the execution time in. - # - # Returns the value yielded by the supplied block. - def self.measure(name) - trans = current_transaction - - return yield unless trans - - real_start = Time.now.to_f - cpu_start = System.cpu_time - - retval = yield - - cpu_stop = System.cpu_time - real_stop = Time.now.to_f - - real_time = (real_stop - real_start) * 1000.0 - cpu_time = cpu_stop - cpu_start - - trans.increment("#{name}_real_time", real_time) - trans.increment("#{name}_cpu_time", cpu_time) - trans.increment("#{name}_call_count", 1) - - retval - end - - # Adds a tag to the current transaction (if any) - # - # name - The name of the tag to add. - # value - The value of the tag. - def self.tag_transaction(name, value) - trans = current_transaction - - trans&.add_tag(name, value) - end - - # Sets the action of the current transaction (if any) - # - # action - The name of the action. - def self.action=(action) - trans = current_transaction - - trans&.action = action - end - - # Tracks an event. - # - # See `Gitlab::Metrics::Transaction#add_event` for more details. - def self.add_event(*args) - trans = current_transaction - - trans&.add_event(*args) - end - - # Returns the prefix to use for the name of a series. - def self.series_prefix - @series_prefix ||= Sidekiq.server? ? 'sidekiq_' : 'rails_' - end - - # Allow access from other metrics related middlewares - def self.current_transaction - Transaction.current - end - - # When enabled this should be set before being used as the usual pattern - # "@foo ||= bar" is _not_ thread-safe. - if enabled? - @pool = ConnectionPool.new(size: settings[:pool_size], timeout: settings[:timeout]) do - host = settings[:host] - port = settings[:port] - - InfluxDB::Client. - new(udp: { host: host, port: port }) - end + influx_metrics_enabled? || prometheus_metrics_enabled? end end end diff --git a/lib/gitlab/metrics/influx_db.rb b/lib/gitlab/metrics/influx_db.rb new file mode 100644 index 00000000000..3a39791edbf --- /dev/null +++ b/lib/gitlab/metrics/influx_db.rb @@ -0,0 +1,170 @@ +module Gitlab + module Metrics + module InfluxDb + extend Gitlab::CurrentSettings + extend self + + MUTEX = Mutex.new + private_constant :MUTEX + + def influx_metrics_enabled? + settings[:enabled] || false + end + + RAILS_ROOT = Rails.root.to_s + METRICS_ROOT = Rails.root.join('lib', 'gitlab', 'metrics').to_s + PATH_REGEX = /^#{RAILS_ROOT}\/?/ + + def settings + @settings ||= { + enabled: current_application_settings[:metrics_enabled], + pool_size: current_application_settings[:metrics_pool_size], + timeout: current_application_settings[:metrics_timeout], + method_call_threshold: current_application_settings[:metrics_method_call_threshold], + host: current_application_settings[:metrics_host], + port: current_application_settings[:metrics_port], + sample_interval: current_application_settings[:metrics_sample_interval] || 15, + packet_size: current_application_settings[:metrics_packet_size] || 1 + } + end + + def mri? + RUBY_ENGINE == 'ruby' + end + + def method_call_threshold + # This is memoized since this method is called for every instrumented + # method. Loading data from an external cache on every method call slows + # things down too much. + @method_call_threshold ||= settings[:method_call_threshold] + end + + def submit_metrics(metrics) + prepared = prepare_metrics(metrics) + + pool&.with do |connection| + prepared.each_slice(settings[:packet_size]) do |slice| + begin + connection.write_points(slice) + rescue StandardError + end + end + end + rescue Errno::EADDRNOTAVAIL, SocketError => ex + Gitlab::EnvironmentLogger.error('Cannot resolve InfluxDB address. GitLab Performance Monitoring will not work.') + Gitlab::EnvironmentLogger.error(ex) + end + + def prepare_metrics(metrics) + metrics.map do |hash| + new_hash = hash.symbolize_keys + + new_hash[:tags].each do |key, value| + if value.blank? + new_hash[:tags].delete(key) + else + new_hash[:tags][key] = escape_value(value) + end + end + + new_hash + end + end + + def escape_value(value) + value.to_s.gsub('=', '\\=') + end + + # Measures the execution time of a block. + # + # Example: + # + # Gitlab::Metrics.measure(:find_by_username_duration) do + # User.find_by_username(some_username) + # end + # + # name - The name of the field to store the execution time in. + # + # Returns the value yielded by the supplied block. + def measure(name) + trans = current_transaction + + return yield unless trans + + real_start = Time.now.to_f + cpu_start = System.cpu_time + + retval = yield + + cpu_stop = System.cpu_time + real_stop = Time.now.to_f + + real_time = (real_stop - real_start) * 1000.0 + cpu_time = cpu_stop - cpu_start + + trans.increment("#{name}_real_time", real_time) + trans.increment("#{name}_cpu_time", cpu_time) + trans.increment("#{name}_call_count", 1) + + retval + end + + # Adds a tag to the current transaction (if any) + # + # name - The name of the tag to add. + # value - The value of the tag. + def tag_transaction(name, value) + trans = current_transaction + + trans&.add_tag(name, value) + end + + # Sets the action of the current transaction (if any) + # + # action - The name of the action. + def action=(action) + trans = current_transaction + + trans&.action = action + end + + # Tracks an event. + # + # See `Gitlab::Metrics::Transaction#add_event` for more details. + def add_event(*args) + trans = current_transaction + + trans&.add_event(*args) + end + + # Returns the prefix to use for the name of a series. + def series_prefix + @series_prefix ||= Sidekiq.server? ? 'sidekiq_' : 'rails_' + end + + # Allow access from other metrics related middlewares + def current_transaction + Transaction.current + end + + # When enabled this should be set before being used as the usual pattern + # "@foo ||= bar" is _not_ thread-safe. + def pool + if influx_metrics_enabled? + if @pool.nil? + MUTEX.synchronize do + @pool ||= ConnectionPool.new(size: settings[:pool_size], timeout: settings[:timeout]) do + host = settings[:host] + port = settings[:port] + + InfluxDB::Client. + new(udp: { host: host, port: port }) + end + end + end + @pool + end + end + end + end +end diff --git a/lib/gitlab/metrics/null_metric.rb b/lib/gitlab/metrics/null_metric.rb new file mode 100644 index 00000000000..3b5a2907195 --- /dev/null +++ b/lib/gitlab/metrics/null_metric.rb @@ -0,0 +1,10 @@ +module Gitlab + module Metrics + # Mocks ::Prometheus::Client::Metric and all derived metrics + class NullMetric + def method_missing(name, *args, &block) + nil + end + end + end +end diff --git a/lib/gitlab/metrics/prometheus.rb b/lib/gitlab/metrics/prometheus.rb new file mode 100644 index 00000000000..60686509332 --- /dev/null +++ b/lib/gitlab/metrics/prometheus.rb @@ -0,0 +1,41 @@ +require 'prometheus/client' + +module Gitlab + module Metrics + module Prometheus + include Gitlab::CurrentSettings + + def prometheus_metrics_enabled? + @prometheus_metrics_enabled ||= current_application_settings[:prometheus_metrics_enabled] || false + end + + def registry + @registry ||= ::Prometheus::Client.registry + end + + def counter(name, docstring, base_labels = {}) + provide_metric(name) || registry.counter(name, docstring, base_labels) + end + + def summary(name, docstring, base_labels = {}) + provide_metric(name) || registry.summary(name, docstring, base_labels) + end + + def gauge(name, docstring, base_labels = {}) + provide_metric(name) || registry.gauge(name, docstring, base_labels) + end + + def histogram(name, docstring, base_labels = {}, buckets = ::Prometheus::Client::Histogram::DEFAULT_BUCKETS) + provide_metric(name) || registry.histogram(name, docstring, base_labels, buckets) + end + + def provide_metric(name) + if prometheus_metrics_enabled? + registry.get(name) + else + NullMetric.new + end + end + end + end +end diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index afd24b4dcc5..7307f8c2c87 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -12,6 +12,7 @@ module Gitlab def initialize(auth_hash) self.auth_hash = auth_hash + update_email end def persisted? @@ -174,6 +175,22 @@ module Gitlab } end + def sync_email_from_provider? + auth_hash.provider.to_s == Gitlab.config.omniauth.sync_email_from_provider.to_s + end + + def update_email + if auth_hash.has_email? && sync_email_from_provider? + if persisted? + gl_user.skip_reconfirmation! + gl_user.email = auth_hash.email + end + + gl_user.external_email = true + gl_user.email_provider = auth_hash.provider + end + end + def log Gitlab::AppLogger end diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb index 9ff6829cd49..10eb99fb461 100644 --- a/lib/gitlab/path_regex.rb +++ b/lib/gitlab/path_regex.rb @@ -49,6 +49,7 @@ module Gitlab sent_notifications services snippets + system teams u unicorn_test diff --git a/lib/gitlab/performance_bar.rb b/lib/gitlab/performance_bar.rb new file mode 100644 index 00000000000..163a40ad306 --- /dev/null +++ b/lib/gitlab/performance_bar.rb @@ -0,0 +1,7 @@ +module Gitlab + module PerformanceBar + def self.enabled? + Feature.enabled?('gitlab_performance_bar') + end + end +end diff --git a/lib/gitlab/performance_bar/peek_performance_bar_with_rack_body.rb b/lib/gitlab/performance_bar/peek_performance_bar_with_rack_body.rb new file mode 100644 index 00000000000..d939a6ea18d --- /dev/null +++ b/lib/gitlab/performance_bar/peek_performance_bar_with_rack_body.rb @@ -0,0 +1,22 @@ +# This solves a bug with a X-Senfile header that wouldn't be set properly, see +# https://github.com/peek/peek-performance_bar/pull/27 +module Gitlab + module PerformanceBar + module PeekPerformanceBarWithRackBody + def call(env) + @env = env + reset_stats + + @total_requests += 1 + first_request if @total_requests == 1 + + env['process.request_start'] = @start.to_f + env['process.total_requests'] = total_requests + + status, headers, body = @app.call(env) + body = Rack::BodyProxy.new(body) { record_request } + [status, headers, body] + end + end + end +end diff --git a/lib/gitlab/performance_bar/peek_query_tracker.rb b/lib/gitlab/performance_bar/peek_query_tracker.rb new file mode 100644 index 00000000000..7ab80f5ee0f --- /dev/null +++ b/lib/gitlab/performance_bar/peek_query_tracker.rb @@ -0,0 +1,39 @@ +# Inspired by https://github.com/peek/peek-pg/blob/master/lib/peek/views/pg.rb +module Gitlab + module PerformanceBar + module PeekQueryTracker + def sorted_queries + PEEK_DB_CLIENT.query_details. + sort { |a, b| b[:duration] <=> a[:duration] } + end + + def results + super.merge(queries: sorted_queries) + end + + private + + def setup_subscribers + super + + # Reset each counter when a new request starts + before_request do + PEEK_DB_CLIENT.query_details = [] + end + + subscribe('sql.active_record') do |_, start, finish, _, data| + if RequestStore.active? && RequestStore.store[:peek_enabled] + track_query(data[:sql].strip, data[:binds], start, finish) + end + end + end + + def track_query(raw_query, bindings, start, finish) + query = Gitlab::Sherlock::Query.new(raw_query, start, finish) + query_info = { duration: '%.3f' % query.duration, sql: query.formatted_query } + + PEEK_DB_CLIENT.query_details << query_info + end + end + end +end diff --git a/lib/gitlab/slash_commands/command_definition.rb b/lib/gitlab/slash_commands/command_definition.rb index 12a385f90fd..caab8856014 100644 --- a/lib/gitlab/slash_commands/command_definition.rb +++ b/lib/gitlab/slash_commands/command_definition.rb @@ -48,17 +48,23 @@ module Gitlab end def to_h(opts) + context = OpenStruct.new(opts) + desc = description if desc.respond_to?(:call) - context = OpenStruct.new(opts) desc = context.instance_exec(&desc) rescue '' end + prms = params + if prms.respond_to?(:call) + prms = Array(context.instance_exec(&prms)) rescue params + end + { name: name, aliases: aliases, description: desc, - params: params + params: prms } end diff --git a/lib/gitlab/slash_commands/dsl.rb b/lib/gitlab/slash_commands/dsl.rb index 614bafbe1b2..1b5b4566d81 100644 --- a/lib/gitlab/slash_commands/dsl.rb +++ b/lib/gitlab/slash_commands/dsl.rb @@ -40,8 +40,8 @@ module Gitlab # command :command_key do |arguments| # # Awesome code block # end - def params(*params) - @params = params + def params(*params, &block) + @params = block_given? ? block : params end # Allows to give an explanation of what the command will do when diff --git a/lib/gitlab/uploads_transfer.rb b/lib/gitlab/uploads_transfer.rb index 7d0c47c5361..b5f41240529 100644 --- a/lib/gitlab/uploads_transfer.rb +++ b/lib/gitlab/uploads_transfer.rb @@ -1,7 +1,7 @@ module Gitlab class UploadsTransfer < ProjectTransfer def root_dir - File.join(CarrierWave.root, GitlabUploader.base_dir) + File.join(CarrierWave.root, FileUploader.base_dir) end end end diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index ccb456bcc94..23af9318d1a 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -61,7 +61,12 @@ module Gitlab elsif object.for_snippet? snippet = Snippet.find(object.noteable_id) - project_snippet_url(snippet, anchor: dom_id(object)) + + if snippet.is_a?(PersonalSnippet) + snippet_url(snippet, anchor: dom_id(object)) + else + project_snippet_url(snippet, anchor: dom_id(object)) + end end end diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index 85da4c8660b..2b53798e70f 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -41,9 +41,9 @@ module Gitlab def options { - 'Private' => PRIVATE, - 'Internal' => INTERNAL, - 'Public' => PUBLIC + N_('VisibilityLevel|Private') => PRIVATE, + N_('VisibilityLevel|Internal') => INTERNAL, + N_('VisibilityLevel|Public') => PUBLIC } end diff --git a/lib/peek/rblineprof/custom_controller_helpers.rb b/lib/peek/rblineprof/custom_controller_helpers.rb new file mode 100644 index 00000000000..99f9c2c9b04 --- /dev/null +++ b/lib/peek/rblineprof/custom_controller_helpers.rb @@ -0,0 +1,96 @@ +module Peek + module Rblineprof + module CustomControllerHelpers + extend ActiveSupport::Concern + + # This will become useless once https://github.com/peek/peek-rblineprof/pull/5 + # is merged + def pygmentize(file_name, code, lexer = nil) + if lexer.present? + Gitlab::Highlight.highlight(file_name, code) + else + "<pre>#{Rack::Utils.escape_html(code)}</pre>" + end + end + + # rubocop:disable all + def inject_rblineprof + ret = nil + profile = lineprof(rblineprof_profiler_regex) do + ret = yield + end + + if response.content_type =~ %r|text/html| + sort = params[:lineprofiler_sort] + mode = params[:lineprofiler_mode] || 'cpu' + min = (params[:lineprofiler_min] || 5).to_i * 1000 + summary = params[:lineprofiler_summary] + + # Sort each file by the longest calculated time + per_file = profile.map do |file, lines| + total, child, excl, total_cpu, child_cpu, excl_cpu = lines[0] + + wall = summary == 'exclusive' ? excl : total + cpu = summary == 'exclusive' ? excl_cpu : total_cpu + idle = summary == 'exclusive' ? (excl - excl_cpu) : (total - total_cpu) + + [ + file, lines, + wall, cpu, idle, + sort == 'idle' ? idle : sort == 'cpu' ? cpu : wall + ] + end.sort_by{ |a,b,c,d,e,f| -f } + + output = '' + per_file.each do |file_name, lines, file_wall, file_cpu, file_idle, file_sort| + + output << "<div class='peek-rblineprof-file'><div class='heading'>" + + show_src = file_sort > min + tmpl = show_src ? "<a href='#' class='js-lineprof-file'>%s</a>" : "%s" + + if mode == 'cpu' + output << sprintf("<span class='duration'>% 8.1fms + % 8.1fms</span> #{tmpl}", file_cpu / 1000.0, file_idle / 1000.0, file_name.sub(Rails.root.to_s + '/', '')) + else + output << sprintf("<span class='duration'>% 8.1fms</span> #{tmpl}", file_wall/1000.0, file_name.sub(Rails.root.to_s + '/', '')) + end + + output << "</div>" # .heading + + next unless show_src + + output << "<div class='data'>" + code = [] + times = [] + File.readlines(file_name).each_with_index do |line, i| + code << line + wall, cpu, calls = lines[i + 1] + + if calls && calls > 0 + if mode == 'cpu' + idle = wall - cpu + times << sprintf("% 8.1fms + % 8.1fms (% 5d)", cpu / 1000.0, idle / 1000.0, calls) + else + times << sprintf("% 8.1fms (% 5d)", wall / 1000.0, calls) + end + else + times << ' ' + end + end + output << "<pre class='duration'>#{times.join("\n")}</pre>" + # The following line was changed from + # https://github.com/peek/peek-rblineprof/blob/8d3b7a283a27de2f40abda45974516693d882258/lib/peek/rblineprof/controller_helpers.rb#L125 + # This will become useless once https://github.com/peek/peek-rblineprof/pull/16 + # is merged and is implemented. + output << "<pre class='code highlight white'>#{pygmentize(file_name, code.join, 'ruby')}</pre>" + output << "</div></div>" # .data then .peek-rblineprof-file + end + + response.body += "<div class='peek-rblineprof-modal' id='line-profile'>#{output}</div>".html_safe + end + + ret + end + end + end +end diff --git a/lib/rouge/lexers/math.rb b/lib/rouge/lexers/math.rb index 80784adfd76..939b23a3421 100644 --- a/lib/rouge/lexers/math.rb +++ b/lib/rouge/lexers/math.rb @@ -1,21 +1,9 @@ module Rouge module Lexers - class Math < Lexer + class Math < PlainText title "A passthrough lexer used for LaTeX input" - desc "A boring lexer that doesn't highlight anything" - + desc "PLEASE REFACTOR - this should be handled by SyntaxHighlightFilter" tag 'math' - 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/rouge/lexers/plantuml.rb b/lib/rouge/lexers/plantuml.rb index 7d5700b7f6d..63c461764fc 100644 --- a/lib/rouge/lexers/plantuml.rb +++ b/lib/rouge/lexers/plantuml.rb @@ -1,21 +1,9 @@ module Rouge module Lexers - class Plantuml < Lexer + class Plantuml < PlainText title "A passthrough lexer used for PlantUML input" - desc "A boring lexer that doesn't highlight anything" - + desc "PLEASE REFACTOR - this should be handled by SyntaxHighlightFilter" 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/check.rake b/lib/tasks/gitlab/check.rake index 63c5e9b9c83..858f1cd7b34 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -336,12 +336,9 @@ namespace :gitlab do ######################## def check_initd_configured_correctly - print "Init.d configured correctly? ... " + return if omnibus_gitlab? - if omnibus_gitlab? - puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta) - return - end + print "Init.d configured correctly? ... " path = "/etc/default/gitlab" @@ -379,6 +376,8 @@ namespace :gitlab do end def check_mail_room_running + return if omnibus_gitlab? + print "MailRoom running? ... " path = "/etc/default/gitlab" diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index 3c5bc0146a1..e88111c3725 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -30,11 +30,7 @@ namespace :gitlab do puts "# Gitaly storage configuration generated from #{Gitlab.config.source} on #{Time.current.to_s(:long)}" puts "# This is in TOML format suitable for use in Gitaly's config.toml file." - config = Gitlab.config.repositories.storages.map do |key, val| - { name: key, path: val['path'] } - end - - puts TOML.dump(storage: config) + puts gitaly_configuration_toml end private @@ -42,10 +38,10 @@ namespace :gitlab do # We cannot create config.toml files for all possible Gitaly configuations. # For instance, if Gitaly is running on another machine then it makes no # sense to write a config.toml file on the current machine. This method will - # only write a config.toml file in the most common and simplest case: the - # case where we have exactly one Gitaly process and we are sure it is - # running locally because it uses a Unix socket. - def create_gitaly_configuration + # only generate a configuration for the most common and simplest case: when + # we have exactly one Gitaly process and we are sure it is running locally + # because it uses a Unix socket. + def gitaly_configuration_toml storages = [] address = nil @@ -63,8 +59,12 @@ namespace :gitlab do storages << { name: key, path: val['path'] } end + TOML.dump(socket_path: address.sub(%r{\Aunix:}, ''), storage: storages) + end + + def create_gitaly_configuration File.open("config.toml", "w") do |f| - f.puts TOML.dump(socket_path: address.sub(%r{\Aunix:}, ''), storages: storages) + f.puts gitaly_configuration_toml end rescue ArgumentError => e puts "Skipping config.toml generation:" diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po new file mode 100644 index 00000000000..e6caf83252d --- /dev/null +++ b/locale/bg/gitlab.po @@ -0,0 +1,260 @@ +# Lyubomir Vasilev <lyubomirv@abv.bg>, 2017. #zanata +msgid "" +msgstr "" +"Project-Id-Version: gitlab 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-05-04 19:24-0500\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2017-06-05 09:40-0400\n" +"Last-Translator: Lyubomir Vasilev <lyubomirv@abv.bg>\n" +"Language-Team: Bulgarian\n" +"Language: bg\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgid "ByAuthor|by" +msgstr "от" + +msgid "Commit" +msgid_plural "Commits" +msgstr[0] "Подаване" +msgstr[1] "Подавания" + +msgid "" +"Cycle Analytics gives an overview of how much time it takes to go from idea " +"to production in your project." +msgstr "" +"Анализът на циклите дава общ поглед върху това колко време е нужно на една " +"идея да се превърне в завършена функционалност в проекта." + +msgid "CycleAnalyticsStage|Code" +msgstr "Програмиране" + +msgid "CycleAnalyticsStage|Issue" +msgstr "Проблем" + +msgid "CycleAnalyticsStage|Plan" +msgstr "Планиране" + +msgid "CycleAnalyticsStage|Production" +msgstr "Издаване" + +msgid "CycleAnalyticsStage|Review" +msgstr "Преглед и одобрение" + +msgid "CycleAnalyticsStage|Staging" +msgstr "Подготовка за издаване" + +msgid "CycleAnalyticsStage|Test" +msgstr "Тестване" + +msgid "Deploy" +msgid_plural "Deploys" +msgstr[0] "Внедряване" +msgstr[1] "Внедрявания" + +msgid "FirstPushedBy|First" +msgstr "Първо" + +msgid "FirstPushedBy|pushed by" +msgstr "изпращане на промени от" + +msgid "From issue creation until deploy to production" +msgstr "От създаването на проблема до внедряването в крайната версия" + +msgid "From merge request merge until deploy to production" +msgstr "" +"От прилагането на заявката за сливане до внедряването в крайната версия" + +msgid "Introducing Cycle Analytics" +msgstr "Представяме Ви анализът на циклите" + +msgid "Last %d day" +msgid_plural "Last %d days" +msgstr[0] "Последния %d ден" +msgstr[1] "Последните %d дни" + +msgid "Limited to showing %d event at most" +msgid_plural "Limited to showing %d events at most" +msgstr[0] "Ограничено до показване на последното %d събитие" +msgstr[1] "Ограничено до показване на последните %d събития" + +msgid "Median" +msgstr "Медиана" + +msgid "New Issue" +msgid_plural "New Issues" +msgstr[0] "Нов проблем" +msgstr[1] "Нови проблема" + +msgid "Not available" +msgstr "Не е налично" + +msgid "Not enough data" +msgstr "Няма достатъчно данни" + +msgid "OpenedNDaysAgo|Opened" +msgstr "Отворен" + +msgid "Pipeline Health" +msgstr "Състояние" + +msgid "ProjectLifecycle|Stage" +msgstr "Етап" + +msgid "Read more" +msgstr "Прочетете повече" + +msgid "Related Commits" +msgstr "Свързани подавания" + +msgid "Related Deployed Jobs" +msgstr "Свързани задачи за внедряване" + +msgid "Related Issues" +msgstr "Свързани проблеми" + +msgid "Related Jobs" +msgstr "Свързани задачи" + +msgid "Related Merge Requests" +msgstr "Свързани заявки за сливане" + +msgid "Related Merged Requests" +msgstr "Свързани приложени заявки за сливане" + +msgid "Showing %d event" +msgid_plural "Showing %d events" +msgstr[0] "Показване на %d събитие" +msgstr[1] "Показване на %d събития" + +msgid "" +"The coding stage shows the time from the first commit to creating the merge " +"request. The data will automatically be added here once you create your " +"first merge request." +msgstr "" +"Етапът на програмиране показва времето от първото подаване до създаването на " +"заявката за сливане. Данните ще бъдат добавени тук автоматично след като " +"бъде създадена първата заявка за сливане." + +msgid "The collection of events added to the data gathered for that stage." +msgstr "Съвкупността от събития добавени към данните събрани за този етап." + +msgid "" +"The issue stage shows the time it takes from creating an issue to assigning " +"the issue to a milestone, or add the issue to a list on your Issue Board. " +"Begin creating issues to see data for this stage." +msgstr "" +"Етапът на проблемите показва колко е времето от създаването на проблем до " +"определянето на целеви етап на проекта за него, или до добавянето му в " +"списък на дъската за проблеми. Започнете да добавяте проблеми, за да видите " +"данните за този етап." + +msgid "The phase of the development lifecycle." +msgstr "Етапът от цикъла на разработка" + +msgid "" +"The planning stage shows the time from the previous step to pushing your " +"first commit. This time will be added automatically once you push your first " +"commit." +msgstr "" +"Етапът на планиране показва колко е времето от преходната стъпка до " +"изпращането на първото подаване. Това време ще бъде добавено автоматично " +"след като изпратите първото си подаване." + +msgid "" +"The production stage shows the total time it takes between creating an issue " +"and deploying the code to production. The data will be automatically added " +"once you have completed the full idea to production cycle." +msgstr "" +"Етапът на издаване показва общото време, което е нужно от създаването на " +"проблем до внедряването на кода в крайната версия." + +msgid "" +"The review stage shows the time from creating the merge request to merging " +"it. The data will automatically be added after you merge your first merge " +"request." +msgstr "" +"Етапът на преглед и одобрение показва времето от създаването на заявката за " +"сливане до прилагането ѝ. Данните ще бъдат добавени автоматично след като " +"приложите първата си заявка за сливане." + +msgid "" +"The staging stage shows the time between merging the MR and deploying code " +"to the production environment. The data will be automatically added once you " +"deploy to production for the first time." +msgstr "" +"Етапът на подготовка за издаване показва времето между прилагането на " +"заявката за сливане и внедряването на кода в средата на работещата крайна " +"версия. Данните ще бъдат добавени автоматично след като направите първото си " +"внедряване в крайната версия." + +msgid "" +"The testing stage shows the time GitLab CI takes to run every pipeline for " +"the related merge request. The data will automatically be added after your " +"first pipeline finishes running." +msgstr "" +"Етапът на тестване показва времето, което е нужно на „Gitlab CI“ да изпълни " +"всички задачи за свързаната заявка за сливане. Данните ще бъдат добавени " +"автоматично след като приключи изпълнените на първата Ви такава задача." + +msgid "The time taken by each data entry gathered by that stage." +msgstr "Времето, което отнема всеки запис от данни за съответния етап." + +msgid "" +"The value lying at the midpoint of a series of observed values. E.g., " +"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 =" +" 6." +msgstr "" +"Стойността, която се намира в средата на последователността от наблюдавани " +"данни. Например: медианата на 3, 5 и 9 е 5, а медианата на 3, 5, 7 и 8 е " +"(5+7)/2 = 6." + +msgid "Time before an issue gets scheduled" +msgstr "Време преди един проблем да бъде планиран за работа" + +msgid "Time before an issue starts implementation" +msgstr "Време преди работата по проблем да започне" + +msgid "Time between merge request creation and merge/close" +msgstr "" +"Време между създаване на заявка за сливане и прилагането/отхвърлянето ѝ" + +msgid "Time until first merge request" +msgstr "Време преди първата заявка за сливане" + +msgid "Time|hr" +msgid_plural "Time|hrs" +msgstr[0] "час" +msgstr[1] "часа" + +msgid "Time|min" +msgid_plural "Time|mins" +msgstr[0] "мин" +msgstr[1] "мин" + +msgid "Time|s" +msgstr "сек" + +msgid "Total Time" +msgstr "Общо време" + +msgid "Total test time for all commits/merges" +msgstr "Общо време за тестване на всички подавания/сливания" + +msgid "Want to see the data? Please ask an administrator for access." +msgstr "Искате ли да видите данните? Помолете администратор за достъп." + +msgid "We don't have enough data to show this stage." +msgstr "Няма достатъчно данни за този етап." + +msgid "You need permission." +msgstr "Нуждаете се от разрешение." + +msgid "day" +msgid_plural "days" +msgstr[0] "ден" +msgstr[1] "дни" + diff --git a/locale/bg/gitlab.po.time_stamp b/locale/bg/gitlab.po.time_stamp new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/locale/bg/gitlab.po.time_stamp diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po index 1c44ed4b77c..9a660571db9 100644 --- a/locale/de/gitlab.po +++ b/locale/de/gitlab.po @@ -17,14 +17,23 @@ msgstr "" "Last-Translator: \n" "X-Generator: Poedit 2.0.1\n" +msgid "Are you sure you want to delete this pipeline schedule?" +msgstr "" + msgid "ByAuthor|by" msgstr "Von" +msgid "Cancel" +msgstr "" + msgid "Commit" msgid_plural "Commits" msgstr[0] "Commit" msgstr[1] "Commits" +msgid "Cron Timezone" +msgstr "" + msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." msgstr "Cycle Analytics liefern einen Überblick darüber, wie viel Zeit in Ihrem Projekt von einer Idee bis zum Produktivdeployment vergeht." @@ -49,11 +58,32 @@ msgstr "Staging" msgid "CycleAnalyticsStage|Test" msgstr "Test" +msgid "Delete" +msgstr "" + msgid "Deploy" msgid_plural "Deploys" msgstr[0] "Deployment" msgstr[1] "Deployments" +msgid "Description" +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "Edit Pipeline Schedule %{id}" +msgstr "" + +msgid "Failed to change the owner" +msgstr "" + +msgid "Failed to remove the pipeline schedule" +msgstr "" + +msgid "Filter" +msgstr "" + msgid "FirstPushedBy|First" msgstr "Erster" @@ -66,6 +96,9 @@ msgstr "Vom Anlegen des Issues bis zum Produktivdeployment" msgid "From merge request merge until deploy to production" msgstr "Vom Merge Request bis zum Produktivdeployment" +msgid "Interval Pattern" +msgstr "" + msgid "Introducing Cycle Analytics" msgstr "Was sind Cycle Analytics?" @@ -74,6 +107,9 @@ msgid_plural "Last %d days" msgstr[0] "Letzter %d Tag" msgstr[1] "Letzten %d Tage" +msgid "Last Pipeline" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "Eingeschränkt auf maximal %d Ereignis" @@ -87,6 +123,12 @@ msgid_plural "New Issues" msgstr[0] "Neues Issue" msgstr[1] "Neue Issues" +msgid "New Pipeline Schedule" +msgstr "" + +msgid "No schedules" +msgstr "" + msgid "Not available" msgstr "Nicht verfügbar" @@ -96,9 +138,45 @@ msgstr "Nicht genügend Daten" msgid "OpenedNDaysAgo|Opened" msgstr "Erstellt" +msgid "Owner" +msgstr "" + msgid "Pipeline Health" msgstr "Pipeline Kennzahlen" +msgid "Pipeline Schedule" +msgstr "" + +msgid "Pipeline Schedules" +msgstr "" + +msgid "PipelineSchedules|Activated" +msgstr "" + +msgid "PipelineSchedules|Active" +msgstr "" + +msgid "PipelineSchedules|All" +msgstr "" + +msgid "PipelineSchedules|Inactive" +msgstr "" + +msgid "PipelineSchedules|Next Run" +msgstr "" + +msgid "PipelineSchedules|None" +msgstr "" + +msgid "PipelineSchedules|Provide a short description for this pipeline" +msgstr "" + +msgid "PipelineSchedules|Take ownership" +msgstr "" + +msgid "PipelineSchedules|Target" +msgstr "" + msgid "ProjectLifecycle|Stage" msgstr "Phase" @@ -123,11 +201,26 @@ msgstr "Zugehörige Merge Requests" msgid "Related Merged Requests" msgstr "Zugehörige abgeschlossene Merge Requests" +msgid "Save pipeline schedule" +msgstr "" + +msgid "Schedule a new pipeline" +msgstr "" + +msgid "Select a timezone" +msgstr "" + +msgid "Select target branch" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "Zeige %d Ereignis" msgstr[1] "Zeige %d Ereignisse" +msgid "Target Branch" +msgstr "" + msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." msgstr "Die Code-Phase stellt die Zeit vom ersten Commit bis zum Erstellen eines Merge Requests dar. Sobald Sie Ihren ersten Merge Request anlegen, werden dessen Daten automatisch ergänzt." diff --git a/locale/en/gitlab.po b/locale/en/gitlab.po index a43bafbbe28..4e44731fc5a 100644 --- a/locale/en/gitlab.po +++ b/locale/en/gitlab.po @@ -17,14 +17,23 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n != 1;\n" "\n" +msgid "Are you sure you want to delete this pipeline schedule?" +msgstr "" + msgid "ByAuthor|by" msgstr "" +msgid "Cancel" +msgstr "" + msgid "Commit" msgid_plural "Commits" msgstr[0] "" msgstr[1] "" +msgid "Cron Timezone" +msgstr "" + msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." msgstr "" @@ -49,11 +58,32 @@ msgstr "" msgid "CycleAnalyticsStage|Test" msgstr "" +msgid "Delete" +msgstr "" + msgid "Deploy" msgid_plural "Deploys" msgstr[0] "" msgstr[1] "" +msgid "Description" +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "Edit Pipeline Schedule %{id}" +msgstr "" + +msgid "Failed to change the owner" +msgstr "" + +msgid "Failed to remove the pipeline schedule" +msgstr "" + +msgid "Filter" +msgstr "" + msgid "FirstPushedBy|First" msgstr "" @@ -66,6 +96,9 @@ msgstr "" msgid "From merge request merge until deploy to production" msgstr "" +msgid "Interval Pattern" +msgstr "" + msgid "Introducing Cycle Analytics" msgstr "" @@ -74,6 +107,9 @@ msgid_plural "Last %d days" msgstr[0] "" msgstr[1] "" +msgid "Last Pipeline" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "" @@ -87,6 +123,12 @@ msgid_plural "New Issues" msgstr[0] "" msgstr[1] "" +msgid "New Pipeline Schedule" +msgstr "" + +msgid "No schedules" +msgstr "" + msgid "Not available" msgstr "" @@ -96,9 +138,45 @@ msgstr "" msgid "OpenedNDaysAgo|Opened" msgstr "" +msgid "Owner" +msgstr "" + msgid "Pipeline Health" msgstr "" +msgid "Pipeline Schedule" +msgstr "" + +msgid "Pipeline Schedules" +msgstr "" + +msgid "PipelineSchedules|Activated" +msgstr "" + +msgid "PipelineSchedules|Active" +msgstr "" + +msgid "PipelineSchedules|All" +msgstr "" + +msgid "PipelineSchedules|Inactive" +msgstr "" + +msgid "PipelineSchedules|Next Run" +msgstr "" + +msgid "PipelineSchedules|None" +msgstr "" + +msgid "PipelineSchedules|Provide a short description for this pipeline" +msgstr "" + +msgid "PipelineSchedules|Take ownership" +msgstr "" + +msgid "PipelineSchedules|Target" +msgstr "" + msgid "ProjectLifecycle|Stage" msgstr "" @@ -123,11 +201,26 @@ msgstr "" msgid "Related Merged Requests" msgstr "" +msgid "Save pipeline schedule" +msgstr "" + +msgid "Schedule a new pipeline" +msgstr "" + +msgid "Select a timezone" +msgstr "" + +msgid "Select target branch" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "" msgstr[1] "" +msgid "Target Branch" +msgstr "" + msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." msgstr "" diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po index b61846b9c7d..78d28d69885 100644 --- a/locale/es/gitlab.po +++ b/locale/es/gitlab.po @@ -7,24 +7,170 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2017-05-20 22:37-0500\n" +"PO-Revision-Date: 2017-06-07 12:29-0500\n" "Language-Team: Spanish\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"Last-Translator: \n" -"X-Generator: Poedit 2.0.1\n" +"Last-Translator: Bob Van Landuyt <bob@gitlab.com>\n" +"X-Generator: Poedit 2.0.2\n" + +msgid "About auto deploy" +msgstr "Acerca del auto despliegue" + +msgid "Activity" +msgstr "Actividad" + +msgid "Add Changelog" +msgstr "Agregar Changelog" + +msgid "Add Contribution guide" +msgstr "Agregar guía de contribución" + +msgid "Add License" +msgstr "Agregar Licencia" + +msgid "Add an SSH key to your profile to pull or push via SSH." +msgstr "Agregar una clave SSH a tu perfil para actualizar o enviar a través de SSH." + +msgid "Add new directory" +msgstr "Agregar nuevo directorio" + +msgid "Archived project! Repository is read-only" +msgstr "¡Proyecto archivado! El repositorio es de sólo lectura" + +msgid "Branch" +msgid_plural "Branches" +msgstr[0] "Rama" +msgstr[1] "Ramas" + +msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}" +msgstr "La rama <strong>%{branch_name}</strong> fue creada. Para configurar el auto despliegue, escoge una plantilla Yaml para GitLab CI y envía tus cambios. %{link_to_autodeploy_doc}" + +msgid "Branches" +msgstr "Ramas" msgid "ByAuthor|by" msgstr "por" +msgid "CI configuration" +msgstr "Configuración de CI" + +msgid "Changelog" +msgstr "Changelog" + +msgid "Charts" +msgstr "Gráficos" + +msgid "CiStatusLabel|canceled" +msgstr "cancelado" + +msgid "CiStatusLabel|created" +msgstr "creado" + +msgid "CiStatusLabel|failed" +msgstr "fallado" + +msgid "CiStatusLabel|manual action" +msgstr "acción manual" + +msgid "CiStatusLabel|passed" +msgstr "pasó" + +msgid "CiStatusLabel|passed with warnings" +msgstr "pasó con advertencias" + +msgid "CiStatusLabel|pending" +msgstr "pendiente" + +msgid "CiStatusLabel|skipped" +msgstr "omitido" + +msgid "CiStatusLabel|waiting for manual action" +msgstr "esperando acción manual" + +msgid "CiStatusText|blocked" +msgstr "bloqueado" + +msgid "CiStatusText|canceled" +msgstr "cancelado" + +msgid "CiStatusText|created" +msgstr "creado" + +msgid "CiStatusText|failed" +msgstr "fallado" + +msgid "CiStatusText|manual" +msgstr "manual" + +msgid "CiStatusText|passed" +msgstr "pasó" + +msgid "CiStatusText|pending" +msgstr "pendiente" + +msgid "CiStatusText|skipped" +msgstr "omitido" + +msgid "CiStatus|running" +msgstr "en ejecución" + msgid "Commit" msgid_plural "Commits" msgstr[0] "Cambio" msgstr[1] "Cambios" +msgid "CommitMessage|Add %{file_name}" +msgstr "Agregar %{file_name}" + +msgid "Commits" +msgstr "Cambios" + +msgid "Commits|History" +msgstr "Historial" + +msgid "Compare" +msgstr "Comparar" + +msgid "Contribution guide" +msgstr "Guía de contribución" + +msgid "Contributors" +msgstr "Contribuidores" + +msgid "Copy URL to clipboard" +msgstr "Copiar URL al portapapeles" + +msgid "Copy commit SHA to clipboard" +msgstr "Copiar SHA del cambio al portapapeles" + +msgid "Create New Directory" +msgstr "Crear Nuevo Directorio" + +msgid "Create directory" +msgstr "Crear directorio" + +msgid "Create empty bare repository" +msgstr "Crear repositorio vacío" + +msgid "Create merge request" +msgstr "Crear solicitud de fusión" + +msgid "CreateNewFork|Fork" +msgstr "Bifurcar" + +msgid "Custom notification events" +msgstr "Eventos de notificaciones personalizadas" + +msgid "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 %{notification_link}." +msgstr "Los niveles de notificación personalizados son los mismos que los niveles participantes. Con los niveles de notificación personalizados, también recibirá notificaciones para eventos seleccionados. Para obtener más información, consulte %{notification_link}." + +msgid "Cycle Analytics" +msgstr "Cycle Analytics" + msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." msgstr "Cycle Analytics ofrece una visión general de cuánto tiempo tarda en pasar de idea a producción en su proyecto." @@ -43,7 +189,6 @@ msgstr "Producción" msgid "CycleAnalyticsStage|Review" msgstr "Revisión" -#, fuzzy msgid "CycleAnalyticsStage|Staging" msgstr "Puesta en escena" @@ -55,26 +200,98 @@ msgid_plural "Deploys" msgstr[0] "Despliegue" msgstr[1] "Despliegues" +msgid "Directory name" +msgstr "Nombre del directorio" + +msgid "Don't show again" +msgstr "No mostrar de nuevo" + +msgid "Download tar" +msgstr "Descargar tar" + +msgid "Download tar.bz2" +msgstr "Descargar tar.bz2" + +msgid "Download tar.gz" +msgstr "Descargar tar.gz" + +msgid "Download zip" +msgstr "Descargar zip" + +msgid "DownloadArtifacts|Download" +msgstr "Descargar" + +msgid "DownloadSource|Download" +msgstr "Descargar" + +msgid "Files" +msgstr "Archivos" + +msgid "Find by path" +msgstr "Buscar por ruta" + +msgid "Find file" +msgstr "Buscar archivo" + msgid "FirstPushedBy|First" msgstr "Primer" msgid "FirstPushedBy|pushed by" msgstr "enviado por" +msgid "ForkedFromProjectPath|Forked from" +msgstr "Bifurcado de" + +msgid "Forks" +msgstr "Bifurcaciones" + msgid "From issue creation until deploy to production" msgstr "Desde la creación de la incidencia hasta el despliegue a producción" msgid "From merge request merge until deploy to production" msgstr "Desde la integración de la solicitud de fusión hasta el despliegue a producción" +msgid "Go to your fork" +msgstr "Ir a tu bifurcación" + +msgid "GoToYourFork|Fork" +msgstr "Bifurcación" + +msgid "Home" +msgstr "Inicio" + +msgid "Housekeeping successfully started" +msgstr "Servicio de limpieza iniciado con éxito" + +msgid "Import repository" +msgstr "Importar repositorio" + msgid "Introducing Cycle Analytics" msgstr "Introducción a Cycle Analytics" +msgid "LFSStatus|Disabled" +msgstr "Deshabilitado" + +msgid "LFSStatus|Enabled" +msgstr "Habilitado" + msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "Último %d día" msgstr[1] "Últimos %d días" +msgid "Last Update" +msgstr "Última actualización" + +msgid "Last commit" +msgstr "Último cambio" + +msgid "Leave group" +msgstr "Abandonar grupo" + +msgid "Leave project" +msgstr "Abandonar proyecto" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "Limitado a mostrar máximo %d evento" @@ -83,29 +300,167 @@ msgstr[1] "Limitado a mostrar máximo %d eventos" msgid "Median" msgstr "Mediana" +msgid "MissingSSHKeyWarningLink|add an SSH key" +msgstr "agregar una clave SSH" + msgid "New Issue" msgid_plural "New Issues" msgstr[0] "Nueva incidencia" msgstr[1] "Nuevas incidencias" +msgid "New branch" +msgstr "Nueva rama" + +msgid "New directory" +msgstr "Nuevo directorio" + +msgid "New file" +msgstr "Nuevo archivo" + +msgid "New issue" +msgstr "Nueva incidencia" + +msgid "New merge request" +msgstr "Nueva solicitud de fusión" + +msgid "New snippet" +msgstr "Nuevo fragmento de código" + +msgid "New tag" +msgstr "Nueva etiqueta" + +msgid "No repository" +msgstr "No hay repositorio" + msgid "Not available" msgstr "No disponible" msgid "Not enough data" msgstr "No hay suficientes datos" +msgid "Notification events" +msgstr "Eventos de notificación" + +msgid "NotificationEvent|Close issue" +msgstr "Cerrar incidencia" + +msgid "NotificationEvent|Close merge request" +msgstr "Cerrar solicitud de fusión" + +msgid "NotificationEvent|Failed pipeline" +msgstr "Pipeline fallido" + +msgid "NotificationEvent|Merge merge request" +msgstr "Integrar solicitud de fusión" + +msgid "NotificationEvent|New issue" +msgstr "Nueva incidencia" + +msgid "NotificationEvent|New merge request" +msgstr "Nueva solicitud de fusión" + +msgid "NotificationEvent|New note" +msgstr "Nueva nota" + +msgid "NotificationEvent|Reassign issue" +msgstr "Reasignar incidencia" + +msgid "NotificationEvent|Reassign merge request" +msgstr "Reasignar solicitud de fusión" + +msgid "NotificationEvent|Reopen issue" +msgstr "Reabrir incidencia" + +msgid "NotificationEvent|Successful pipeline" +msgstr "Pipeline exitoso" + +msgid "NotificationLevel|Custom" +msgstr "Personalizado" + +msgid "NotificationLevel|Disabled" +msgstr "Deshabilitado" + +msgid "NotificationLevel|Global" +msgstr "Global" + +msgid "NotificationLevel|On mention" +msgstr "Cuando me mencionan" + +msgid "NotificationLevel|Participate" +msgstr "Participación" + +msgid "NotificationLevel|Watch" +msgstr "Vigilancia" + msgid "OpenedNDaysAgo|Opened" msgstr "Abierto" msgid "Pipeline Health" msgstr "Estado del Pipeline" +msgid "Project '%{project_name}' queued for deletion." +msgstr "Proyecto ‘%{project_name}’ en cola para eliminación." + +msgid "Project '%{project_name}' was successfully created." +msgstr "Proyecto ‘%{project_name}’ fue creado satisfactoriamente." + +msgid "Project '%{project_name}' was successfully updated." +msgstr "Proyecto ‘%{project_name}’ fue actualizado satisfactoriamente." + +msgid "Project '%{project_name}' will be deleted." +msgstr "Proyecto ‘%{project_name}’ será eliminado." + +msgid "Project access must be granted explicitly to each user." +msgstr "El acceso al proyecto debe concederse explícitamente a cada usuario." + +msgid "Project export could not be deleted." +msgstr "No se pudo eliminar la exportación del proyecto." + +msgid "Project export has been deleted." +msgstr "La exportación del proyecto ha sido eliminada." + +msgid "Project export link has expired. Please generate a new export from your project settings." +msgstr "El enlace de exportación del proyecto ha caducado. Por favor, genera una nueva exportación desde la configuración del proyecto." + +msgid "Project export started. A download link will be sent by email." +msgstr "Se inició la exportación del proyecto. Se enviará un enlace de descarga por correo electrónico." + +msgid "Project home" +msgstr "Inicio del proyecto" + +msgid "ProjectFeature|Disabled" +msgstr "Deshabilitada" + +msgid "ProjectFeature|Everyone with access" +msgstr "Todos con acceso" + +msgid "ProjectFeature|Only team members" +msgstr "Solo miembros del equipo" + +msgid "ProjectFileTree|Name" +msgstr "Nombre" + +msgid "ProjectLastActivity|Never" +msgstr "Nunca" + msgid "ProjectLifecycle|Stage" msgstr "Etapa" +msgid "ProjectNetworkGraph|Graph" +msgstr "Historial gráfico" + msgid "Read more" msgstr "Leer más" +msgid "Readme" +msgstr "Readme" + +msgid "RefSwitcher|Branches" +msgstr "Ramas" + +msgid "RefSwitcher|Tags" +msgstr "Etiquetas" + msgid "Related Commits" msgstr "Cambios Relacionados" @@ -124,17 +479,67 @@ msgstr "Solicitudes de fusión Relacionadas" msgid "Related Merged Requests" msgstr "Solicitudes de fusión Relacionadas" +msgid "Remind later" +msgstr "Recordar después" + +msgid "Remove project" +msgstr "Eliminar proyecto" + +msgid "Request Access" +msgstr "Solicitar acceso" + +msgid "Search branches and tags" +msgstr "Buscar ramas y etiquetas" + +msgid "Select Archive Format" +msgstr "Seleccionar formato de archivo" + +msgid "Set a password on your account to pull or push via %{protocol}" +msgstr "Establezca una contraseña en su cuenta para actualizar o enviar a través de% {protocol}" + +msgid "Set up CI" +msgstr "Configurar CI" + +msgid "Set up Koding" +msgstr "Configurar Koding" + +msgid "Set up auto deploy" +msgstr "Configurar auto despliegue" + +msgid "SetPasswordToCloneLink|set a password" +msgstr "establecer una contraseña" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "Mostrando %d evento" msgstr[1] "Mostrando %d eventos" +msgid "Source code" +msgstr "Código fuente" + +msgid "StarProject|Star" +msgstr "Destacar" + +msgid "Switch branch/tag" +msgstr "Cambiar rama/etiqueta" + +msgid "Tag" +msgid_plural "Tags" +msgstr[0] "Etiqueta" +msgstr[1] "Etiquetas" + +msgid "Tags" +msgstr "Etiquetas" + msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." msgstr "La etapa de desarrollo muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquí una vez creada tu primera solicitud de fusión." msgid "The collection of events added to the data gathered for that stage." msgstr "La colección de eventos agregados a los datos recopilados para esa etapa." +msgid "The fork relationship has been removed." +msgstr "La relación con la bifurcación se ha eliminado." + msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." msgstr "La etapa de incidencia muestra el tiempo que toma desde la creación de un tema hasta asignar el tema a un hito, o añadir el tema a una lista en el panel de temas. Empieza a crear temas para ver los datos de esta etapa." @@ -147,6 +552,15 @@ msgstr "La etapa de planificación muestra el tiempo desde el paso anterior hast msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle." msgstr "La etapa de producción muestra el tiempo total que tarda entre la creación de una incidencia y el despliegue del código a producción. Los datos se añadirán automáticamente una vez haya finalizado por completo el ciclo de idea a producción." +msgid "The project can be accessed by any logged in user." +msgstr "El proyecto puede ser accedido por cualquier usuario conectado." + +msgid "The project can be accessed without any authentication." +msgstr "El proyecto puede accederse sin ninguna autenticación." + +msgid "The repository for this project does not exist." +msgstr "El repositorio para este proyecto no existe." + msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." msgstr "La etapa de revisión muestra el tiempo desde la creación de la solicitud de fusión hasta que los cambios se fusionaron. Los datos se añadirán automáticamente después de fusionar su primera solicitud de fusión." @@ -162,6 +576,9 @@ msgstr "El tiempo utilizado por cada entrada de datos obtenido por esa etapa." msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6." msgstr "El valor en el punto medio de una serie de valores observados. Por ejemplo, entre 3, 5, 9, la mediana es 5. Entre 3, 5, 7, 8, la mediana es (5 + 7) / 2 = 6." +msgid "This means you can not push code until you create an empty repository or import existing one." +msgstr "Esto significa que no puede enviar código hasta que cree un repositorio vacío o importe uno existente." + msgid "Time before an issue gets scheduled" msgstr "Tiempo antes de que una incidencia sea programada" @@ -174,6 +591,129 @@ msgstr "Tiempo entre la creación de la solicitud de fusión y la integración o msgid "Time until first merge request" msgstr "Tiempo hasta la primera solicitud de fusión" +msgid "Timeago|%s days ago" +msgstr "hace %s días" + +msgid "Timeago|%s days remaining" +msgstr "%s días restantes" + +msgid "Timeago|%s hours remaining" +msgstr "%s horas restantes" + +msgid "Timeago|%s minutes ago" +msgstr "hace %s minutos" + +msgid "Timeago|%s minutes remaining" +msgstr "%s minutos restantes" + +msgid "Timeago|%s months ago" +msgstr "hace %s meses" + +msgid "Timeago|%s months remaining" +msgstr "%s meses restantes" + +msgid "Timeago|%s seconds remaining" +msgstr "%s segundos restantes" + +msgid "Timeago|%s weeks ago" +msgstr "hace %s semanas" + +msgid "Timeago|%s weeks remaining" +msgstr "%s semanas restantes" + +msgid "Timeago|%s years ago" +msgstr "hace %s años" + +msgid "Timeago|%s years remaining" +msgstr "%s años restantes" + +msgid "Timeago|1 day remaining" +msgstr "1 día restante" + +msgid "Timeago|1 hour remaining" +msgstr "1 hora restante" + +msgid "Timeago|1 minute remaining" +msgstr "1 minuto restante" + +msgid "Timeago|1 month remaining" +msgstr "1 mes restante" + +msgid "Timeago|1 week remaining" +msgstr "1 semana restante" + +msgid "Timeago|1 year remaining" +msgstr "1 año restante" + +msgid "Timeago|Past due" +msgstr "Atrasado" + +msgid "Timeago|a day ago" +msgstr "hace un día" + +msgid "Timeago|a month ago" +msgstr "hace 1 mes" + +msgid "Timeago|a week ago" +msgstr "hace 1 semana" + +msgid "Timeago|a while" +msgstr "hace un momento" + +msgid "Timeago|a year ago" +msgstr "hace 1 año" + +msgid "Timeago|about %s hours ago" +msgstr "hace alrededor de %s horas" + +msgid "Timeago|about a minute ago" +msgstr "hace alrededor de 1 minuto" + +msgid "Timeago|about an hour ago" +msgstr "hace alrededor de 1 hora" + +msgid "Timeago|in %s days" +msgstr "en %s días" + +msgid "Timeago|in %s hours" +msgstr "en %s horas" + +msgid "Timeago|in %s minutes" +msgstr "en %s minutos" + +msgid "Timeago|in %s months" +msgstr "en %s meses" + +msgid "Timeago|in %s seconds" +msgstr "en %s segundos" + +msgid "Timeago|in %s weeks" +msgstr "en %s semanas" + +msgid "Timeago|in %s years" +msgstr "en %s años" + +msgid "Timeago|in 1 day" +msgstr "en 1 día" + +msgid "Timeago|in 1 hour" +msgstr "en 1 hora" + +msgid "Timeago|in 1 minute" +msgstr "en 1 minuto" + +msgid "Timeago|in 1 month" +msgstr "en 1 mes" + +msgid "Timeago|in 1 week" +msgstr "en 1 semana" + +msgid "Timeago|in 1 year" +msgstr "en 1 año" + +msgid "Timeago|less than a minute ago" +msgstr "hace menos de 1 minuto" + msgid "Time|hr" msgid_plural "Time|hrs" msgstr[0] "hr" @@ -193,16 +733,91 @@ msgstr "Tiempo Total" msgid "Total test time for all commits/merges" msgstr "Tiempo total de pruebas para todos los cambios o integraciones" +msgid "Unstar" +msgstr "No Destacar" + +msgid "Upload New File" +msgstr "Subir nuevo archivo" + +msgid "Upload file" +msgstr "Subir archivo" + +msgid "Use your global notification setting" +msgstr "Utiliza tu configuración de notificación global" + +msgid "VisibilityLevel|Internal" +msgstr "Interno" + +msgid "VisibilityLevel|Private" +msgstr "Privado" + +msgid "VisibilityLevel|Public" +msgstr "Público" + msgid "Want to see the data? Please ask an administrator for access." msgstr "¿Quieres ver los datos? Por favor pide acceso al administrador." msgid "We don't have enough data to show this stage." msgstr "No hay suficientes datos para mostrar en esta etapa." +msgid "Withdraw Access Request" +msgstr "Retirar Solicitud de Acceso" + +msgid "" +"You are going to remove %{project_name_with_namespace}.\n" +"Removed project CANNOT be restored!\n" +"Are you ABSOLUTELY sure?" +msgstr "" +"Va a eliminar %{project_name_with_namespace}.\n" +"¡El proyecto eliminado NO puede ser restaurado!\n" +"¿Estás TOTALMENTE seguro?" + +msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?" +msgstr "Vas a eliminar el enlace de la bifurcación con el proyecto original %{forked_from_project}. ¿Estás TOTALMENTE seguro?" + +msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?" +msgstr "Vas a transferir %{project_name_with_namespace} a otro propietario. ¿Estás TOTALMENTE seguro?" + +msgid "You can only add files when you are on a branch" +msgstr "Sólo puede agregar archivos cuando estas en una rama" + +msgid "You must sign in to star a project" +msgstr "Debes iniciar sesión para destacar un proyecto" + msgid "You need permission." msgstr "Necesitas permisos." +msgid "You will not get any notifications via email" +msgstr "No recibirás ninguna notificación por correo electrónico" + +msgid "You will only receive notifications for the events you choose" +msgstr "Solo recibirás notificaciones de los eventos que elijas" + +msgid "You will only receive notifications for threads you have participated in" +msgstr "Solo recibirás notificaciones de los temas en los que has participado" + +msgid "You will receive notifications for any activity" +msgstr "Recibirás notificaciones para cualquier actividad" + +msgid "You will receive notifications only for comments in which you were @mentioned" +msgstr "Recibirás notificaciones sólo para los comentarios en los que se te mencionó" + +msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account" +msgstr "No podrás actualizar o enviar código al proyecto a través de %{protocol} hasta que %{set_password_link} en tu cuenta" + +msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile" +msgstr "No podrás actualizar o enviar código al proyecto a través de SSH hasta que %{add_ssh_key_link} en su perfil" + +msgid "Your name" +msgstr "Tu nombre" + +msgid "committed" +msgstr "cambió" + msgid "day" msgid_plural "days" msgstr[0] "día" msgstr[1] "días" + +msgid "notification emails" +msgstr "correos electrónicos de notificación" diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po new file mode 100644 index 00000000000..2000fa433b4 --- /dev/null +++ b/locale/fr/gitlab.po @@ -0,0 +1,207 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the gitlab package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# Dremor <egeorget@opmbx.org>, 2017. #zanata +msgid "" +msgstr "" +"Project-Id-Version: gitlab 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2017-06-14 04:21-0400\n" +"Last-Translator: Dremor <egeorget@opmbx.org>\n" +"Language-Team: French (https://www.transifex.com/gitlab-fr/teams/75145/fr/)\n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Zanata 3.9.6\n" + +msgid "ByAuthor|by" +msgstr "par" + +msgid "Commit" +msgid_plural "Commits" +msgstr[0] "Validation" +msgstr[1] "Validations" + +msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." +msgstr "L’analyseur de cycle permet d’avoir une vue d’ensemble du temps nécessaire pour aller d’une idée à sa mise en production pour votre projet." + +msgid "CycleAnalyticsStage|Code" +msgstr "Code" + +msgid "CycleAnalyticsStage|Issue" +msgstr "Incident" + +msgid "CycleAnalyticsStage|Plan" +msgstr "Planification" + +msgid "CycleAnalyticsStage|Production" +msgstr "Production" + +msgid "CycleAnalyticsStage|Review" +msgstr "Examen" + +msgid "CycleAnalyticsStage|Staging" +msgstr "Pré-production" + +msgid "CycleAnalyticsStage|Test" +msgstr "Test" + +msgid "Deploy" +msgid_plural "Deploys" +msgstr[0] "Déploiement" +msgstr[1] "Déploiements" + +msgid "FirstPushedBy|First" +msgstr "En premier" + +msgid "FirstPushedBy|pushed by" +msgstr "poussé par" + +msgid "From issue creation until deploy to production" +msgstr "Depuis la création de l'incident jusqu'au déploiement en production" + +msgid "From merge request merge until deploy to production" +msgstr "Depuis la fusion de la demande de fusion jusqu'au déploiement en production" + +msgid "Introducing Cycle Analytics" +msgstr "Introduction à l'analyseur de cycle" + +msgid "Last %d day" +msgid_plural "Last %d days" +msgstr[0] "Le dernier %d jour" +msgstr[1] "Les derniers %d jours" + +msgid "Limited to showing %d event at most" +msgid_plural "Limited to showing %d events at most" +msgstr[0] "Limiter l'affichage au plus à %d évènement" +msgstr[1] "Limiter l'affichage au plus à %d évènements" + +msgid "Median" +msgstr "Médian" + +msgid "New Issue" +msgid_plural "New Issues" +msgstr[0] "Nouvel incident" +msgstr[1] "Nouveaux incidents" + +msgid "Not available" +msgstr "Indisponible" + +msgid "Not enough data" +msgstr "Données insuffisantes" + +msgid "OpenedNDaysAgo|Opened" +msgstr "Ouvert" + +msgid "Pipeline Health" +msgstr "Santé du Pipeline" + +msgid "ProjectLifecycle|Stage" +msgstr "Étape" + +msgid "Read more" +msgstr "Lire plus" + +msgid "Related Commits" +msgstr "Validations liés" + +msgid "Related Deployed Jobs" +msgstr "Tâches de déploiement liés" + +msgid "Related Issues" +msgstr "Incidents liés" + +msgid "Related Jobs" +msgstr "Tâches liées" + +msgid "Related Merge Requests" +msgstr "Demandes de fusion liées" + +msgid "Related Merged Requests" +msgstr "Demandes fusionnées liées" + +msgid "Showing %d event" +msgid_plural "Showing %d events" +msgstr[0] "Affichage de %d évènement" +msgstr[1] "Affichage de %d évènements" + +msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." +msgstr "L’étape de développement montre le temps entre la première validation et la création de la demande de fusion. Les données seront automatiquement ajoutées ici une fois que vous aurez créé votre première demande de fusion." + +msgid "The collection of events added to the data gathered for that stage." +msgstr "L’ensemble d’évènements ajoutés aux données récupérées pour cette étape." + +msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." +msgstr "L'étape des incidents montre le temps nécessaire entre la création d'un incident et son assignation à un jalon, ou son ajout à une liste d'un tableau d'incident. Débutez à créer des incidents pour voir des données pour cette étape." + +msgid "The phase of the development lifecycle." +msgstr "Les étapes du cycle de développement." + +msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit." +msgstr "L’étape de planification montre le temps entre l’étape précédente et l’envoi de votre première validation. Ce temps sera automatiquement ajouté quand vous pousserez votre première validation." + +msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle." +msgstr "L’étape de mise en production montre le temps nécessaire entre la création d’un incident et le déploiement du code en production. Les données seront automatiquement ajoutées une fois que vous aurez complété le cycle complet, depuis l’idée jusqu’à la mise en production." + +msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." +msgstr "L’étape d’évaluation montre le temps entre la création de la demande de fusion et la fusion effective de celle-ci. Ces données seront automatiquement ajoutées après que vous ayez fusionné votre première demande de fusion." + +msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time." +msgstr "L’étape de pré-production indique le temps entre la fusion de la RF et le déploiement du code dans l’environnent de production. Les données seront automatiquement ajoutées une fois que vous déploierez en production pour la première fois." + +msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running." +msgstr "L’étape de test montre le temps que le CI de GitLab met pour exécuter chaque pipeline liés à la demande de fusion. Les données seront automatiquement ajoutées après que votre premier pipeline s’achèvera." + +msgid "The time taken by each data entry gathered by that stage." +msgstr "Le temps pris par chaque entrée récoltée durant cette étape." + +msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6." +msgstr "La valeur située au point médian d’une série de valeur observée. C.à.d., entre 3, 5, 9, le médian est 5. Entre 3, 5, 7, 8, le médian est (5+7)/2 = 6." + +msgid "Time before an issue gets scheduled" +msgstr "Temps avant qu’un incident ne soit planifié" + +msgid "Time before an issue starts implementation" +msgstr "Temps avant que résolution ne débute" + +msgid "Time between merge request creation and merge/close" +msgstr "Temps entre la création d'une demande de fusion et sa fusion/clôture" + +msgid "Time until first merge request" +msgstr "Temps jusqu’à la première demande de fusion" + +msgid "Time|hr" +msgid_plural "Time|hrs" +msgstr[0] "hr" +msgstr[1] "hrs" + +msgid "Time|min" +msgid_plural "Time|mins" +msgstr[0] "min" +msgstr[1] "mins" + +msgid "Time|s" +msgstr "s" + +msgid "Total Time" +msgstr "Temps total" + +msgid "Total test time for all commits/merges" +msgstr "Temps total de test pour toutes les validations/fusions" + +msgid "Want to see the data? Please ask an administrator for access." +msgstr "Vous voulez voir les données ? Merci de contacter un administrateur pour en obtenir l’accès." + +msgid "We don't have enough data to show this stage." +msgstr "Nous n'avons pas suffisamment de données pour afficher cette étape." + +msgid "You need permission." +msgstr "Vous avez besoin d’une autorisation." + +msgid "day" +msgid_plural "days" +msgstr[0] "jour" +msgstr[1] "jours" diff --git a/locale/fr/gitlab.po.time_stamp b/locale/fr/gitlab.po.time_stamp new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/locale/fr/gitlab.po.time_stamp diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3967d40ea9e..050f6c446c1 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-05-04 19:24-0500\n" -"PO-Revision-Date: 2017-05-04 19:24-0500\n" +"POT-Creation-Date: 2017-06-07 21:22+0200\n" +"PO-Revision-Date: 2017-06-07 21:22+0200\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -18,14 +18,23 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" +msgid "Are you sure you want to delete this pipeline schedule?" +msgstr "" + msgid "ByAuthor|by" msgstr "" +msgid "Cancel" +msgstr "" + msgid "Commit" msgid_plural "Commits" msgstr[0] "" msgstr[1] "" +msgid "Cron Timezone" +msgstr "" + msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." msgstr "" @@ -50,11 +59,32 @@ msgstr "" msgid "CycleAnalyticsStage|Test" msgstr "" +msgid "Delete" +msgstr "" + msgid "Deploy" msgid_plural "Deploys" msgstr[0] "" msgstr[1] "" +msgid "Description" +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "Edit Pipeline Schedule %{id}" +msgstr "" + +msgid "Failed to change the owner" +msgstr "" + +msgid "Failed to remove the pipeline schedule" +msgstr "" + +msgid "Filter" +msgstr "" + msgid "FirstPushedBy|First" msgstr "" @@ -67,6 +97,9 @@ msgstr "" msgid "From merge request merge until deploy to production" msgstr "" +msgid "Interval Pattern" +msgstr "" + msgid "Introducing Cycle Analytics" msgstr "" @@ -75,6 +108,9 @@ msgid_plural "Last %d days" msgstr[0] "" msgstr[1] "" +msgid "Last Pipeline" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "" @@ -88,6 +124,12 @@ msgid_plural "New Issues" msgstr[0] "" msgstr[1] "" +msgid "New Pipeline Schedule" +msgstr "" + +msgid "No schedules" +msgstr "" + msgid "Not available" msgstr "" @@ -97,9 +139,45 @@ msgstr "" msgid "OpenedNDaysAgo|Opened" msgstr "" +msgid "Owner" +msgstr "" + msgid "Pipeline Health" msgstr "" +msgid "Pipeline Schedule" +msgstr "" + +msgid "Pipeline Schedules" +msgstr "" + +msgid "PipelineSchedules|Activated" +msgstr "" + +msgid "PipelineSchedules|Active" +msgstr "" + +msgid "PipelineSchedules|All" +msgstr "" + +msgid "PipelineSchedules|Inactive" +msgstr "" + +msgid "PipelineSchedules|Next Run" +msgstr "" + +msgid "PipelineSchedules|None" +msgstr "" + +msgid "PipelineSchedules|Provide a short description for this pipeline" +msgstr "" + +msgid "PipelineSchedules|Take ownership" +msgstr "" + +msgid "PipelineSchedules|Target" +msgstr "" + msgid "ProjectLifecycle|Stage" msgstr "" @@ -124,11 +202,26 @@ msgstr "" msgid "Related Merged Requests" msgstr "" +msgid "Save pipeline schedule" +msgstr "" + +msgid "Schedule a new pipeline" +msgstr "" + +msgid "Select a timezone" +msgstr "" + +msgid "Select target branch" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "" msgstr[1] "" +msgid "Target Branch" +msgstr "" + msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." msgstr "" diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po new file mode 100644 index 00000000000..5ad41f92b64 --- /dev/null +++ b/locale/pt_BR/gitlab.po @@ -0,0 +1,260 @@ +# Alexandre Alencar <alexandre.alencar@gmail.com>, 2017. #zanata +# Fabio Beneditto <fabiobeneditto@gmail.com>, 2017. #zanata +# Leandro Nunes dos Santos <leandronunes@gmail.com>, 2017. #zanata +msgid "" +msgstr "" +"Project-Id-Version: gitlab 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-05-04 19:24-0500\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2017-06-05 03:29-0400\n" +"Last-Translator: Alexandre Alencar <alexandre.alencar@gmail.com>\n" +"Language-Team: Portuguese (Brazil)\n" +"Language: pt-BR\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgid "ByAuthor|by" +msgstr "por" + +msgid "Commit" +msgid_plural "Commits" +msgstr[0] "Commit" +msgstr[1] "Commits" + +msgid "" +"Cycle Analytics gives an overview of how much time it takes to go from idea " +"to production in your project." +msgstr "" +"A Análise de Ciclo fornece uma visão geral de quanto tempo uma ideia demora " +"para ir para produção em seu projeto." + +msgid "CycleAnalyticsStage|Code" +msgstr "Código" + +msgid "CycleAnalyticsStage|Issue" +msgstr "Tarefa" + +msgid "CycleAnalyticsStage|Plan" +msgstr "Plano" + +msgid "CycleAnalyticsStage|Production" +msgstr "Produção" + +msgid "CycleAnalyticsStage|Review" +msgstr "Revisão" + +msgid "CycleAnalyticsStage|Staging" +msgstr "Homologação" + +msgid "CycleAnalyticsStage|Test" +msgstr "Teste" + +msgid "Deploy" +msgid_plural "Deploys" +msgstr[0] "Implantação" +msgstr[1] "Implantações" + +msgid "FirstPushedBy|First" +msgstr "Primeiro" + +msgid "FirstPushedBy|pushed by" +msgstr "publicado por" + +msgid "From issue creation until deploy to production" +msgstr "Da criação de tarefas até a implantação para a produção" + +msgid "From merge request merge until deploy to production" +msgstr "Da incorporação do merge request até a implantação em produção" + +msgid "Introducing Cycle Analytics" +msgstr "Apresentando a Análise de Ciclo" + +msgid "Last %d day" +msgid_plural "Last %d days" +msgstr[0] "Último %d dia" +msgstr[1] "Últimos %d dias" + +msgid "Limited to showing %d event at most" +msgid_plural "Limited to showing %d events at most" +msgstr[0] "Limitado a mostrar %d evento no máximo" +msgstr[1] "Limitado a mostrar %d eventos no máximo" + +msgid "Median" +msgstr "Mediana" + +msgid "New Issue" +msgid_plural "New Issues" +msgstr[0] "Nova Tarefa" +msgstr[1] "Novas Tarefas" + +msgid "Not available" +msgstr "Não disponível" + +msgid "Not enough data" +msgstr "Dados insuficientes" + +msgid "OpenedNDaysAgo|Opened" +msgstr "Aberto" + +msgid "Pipeline Health" +msgstr "Saúde da Pipeline" + +msgid "ProjectLifecycle|Stage" +msgstr "Etapa" + +msgid "Read more" +msgstr "Ler mais" + +msgid "Related Commits" +msgstr "Commits Relacionados" + +msgid "Related Deployed Jobs" +msgstr "Jobs Relacionados Incorporados" + +msgid "Related Issues" +msgstr "Tarefas Relacionadas" + +msgid "Related Jobs" +msgstr "Jobs Relacionados" + +msgid "Related Merge Requests" +msgstr "Merge Requests Relacionados" + +msgid "Related Merged Requests" +msgstr "Merge Requests Relacionados" + +msgid "Showing %d event" +msgid_plural "Showing %d events" +msgstr[0] "Mostrando %d evento" +msgstr[1] "Mostrando %d eventos" + +msgid "" +"The coding stage shows the time from the first commit to creating the merge " +"request. The data will automatically be added here once you create your " +"first merge request." +msgstr "" +"O estágio de codificação mostra o tempo desde o primeiro commit até a " +"criação do merge request. \n" +"Os dados serão automaticamente adicionados aqui uma vez que você tenha " +"criado seu primeiro merge request." + +msgid "The collection of events added to the data gathered for that stage." +msgstr "" +"A coleção de eventos adicionados aos dados coletados para esse estágio." + +msgid "" +"The issue stage shows the time it takes from creating an issue to assigning " +"the issue to a milestone, or add the issue to a list on your Issue Board. " +"Begin creating issues to see data for this stage." +msgstr "" +"O estágio em questão mostra o tempo que leva desde a criação de uma tarefa " +"até a sua assinatura para um milestone, ou a sua adição para a lista no seu " +"Painel de Tarefas. Comece a criar tarefas para ver dados para esta etapa." + +msgid "The phase of the development lifecycle." +msgstr "A fase do ciclo de vida do desenvolvimento." + +msgid "" +"The planning stage shows the time from the previous step to pushing your " +"first commit. This time will be added automatically once you push your first " +"commit." +msgstr "" +"A fase de planejamento mostra o tempo do passo anterior até empurrar o seu " +"primeiro commit. Este tempo será adicionado automaticamente assim que você " +"realizar seu primeiro commit." + +msgid "" +"The production stage shows the total time it takes between creating an issue " +"and deploying the code to production. The data will be automatically added " +"once you have completed the full idea to production cycle." +msgstr "" +"O estágio de produção mostra o tempo total que leva entre criar uma tarefa e " +"implantar o código na produção. Os dados serão adicionados automaticamente " +"até que você complete todo o ciclo de produção." + +msgid "" +"The review stage shows the time from creating the merge request to merging " +"it. The data will automatically be added after you merge your first merge " +"request." +msgstr "" +"A etapa de revisão mostra o tempo de criação de um merge request até que o " +"merge seja feito. Os dados serão automaticamente adicionados depois que você " +"fizer seu primeiro merge request." + +msgid "" +"The staging stage shows the time between merging the MR and deploying code " +"to the production environment. The data will be automatically added once you " +"deploy to production for the first time." +msgstr "" +"O estágio de estágio mostra o tempo entre a fusão do MR e o código de " +"implantação para o ambiente de produção. Os dados serão automaticamente " +"adicionados depois de implantar na produção pela primeira vez." + +msgid "" +"The testing stage shows the time GitLab CI takes to run every pipeline for " +"the related merge request. The data will automatically be added after your " +"first pipeline finishes running." +msgstr "" +"A fase de teste mostra o tempo que o GitLab CI leva para executar cada " +"pipeline para o merge request relacionado. Os dados serão automaticamente " +"adicionados após a conclusão do primeiro pipeline." + +msgid "The time taken by each data entry gathered by that stage." +msgstr "O tempo necessário para cada entrada de dados reunida por essa etapa." + +msgid "" +"The value lying at the midpoint of a series of observed values. E.g., " +"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 =" +" 6." +msgstr "" +"O valor situado no ponto médio de uma série de valores observados. Ex., " +"entre 3, 5, 9, a mediana é 5. Entre 3, 5, 7, 8, a mediana é (5 + 7) / 2 = 6." + +msgid "Time before an issue gets scheduled" +msgstr "Tempo até que uma tarefa seja planejada" + +msgid "Time before an issue starts implementation" +msgstr "Tempo até que uma tarefa comece a ser implementada" + +msgid "Time between merge request creation and merge/close" +msgstr "Tempo entre a criação do merge request e o merge/fechamento" + +msgid "Time until first merge request" +msgstr "Tempo até o primeiro merge request" + +msgid "Time|hr" +msgid_plural "Time|hrs" +msgstr[0] "h" +msgstr[1] "hs" + +msgid "Time|min" +msgid_plural "Time|mins" +msgstr[0] "min" +msgstr[1] "mins" + +msgid "Time|s" +msgstr "s" + +msgid "Total Time" +msgstr "Tempo Total" + +msgid "Total test time for all commits/merges" +msgstr "Tempo de teste total para todos os commits/merges" + +msgid "Want to see the data? Please ask an administrator for access." +msgstr "Precisa visualizar os dados? Solicite acesso ao administrador." + +msgid "We don't have enough data to show this stage." +msgstr "Não temos dados suficientes para mostrar esta fase." + +msgid "You need permission." +msgstr "Você precisa de permissão." + +msgid "day" +msgid_plural "days" +msgstr[0] "dia" +msgstr[1] "dias" + diff --git a/locale/pt_BR/gitlab.po.time_stamp b/locale/pt_BR/gitlab.po.time_stamp new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/locale/pt_BR/gitlab.po.time_stamp diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po index c2d69b122e2..11434460207 100644 --- a/locale/zh_CN/gitlab.po +++ b/locale/zh_CN/gitlab.po @@ -2,32 +2,38 @@ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the gitlab package. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. -# -#, fuzzy +# msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-05-04 19:24-0500\n" "PO-Revision-Date: 2017-05-04 19:24-0500\n" "Last-Translator: HuangTao <htve@outlook.com>, 2017\n" -"Language-Team: Chinese (China) (https://www.transifex.com/gitlab-zh/teams/75177/zh_CN/)\n" +"Language-Team: Chinese (China) (https://www.transifex.com/gitlab-zh/teams/7517" +"7/zh_CN/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: zh_CN\n" "Plural-Forms: nplurals=1; plural=0;\n" +msgid "Are you sure you want to delete this pipeline schedule?" +msgstr "" + msgid "ByAuthor|by" msgstr "作者:" +msgid "Cancel" +msgstr "" + msgid "Commit" msgid_plural "Commits" msgstr[0] "提交" -msgid "" -"Cycle Analytics gives an overview of how much time it takes to go from idea " -"to production in your project." +msgid "Cron Timezone" +msgstr "" + +msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." msgstr "周期分析概述了项目从想法到产品实现的各阶段所需的时间。" msgid "CycleAnalyticsStage|Code" @@ -51,10 +57,31 @@ msgstr "预发布" msgid "CycleAnalyticsStage|Test" msgstr "测试" +msgid "Delete" +msgstr "" + msgid "Deploy" msgid_plural "Deploys" msgstr[0] "部署" +msgid "Description" +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "Edit Pipeline Schedule %{id}" +msgstr "" + +msgid "Failed to change the owner" +msgstr "" + +msgid "Failed to remove the pipeline schedule" +msgstr "" + +msgid "Filter" +msgstr "" + msgid "FirstPushedBy|First" msgstr "首次推送" @@ -67,6 +94,9 @@ msgstr "从创建议题到部署至生产环境" msgid "From merge request merge until deploy to production" msgstr "从合并请求被合并后到部署至生产环境" +msgid "Interval Pattern" +msgstr "" + msgid "Introducing Cycle Analytics" msgstr "周期分析简介" @@ -74,6 +104,9 @@ msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "最后 %d 天" +msgid "Last Pipeline" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "最多显示 %d 个事件" @@ -85,6 +118,12 @@ msgid "New Issue" msgid_plural "New Issues" msgstr[0] "新议题" +msgid "New Pipeline Schedule" +msgstr "" + +msgid "No schedules" +msgstr "" + msgid "Not available" msgstr "数据不足" @@ -94,9 +133,45 @@ msgstr "数据不足" msgid "OpenedNDaysAgo|Opened" msgstr "开始于" +msgid "Owner" +msgstr "" + msgid "Pipeline Health" msgstr "流水线健康指标" +msgid "Pipeline Schedule" +msgstr "" + +msgid "Pipeline Schedules" +msgstr "" + +msgid "PipelineSchedules|Activated" +msgstr "" + +msgid "PipelineSchedules|Active" +msgstr "" + +msgid "PipelineSchedules|All" +msgstr "" + +msgid "PipelineSchedules|Inactive" +msgstr "" + +msgid "PipelineSchedules|Next Run" +msgstr "" + +msgid "PipelineSchedules|None" +msgstr "" + +msgid "PipelineSchedules|Provide a short description for this pipeline" +msgstr "" + +msgid "PipelineSchedules|Take ownership" +msgstr "" + +msgid "PipelineSchedules|Target" +msgstr "" + msgid "ProjectLifecycle|Stage" msgstr "项目生命周期" @@ -121,65 +196,56 @@ msgstr "相关的合并请求" msgid "Related Merged Requests" msgstr "相关已合并的合并请求" +msgid "Save pipeline schedule" +msgstr "" + +msgid "Schedule a new pipeline" +msgstr "" + +msgid "Select a timezone" +msgstr "" + +msgid "Select target branch" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "显示 %d 个事件" -msgid "" -"The coding stage shows the time from the first commit to creating the merge " -"request. The data will automatically be added here once you create your " -"first merge request." +msgid "Target Branch" +msgstr "" + +msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." msgstr "编码阶段概述了从第一次提交到创建合并请求的时间。创建第一个合并请求后,数据将自动添加到此处。" msgid "The collection of events added to the data gathered for that stage." msgstr "与该阶段相关的事件。" -msgid "" -"The issue stage shows the time it takes from creating an issue to assigning " -"the issue to a milestone, or add the issue to a list on your Issue Board. " -"Begin creating issues to see data for this stage." +msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." msgstr "议题阶段概述了从创建议题到将议题设置里程碑或将议题添加到议题看板的时间。开始创建议题以查看此阶段的数据。" msgid "The phase of the development lifecycle." msgstr "项目生命周期中的各个阶段。" -msgid "" -"The planning stage shows the time from the previous step to pushing your " -"first commit. This time will be added automatically once you push your first" -" commit." +msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit." msgstr "计划阶段概述了从议题添加到日程后到推送首次提交的时间。当首次推送提交后,数据将自动添加到此处。" -msgid "" -"The production stage shows the total time it takes between creating an issue" -" and deploying the code to production. The data will be automatically added " -"once you have completed the full idea to production cycle." +msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle." msgstr "生产阶段概述了从创建一个议题到将代码部署到生产环境的总时间。当完成想法到部署生产的循环,数据将自动添加到此处。" -msgid "" -"The review stage shows the time from creating the merge request to merging " -"it. The data will automatically be added after you merge your first merge " -"request." +msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." msgstr "评审阶段概述了从创建合并请求到被合并的时间。当创建第一个合并请求后,数据将自动添加到此处。" -msgid "" -"The staging stage shows the time between merging the MR and deploying code " -"to the production environment. The data will be automatically added once you" -" deploy to production for the first time." +msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time." msgstr "预发布阶段概述了从合并请求被合并到部署至生产环境的总时间。首次部署到生产环境后,数据将自动添加到此处。" -msgid "" -"The testing stage shows the time GitLab CI takes to run every pipeline for " -"the related merge request. The data will automatically be added after your " -"first pipeline finishes running." +msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running." msgstr "测试阶段概述了GitLab CI为相关合并请求运行每个流水线所需的时间。当第一个流水线运行完成后,数据将自动添加到此处。" msgid "The time taken by each data entry gathered by that stage." msgstr "该阶段每条数据所花的时间" -msgid "" -"The value lying at the midpoint of a series of observed values. E.g., " -"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 " -"= 6." +msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6." msgstr "中位数是一个数列中最中间的值。例如在 3、5、9 之间,中位数是 5。在 3、5、7、8 之间,中位数是 (5 + 7)/ 2 = 6。" msgid "Time before an issue gets scheduled" diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po index 6d56b2897fa..81b2ff863ea 100644 --- a/locale/zh_HK/gitlab.po +++ b/locale/zh_HK/gitlab.po @@ -2,32 +2,38 @@ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the gitlab package. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. -# -#, fuzzy +# msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-05-04 19:24-0500\n" "PO-Revision-Date: 2017-05-04 19:24-0500\n" "Last-Translator: HuangTao <htve@outlook.com>, 2017\n" -"Language-Team: Chinese (Hong Kong) (https://www.transifex.com/gitlab-zh/teams/75177/zh_HK/)\n" +"Language-Team: Chinese (Hong Kong) (https://www.transifex.com/gitlab-zh/teams/" +"75177/zh_HK/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: zh_HK\n" "Plural-Forms: nplurals=1; plural=0;\n" +msgid "Are you sure you want to delete this pipeline schedule?" +msgstr "" + msgid "ByAuthor|by" msgstr "作者:" +msgid "Cancel" +msgstr "" + msgid "Commit" msgid_plural "Commits" msgstr[0] "提交" -msgid "" -"Cycle Analytics gives an overview of how much time it takes to go from idea " -"to production in your project." +msgid "Cron Timezone" +msgstr "" + +msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." msgstr "週期分析概述了項目從想法到產品實現的各階段所需的時間。" msgid "CycleAnalyticsStage|Code" @@ -51,10 +57,31 @@ msgstr "預發布" msgid "CycleAnalyticsStage|Test" msgstr "測試" +msgid "Delete" +msgstr "" + msgid "Deploy" msgid_plural "Deploys" msgstr[0] "部署" +msgid "Description" +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "Edit Pipeline Schedule %{id}" +msgstr "" + +msgid "Failed to change the owner" +msgstr "" + +msgid "Failed to remove the pipeline schedule" +msgstr "" + +msgid "Filter" +msgstr "" + msgid "FirstPushedBy|First" msgstr "首次推送" @@ -67,6 +94,9 @@ msgstr "從創建議題到部署到生產環境" msgid "From merge request merge until deploy to production" msgstr "從合併請求的合併到部署至生產環境" +msgid "Interval Pattern" +msgstr "" + msgid "Introducing Cycle Analytics" msgstr "週期分析簡介" @@ -74,6 +104,9 @@ msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "最後 %d 天" +msgid "Last Pipeline" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "最多顯示 %d 個事件" @@ -85,6 +118,12 @@ msgid "New Issue" msgid_plural "New Issues" msgstr[0] "新議題" +msgid "New Pipeline Schedule" +msgstr "" + +msgid "No schedules" +msgstr "" + msgid "Not available" msgstr "不可用" @@ -94,9 +133,45 @@ msgstr "數據不足" msgid "OpenedNDaysAgo|Opened" msgstr "開始於" +msgid "Owner" +msgstr "" + msgid "Pipeline Health" msgstr "流水線健康指標" +msgid "Pipeline Schedule" +msgstr "" + +msgid "Pipeline Schedules" +msgstr "" + +msgid "PipelineSchedules|Activated" +msgstr "" + +msgid "PipelineSchedules|Active" +msgstr "" + +msgid "PipelineSchedules|All" +msgstr "" + +msgid "PipelineSchedules|Inactive" +msgstr "" + +msgid "PipelineSchedules|Next Run" +msgstr "" + +msgid "PipelineSchedules|None" +msgstr "" + +msgid "PipelineSchedules|Provide a short description for this pipeline" +msgstr "" + +msgid "PipelineSchedules|Take ownership" +msgstr "" + +msgid "PipelineSchedules|Target" +msgstr "" + msgid "ProjectLifecycle|Stage" msgstr "項目生命週期" @@ -121,65 +196,56 @@ msgstr "相關的合併請求" msgid "Related Merged Requests" msgstr "相關已合併的合並請求" +msgid "Save pipeline schedule" +msgstr "" + +msgid "Schedule a new pipeline" +msgstr "" + +msgid "Select a timezone" +msgstr "" + +msgid "Select target branch" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "顯示 %d 個事件" -msgid "" -"The coding stage shows the time from the first commit to creating the merge " -"request. The data will automatically be added here once you create your " -"first merge request." +msgid "Target Branch" +msgstr "" + +msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." msgstr "編碼階段概述了從第一次提交到創建合併請求的時間。創建第壹個合並請求後,數據將自動添加到此處。" msgid "The collection of events added to the data gathered for that stage." msgstr "與該階段相關的事件。" -msgid "" -"The issue stage shows the time it takes from creating an issue to assigning " -"the issue to a milestone, or add the issue to a list on your Issue Board. " -"Begin creating issues to see data for this stage." +msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." msgstr "議題階段概述了從創建議題到將議題設置裏程碑或將議題添加到議題看板的時間。創建一個議題後,數據將自動添加到此處。" msgid "The phase of the development lifecycle." msgstr "項目生命週期中的各個階段。" -msgid "" -"The planning stage shows the time from the previous step to pushing your " -"first commit. This time will be added automatically once you push your first" -" commit." +msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit." msgstr "計劃階段概述了從議題添加到日程後到推送首次提交的時間。當首次推送提交後,數據將自動添加到此處。" -msgid "" -"The production stage shows the total time it takes between creating an issue" -" and deploying the code to production. The data will be automatically added " -"once you have completed the full idea to production cycle." +msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle." msgstr "生產階段概述了從創建議題到將代碼部署到生產環境的時間。當完成完整的想法到部署生產,數據將自動添加到此處。" -msgid "" -"The review stage shows the time from creating the merge request to merging " -"it. The data will automatically be added after you merge your first merge " -"request." +msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." msgstr "評審階段概述了從創建合並請求到合併的時間。當創建第壹個合並請求後,數據將自動添加到此處。" -msgid "" -"The staging stage shows the time between merging the MR and deploying code " -"to the production environment. The data will be automatically added once you" -" deploy to production for the first time." +msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time." msgstr "預發布階段概述了合並請求的合併到部署代碼到生產環境的總時間。當首次部署到生產環境後,數據將自動添加到此處。" -msgid "" -"The testing stage shows the time GitLab CI takes to run every pipeline for " -"the related merge request. The data will automatically be added after your " -"first pipeline finishes running." +msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running." msgstr "測試階段概述了GitLab CI為相關合併請求運行每個流水線所需的時間。當第壹個流水線運行完成後,數據將自動添加到此處。" msgid "The time taken by each data entry gathered by that stage." msgstr "該階段每條數據所花的時間" -msgid "" -"The value lying at the midpoint of a series of observed values. E.g., " -"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 " -"= 6." +msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6." msgstr "中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。" msgid "Time before an issue gets scheduled" diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po index 0caf35a915b..e40723a9d8d 100644 --- a/locale/zh_TW/gitlab.po +++ b/locale/zh_TW/gitlab.po @@ -2,32 +2,38 @@ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the gitlab package. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. -# -#, fuzzy +# msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-05-04 19:24-0500\n" "PO-Revision-Date: 2017-05-04 19:24-0500\n" "Last-Translator: HuangTao <htve@outlook.com>, 2017\n" -"Language-Team: Chinese (Taiwan) (https://www.transifex.com/gitlab-zh/teams/75177/zh_TW/)\n" +"Language-Team: Chinese (Taiwan) (https://www.transifex.com/gitlab-zh/teams/751" +"77/zh_TW/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: zh_TW\n" "Plural-Forms: nplurals=1; plural=0;\n" +msgid "Are you sure you want to delete this pipeline schedule?" +msgstr "" + msgid "ByAuthor|by" msgstr "作者:" +msgid "Cancel" +msgstr "" + msgid "Commit" msgid_plural "Commits" msgstr[0] "送交" -msgid "" -"Cycle Analytics gives an overview of how much time it takes to go from idea " -"to production in your project." +msgid "Cron Timezone" +msgstr "" + +msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." msgstr "週期分析概述了你的專案從想法到產品實現,各階段所需的時間。" msgid "CycleAnalyticsStage|Code" @@ -51,10 +57,31 @@ msgstr "預備" msgid "CycleAnalyticsStage|Test" msgstr "測試" +msgid "Delete" +msgstr "" + msgid "Deploy" msgid_plural "Deploys" msgstr[0] "部署" +msgid "Description" +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "Edit Pipeline Schedule %{id}" +msgstr "" + +msgid "Failed to change the owner" +msgstr "" + +msgid "Failed to remove the pipeline schedule" +msgstr "" + +msgid "Filter" +msgstr "" + msgid "FirstPushedBy|First" msgstr "首次推送" @@ -67,6 +94,9 @@ msgstr "從議題建立至線上部署" msgid "From merge request merge until deploy to production" msgstr "從請求被合併後至線上部署" +msgid "Interval Pattern" +msgstr "" + msgid "Introducing Cycle Analytics" msgstr "週期分析簡介" @@ -74,6 +104,9 @@ msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "最後 %d 天" +msgid "Last Pipeline" +msgstr "" + msgid "Limited to showing %d event at most" msgid_plural "Limited to showing %d events at most" msgstr[0] "最多顯示 %d 個事件" @@ -85,6 +118,12 @@ msgid "New Issue" msgid_plural "New Issues" msgstr[0] "新議題" +msgid "New Pipeline Schedule" +msgstr "" + +msgid "No schedules" +msgstr "" + msgid "Not available" msgstr "無法使用" @@ -94,9 +133,45 @@ msgstr "資料不足" msgid "OpenedNDaysAgo|Opened" msgstr "開始於" +msgid "Owner" +msgstr "" + msgid "Pipeline Health" msgstr "流水線健康指標" +msgid "Pipeline Schedule" +msgstr "" + +msgid "Pipeline Schedules" +msgstr "" + +msgid "PipelineSchedules|Activated" +msgstr "" + +msgid "PipelineSchedules|Active" +msgstr "" + +msgid "PipelineSchedules|All" +msgstr "" + +msgid "PipelineSchedules|Inactive" +msgstr "" + +msgid "PipelineSchedules|Next Run" +msgstr "" + +msgid "PipelineSchedules|None" +msgstr "" + +msgid "PipelineSchedules|Provide a short description for this pipeline" +msgstr "" + +msgid "PipelineSchedules|Take ownership" +msgstr "" + +msgid "PipelineSchedules|Target" +msgstr "" + msgid "ProjectLifecycle|Stage" msgstr "專案生命週期" @@ -121,69 +196,60 @@ msgstr "相關的合併請求" msgid "Related Merged Requests" msgstr "相關已合併的請求" +msgid "Save pipeline schedule" +msgstr "" + +msgid "Schedule a new pipeline" +msgstr "" + +msgid "Select a timezone" +msgstr "" + +msgid "Select target branch" +msgstr "" + msgid "Showing %d event" msgid_plural "Showing %d events" msgstr[0] "顯示 %d 個事件" -msgid "" -"The coding stage shows the time from the first commit to creating the merge " -"request. The data will automatically be added here once you create your " -"first merge request." +msgid "Target Branch" +msgstr "" + +msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." msgstr "程式開發階段顯示從第一次送交到建立合併請求的時間。建立第一個合併請求後,資料將自動填入。" msgid "The collection of events added to the data gathered for that stage." msgstr "與該階段相關的事件。" -msgid "" -"The issue stage shows the time it takes from creating an issue to assigning " -"the issue to a milestone, or add the issue to a list on your Issue Board. " -"Begin creating issues to see data for this stage." +msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." msgstr "議題階段顯示從議題建立到設置里程碑、或將該議題加至議題看板的時間。建立第一個議題後,資料將自動填入。" msgid "The phase of the development lifecycle." msgstr "專案開發生命週期的各個階段。" -msgid "" -"The planning stage shows the time from the previous step to pushing your " -"first commit. This time will be added automatically once you push your first" -" commit." -msgstr "計劃階段顯示從議題添加到日程後至推送第一個送交的時間。當第一次推送送交後,資料將自動填入。" +msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit." +msgstr "計劃階段所顯示的是議題被排程後至第一個送交被推送的時間。一旦完成(或執行)首次的推送,資料將自動填入。" -msgid "" -"The production stage shows the total time it takes between creating an issue" -" and deploying the code to production. The data will be automatically added " -"once you have completed the full idea to production cycle." +msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle." msgstr "上線階段顯示從建立一個議題到部署程式至線上的總時間。當完成從想法到產品實現的循環後,資料將自動填入。" -msgid "" -"The review stage shows the time from creating the merge request to merging " -"it. The data will automatically be added after you merge your first merge " -"request." +msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." msgstr "複閱階段顯示從合併請求建立後至被合併的時間。當建立第一個合併請求後,資料將自動填入。" -msgid "" -"The staging stage shows the time between merging the MR and deploying code " -"to the production environment. The data will be automatically added once you" -" deploy to production for the first time." +msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time." msgstr "預備階段顯示從合併請求被合併後至部署上線的時間。當第一次部署上線後,資料將自動填入。" -msgid "" -"The testing stage shows the time GitLab CI takes to run every pipeline for " -"the related merge request. The data will automatically be added after your " -"first pipeline finishes running." +msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running." msgstr "測試階段顯示相關合併請求的流水線所花的時間。當第一個流水線運作完畢後,資料將自動填入。" msgid "The time taken by each data entry gathered by that stage." msgstr "每筆該階段相關資料所花的時間。" -msgid "" -"The value lying at the midpoint of a series of observed values. E.g., " -"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 " -"= 6." +msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6." msgstr "中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。" msgid "Time before an issue gets scheduled" -msgstr "議題被列入日程表的時間" +msgstr "議題等待排程的時間" msgid "Time before an issue starts implementation" msgstr "議題等待開始實作的時間" diff --git a/package.json b/package.json index 29165fd4182..045f07ee2f9 100644 --- a/package.json +++ b/package.json @@ -72,13 +72,13 @@ "eslint-plugin-jasmine": "^2.1.0", "eslint-plugin-promise": "^3.5.0", "istanbul": "^0.4.5", - "jasmine-core": "^2.5.2", + "jasmine-core": "^2.6.3", "jasmine-jquery": "^2.1.1", - "karma": "^1.4.1", + "karma": "^1.7.0", + "karma-chrome-launcher": "^2.1.1", "karma-coverage-istanbul-reporter": "^0.2.0", "karma-jasmine": "^1.1.0", "karma-mocha-reporter": "^2.2.2", - "karma-phantomjs-launcher": "^1.0.2", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^2.0.2", "nodemon": "^1.11.0", diff --git a/rubocop/cop/activerecord_serialize.rb b/rubocop/cop/activerecord_serialize.rb index bfa0cff9a67..9bdcc3b4c34 100644 --- a/rubocop/cop/activerecord_serialize.rb +++ b/rubocop/cop/activerecord_serialize.rb @@ -1,24 +1,18 @@ +require_relative '../model_helpers' + module RuboCop module Cop # Cop that prevents the use of `serialize` in ActiveRecord models. class ActiverecordSerialize < RuboCop::Cop::Cop + include ModelHelpers + MSG = 'Do not store serialized data in the database, use separate columns and/or tables instead'.freeze def on_send(node) - return unless in_models?(node) + return unless in_model?(node) add_offense(node, :selector) if node.children[1] == :serialize end - - def models_path - File.join(Dir.pwd, 'app', 'models') - end - - def in_models?(node) - path = node.location.expression.source_buffer.name - - path.start_with?(models_path) - end end end end diff --git a/rubocop/cop/migration/add_timestamps.rb b/rubocop/cop/migration/add_timestamps.rb new file mode 100644 index 00000000000..08ddd91e54d --- /dev/null +++ b/rubocop/cop/migration/add_timestamps.rb @@ -0,0 +1,25 @@ +require_relative '../../migration_helpers' + +module RuboCop + module Cop + module Migration + # Cop that checks if 'add_timestamps' method is called with timezone information. + class AddTimestamps < RuboCop::Cop::Cop + include MigrationHelpers + + MSG = 'Do not use `add_timestamps`, use `add_timestamps_with_timezone` instead'.freeze + + # Check methods. + def on_send(node) + return unless in_migration?(node) + + add_offense(node, :selector) if method_name(node) == :add_timestamps + end + + def method_name(node) + node.children[1] + end + end + end + end +end diff --git a/rubocop/cop/migration/datetime.rb b/rubocop/cop/migration/datetime.rb new file mode 100644 index 00000000000..651935dd53e --- /dev/null +++ b/rubocop/cop/migration/datetime.rb @@ -0,0 +1,36 @@ +require_relative '../../migration_helpers' + +module RuboCop + module Cop + module Migration + # Cop that checks if datetime data type is added with timezone information. + class Datetime < RuboCop::Cop::Cop + include MigrationHelpers + + MSG = 'Do not use the `datetime` data type, use `datetime_with_timezone` instead'.freeze + + # Check methods in table creation. + def on_def(node) + return unless in_migration?(node) + + node.each_descendant(:send) do |send_node| + add_offense(send_node, :selector) if method_name(send_node) == :datetime + end + end + + # Check methods. + def on_send(node) + return unless in_migration?(node) + + node.each_descendant do |descendant| + add_offense(node, :expression) if descendant.type == :sym && descendant.children.last == :datetime + end + end + + def method_name(node) + node.children[1] + end + end + end + end +end diff --git a/rubocop/cop/migration/timestamps.rb b/rubocop/cop/migration/timestamps.rb new file mode 100644 index 00000000000..71a9420cc3b --- /dev/null +++ b/rubocop/cop/migration/timestamps.rb @@ -0,0 +1,27 @@ +require_relative '../../migration_helpers' + +module RuboCop + module Cop + module Migration + # Cop that checks if 'timestamps' method is called with timezone information. + class Timestamps < RuboCop::Cop::Cop + include MigrationHelpers + + MSG = 'Do not use `timestamps`, use `timestamps_with_timezone` instead'.freeze + + # Check methods in table creation. + def on_def(node) + return unless in_migration?(node) + + node.each_descendant(:send) do |send_node| + add_offense(send_node, :selector) if method_name(send_node) == :timestamps + end + end + + def method_name(node) + node.children[1] + end + end + end + end +end diff --git a/rubocop/cop/polymorphic_associations.rb b/rubocop/cop/polymorphic_associations.rb new file mode 100644 index 00000000000..7d554704550 --- /dev/null +++ b/rubocop/cop/polymorphic_associations.rb @@ -0,0 +1,23 @@ +require_relative '../model_helpers' + +module RuboCop + module Cop + # Cop that prevents the use of polymorphic associations + class PolymorphicAssociations < RuboCop::Cop::Cop + include ModelHelpers + + MSG = 'Do not use polymorphic associations, use separate tables instead'.freeze + + def on_send(node) + return unless in_model?(node) + return unless node.children[1] == :belongs_to + + node.children.last.each_node(:pair) do |pair| + key_name = pair.children[0].children[0] + + add_offense(pair, :expression) if key_name == :polymorphic + end + end + end + end +end diff --git a/rubocop/cop/rspec/single_line_hook.rb b/rubocop/cop/rspec/single_line_hook.rb new file mode 100644 index 00000000000..be611054323 --- /dev/null +++ b/rubocop/cop/rspec/single_line_hook.rb @@ -0,0 +1,38 @@ +require 'rubocop-rspec' + +module RuboCop + module Cop + module RSpec + # This cop checks for single-line hook blocks + # + # @example + # + # # bad + # before { do_something } + # after(:each) { undo_something } + # + # # good + # before do + # do_something + # end + # + # after(:each) do + # undo_something + # end + class SingleLineHook < Cop + MESSAGE = "Don't use single-line hook blocks.".freeze + + def_node_search :rspec_hook?, <<~PATTERN + (send nil {:after :around :before} ...) + PATTERN + + def on_block(node) + return unless rspec_hook?(node) + return unless node.single_line? + + add_offense(node, :expression, MESSAGE) + end + end + end + end +end diff --git a/rubocop/model_helpers.rb b/rubocop/model_helpers.rb new file mode 100644 index 00000000000..309723dc34c --- /dev/null +++ b/rubocop/model_helpers.rb @@ -0,0 +1,11 @@ +module RuboCop + module ModelHelpers + # Returns true if the given node originated from the models directory. + def in_model?(node) + path = node.location.expression.source_buffer.name + models_path = File.join(Dir.pwd, 'app', 'models') + + path.start_with?(models_path) + end + end +end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index 22815090508..55d7708fa8c 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -2,12 +2,17 @@ require_relative 'cop/custom_error_class' require_relative 'cop/gem_fetcher' require_relative 'cop/activerecord_serialize' require_relative 'cop/redirect_with_status' +require_relative 'cop/polymorphic_associations' require_relative 'cop/migration/add_column' require_relative 'cop/migration/add_column_with_default_to_large_table' require_relative 'cop/migration/add_concurrent_foreign_key' require_relative 'cop/migration/add_concurrent_index' require_relative 'cop/migration/add_index' +require_relative 'cop/migration/add_timestamps' +require_relative 'cop/migration/datetime' require_relative 'cop/migration/remove_concurrent_index' require_relative 'cop/migration/remove_index' require_relative 'cop/migration/reversible_add_column_with_default' +require_relative 'cop/migration/timestamps' require_relative 'cop/migration/update_column_in_batches' +require_relative 'cop/rspec/single_line_hook' diff --git a/scripts/static-analysis b/scripts/static-analysis index 7dc8f679036..6d35684b97f 100755 --- a/scripts/static-analysis +++ b/scripts/static-analysis @@ -3,7 +3,7 @@ require ::File.expand_path('../lib/gitlab/popen', __dir__) tasks = [ - %w[bundle exec bundle-audit check --update --ignore CVE-2016-4658], + %w[bundle exec bundle-audit check --update --ignore CVE-2016-4658 CVE-2017-5029], %w[bundle exec rake config_lint], %w[bundle exec rake flay], %w[bundle exec rake haml_lint], diff --git a/scripts/trigger-build b/scripts/trigger-build index e4603533872..dcda70d7ed8 100755 --- a/scripts/trigger-build +++ b/scripts/trigger-build @@ -9,7 +9,7 @@ params = { "token" => ENV["BUILD_TRIGGER_TOKEN"], "variables[GITLAB_VERSION]" => ENV["CI_COMMIT_SHA"], "variables[ALTERNATIVE_SOURCES]" => true, - "variables[ee]" => ENV["EE_PACKAGE"] + "variables[ee]" => ENV["EE_PACKAGE"] || "false" } Dir.glob("*_VERSION").each do |version_file| @@ -19,4 +19,9 @@ end res = Net::HTTP.post_form(uri, params) pipeline_id = JSON.parse(res.body)['id'] -puts "Triggered pipeline can be found at https://gitlab.com/gitlab-org/omnibus-gitlab/pipelines/#{pipeline_id}" +unless pipeline_id.nil? + puts "Triggered pipeline can be found at https://gitlab.com/gitlab-org/omnibus-gitlab/pipelines/#{pipeline_id}" +else + puts "Trigger failed. The response from trigger is: " + puts res.body +end diff --git a/spec/controllers/admin/identities_controller_spec.rb b/spec/controllers/admin/identities_controller_spec.rb index c131d22a30a..a29853bf8df 100644 --- a/spec/controllers/admin/identities_controller_spec.rb +++ b/spec/controllers/admin/identities_controller_spec.rb @@ -2,7 +2,10 @@ require 'spec_helper' describe Admin::IdentitiesController do let(:admin) { create(:admin) } - before { sign_in(admin) } + + before do + sign_in(admin) + end describe 'UPDATE identity' do let(:user) { create(:omniauth_user, provider: 'ldapmain', extern_uid: 'uid=myuser,ou=people,dc=example,dc=com') } diff --git a/spec/controllers/admin/services_controller_spec.rb b/spec/controllers/admin/services_controller_spec.rb index c94616d8508..4ca0cfc74e9 100644 --- a/spec/controllers/admin/services_controller_spec.rb +++ b/spec/controllers/admin/services_controller_spec.rb @@ -3,7 +3,9 @@ require 'spec_helper' describe Admin::ServicesController do let(:admin) { create(:admin) } - before { sign_in(admin) } + before do + sign_in(admin) + end describe 'GET #edit' do let!(:project) { create(:empty_project) } diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 2c9d1ffc9c2..b40f647644d 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -170,27 +170,39 @@ describe AutocompleteController do end context 'author of issuable included' do - before do - sign_in(user) - end - let(:body) { JSON.parse(response.body) } - it 'includes the author' do - get(:users, author_id: non_member.id) + context 'authenticated' do + before do + sign_in(user) + end - expect(body.first["username"]).to eq non_member.username + it 'includes the author' do + get(:users, author_id: non_member.id) + + expect(body.first["username"]).to eq non_member.username + end + + it 'rejects non existent user ids' do + get(:users, author_id: 99999) + + expect(body.collect { |u| u['id'] }).not_to include(99999) + end end - it 'rejects non existent user ids' do - get(:users, author_id: 99999) + context 'without authenticating' do + it 'returns empty result' do + get(:users, author_id: non_member.id) - expect(body.collect { |u| u['id'] }).not_to include(99999) + expect(body).to be_empty + end end end context 'skip_users parameter included' do - before { sign_in(user) } + before do + sign_in(user) + end it 'skips the user IDs passed' do get(:users, skip_users: [user, user2].map(&:id)) diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb new file mode 100644 index 00000000000..424f39fd3b8 --- /dev/null +++ b/spec/controllers/dashboard/milestones_controller_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Dashboard::MilestonesController do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:project_milestone) { create(:milestone, project: project) } + let(:milestone) do + DashboardMilestone.build( + [project], + project_milestone.title + ) + end + let(:issue) { create(:issue, project: project, milestone: project_milestone) } + let!(:label) { create(:label, project: project, title: 'Issue Label', issues: [issue]) } + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: project_milestone) } + let(:milestone_path) { dashboard_milestone_path(milestone.safe_title, title: milestone.title) } + + before do + sign_in(user) + project.team << [user, :master] + end + + it_behaves_like 'milestone tabs' + + describe "#show" do + render_views + + def view_milestone + get :show, id: milestone.safe_title, title: milestone.title + end + + it 'shows milestone page' do + view_milestone + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb index 60db0192dfd..cce53f6697c 100644 --- a/spec/controllers/groups/group_members_controller_spec.rb +++ b/spec/controllers/groups/group_members_controller_spec.rb @@ -16,10 +16,14 @@ describe Groups::GroupMembersController do describe 'POST create' do let(:group_user) { create(:user) } - before { sign_in(user) } + before do + sign_in(user) + end context 'when user does not have enough rights' do - before { group.add_developer(user) } + before do + group.add_developer(user) + end it 'returns 403' do post :create, group_id: group, @@ -32,7 +36,9 @@ describe Groups::GroupMembersController do end context 'when user has enough rights' do - before { group.add_owner(user) } + before do + group.add_owner(user) + end it 'adds user to members' do post :create, group_id: group, @@ -59,7 +65,9 @@ describe Groups::GroupMembersController do describe 'DELETE destroy' do let(:member) { create(:group_member, :developer, group: group) } - before { sign_in(user) } + before do + sign_in(user) + end context 'when member is not found' do it 'returns 403' do @@ -71,7 +79,9 @@ describe Groups::GroupMembersController do context 'when member is found' do context 'when user does not have enough rights' do - before { group.add_developer(user) } + before do + group.add_developer(user) + end it 'returns 403' do delete :destroy, group_id: group, id: member @@ -82,7 +92,9 @@ describe Groups::GroupMembersController do end context 'when user has enough rights' do - before { group.add_owner(user) } + before do + group.add_owner(user) + end it '[HTML] removes user from members' do delete :destroy, group_id: group, id: member @@ -103,7 +115,9 @@ describe Groups::GroupMembersController do end describe 'DELETE leave' do - before { sign_in(user) } + before do + sign_in(user) + end context 'when member is not found' do it 'returns 404' do @@ -115,7 +129,9 @@ describe Groups::GroupMembersController do context 'when member is found' do context 'and is not an owner' do - before { group.add_developer(user) } + before do + group.add_developer(user) + end it 'removes user from members' do delete :leave, group_id: group @@ -124,10 +140,19 @@ describe Groups::GroupMembersController do expect(response).to redirect_to(dashboard_groups_path) expect(group.users).not_to include user end + + it 'supports json request' do + delete :leave, group_id: group, format: :json + + expect(response).to have_http_status(200) + expect(json_response['notice']).to eq "You left the \"#{group.name}\" group." + end end context 'and is an owner' do - before { group.add_owner(user) } + before do + group.add_owner(user) + end it 'cannot removes himself from the group' do delete :leave, group_id: group @@ -137,7 +162,9 @@ describe Groups::GroupMembersController do end context 'and is a requester' do - before { group.request_access(user) } + before do + group.request_access(user) + end it 'removes user from members' do delete :leave, group_id: group @@ -152,7 +179,9 @@ describe Groups::GroupMembersController do end describe 'POST request_access' do - before { sign_in(user) } + before do + sign_in(user) + end it 'creates a new GroupMember that is not a team member' do post :request_access, group_id: group @@ -167,7 +196,9 @@ describe Groups::GroupMembersController do describe 'POST approve_access_request' do let(:member) { create(:group_member, :access_request, group: group) } - before { sign_in(user) } + before do + sign_in(user) + end context 'when member is not found' do it 'returns 403' do @@ -179,7 +210,9 @@ describe Groups::GroupMembersController do context 'when member is found' do context 'when user does not have enough rights' do - before { group.add_developer(user) } + before do + group.add_developer(user) + end it 'returns 403' do post :approve_access_request, group_id: group, id: member @@ -190,7 +223,9 @@ describe Groups::GroupMembersController do end context 'when user has enough rights' do - before { group.add_owner(user) } + before do + group.add_owner(user) + end it 'adds user to members' do post :approve_access_request, group_id: group, id: member diff --git a/spec/controllers/health_controller_spec.rb b/spec/controllers/health_controller_spec.rb index b8b6e0c3a88..e7c19b47a6a 100644 --- a/spec/controllers/health_controller_spec.rb +++ b/spec/controllers/health_controller_spec.rb @@ -54,43 +54,4 @@ describe HealthController do end end end - - describe '#metrics' do - context 'authorization token provided' do - before do - request.headers['TOKEN'] = token - end - - it 'returns DB ping metrics' do - get :metrics - expect(response.body).to match(/^db_ping_timeout 0$/) - expect(response.body).to match(/^db_ping_success 1$/) - expect(response.body).to match(/^db_ping_latency [0-9\.]+$/) - end - - it 'returns Redis ping metrics' do - get :metrics - expect(response.body).to match(/^redis_ping_timeout 0$/) - expect(response.body).to match(/^redis_ping_success 1$/) - expect(response.body).to match(/^redis_ping_latency [0-9\.]+$/) - end - - it 'returns file system check metrics' do - get :metrics - expect(response.body).to match(/^filesystem_access_latency{shard="default"} [0-9\.]+$/) - expect(response.body).to match(/^filesystem_accessible{shard="default"} 1$/) - expect(response.body).to match(/^filesystem_write_latency{shard="default"} [0-9\.]+$/) - expect(response.body).to match(/^filesystem_writable{shard="default"} 1$/) - expect(response.body).to match(/^filesystem_read_latency{shard="default"} [0-9\.]+$/) - expect(response.body).to match(/^filesystem_readable{shard="default"} 1$/) - end - end - - context 'without authorization token' do - it 'returns proper response' do - get :metrics - expect(response.status).to eq(404) - end - end - end end diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb new file mode 100644 index 00000000000..044c9f179ed --- /dev/null +++ b/spec/controllers/metrics_controller_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +describe MetricsController do + include StubENV + + let(:token) { current_application_settings.health_check_access_token } + let(:json_response) { JSON.parse(response.body) } + let(:metrics_multiproc_dir) { Dir.mktmpdir } + + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + stub_env('prometheus_multiproc_dir', metrics_multiproc_dir) + allow(Gitlab::Metrics).to receive(:prometheus_metrics_enabled?).and_return(true) + end + + describe '#index' do + context 'authorization token provided' do + before do + request.headers['TOKEN'] = token + end + + it 'returns DB ping metrics' do + get :index + + expect(response.body).to match(/^db_ping_timeout 0$/) + expect(response.body).to match(/^db_ping_success 1$/) + expect(response.body).to match(/^db_ping_latency [0-9\.]+$/) + end + + it 'returns Redis ping metrics' do + get :index + + expect(response.body).to match(/^redis_ping_timeout 0$/) + expect(response.body).to match(/^redis_ping_success 1$/) + expect(response.body).to match(/^redis_ping_latency [0-9\.]+$/) + end + + it 'returns file system check metrics' do + get :index + + expect(response.body).to match(/^filesystem_access_latency{shard="default"} [0-9\.]+$/) + expect(response.body).to match(/^filesystem_accessible{shard="default"} 1$/) + expect(response.body).to match(/^filesystem_write_latency{shard="default"} [0-9\.]+$/) + expect(response.body).to match(/^filesystem_writable{shard="default"} 1$/) + expect(response.body).to match(/^filesystem_read_latency{shard="default"} [0-9\.]+$/) + expect(response.body).to match(/^filesystem_readable{shard="default"} 1$/) + end + + context 'prometheus metrics are disabled' do + before do + allow(Gitlab::Metrics).to receive(:prometheus_metrics_enabled?).and_return(false) + end + + it 'returns proper response' do + get :index + + expect(response.status).to eq(404) + end + end + end + + context 'without authorization token' do + it 'returns proper response' do + get :index + + expect(response.status).to eq(404) + end + end + end +end diff --git a/spec/controllers/notification_settings_controller_spec.rb b/spec/controllers/notification_settings_controller_spec.rb index 9e3a31e1a6b..6b690407ce3 100644 --- a/spec/controllers/notification_settings_controller_spec.rb +++ b/spec/controllers/notification_settings_controller_spec.rb @@ -58,7 +58,10 @@ describe NotificationSettingsController do expect(response.status).to eq 200 expect(notification_setting.level).to eq("custom") - expect(notification_setting.events).to eq(custom_events) + + custom_events.each do |event, value| + expect(notification_setting.event_enabled?(event)).to eq(value) + end end end end @@ -86,7 +89,10 @@ describe NotificationSettingsController do expect(response.status).to eq 200 expect(notification_setting.level).to eq("custom") - expect(notification_setting.events).to eq(custom_events) + + custom_events.each do |event, value| + expect(notification_setting.event_enabled?(event)).to eq(value) + end end end end @@ -94,7 +100,10 @@ describe NotificationSettingsController do context 'not authorized' do let(:private_project) { create(:empty_project, :private) } - before { sign_in(user) } + + before do + sign_in(user) + end it 'returns 404' do post :create, @@ -120,7 +129,9 @@ describe NotificationSettingsController do end context 'when authorized' do - before{ sign_in(user) } + before do + sign_in(user) + end it 'returns success' do put :update, @@ -152,7 +163,9 @@ describe NotificationSettingsController do context 'not authorized' do let(:other_user) { create(:user) } - before { sign_in(other_user) } + before do + sign_in(other_user) + end it 'returns 404' do put :update, diff --git a/spec/controllers/profiles/personal_access_tokens_controller_spec.rb b/spec/controllers/profiles/personal_access_tokens_controller_spec.rb index 98a43e278b2..ed08a4c1bf2 100644 --- a/spec/controllers/profiles/personal_access_tokens_controller_spec.rb +++ b/spec/controllers/profiles/personal_access_tokens_controller_spec.rb @@ -4,7 +4,9 @@ describe Profiles::PersonalAccessTokensController do let(:user) { create(:user) } let(:token_attributes) { attributes_for(:personal_access_token) } - before { sign_in(user) } + before do + sign_in(user) + end describe '#create' do def created_token @@ -38,7 +40,9 @@ describe Profiles::PersonalAccessTokensController do let!(:inactive_personal_access_token) { create(:personal_access_token, :revoked, user: user) } let!(:impersonation_personal_access_token) { create(:personal_access_token, :impersonation, user: user) } - before { get :index } + before do + get :index + end it "retrieves active personal access tokens" do expect(assigns(:active_personal_access_tokens)).to include(active_personal_access_token) diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb new file mode 100644 index 00000000000..9d60dab12d1 --- /dev/null +++ b/spec/controllers/profiles_controller_spec.rb @@ -0,0 +1,31 @@ +require('spec_helper') + +describe ProfilesController do + describe "PUT update" do + it "allows an email update from a user without an external email address" do + user = create(:user) + sign_in(user) + + put :update, + user: { email: "john@gmail.com", name: "John" } + + user.reload + + expect(response.status).to eq(302) + expect(user.unconfirmed_email).to eq('john@gmail.com') + end + + it "ignores an email update from a user with an external email address" do + ldap_user = create(:omniauth_user, external_email: true) + sign_in(ldap_user) + + put :update, + user: { email: "john@gmail.com", name: "John" } + + ldap_user.reload + + expect(response.status).to eq(302) + expect(ldap_user.unconfirmed_email).not_to eq('john@gmail.com') + end + end +end diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb index 432f3c53c90..0f2664262e8 100644 --- a/spec/controllers/projects/boards/lists_controller_spec.rb +++ b/spec/controllers/projects/boards/lists_controller_spec.rb @@ -27,7 +27,7 @@ describe Projects::Boards::ListsController do parsed_response = JSON.parse(response.body) expect(response).to match_response_schema('lists') - expect(parsed_response.length).to eq 2 + expect(parsed_response.length).to eq 3 end context 'with unauthorized user' do diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index f285e5333d6..f9e21f9d8f6 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -367,19 +367,5 @@ describe Projects::BranchesController do expect(parsed_response.first).to eq 'master' end end - - context 'show_all = true' do - it 'returns all the branches name' do - get :index, - namespace_id: project.namespace, - project_id: project, - format: :json, - show_all: true - - parsed_response = JSON.parse(response.body) - - expect(parsed_response.length).to eq(project.repository.branches.count) - end - end end end diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index 69e4706dc71..7fb08df1950 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -281,7 +281,9 @@ describe Projects::CommitController do end context 'when the path does not exist in the diff' do - before { diff_for_path(id: commit.id, old_path: existing_path.succ, new_path: existing_path.succ) } + before do + diff_for_path(id: commit.id, old_path: existing_path.succ, new_path: existing_path.succ) + end it 'returns a 404' do expect(response).to have_http_status(404) @@ -302,7 +304,9 @@ describe Projects::CommitController do end context 'when the commit does not exist' do - before { diff_for_path(id: commit.id.succ, old_path: existing_path, new_path: existing_path) } + before do + diff_for_path(id: commit.id.succ, old_path: existing_path, new_path: existing_path) + end it 'returns a 404' do expect(response).to have_http_status(404) diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index 15ac4e0925a..8f4694c9854 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -128,7 +128,9 @@ describe Projects::CompareController do end context 'when the path does not exist in the diff' do - before { diff_for_path(from: ref_from, to: ref_to, old_path: existing_path.succ, new_path: existing_path.succ) } + before do + diff_for_path(from: ref_from, to: ref_to, old_path: existing_path.succ, new_path: existing_path.succ) + end it 'returns a 404' do expect(response).to have_http_status(404) @@ -149,7 +151,9 @@ describe Projects::CompareController do end context 'when the from ref does not exist' do - before { diff_for_path(from: ref_from.succ, to: ref_to, old_path: existing_path, new_path: existing_path) } + before do + diff_for_path(from: ref_from.succ, to: ref_to, old_path: existing_path, new_path: existing_path) + end it 'returns a 404' do expect(response).to have_http_status(404) @@ -157,7 +161,9 @@ describe Projects::CompareController do end context 'when the to ref does not exist' do - before { diff_for_path(from: ref_from, to: ref_to.succ, old_path: existing_path, new_path: existing_path) } + before do + diff_for_path(from: ref_from, to: ref_to.succ, old_path: existing_path, new_path: existing_path) + end it 'returns a 404' do expect(response).to have_http_status(404) diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb index 8282d79298f..dc8290c438e 100644 --- a/spec/controllers/projects/forks_controller_spec.rb +++ b/spec/controllers/projects/forks_controller_spec.rb @@ -14,7 +14,9 @@ describe Projects::ForksController do end context 'when fork is public' do - before { forked_project.update_attribute(:visibility_level, Project::PUBLIC) } + before do + forked_project.update_attribute(:visibility_level, Project::PUBLIC) + end it 'is visible for non logged in users' do get_forks @@ -35,7 +37,9 @@ describe Projects::ForksController do end context 'when user is logged in' do - before { sign_in(project.creator) } + before do + sign_in(project.creator) + end context 'when user is not a Project member neither a group member' do it 'does not see the Project listed' do @@ -46,7 +50,9 @@ describe Projects::ForksController do end context 'when user is a member of the Project' do - before { forked_project.team << [project.creator, :developer] } + before do + forked_project.team << [project.creator, :developer] + end it 'sees the project listed' do get_forks @@ -56,7 +62,9 @@ describe Projects::ForksController do end context 'when user is a member of the Group' do - before { forked_project.group.add_developer(project.creator) } + before do + forked_project.group.add_developer(project.creator) + end it 'sees the project listed' do get_forks diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb index ca4a8e871c0..b5435357f53 100644 --- a/spec/controllers/projects/group_links_controller_spec.rb +++ b/spec/controllers/projects/group_links_controller_spec.rb @@ -22,7 +22,10 @@ describe Projects::GroupLinksController do end context 'when user has access to group he want to link project to' do - before { group.add_developer(user) } + before do + group.add_developer(user) + end + include_context 'link project to group' it 'links project with selected group' do diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index a38ae2eb990..f853bfe370c 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -212,7 +212,9 @@ describe Projects::IssuesController do let(:another_project) { create(:empty_project, :private) } context 'when user has access to move issue' do - before { another_project.team << [user, :reporter] } + before do + another_project.team << [user, :reporter] + end it 'moves issue to another project' do move_issue @@ -250,16 +252,21 @@ describe Projects::IssuesController do end context 'when an issue is identified as spam' do - before { allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) } + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end context 'when captcha is not verified' do def update_spam_issue update_issue(title: 'Spam Title', description: 'Spam lives here') end - before { allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) } + before do + allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) + end it 'rejects an issue recognized as a spam' do + expect(Gitlab::Recaptcha).to receive(:load_configurations!).and_return(true) expect { update_spam_issue }.not_to change{ issue.reload.title } end @@ -619,14 +626,18 @@ describe Projects::IssuesController do end context 'when an issue is identified as spam' do - before { allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) } + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end context 'when captcha is not verified' do def post_spam_issue post_new_issue(title: 'Spam Title', description: 'Spam lives here') end - before { allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) } + before do + allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) + end it 'rejects an issue recognized as a spam' do expect { post_spam_issue }.not_to change(Issue, :count) @@ -738,7 +749,10 @@ describe Projects::IssuesController do describe "DELETE #destroy" do context "when the user is a developer" do - before { sign_in(user) } + before do + sign_in(user) + end + it "rejects a developer to destroy an issue" do delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid expect(response).to have_http_status(404) @@ -750,7 +764,9 @@ describe Projects::IssuesController do let(:namespace) { create(:namespace, owner: owner) } let(:project) { create(:empty_project, namespace: namespace) } - before { sign_in(owner) } + before do + sign_in(owner) + end it "deletes the issue" do delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 7211acc53dc..472e5fc51a0 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -28,7 +28,7 @@ describe Projects::JobsController do get_index(scope: 'running') end - it 'has only running builds' do + it 'has only running jobs' do expect(response).to have_http_status(:ok) expect(assigns(:builds).first.status).to eq('running') end @@ -41,7 +41,7 @@ describe Projects::JobsController do get_index(scope: 'finished') end - it 'has only finished builds' do + it 'has only finished jobs' do expect(response).to have_http_status(:ok) expect(assigns(:builds).first.status).to eq('success') end @@ -67,23 +67,16 @@ describe Projects::JobsController do context 'number of queries' do before do Ci::Build::AVAILABLE_STATUSES.each do |status| - create_build(status, status) + create_job(status, status) end - - RequestStore.begin! - end - - after do - RequestStore.end! - RequestStore.clear! end - it "verifies number of queries" do + it 'verifies number of queries', :request_store do recorded = ActiveRecord::QueryRecorder.new { get_index } - expect(recorded.count).to be_within(5).of(8) + expect(recorded.count).to be_within(5).of(7) end - def create_build(name, status) + def create_job(name, status) pipeline = create(:ci_pipeline, project: project) create(:ci_build, :tags, :triggered, :artifacts, pipeline: pipeline, name: name, status: status) @@ -101,21 +94,21 @@ describe Projects::JobsController do end describe 'GET show' do - let!(:build) { create(:ci_build, :failed, pipeline: pipeline) } + let!(:job) { create(:ci_build, :failed, pipeline: pipeline) } context 'when requesting HTML' do - context 'when build exists' do + context 'when job exists' do before do - get_show(id: build.id) + get_show(id: job.id) end - it 'has a build' do + it 'has a job' do expect(response).to have_http_status(:ok) - expect(assigns(:build).id).to eq(build.id) + expect(assigns(:build).id).to eq(job.id) end end - context 'when build does not exist' do + context 'when job does not exist' do before do get_show(id: 1234) end @@ -135,12 +128,12 @@ describe Projects::JobsController do allow_any_instance_of(Ci::Build).to receive(:merge_request).and_return(merge_request) - get_show(id: build.id, format: :json) + get_show(id: job.id, format: :json) end it 'exposes needed information' do expect(response).to have_http_status(:ok) - expect(json_response['raw_path']).to match(/builds\/\d+\/raw\z/) + expect(json_response['raw_path']).to match(/jobs\/\d+\/raw\z/) expect(json_response.dig('merge_request', 'path')).to match(/merge_requests\/\d+\z/) expect(json_response['new_issue_path']) .to include('/issues/new') @@ -162,35 +155,35 @@ describe Projects::JobsController do get_trace end - context 'when build has a trace' do - let(:build) { create(:ci_build, :trace, pipeline: pipeline) } + context 'when job has a trace' do + let(:job) { create(:ci_build, :trace, pipeline: pipeline) } it 'returns a trace' do expect(response).to have_http_status(:ok) - expect(json_response['id']).to eq build.id - expect(json_response['status']).to eq build.status + expect(json_response['id']).to eq job.id + expect(json_response['status']).to eq job.status expect(json_response['html']).to eq('BUILD TRACE') end end - context 'when build has no traces' do - let(:build) { create(:ci_build, pipeline: pipeline) } + context 'when job has no traces' do + let(:job) { create(:ci_build, pipeline: pipeline) } it 'returns no traces' do expect(response).to have_http_status(:ok) - expect(json_response['id']).to eq build.id - expect(json_response['status']).to eq build.status + expect(json_response['id']).to eq job.id + expect(json_response['status']).to eq job.status expect(json_response['html']).to be_nil end end - context 'when build has a trace with ANSI sequence and Unicode' do - let(:build) { create(:ci_build, :unicode_trace, pipeline: pipeline) } + context 'when job has a trace with ANSI sequence and Unicode' do + let(:job) { create(:ci_build, :unicode_trace, pipeline: pipeline) } it 'returns a trace with Unicode' do expect(response).to have_http_status(:ok) - expect(json_response['id']).to eq build.id - expect(json_response['status']).to eq build.status + expect(json_response['id']).to eq job.id + expect(json_response['status']).to eq job.status expect(json_response['html']).to include("ヾ(´༎ຶД༎ຶ`)ノ") end end @@ -198,23 +191,23 @@ describe Projects::JobsController do def get_trace get :trace, namespace_id: project.namespace, project_id: project, - id: build.id, + id: job.id, format: :json end end describe 'GET status.json' do - let(:build) { create(:ci_build, pipeline: pipeline) } - let(:status) { build.detailed_status(double('user')) } + let(:job) { create(:ci_build, pipeline: pipeline) } + let(:status) { job.detailed_status(double('user')) } before do get :status, namespace_id: project.namespace, project_id: project, - id: build.id, + id: job.id, format: :json end - it 'return a detailed build status in json' do + it 'return a detailed job status in json' do expect(response).to have_http_status(:ok) expect(json_response['text']).to eq status.text expect(json_response['label']).to eq status.label @@ -231,17 +224,17 @@ describe Projects::JobsController do post_retry end - context 'when build is retryable' do - let(:build) { create(:ci_build, :retryable, pipeline: pipeline) } + context 'when job is retryable' do + let(:job) { create(:ci_build, :retryable, pipeline: pipeline) } - it 'redirects to the retried build page' do + it 'redirects to the retried job page' do expect(response).to have_http_status(:found) expect(response).to redirect_to(namespace_project_job_path(id: Ci::Build.last.id)) end end - context 'when build is not retryable' do - let(:build) { create(:ci_build, pipeline: pipeline) } + context 'when job is not retryable' do + let(:job) { create(:ci_build, pipeline: pipeline) } it 'renders unprocessable_entity' do expect(response).to have_http_status(:unprocessable_entity) @@ -251,7 +244,7 @@ describe Projects::JobsController do def post_retry post :retry, namespace_id: project.namespace, project_id: project, - id: build.id + id: job.id end end @@ -267,21 +260,21 @@ describe Projects::JobsController do post_play end - context 'when build is playable' do - let(:build) { create(:ci_build, :playable, pipeline: pipeline) } + context 'when job is playable' do + let(:job) { create(:ci_build, :playable, pipeline: pipeline) } - it 'redirects to the played build page' do + it 'redirects to the played job page' do expect(response).to have_http_status(:found) - expect(response).to redirect_to(namespace_project_job_path(id: build.id)) + expect(response).to redirect_to(namespace_project_job_path(id: job.id)) end it 'transits to pending' do - expect(build.reload).to be_pending + expect(job.reload).to be_pending end end - context 'when build is not playable' do - let(:build) { create(:ci_build, pipeline: pipeline) } + context 'when job is not playable' do + let(:job) { create(:ci_build, pipeline: pipeline) } it 'renders unprocessable_entity' do expect(response).to have_http_status(:unprocessable_entity) @@ -291,7 +284,7 @@ describe Projects::JobsController do def post_play post :play, namespace_id: project.namespace, project_id: project, - id: build.id + id: job.id end end @@ -303,21 +296,21 @@ describe Projects::JobsController do post_cancel end - context 'when build is cancelable' do - let(:build) { create(:ci_build, :cancelable, pipeline: pipeline) } + context 'when job is cancelable' do + let(:job) { create(:ci_build, :cancelable, pipeline: pipeline) } - it 'redirects to the canceled build page' do + it 'redirects to the canceled job page' do expect(response).to have_http_status(:found) - expect(response).to redirect_to(namespace_project_job_path(id: build.id)) + expect(response).to redirect_to(namespace_project_job_path(id: job.id)) end it 'transits to canceled' do - expect(build.reload).to be_canceled + expect(job.reload).to be_canceled end end - context 'when build is not cancelable' do - let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } + context 'when job is not cancelable' do + let(:job) { create(:ci_build, :canceled, pipeline: pipeline) } it 'returns unprocessable_entity' do expect(response).to have_http_status(:unprocessable_entity) @@ -327,7 +320,7 @@ describe Projects::JobsController do def post_cancel post :cancel, namespace_id: project.namespace, project_id: project, - id: build.id + id: job.id end end @@ -337,7 +330,7 @@ describe Projects::JobsController do sign_in(user) end - context 'when builds are cancelable' do + context 'when jobs are cancelable' do before do create_list(:ci_build, 2, :cancelable, pipeline: pipeline) @@ -354,7 +347,7 @@ describe Projects::JobsController do end end - context 'when builds are not cancelable' do + context 'when jobs are not cancelable' do before do create_list(:ci_build, 2, :canceled, pipeline: pipeline) @@ -381,26 +374,26 @@ describe Projects::JobsController do post_erase end - context 'when build is erasable' do - let(:build) { create(:ci_build, :erasable, :trace, pipeline: pipeline) } + context 'when job is erasable' do + let(:job) { create(:ci_build, :erasable, :trace, pipeline: pipeline) } - it 'redirects to the erased build page' do + it 'redirects to the erased job page' do expect(response).to have_http_status(:found) - expect(response).to redirect_to(namespace_project_job_path(id: build.id)) + expect(response).to redirect_to(namespace_project_job_path(id: job.id)) end it 'erases artifacts' do - expect(build.artifacts_file.exists?).to be_falsey - expect(build.artifacts_metadata.exists?).to be_falsey + expect(job.artifacts_file.exists?).to be_falsey + expect(job.artifacts_metadata.exists?).to be_falsey end it 'erases trace' do - expect(build.trace.exist?).to be_falsey + expect(job.trace.exist?).to be_falsey end end - context 'when build is not erasable' do - let(:build) { create(:ci_build, :erased, pipeline: pipeline) } + context 'when job is not erasable' do + let(:job) { create(:ci_build, :erased, pipeline: pipeline) } it 'returns unprocessable_entity' do expect(response).to have_http_status(:unprocessable_entity) @@ -410,7 +403,7 @@ describe Projects::JobsController do def post_erase post :erase, namespace_id: project.namespace, project_id: project, - id: build.id + id: job.id end end @@ -419,8 +412,8 @@ describe Projects::JobsController do get_raw end - context 'when build has a trace file' do - let(:build) { create(:ci_build, :trace, pipeline: pipeline) } + context 'when job has a trace file' do + let(:job) { create(:ci_build, :trace, pipeline: pipeline) } it 'send a trace file' do expect(response).to have_http_status(:ok) @@ -429,8 +422,8 @@ describe Projects::JobsController do end end - context 'when build does not have a trace file' do - let(:build) { create(:ci_build, pipeline: pipeline) } + context 'when job does not have a trace file' do + let(:job) { create(:ci_build, pipeline: pipeline) } it 'returns not_found' do expect(response).to have_http_status(:not_found) @@ -440,7 +433,7 @@ describe Projects::JobsController do def get_raw post :raw, namespace_id: project.namespace, project_id: project, - id: build.id + id: job.id end end end diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb index 130b0b744b5..bf1776eb320 100644 --- a/spec/controllers/projects/labels_controller_spec.rb +++ b/spec/controllers/projects/labels_controller_spec.rb @@ -117,7 +117,7 @@ describe Projects::LabelsController do let!(:promoted_label_name) { "Promoted Label" } let!(:label_1) { create(:label, title: promoted_label_name, project: project) } - context 'not group owner' do + context 'not group reporters' do it 'denies access' do post :promote, namespace_id: project.namespace.to_param, project_id: project, id: label_1.to_param @@ -125,9 +125,9 @@ describe Projects::LabelsController do end end - context 'group owner' do + context 'group reporter' do before do - GroupMember.add_users(group, [user], :owner) + group.add_reporter(user) end it 'gives access' do diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 08024a2148b..d8a3a510f97 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -19,7 +19,10 @@ describe Projects::MergeRequestsController do render_views let(:fork_project) { create(:forked_project_with_submodules) } - before { fork_project.team << [user, :master] } + + before do + fork_project.team << [user, :master] + end context 'when rendering HTML response' do it 'renders new merge request widget template' do @@ -119,14 +122,14 @@ describe Projects::MergeRequestsController do end end - context 'number of queries' do + context 'number of queries', :request_store do it 'verifies number of queries' do # pre-create objects merge_request recorded = ActiveRecord::QueryRecorder.new { go(format: :json) } - expect(recorded.count).to be_within(5).of(50) + expect(recorded.count).to be_within(5).of(30) expect(recorded.cached_count).to eq(0) end end @@ -328,7 +331,9 @@ describe Projects::MergeRequestsController do end context 'when the sha parameter does not match the source SHA' do - before { post :merge, base_params.merge(sha: 'foo') } + before do + post :merge, base_params.merge(sha: 'foo') + end it 'returns :sha_mismatch' do expect(json_response).to eq('status' => 'sha_mismatch') @@ -473,7 +478,9 @@ describe Projects::MergeRequestsController do let(:namespace) { create(:namespace, owner: owner) } let(:project) { create(:project, namespace: namespace) } - before { sign_in owner } + before do + sign_in owner + end it "deletes the merge request" do delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid @@ -505,7 +512,9 @@ describe Projects::MergeRequestsController do context 'with default params' do context 'as html' do - before { go(format: 'html') } + before do + go(format: 'html') + end it 'renders the diff template' do expect(response).to render_template('diffs') @@ -513,7 +522,9 @@ describe Projects::MergeRequestsController do end context 'as json' do - before { go(format: 'json') } + before do + go(format: 'json') + end it 'renders the diffs template to a string' do expect(response).to render_template('projects/merge_requests/show/_diffs') @@ -544,7 +555,9 @@ describe Projects::MergeRequestsController do context 'with ignore_whitespace_change' do context 'as html' do - before { go(format: 'html', w: 1) } + before do + go(format: 'html', w: 1) + end it 'renders the diff template' do expect(response).to render_template('diffs') @@ -552,7 +565,9 @@ describe Projects::MergeRequestsController do end context 'as json' do - before { go(format: 'json', w: 1) } + before do + go(format: 'json', w: 1) + end it 'renders the diffs template to a string' do expect(response).to render_template('projects/merge_requests/show/_diffs') @@ -562,7 +577,9 @@ describe Projects::MergeRequestsController do end context 'with view' do - before { go(view: 'parallel') } + before do + go(view: 'parallel') + end it 'saves the preferred diff view in a cookie' do expect(response.cookies['diff_view']).to eq('parallel') @@ -605,7 +622,9 @@ describe Projects::MergeRequestsController do end context 'when the path does not exist in the diff' do - before { diff_for_path(id: merge_request.iid, old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb') } + before do + diff_for_path(id: merge_request.iid, old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb') + end it 'returns a 404' do expect(response).to have_http_status(404) @@ -626,7 +645,9 @@ describe Projects::MergeRequestsController do end context 'when the merge request does not exist' do - before { diff_for_path(id: merge_request.iid.succ, old_path: existing_path, new_path: existing_path) } + before do + diff_for_path(id: merge_request.iid.succ, old_path: existing_path, new_path: existing_path) + end it 'returns a 404' do expect(response).to have_http_status(404) @@ -670,7 +691,9 @@ describe Projects::MergeRequestsController do context 'when the source branch is in a different project to the target' do let(:other_project) { create(:project) } - before { other_project.team << [user, :master] } + before do + other_project.team << [user, :master] + end context 'when the path exists in the diff' do it 'disables diff notes' do @@ -690,7 +713,9 @@ describe Projects::MergeRequestsController do end context 'when the path does not exist in the diff' do - before { diff_for_path(old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb', merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) } + before do + diff_for_path(old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb', merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) + end it 'returns a 404' do expect(response).to have_http_status(404) @@ -913,7 +938,9 @@ describe Projects::MergeRequestsController do end context 'when the file does not exist cannot be resolved in the UI' do - before { conflict_for_path('files/ruby/regexp.rb') } + before do + conflict_for_path('files/ruby/regexp.rb') + end it 'returns a 404 status code' do expect(response).to have_http_status(:not_found) @@ -923,7 +950,9 @@ describe Projects::MergeRequestsController do context 'with an existing file' do let(:path) { 'files/ruby/regex.rb' } - before { conflict_for_path(path) } + before do + conflict_for_path(path) + end it 'returns a 200 status code' do expect(response).to have_http_status(:ok) @@ -1195,7 +1224,9 @@ describe Projects::MergeRequestsController do end context 'when head_pipeline does not exist' do - before { get_pipeline_status } + before do + get_pipeline_status + end it 'return empty' do expect(response).to have_http_status(:ok) diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index c880da1e36a..734532668d3 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -5,9 +5,12 @@ describe Projects::PipelinesController do let(:user) { create(:user) } let(:project) { create(:empty_project, :public) } + let(:feature) { ProjectFeature::DISABLED } before do project.add_developer(user) + project.project_feature.update( + builds_access_level: feature) sign_in(user) end @@ -49,21 +52,14 @@ describe Projects::PipelinesController do expect(json_response['details']).to have_key 'stages' end - context 'when the pipeline has multiple stages and groups' do + context 'when the pipeline has multiple stages and groups', :request_store do before do - RequestStore.begin! - create_build('build', 0, 'build') create_build('test', 1, 'rspec 0') create_build('deploy', 2, 'production') create_build('post deploy', 3, 'pages 0') end - after do - RequestStore.end! - RequestStore.clear! - end - let(:project) { create(:project) } let(:pipeline) do create(:ci_empty_pipeline, project: project, user: user, sha: project.commit.id) @@ -160,16 +156,26 @@ describe Projects::PipelinesController do format: :json end - it 'retries a pipeline without returning any content' do - expect(response).to have_http_status(:no_content) - expect(build.reload).to be_retried + context 'when builds are enabled' do + let(:feature) { ProjectFeature::ENABLED } + + it 'retries a pipeline without returning any content' do + expect(response).to have_http_status(:no_content) + expect(build.reload).to be_retried + end + end + + context 'when builds are disabled' do + it 'fails to retry pipeline' do + expect(response).to have_http_status(:not_found) + end end end describe 'POST cancel.json' do let!(:pipeline) { create(:ci_pipeline, project: project) } let!(:build) { create(:ci_build, :running, pipeline: pipeline) } - + before do post :cancel, namespace_id: project.namespace, project_id: project, @@ -177,9 +183,19 @@ describe Projects::PipelinesController do format: :json end - it 'cancels a pipeline without returning any content' do - expect(response).to have_http_status(:no_content) - expect(pipeline.reload).to be_canceled + context 'when builds are enabled' do + let(:feature) { ProjectFeature::ENABLED } + + it 'cancels a pipeline without returning any content' do + expect(response).to have_http_status(:no_content) + expect(pipeline.reload).to be_canceled + end + end + + context 'when builds are disabled' do + it 'fails to retry pipeline' do + expect(response).to have_http_status(:not_found) + end end end end diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index 2294d5df581..f2b59ba82ca 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -16,10 +16,14 @@ describe Projects::ProjectMembersController do describe 'POST create' do let(:project_user) { create(:user) } - before { sign_in(user) } + before do + sign_in(user) + end context 'when user does not have enough rights' do - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end it 'returns 404' do post :create, namespace_id: project.namespace, @@ -33,7 +37,9 @@ describe Projects::ProjectMembersController do end context 'when user has enough rights' do - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it 'adds user to members' do expect_any_instance_of(Members::CreateService).to receive(:execute).and_return(status: :success) @@ -64,7 +70,9 @@ describe Projects::ProjectMembersController do describe 'DELETE destroy' do let(:member) { create(:project_member, :developer, project: project) } - before { sign_in(user) } + before do + sign_in(user) + end context 'when member is not found' do it 'returns 404' do @@ -78,7 +86,9 @@ describe Projects::ProjectMembersController do context 'when member is found' do context 'when user does not have enough rights' do - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end it 'returns 404' do delete :destroy, namespace_id: project.namespace, @@ -91,7 +101,9 @@ describe Projects::ProjectMembersController do end context 'when user has enough rights' do - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it '[HTML] removes user from members' do delete :destroy, namespace_id: project.namespace, @@ -117,7 +129,9 @@ describe Projects::ProjectMembersController do end describe 'DELETE leave' do - before { sign_in(user) } + before do + sign_in(user) + end context 'when member is not found' do it 'returns 404' do @@ -130,7 +144,9 @@ describe Projects::ProjectMembersController do context 'when member is found' do context 'and is not an owner' do - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end it 'removes user from members' do delete :leave, namespace_id: project.namespace, @@ -145,7 +161,9 @@ describe Projects::ProjectMembersController do context 'and is an owner' do let(:project) { create(:empty_project, namespace: user.namespace) } - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it 'cannot remove himself from the project' do delete :leave, namespace_id: project.namespace, @@ -156,7 +174,9 @@ describe Projects::ProjectMembersController do end context 'and is a requester' do - before { project.request_access(user) } + before do + project.request_access(user) + end it 'removes user from members' do delete :leave, namespace_id: project.namespace, @@ -172,7 +192,9 @@ describe Projects::ProjectMembersController do end describe 'POST request_access' do - before { sign_in(user) } + before do + sign_in(user) + end it 'creates a new ProjectMember that is not a team member' do post :request_access, namespace_id: project.namespace, @@ -190,7 +212,9 @@ describe Projects::ProjectMembersController do describe 'POST approve' do let(:member) { create(:project_member, :access_request, project: project) } - before { sign_in(user) } + before do + sign_in(user) + end context 'when member is not found' do it 'returns 404' do @@ -204,7 +228,9 @@ describe Projects::ProjectMembersController do context 'when member is found' do context 'when user does not have enough rights' do - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end it 'returns 404' do post :approve_access_request, namespace_id: project.namespace, @@ -217,7 +243,9 @@ describe Projects::ProjectMembersController do end context 'when user has enough rights' do - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it 'adds user to members' do post :approve_access_request, namespace_id: project.namespace, @@ -252,7 +280,10 @@ describe Projects::ProjectMembersController do end context 'when user can access source project members' do - before { another_project.team << [user, :guest] } + before do + another_project.team << [user, :guest] + end + include_context 'import applied' it 'imports source project members' do diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index 24a59caff4e..2434f822c6f 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -46,7 +46,9 @@ describe Projects::SnippetsController do end context 'when signed in as the author' do - before { sign_in(user) } + before do + sign_in(user) + end it 'renders the snippet' do get :index, namespace_id: project.namespace, project_id: project @@ -57,7 +59,9 @@ describe Projects::SnippetsController do end context 'when signed in as a project member' do - before { sign_in(user2) } + before do + sign_in(user2) + end it 'renders the snippet' do get :index, namespace_id: project.namespace, project_id: project @@ -78,8 +82,18 @@ describe Projects::SnippetsController do post :create, { namespace_id: project.namespace.to_param, project_id: project, - project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params) + project_snippet: { title: 'Title', content: 'Content', description: 'Description' }.merge(snippet_params) }.merge(additional_params) + + Snippet.last + end + + it 'creates the snippet correctly' do + snippet = create_snippet(project, visibility_level: Snippet::PRIVATE) + + expect(snippet.title).to eq('Title') + expect(snippet.content).to eq('Content') + expect(snippet.description).to eq('Description') end context 'when the snippet is spam' do @@ -307,7 +321,9 @@ describe Projects::SnippetsController do end context 'when signed in as the author' do - before { sign_in(user) } + before do + sign_in(user) + end it 'renders the snippet' do get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param @@ -318,7 +334,9 @@ describe Projects::SnippetsController do end context 'when signed in as a project member' do - before { sign_in(user2) } + before do + sign_in(user2) + end it 'renders the snippet' do get action, namespace_id: project.namespace, project_id: project, id: project_snippet.to_param @@ -339,7 +357,9 @@ describe Projects::SnippetsController do end context 'when signed in' do - before { sign_in(user) } + before do + sign_in(user) + end it 'responds with status 404' do get action, namespace_id: project.namespace, project_id: project, id: 42 diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb index fc97bac64cd..c48f41ca12e 100644 --- a/spec/controllers/projects/tags_controller_spec.rb +++ b/spec/controllers/projects/tags_controller_spec.rb @@ -6,7 +6,9 @@ describe Projects::TagsController do let!(:invalid_release) { create(:release, project: project, tag: 'does-not-exist') } describe 'GET index' do - before { get :index, namespace_id: project.namespace.to_param, project_id: project } + before do + get :index, namespace_id: project.namespace.to_param, project_id: project + end it 'returns the tags for the page' do expect(assigns(:tags).map(&:name)).to eq(['v1.1.0', 'v1.0.0']) @@ -19,7 +21,9 @@ describe Projects::TagsController do end describe 'GET show' do - before { get :show, namespace_id: project.namespace.to_param, project_id: project, id: id } + before do + get :show, namespace_id: project.namespace.to_param, project_id: project, id: id + end context "valid tag" do let(:id) { 'v1.0.0' } diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 4f6fc6691be..240a81367d0 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -29,7 +29,9 @@ describe ProjectsController do describe "GET show" do context "user not project member" do - before { sign_in(user) } + before do + sign_in(user) + end context "user does not have access to project" do let(:private_project) { create(:empty_project, :private) } @@ -108,7 +110,9 @@ describe ProjectsController do context "project with empty repo" do let(:empty_project) { create(:project_empty_repo, :public) } - before { sign_in(user) } + before do + sign_in(user) + end User.project_views.keys.each do |project_view| context "with #{project_view} view set" do @@ -128,7 +132,9 @@ describe ProjectsController do context "project with broken repo" do let(:empty_project) { create(:project_broken_repo, :public) } - before { sign_in(user) } + before do + sign_in(user) + end User.project_views.keys.each do |project_view| context "with #{project_view} view set" do diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index 3173aae664c..a3708ad0908 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -18,7 +18,9 @@ describe SearchController do context 'on restricted projects' do context 'when signed out' do - before { sign_out(user) } + before do + sign_out(user) + end it "doesn't expose comments on issues" do project = create(:empty_project, :public, :issues_private) diff --git a/spec/controllers/sent_notifications_controller_spec.rb b/spec/controllers/sent_notifications_controller_spec.rb index 954fc2eaf21..0cc8a3b68eb 100644 --- a/spec/controllers/sent_notifications_controller_spec.rb +++ b/spec/controllers/sent_notifications_controller_spec.rb @@ -14,7 +14,9 @@ describe SentNotificationsController, type: :controller do describe 'GET unsubscribe' do context 'when the user is not logged in' do context 'when the force param is passed' do - before { get(:unsubscribe, id: sent_notification.reply_key, force: true) } + before do + get(:unsubscribe, id: sent_notification.reply_key, force: true) + end it 'unsubscribes the user' do expect(issue.subscribed?(user, project)).to be_falsey @@ -30,7 +32,9 @@ describe SentNotificationsController, type: :controller do end context 'when the force param is not passed' do - before { get(:unsubscribe, id: sent_notification.reply_key) } + before do + get(:unsubscribe, id: sent_notification.reply_key) + end it 'does not unsubscribe the user' do expect(issue.subscribed?(user, project)).to be_truthy @@ -47,10 +51,14 @@ describe SentNotificationsController, type: :controller do end context 'when the user is logged in' do - before { sign_in(user) } + before do + sign_in(user) + end context 'when the ID passed does not exist' do - before { get(:unsubscribe, id: sent_notification.reply_key.reverse) } + before do + get(:unsubscribe, id: sent_notification.reply_key.reverse) + end it 'does not unsubscribe the user' do expect(issue.subscribed?(user, project)).to be_truthy @@ -66,7 +74,9 @@ describe SentNotificationsController, type: :controller do end context 'when the force param is passed' do - before { get(:unsubscribe, id: sent_notification.reply_key, force: true) } + before do + get(:unsubscribe, id: sent_notification.reply_key, force: true) + end it 'unsubscribes the user' do expect(issue.subscribed?(user, project)).to be_falsey @@ -89,7 +99,10 @@ describe SentNotificationsController, type: :controller do end end let(:sent_notification) { create(:sent_notification, project: project, noteable: merge_request, recipient: user) } - before { get(:unsubscribe, id: sent_notification.reply_key) } + + before do + get(:unsubscribe, id: sent_notification.reply_key) + end it 'unsubscribes the user' do expect(merge_request.subscribed?(user, project)).to be_falsey diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index e87e24a33a1..03f4b0ba343 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -142,7 +142,9 @@ describe SessionsController do end context 'when OTP is invalid' do - before { authenticate_2fa(otp_attempt: 'invalid') } + before do + authenticate_2fa(otp_attempt: 'invalid') + end it 'does not authenticate' do expect(subject.current_user).not_to eq user @@ -169,7 +171,9 @@ describe SessionsController do end context 'when OTP is invalid' do - before { authenticate_2fa(otp_attempt: 'invalid') } + before do + authenticate_2fa(otp_attempt: 'invalid') + end it 'does not authenticate' do expect(subject.current_user).not_to eq user diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index 930415a4778..b1339b2a185 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -171,12 +171,50 @@ describe SnippetsController do sign_in(user) post :create, { - personal_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params) + personal_snippet: { title: 'Title', content: 'Content', description: 'Description' }.merge(snippet_params) }.merge(additional_params) Snippet.last end + it 'creates the snippet correctly' do + snippet = create_snippet(visibility_level: Snippet::PRIVATE) + + expect(snippet.title).to eq('Title') + expect(snippet.content).to eq('Content') + expect(snippet.description).to eq('Description') + end + + context 'when the snippet description contains a file' do + let(:picture_file) { '/temp/secret56/picture.jpg' } + let(:text_file) { '/temp/secret78/text.txt' } + let(:description) do + "Description with picture: ![picture](/uploads#{picture_file}) and "\ + "text: [text.txt](/uploads#{text_file})" + end + + before do + allow(FileUtils).to receive(:mkdir_p) + allow(FileUtils).to receive(:move) + end + + subject { create_snippet({ description: description }, { files: [picture_file, text_file] }) } + + it 'creates the snippet' do + expect { subject }.to change { Snippet.count }.by(1) + end + + it 'stores the snippet description correctly' do + snippet = subject + + expected_description = "Description with picture: "\ + "![picture](/uploads/personal_snippet/#{snippet.id}/secret56/picture.jpg) and "\ + "text: [text.txt](/uploads/personal_snippet/#{snippet.id}/secret78/text.txt)" + + expect(snippet.description).to eq(expected_description) + end + end + context 'when the snippet is spam' do before do allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) @@ -399,7 +437,9 @@ describe SnippetsController do end context 'when signed in user is the author' do - before { get :raw, id: personal_snippet.to_param } + before do + get :raw, id: personal_snippet.to_param + end it 'responds with status 200' do expect(assigns(:snippet)).to eq(personal_snippet) diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 8000c9dec61..01a0659479b 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -92,6 +92,40 @@ describe UploadsController do end end end + + context 'temporal with valid image' do + subject do + post :create, model: 'personal_snippet', file: jpg, format: :json + end + + it 'returns a content with original filename, new link, and correct type.' do + subject + + expect(response.body).to match '\"alt\":\"rails_sample\"' + expect(response.body).to match "\"url\":\"/uploads/temp" + end + + it 'does not create an Upload record' do + expect { subject }.not_to change { Upload.count } + end + end + + context 'temporal with valid non-image file' do + subject do + post :create, model: 'personal_snippet', file: txt, format: :json + end + + it 'returns a content with original filename, new link, and correct type.' do + subject + + expect(response.body).to match '\"alt\":\"doc_sample.txt\"' + expect(response.body).to match "\"url\":\"/uploads/temp" + end + + it 'does not create an Upload record' do + expect { subject }.not_to change { Upload.count } + end + end end end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index d33e2ba1e53..842d82cdbe9 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -43,7 +43,9 @@ describe UsersController do end context 'when logged in' do - before { sign_in(user) } + before do + sign_in(user) + end it 'renders show' do get :show, username: user.username @@ -62,7 +64,9 @@ describe UsersController do end context 'when logged in' do - before { sign_in(user) } + before do + sign_in(user) + end it 'renders 404' do get :show, username: 'nonexistent' diff --git a/spec/db/production/settings.rb b/spec/db/production/settings.rb deleted file mode 100644 index 3cbb173c4cc..00000000000 --- a/spec/db/production/settings.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'spec_helper' - -describe 'seed production settings', lib: true do - include StubENV - - context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do - before do - stub_env('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN', '013456789') - end - - it 'writes the token to the database' do - load(File.join(__dir__, '../../../db/fixtures/production/010_settings.rb')) - expect(ApplicationSetting.current.runners_registration_token).to eq('013456789') - end - end -end diff --git a/spec/db/production/settings_spec.rb b/spec/db/production/settings_spec.rb new file mode 100644 index 00000000000..a9d015e0666 --- /dev/null +++ b/spec/db/production/settings_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' +require 'rainbow/ext/string' + +describe 'seed production settings', lib: true do + include StubENV + let(:settings_file) { Rails.root.join('db/fixtures/production/010_settings.rb') } + let(:settings) { Gitlab::CurrentSettings.current_application_settings } + + context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do + before do + stub_env('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN', '013456789') + end + + it 'writes the token to the database' do + load(settings_file) + + expect(settings.runners_registration_token).to eq('013456789') + end + end + + context 'GITLAB_PROMETHEUS_METRICS_ENABLED is set in the environment' do + context 'GITLAB_PROMETHEUS_METRICS_ENABLED is true' do + before do + stub_env('GITLAB_PROMETHEUS_METRICS_ENABLED', 'true') + end + + it 'prometheus_metrics_enabled is set to true ' do + load(settings_file) + + expect(settings.prometheus_metrics_enabled).to eq(true) + end + end + + context 'GITLAB_PROMETHEUS_METRICS_ENABLED is false' do + before do + stub_env('GITLAB_PROMETHEUS_METRICS_ENABLED', 'false') + end + + it 'prometheus_metrics_enabled is set to false' do + load(settings_file) + + expect(settings.prometheus_metrics_enabled).to eq(false) + end + end + + context 'GITLAB_PROMETHEUS_METRICS_ENABLED is false' do + before do + stub_env('GITLAB_PROMETHEUS_METRICS_ENABLED', '') + end + + it 'prometheus_metrics_enabled is set to false' do + load(settings_file) + + expect(settings.prometheus_metrics_enabled).to eq(false) + end + end + end +end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 0bb5a86d9b9..0cc498f0ce9 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -194,8 +194,8 @@ FactoryGirl.define do trait :extended_options do options do { - image: 'ruby:2.1', - services: ['postgres'], + image: { name: 'ruby:2.1', entrypoint: '/bin/sh' }, + services: ['postgres', { name: 'docker:dind', entrypoint: '/bin/sh', command: 'sleep 30', alias: 'docker' }], after_script: %w(ls date), artifacts: { name: 'artifacts_file', diff --git a/spec/factories/ci/stages.rb b/spec/factories/ci/stages.rb index d37eabb3e8c..d3c8bf9d54f 100644 --- a/spec/factories/ci/stages.rb +++ b/spec/factories/ci/stages.rb @@ -1,5 +1,5 @@ FactoryGirl.define do - factory :ci_stage, class: Ci::Stage do + factory :ci_stage, class: Ci::LegacyStage do skip_create transient do @@ -10,7 +10,9 @@ FactoryGirl.define do end initialize_with do - Ci::Stage.new(pipeline, name: name, status: status, warnings: warnings) + Ci::LegacyStage.new(pipeline, name: name, + status: status, + warnings: warnings) end end end diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb index f6a78811cbe..48142d3c49b 100644 --- a/spec/factories/lists.rb +++ b/spec/factories/lists.rb @@ -6,6 +6,12 @@ FactoryGirl.define do sequence(:position) end + factory :backlog_list, parent: :list do + list_type :backlog + label nil + position nil + end + factory :closed_list, parent: :list do list_type :closed label nil diff --git a/spec/factories/snippets.rb b/spec/factories/snippets.rb index 18cb0f5de26..388f662e6e5 100644 --- a/spec/factories/snippets.rb +++ b/spec/factories/snippets.rb @@ -3,6 +3,7 @@ FactoryGirl.define do author title { generate(:title) } content { generate(:title) } + description { generate(:title) } file_name { generate(:filename) } trait :public do diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb new file mode 100644 index 00000000000..1383420fb44 --- /dev/null +++ b/spec/factories/uploads.rb @@ -0,0 +1,8 @@ +FactoryGirl.define do + factory :upload do + model { build(:project) } + path { "uploads/system/project/avatar/avatar.jpg" } + size 100.kilobytes + uploader "AvatarUploader" + end +end diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb index 96d715ef383..595366ce352 100644 --- a/spec/features/admin/admin_appearance_spec.rb +++ b/spec/features/admin/admin_appearance_spec.rb @@ -63,11 +63,11 @@ feature 'Admin Appearance', feature: true do end def logo_selector - '//img[@src^="/uploads/appearance/logo"]' + '//img[@src^="/uploads/system/appearance/logo"]' end def header_logo_selector - '//img[@src^="/uploads/appearance/header_logo"]' + '//img[@src^="/uploads/system/appearance/header_logo"]' end def logo_fixture diff --git a/spec/features/admin/admin_deploy_keys_spec.rb b/spec/features/admin/admin_deploy_keys_spec.rb index c0b6995a84a..5f5fa4e932a 100644 --- a/spec/features/admin/admin_deploy_keys_spec.rb +++ b/spec/features/admin/admin_deploy_keys_spec.rb @@ -11,40 +11,67 @@ RSpec.describe 'admin deploy keys', type: :feature do it 'show all public deploy keys' do visit admin_deploy_keys_path - expect(page).to have_content(deploy_key.title) - expect(page).to have_content(another_deploy_key.title) + page.within(find('.deploy-keys-list', match: :first)) do + expect(page).to have_content(deploy_key.title) + expect(page).to have_content(another_deploy_key.title) + end end - describe 'create new deploy key' do + describe 'create a new deploy key' do + let(:new_ssh_key) { attributes_for(:key)[:key] } + before do visit admin_deploy_keys_path click_link 'New deploy key' end - it 'creates new deploy key' do - fill_deploy_key + it 'creates a new deploy key' do + fill_in 'deploy_key_title', with: 'laptop' + fill_in 'deploy_key_key', with: new_ssh_key + check 'deploy_key_can_push' click_button 'Create' - expect_renders_new_key - end + expect(current_path).to eq admin_deploy_keys_path - it 'creates new deploy key with write access' do - fill_deploy_key - check "deploy_key_can_push" - click_button "Create" + page.within(find('.deploy-keys-list', match: :first)) do + expect(page).to have_content('laptop') + expect(page).to have_content('Yes') + end + end + end - expect_renders_new_key - expect(page).to have_content('Yes') + describe 'update an existing deploy key' do + before do + visit admin_deploy_keys_path + find('tr', text: deploy_key.title).click_link('Edit') end - def expect_renders_new_key + it 'updates an existing deploy key' do + fill_in 'deploy_key_title', with: 'new-title' + check 'deploy_key_can_push' + click_button 'Save changes' + expect(current_path).to eq admin_deploy_keys_path - expect(page).to have_content('laptop') + + page.within(find('.deploy-keys-list', match: :first)) do + expect(page).to have_content('new-title') + expect(page).to have_content('Yes') + end end + end - def fill_deploy_key - fill_in 'deploy_key_title', with: 'laptop' - fill_in 'deploy_key_key', with: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop' + describe 'remove an existing deploy key' do + before do + visit admin_deploy_keys_path + end + + it 'removes an existing deploy key' do + find('tr', text: deploy_key.title).click_link('Remove') + + expect(current_path).to eq admin_deploy_keys_path + page.within(find('.deploy-keys-list', match: :first)) do + expect(page).not_to have_content(deploy_key.title) + end end end end diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index d5f595894d6..cf9d7bca255 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -24,7 +24,9 @@ feature 'Admin Groups', feature: true do it 'creates new group' do visit admin_groups_path - click_link "New group" + page.within '#content-body' do + click_link "New group" + end path_component = 'gitlab' group_name = 'GitLab group name' group_description = 'Description of group for GitLab' diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index 5dcc7d35d82..bc11b090fdb 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -134,7 +134,10 @@ describe "Admin Runners" do describe 'runners registration token' do let!(:token) { current_application_settings.runners_registration_token } - before { visit admin_runners_path } + + before do + visit admin_runners_path + end it 'has a registration token' do expect(page).to have_content("Registration token is #{token}") diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 5099441dce2..27bc25be580 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -20,10 +20,15 @@ feature 'Admin updates settings', feature: true do uncheck 'Gravatar enabled' fill_in 'Home page URL', with: 'https://about.gitlab.com/' fill_in 'Help page text', with: 'Example text' + check 'Hide marketing-related entries from help' + fill_in 'Support page URL', with: 'http://example.com/help' click_button 'Save' expect(current_application_settings.gravatar_enabled).to be_falsey expect(current_application_settings.home_page_url).to eq "https://about.gitlab.com/" + expect(current_application_settings.help_page_text).to eq "Example text" + expect(current_application_settings.help_page_hide_commercial_content).to be_truthy + expect(current_application_settings.help_page_support_url).to eq "http://example.com/help" expect(page).to have_content "Application settings saved successfully" end diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb index 0fb4baeb71c..849ec829f75 100644 --- a/spec/features/admin/admin_users_impersonation_tokens_spec.rb +++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb @@ -12,7 +12,9 @@ describe 'Admin > Users > Impersonation Tokens', feature: true, js: true do find(".table.inactive-tokens") end - before { login_as(admin) } + before do + login_as(admin) + end describe "token creation" do it "allows creation of a token" do diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 301a47169a4..f72651667ee 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -124,7 +124,10 @@ describe "Admin::Users", feature: true do describe 'Impersonation' do let(:another_user) { create(:user) } - before { visit admin_user_path(another_user) } + + before do + visit admin_user_path(another_user) + end context 'before impersonating' do it 'shows impersonate button for other users' do @@ -149,7 +152,9 @@ describe "Admin::Users", feature: true do end context 'when impersonating' do - before { click_link 'Impersonate' } + before do + click_link 'Impersonate' + end it 'logs in as the user when impersonate is clicked' do expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(another_user.username) diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index 32ac265814f..2b8edac4f10 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -231,7 +231,7 @@ describe 'Issue Boards add issue modal', :feature, :js do click_button 'Add 1 issue' end - page.within(first('.board')) do + page.within(find('.board:nth-child(2)')) do expect(page).to have_selector('.card') end end @@ -247,7 +247,7 @@ describe 'Issue Boards add issue modal', :feature, :js do click_button 'Add 1 issue' end - page.within(find('.board:nth-child(2)')) do + page.within(find('.board:nth-child(3)')) do expect(page).to have_selector('.card') end end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index ba27db23ced..968cc9d9c80 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -19,7 +19,7 @@ describe 'Issue Boards', feature: true, js: true do before do visit namespace_project_board_path(project.namespace, project, board) wait_for_requests - expect(page).to have_selector('.board', count: 2) + expect(page).to have_selector('.board', count: 3) end it 'shows blank state' do @@ -36,18 +36,18 @@ describe 'Issue Boards', feature: true, js: true do page.within(find('.board-blank-state')) do click_button("Nevermind, I'll use my own") end - expect(page).to have_selector('.board', count: 1) + expect(page).to have_selector('.board', count: 2) end it 'creates default lists' do - lists = ['To Do', 'Doing', 'Closed'] + lists = ['Backlog', 'To Do', 'Doing', 'Closed'] page.within(find('.board-blank-state')) do click_button('Add default lists') end wait_for_requests - expect(page).to have_selector('.board', count: 3) + expect(page).to have_selector('.board', count: 4) page.all('.board').each_with_index do |list, i| expect(list.find('.board-title')).to have_content(lists[i]) @@ -85,29 +85,25 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - expect(page).to have_selector('.board', count: 3) - expect(find('.board:nth-child(1)')).to have_selector('.card') + expect(page).to have_selector('.board', count: 4) expect(find('.board:nth-child(2)')).to have_selector('.card') expect(find('.board:nth-child(3)')).to have_selector('.card') - end - - it 'shows lists' do - expect(page).to have_selector('.board', count: 3) + expect(find('.board:nth-child(4)')).to have_selector('.card') end it 'shows description tooltip on list title' do - page.within('.board:nth-child(1)') do + page.within('.board:nth-child(2)') do expect(find('.board-title span.has-tooltip')[:title]).to eq('Test') end end it 'shows issues in lists' do - wait_for_board_cards(1, 8) - wait_for_board_cards(2, 2) + wait_for_board_cards(2, 8) + wait_for_board_cards(3, 2) end it 'shows confidential issues with icon' do - page.within(find('.board', match: :first)) do + page.within(find('.board:nth-child(2)')) do expect(page).to have_selector('.confidential-icon', count: 1) end end @@ -118,9 +114,9 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0) expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) - expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1) + expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1) end it 'search list' do @@ -129,32 +125,32 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1) - expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1) expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0) end it 'allows user to delete board' do - page.within(find('.board:nth-child(1)')) do + page.within(find('.board:nth-child(2)')) do find('.board-delete').click end wait_for_requests - expect(page).to have_selector('.board', count: 2) + expect(page).to have_selector('.board', count: 3) end it 'removes checkmark in new list dropdown after deleting' do click_button 'Add list' wait_for_requests - page.within(find('.board:nth-child(1)')) do + page.within(find('.board:nth-child(2)')) do find('.board-delete').click end wait_for_requests - expect(page).to have_selector('.board', count: 2) + expect(page).to have_selector('.board', count: 3) end it 'infinite scrolls list' do @@ -165,18 +161,18 @@ describe 'Issue Boards', feature: true, js: true do visit namespace_project_board_path(project.namespace, project, board) wait_for_requests - page.within(find('.board', match: :first)) do + page.within(find('.board:nth-child(2)')) do expect(page.find('.board-header')).to have_content('58') expect(page).to have_selector('.card', count: 20) expect(page).to have_content('Showing 20 of 58 issues') - evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") + evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") wait_for_requests expect(page).to have_selector('.card', count: 40) expect(page).to have_content('Showing 40 of 58 issues') - evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") + evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") wait_for_requests expect(page).to have_selector('.card', count: 58) @@ -186,83 +182,83 @@ describe 'Issue Boards', feature: true, js: true do context 'closed' do it 'shows list of closed issues' do - wait_for_board_cards(3, 1) + wait_for_board_cards(4, 1) wait_for_requests end it 'moves issue to closed' do - drag(list_from_index: 0, list_to_index: 2) + drag(list_from_index: 1, list_to_index: 3) - wait_for_board_cards(1, 7) - wait_for_board_cards(2, 2) + wait_for_board_cards(2, 7) wait_for_board_cards(3, 2) + wait_for_board_cards(4, 2) - expect(find('.board:nth-child(1)')).not_to have_content(issue9.title) - expect(find('.board:nth-child(3)')).to have_selector('.card', count: 2) - expect(find('.board:nth-child(3)')).to have_content(issue9.title) - expect(find('.board:nth-child(3)')).not_to have_content(planning.title) + expect(find('.board:nth-child(2)')).not_to have_content(issue9.title) + expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2) + expect(find('.board:nth-child(4)')).to have_content(issue9.title) + expect(find('.board:nth-child(4)')).not_to have_content(planning.title) end it 'removes all of the same issue to closed' do - drag(list_from_index: 0, list_to_index: 2) + drag(list_from_index: 1, list_to_index: 3) - wait_for_board_cards(1, 7) - wait_for_board_cards(2, 2) + wait_for_board_cards(2, 7) wait_for_board_cards(3, 2) + wait_for_board_cards(4, 2) - expect(find('.board:nth-child(1)')).not_to have_content(issue9.title) - expect(find('.board:nth-child(3)')).to have_content(issue9.title) - expect(find('.board:nth-child(3)')).not_to have_content(planning.title) + expect(find('.board:nth-child(2)')).not_to have_content(issue9.title) + expect(find('.board:nth-child(4)')).to have_content(issue9.title) + expect(find('.board:nth-child(4)')).not_to have_content(planning.title) end end context 'lists' do it 'changes position of list' do - drag(list_from_index: 1, list_to_index: 0, selector: '.board-header') + drag(list_from_index: 2, list_to_index: 1, selector: '.board-header') - wait_for_board_cards(1, 2) - wait_for_board_cards(2, 8) - wait_for_board_cards(3, 1) + wait_for_board_cards(2, 2) + wait_for_board_cards(3, 8) + wait_for_board_cards(4, 1) - expect(find('.board:nth-child(1)')).to have_content(development.title) - expect(find('.board:nth-child(1)')).to have_content(planning.title) + expect(find('.board:nth-child(2)')).to have_content(development.title) + expect(find('.board:nth-child(2)')).to have_content(planning.title) end it 'issue moves between lists' do - drag(list_from_index: 0, from_index: 1, list_to_index: 1) + drag(list_from_index: 1, from_index: 1, list_to_index: 2) - wait_for_board_cards(1, 7) - wait_for_board_cards(2, 2) - wait_for_board_cards(3, 1) + wait_for_board_cards(2, 7) + wait_for_board_cards(3, 2) + wait_for_board_cards(4, 1) - expect(find('.board:nth-child(2)')).to have_content(issue6.title) - expect(find('.board:nth-child(2)').all('.card').last).not_to have_content(development.title) + expect(find('.board:nth-child(3)')).to have_content(issue6.title) + expect(find('.board:nth-child(3)').all('.card').last).not_to have_content(development.title) end it 'issue moves between lists' do - drag(list_from_index: 1, list_to_index: 0) + drag(list_from_index: 2, list_to_index: 1) - wait_for_board_cards(1, 9) - wait_for_board_cards(2, 1) + wait_for_board_cards(2, 9) wait_for_board_cards(3, 1) + wait_for_board_cards(4, 1) - expect(find('.board:nth-child(1)')).to have_content(issue7.title) - expect(find('.board:nth-child(1)').all('.card').first).not_to have_content(planning.title) + expect(find('.board:nth-child(2)')).to have_content(issue7.title) + expect(find('.board:nth-child(2)').all('.card').first).not_to have_content(planning.title) end it 'issue moves from closed' do - drag(list_from_index: 2, list_to_index: 1) + drag(list_from_index: 3, list_to_index: 2) - expect(find('.board:nth-child(2)')).to have_content(issue8.title) + wait_for_board_cards(2, 8) + wait_for_board_cards(3, 3) + wait_for_board_cards(4, 0) - wait_for_board_cards(1, 8) - wait_for_board_cards(2, 3) - wait_for_board_cards(3, 0) + expect(find('.board:nth-child(3)')).to have_content(issue8.title) end context 'issue card' do it 'shows assignee' do - page.within(find('.board', match: :first)) do + page.within(find('.board:nth-child(2)')) do expect(page).to have_selector('.avatar', count: 1) end end @@ -290,7 +286,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - expect(page).to have_selector('.board', count: 4) + expect(page).to have_selector('.board', count: 5) end it 'creates new list for Backlog label' do @@ -303,7 +299,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - expect(page).to have_selector('.board', count: 4) + expect(page).to have_selector('.board', count: 5) end it 'creates new list for Closed label' do @@ -316,7 +312,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - expect(page).to have_selector('.board', count: 4) + expect(page).to have_selector('.board', count: 5) end it 'keeps dropdown open after adding new list' do @@ -348,7 +344,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests wait_for_requests - expect(page).to have_selector('.board', count: 4) + expect(page).to have_selector('.board', count: 5) end end end @@ -360,8 +356,8 @@ describe 'Issue Boards', feature: true, js: true do submit_filter wait_for_requests - wait_for_board_cards(1, 1) - wait_for_empty_boards((2..3)) + wait_for_board_cards(2, 1) + wait_for_empty_boards((3..4)) end it 'filters by assignee' do @@ -371,8 +367,8 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - wait_for_board_cards(1, 1) - wait_for_empty_boards((2..3)) + wait_for_board_cards(2, 1) + wait_for_empty_boards((3..4)) end it 'filters by milestone' do @@ -381,9 +377,9 @@ describe 'Issue Boards', feature: true, js: true do submit_filter wait_for_requests - wait_for_board_cards(1, 1) - wait_for_board_cards(2, 0) + wait_for_board_cards(2, 1) wait_for_board_cards(3, 0) + wait_for_board_cards(4, 0) end it 'filters by label' do @@ -392,8 +388,8 @@ describe 'Issue Boards', feature: true, js: true do submit_filter wait_for_requests - wait_for_board_cards(1, 1) - wait_for_empty_boards((2..3)) + wait_for_board_cards(2, 1) + wait_for_empty_boards((3..4)) end it 'filters by label with space after reload' do @@ -403,17 +399,17 @@ describe 'Issue Boards', feature: true, js: true do # Test after reload page.evaluate_script 'window.location.reload()' - wait_for_board_cards(1, 1) - wait_for_empty_boards((2..3)) + wait_for_board_cards(2, 1) + wait_for_empty_boards((3..4)) wait_for_requests - page.within(find('.board', match: :first)) do + page.within(find('.board:nth-child(2)')) do expect(page.find('.board-header')).to have_content('1') expect(page).to have_selector('.card', count: 1) end - page.within(find('.board:nth-child(2)')) do + page.within(find('.board:nth-child(3)')) do expect(page.find('.board-header')).to have_content('0') expect(page).to have_selector('.card', count: 0) end @@ -424,12 +420,12 @@ describe 'Issue Boards', feature: true, js: true do click_filter_link(testing.title) submit_filter - wait_for_board_cards(1, 1) + wait_for_board_cards(2, 1) find('.clear-search').click submit_filter - wait_for_board_cards(1, 8) + wait_for_board_cards(2, 8) end it 'infinite scrolls list with label filter' do @@ -443,17 +439,17 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - page.within(find('.board', match: :first)) do + page.within(find('.board:nth-child(2)')) do expect(page.find('.board-header')).to have_content('51') expect(page).to have_selector('.card', count: 20) expect(page).to have_content('Showing 20 of 51 issues') - evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") + evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") expect(page).to have_selector('.card', count: 40) expect(page).to have_content('Showing 40 of 51 issues') - evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") + evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") expect(page).to have_selector('.card', count: 51) expect(page).to have_content('Showing all issues') @@ -471,12 +467,12 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - wait_for_board_cards(1, 1) - wait_for_empty_boards((2..3)) + wait_for_board_cards(2, 1) + wait_for_empty_boards((3..4)) end it 'filters by clicking label button on issue' do - page.within(find('.board', match: :first)) do + page.within(find('.board:nth-child(2)')) do expect(page).to have_selector('.card', count: 8) expect(find('.card', match: :first)).to have_content(bug.title) click_button(bug.title) @@ -489,12 +485,12 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - wait_for_board_cards(1, 1) - wait_for_empty_boards((2..3)) + wait_for_board_cards(2, 1) + wait_for_empty_boards((3..4)) end it 'removes label filter by clicking label button on issue' do - page.within(find('.board', match: :first)) do + page.within(find('.board:nth-child(2)')) do page.within(find('.card', match: :first)) do click_button(bug.title) end diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb index 6c40cb2c9eb..1c289993e28 100644 --- a/spec/features/boards/issue_ordering_spec.rb +++ b/spec/features/boards/issue_ordering_spec.rb @@ -25,11 +25,11 @@ describe 'Issue Boards', :feature, :js do visit namespace_project_board_path(project.namespace, project, board) wait_for_requests - expect(page).to have_selector('.board', count: 2) + expect(page).to have_selector('.board', count: 3) end it 'has un-ordered issue as last issue' do - page.within(first('.board')) do + page.within(find('.board:nth-child(2)')) do expect(all('.card').last).to have_content(issue4.title) end end @@ -39,7 +39,7 @@ describe 'Issue Boards', :feature, :js do wait_for_requests - page.within(first('.board')) do + page.within(find('.board:nth-child(2)')) do expect(first('.card')).to have_content(issue4.title) end end @@ -50,7 +50,7 @@ describe 'Issue Boards', :feature, :js do visit namespace_project_board_path(project.namespace, project, board) wait_for_requests - expect(page).to have_selector('.board', count: 2) + expect(page).to have_selector('.board', count: 3) end it 'moves from middle to top' do @@ -113,50 +113,50 @@ describe 'Issue Boards', :feature, :js do visit namespace_project_board_path(project.namespace, project, board) wait_for_requests - expect(page).to have_selector('.board', count: 3) + expect(page).to have_selector('.board', count: 4) end it 'moves to top of another list' do - drag(list_from_index: 0, list_to_index: 1) + drag(list_from_index: 1, list_to_index: 2) wait_for_requests - expect(first('.board')).to have_selector('.card', count: 2) - expect(all('.board')[1]).to have_selector('.card', count: 4) + expect(find('.board:nth-child(2)')).to have_selector('.card', count: 2) + expect(all('.board')[2]).to have_selector('.card', count: 4) - page.within(all('.board')[1]) do + page.within(all('.board')[2]) do expect(first('.card')).to have_content(issue3.title) end end it 'moves to bottom of another list' do - drag(list_from_index: 0, list_to_index: 1, to_index: 2) + drag(list_from_index: 1, list_to_index: 2, to_index: 2) wait_for_requests - expect(first('.board')).to have_selector('.card', count: 2) - expect(all('.board')[1]).to have_selector('.card', count: 4) + expect(find('.board:nth-child(2)')).to have_selector('.card', count: 2) + expect(all('.board')[2]).to have_selector('.card', count: 4) - page.within(all('.board')[1]) do + page.within(all('.board')[2]) do expect(all('.card').last).to have_content(issue3.title) end end it 'moves to index of another list' do - drag(list_from_index: 0, list_to_index: 1, to_index: 1) + drag(list_from_index: 1, list_to_index: 2, to_index: 1) wait_for_requests - expect(first('.board')).to have_selector('.card', count: 2) - expect(all('.board')[1]).to have_selector('.card', count: 4) + expect(find('.board:nth-child(2)')).to have_selector('.card', count: 2) + expect(all('.board')[2]).to have_selector('.card', count: 4) - page.within(all('.board')[1]) do + page.within(all('.board')[2]) do expect(all('.card')[1]).to have_content(issue3.title) end end end - def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0) + def drag(selector: '.board-list', list_from_index: 1, from_index: 0, to_index: 0, list_to_index: 1) drag_to(selector: selector, scrollable: '#board-app', list_from_index: list_from_index, diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index 0e98f994018..7ba60247587 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -15,22 +15,22 @@ describe 'Issue Boards new issue', feature: true, js: true do visit namespace_project_board_path(project.namespace, project, board) wait_for_requests - expect(page).to have_selector('.board', count: 2) + expect(page).to have_selector('.board', count: 3) end it 'displays new issue button' do - expect(page).to have_selector('.board-issue-count-holder .btn', count: 1) + expect(first('.board')).to have_selector('.issue-count-badge-add-button', count: 1) end it 'does not display new issue button in closed list' do - page.within('.board:nth-child(2)') do - expect(page).not_to have_selector('.board-issue-count-holder .btn') + page.within('.board:nth-child(3)') do + expect(page).not_to have_selector('.issue-count-badge-add-button') end end it 'shows form when clicking button' do page.within(first('.board')) do - find('.board-issue-count-holder .btn').click + find('.issue-count-badge-add-button').click expect(page).to have_selector('.board-new-issue-form') end @@ -38,7 +38,7 @@ describe 'Issue Boards new issue', feature: true, js: true do it 'hides form when clicking cancel' do page.within(first('.board')) do - find('.board-issue-count-holder .btn').click + find('.issue-count-badge-add-button').click expect(page).to have_selector('.board-new-issue-form') @@ -50,7 +50,7 @@ describe 'Issue Boards new issue', feature: true, js: true do it 'creates new issue' do page.within(first('.board')) do - find('.board-issue-count-holder .btn').click + find('.issue-count-badge-add-button').click end page.within(first('.board-new-issue-form')) do @@ -60,14 +60,14 @@ describe 'Issue Boards new issue', feature: true, js: true do wait_for_requests - page.within(first('.board .board-issue-count')) do + page.within(first('.board .issue-count-badge-count')) do expect(page).to have_content('1') end end it 'shows sidebar when creating new issue' do page.within(first('.board')) do - find('.board-issue-count-holder .btn').click + find('.issue-count-badge-add-button').click end page.within(first('.board-new-issue-form')) do @@ -88,7 +88,7 @@ describe 'Issue Boards new issue', feature: true, js: true do end it 'does not display new issue button' do - expect(page).to have_selector('.board-issue-count-holder .btn', count: 0) + expect(page).to have_selector('.issue-count-badge-add-button', count: 0) end end end diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 34f4d765117..235e4899707 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -13,7 +13,7 @@ describe 'Issue Boards', feature: true, js: true do let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) } let(:board) { create(:board, project: project) } let!(:list) { create(:list, board: board, label: development, position: 0) } - let(:card) { first('.board').first('.card') } + let(:card) { find('.board:nth-child(2)').first('.card') } before do Timecop.freeze @@ -74,7 +74,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - page.within(first('.board')) do + page.within(find('.board:nth-child(2)')) do expect(page).to have_selector('.card', count: 1) end end @@ -101,7 +101,7 @@ describe 'Issue Boards', feature: true, js: true do end it 'removes the assignee' do - card_two = first('.board').find('.card:nth-child(2)') + card_two = find('.board:nth-child(2)').find('.card:nth-child(2)') click_card(card_two) page.within('.assignee') do @@ -154,7 +154,7 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_content(user.name) end - page.within(first('.board')) do + page.within(find('.board:nth-child(2)')) do find('.card:nth-child(2)').trigger('click') end diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb index b0e2953dda2..7eb254f8451 100644 --- a/spec/features/dashboard/groups_list_spec.rb +++ b/spec/features/dashboard/groups_list_spec.rb @@ -6,40 +6,124 @@ describe 'Dashboard Groups page', js: true, feature: true do let!(:nested_group) { create(:group, :nested) } let!(:another_group) { create(:group) } - before do + it 'shows groups user is member of' do group.add_owner(user) nested_group.add_owner(user) login_as(user) - visit dashboard_groups_path - end - it 'shows groups user is member of' do expect(page).to have_content(group.full_name) expect(page).to have_content(nested_group.full_name) expect(page).not_to have_content(another_group.full_name) end - it 'filters groups' do - fill_in 'filter_groups', with: group.name - wait_for_requests + describe 'when filtering groups' do + before do + group.add_owner(user) + nested_group.add_owner(user) - expect(page).to have_content(group.full_name) - expect(page).not_to have_content(nested_group.full_name) - expect(page).not_to have_content(another_group.full_name) + login_as(user) + + visit dashboard_groups_path + end + + it 'filters groups' do + fill_in 'filter_groups', with: group.name + wait_for_requests + + expect(page).to have_content(group.full_name) + expect(page).not_to have_content(nested_group.full_name) + expect(page).not_to have_content(another_group.full_name) + end + + it 'resets search when user cleans the input' do + fill_in 'filter_groups', with: group.name + wait_for_requests + + fill_in 'filter_groups', with: "" + wait_for_requests + + expect(page).to have_content(group.full_name) + expect(page).to have_content(nested_group.full_name) + expect(page).not_to have_content(another_group.full_name) + expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2 + end end - it 'resets search when user cleans the input' do - fill_in 'filter_groups', with: group.name - wait_for_requests + describe 'group with subgroups' do + let!(:subgroup) { create(:group, :public, parent: group) } - fill_in 'filter_groups', with: "" - wait_for_requests + before do + group.add_owner(user) + subgroup.add_owner(user) - expect(page).to have_content(group.full_name) - expect(page).to have_content(nested_group.full_name) - expect(page).not_to have_content(another_group.full_name) - expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2 + login_as(user) + + visit dashboard_groups_path + end + + it 'shows subgroups inside of its parent group' do + expect(page).to have_selector('.groups-list-tree-container .group-list-tree', count: 2) + expect(page).to have_selector(".groups-list-tree-container #group-#{group.id} #group-#{subgroup.id}", count: 1) + end + + it 'can toggle parent group' do + # Expanded by default + expect(page).to have_selector("#group-#{group.id} .fa-caret-down", count: 1) + expect(page).not_to have_selector("#group-#{group.id} .fa-caret-right") + + # Collapse + find("#group-#{group.id}").trigger('click') + + expect(page).not_to have_selector("#group-#{group.id} .fa-caret-down") + expect(page).to have_selector("#group-#{group.id} .fa-caret-right", count: 1) + expect(page).not_to have_selector("#group-#{group.id} #group-#{subgroup.id}") + + # Expand + find("#group-#{group.id}").trigger('click') + + expect(page).to have_selector("#group-#{group.id} .fa-caret-down", count: 1) + expect(page).not_to have_selector("#group-#{group.id} .fa-caret-right") + expect(page).to have_selector("#group-#{group.id} #group-#{subgroup.id}") + end + end + + describe 'when using pagination' do + let(:group2) { create(:group) } + + before do + group.add_owner(user) + group2.add_owner(user) + + allow(Kaminari.config).to receive(:default_per_page).and_return(1) + + login_as(user) + visit dashboard_groups_path + end + + it 'shows pagination' do + expect(page).to have_selector('.gl-pagination') + expect(page).to have_selector('.gl-pagination .page', count: 2) + end + + it 'loads results for next page' do + # Check first page + expect(page).to have_content(group2.full_name) + expect(page).to have_selector("#group-#{group2.id}") + expect(page).not_to have_content(group.full_name) + expect(page).not_to have_selector("#group-#{group.id}") + + # Go to next page + find(".gl-pagination .page:not(.active) a").trigger('click') + + wait_for_requests + + # Check second page + expect(page).to have_content(group.full_name) + expect(page).to have_selector("#group-#{group.id}") + expect(page).not_to have_content(group2.full_name) + expect(page).not_to have_selector("#group-#{group2.id}") + end end end diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb index 9cebe52c444..bcb52f602b0 100644 --- a/spec/features/dashboard/merge_requests_spec.rb +++ b/spec/features/dashboard/merge_requests_spec.rb @@ -12,7 +12,9 @@ describe 'Dashboard Merge Requests' do end describe 'new merge request dropdown' do - before { visit merge_requests_dashboard_path } + before do + visit merge_requests_dashboard_path + end it 'shows projects only with merge requests feature enabled', js: true do find('.new-project-item-select-button').trigger('click') diff --git a/spec/features/dashboard/milestone_tabs_spec.rb b/spec/features/dashboard/milestone_tabs_spec.rb new file mode 100644 index 00000000000..0c7b992c500 --- /dev/null +++ b/spec/features/dashboard/milestone_tabs_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe 'Dashboard milestone tabs', :js, :feature do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let!(:label) { create(:label, project: project) } + let(:project_milestone) { create(:milestone, project: project) } + let(:milestone) do + DashboardMilestone.build( + [project], + project_milestone.title + ) + end + let!(:merge_request) { create(:labeled_merge_request, source_project: project, target_project: project, milestone: project_milestone, labels: [label]) } + + before do + project.add_master(user) + login_as(user) + + visit dashboard_milestone_path(milestone.safe_title, title: milestone.title) + end + + it 'loads merge requests async' do + click_link 'Merge Requests' + + expect(page).to have_selector('.merge_requests-sortable-list') + end + + it 'loads participants async' do + click_link 'Participants' + + expect(page).to have_selector('#tab-participants .bordered-list') + end + + it 'loads labels async' do + click_link 'Labels' + + expect(page).to have_selector('#tab-labels .bordered-list') + end +end diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb index cdf919af9b5..0ba87d921d0 100644 --- a/spec/features/dashboard/project_member_activity_index_spec.rb +++ b/spec/features/dashboard/project_member_activity_index_spec.rb @@ -17,19 +17,25 @@ feature 'Project member activity', feature: true, js: true do subject { page.find(".event-title").text } context 'when a user joins the project' do - before { visit_activities_and_wait_with_event(Event::JOINED) } + before do + visit_activities_and_wait_with_event(Event::JOINED) + end it { is_expected.to eq("#{user.name} joined project") } end context 'when a user leaves the project' do - before { visit_activities_and_wait_with_event(Event::LEFT) } + before do + visit_activities_and_wait_with_event(Event::LEFT) + end it { is_expected.to eq("#{user.name} left project") } end context 'when a users membership expires for the project' do - before { visit_activities_and_wait_with_event(Event::EXPIRED) } + before do + visit_activities_and_wait_with_event(Event::EXPIRED) + end it "presents the correct message" do message = "#{user.name} removed due to membership expiration from project" diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb index c4d5077e5e1..36b0c371e6e 100644 --- a/spec/features/expand_collapse_diffs_spec.rb +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -140,7 +140,9 @@ feature 'Expand and collapse diffs', js: true, feature: true do end context 'reloading the page' do - before { refresh } + before do + refresh + end it 'collapses the large diff by default' do expect(large_diff).not_to have_selector('.code') @@ -262,7 +264,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do # Wait for elements to appear to ensure full page reload expect(page).to have_content('This diff was suppressed by a .gitattributes entry') - expect(page).to have_content('This diff could not be displayed because it is too large.') + expect(page).to have_content('This source diff could not be displayed because it is too large.') expect(page).to have_content('too_large_image.jpg') find('.note-textarea') diff --git a/spec/features/explore/new_menu_spec.rb b/spec/features/explore/new_menu_spec.rb new file mode 100644 index 00000000000..15a6354211b --- /dev/null +++ b/spec/features/explore/new_menu_spec.rb @@ -0,0 +1,172 @@ +require 'spec_helper' + +feature 'Top Plus Menu', feature: true, js: true do + let(:user) { create :user } + let(:guest_user) { create :user} + let(:group) { create(:group) } + let(:project) { create(:project, :repository, creator: user, namespace: user.namespace) } + let(:public_project) { create(:project, :public) } + + before do + group.add_owner(user) + group.add_guest(guest_user) + + project.add_guest(guest_user) + end + + context 'used by full user' do + before do + login_as(user) + end + + scenario 'click on New project shows new project page' do + visit root_dashboard_path + + click_topmenuitem("New project") + + expect(page).to have_content('Project path') + expect(page).to have_content('Project name') + end + + scenario 'click on New group shows new group page' do + visit root_dashboard_path + + click_topmenuitem("New group") + + expect(page).to have_content('Group path') + expect(page).to have_content('Group name') + end + + scenario 'click on New snippet shows new snippet page' do + visit root_dashboard_path + + click_topmenuitem("New snippet") + + expect(page).to have_content('New Snippet') + expect(page).to have_content('Title') + end + + scenario 'click on New issue shows new issue page' do + visit namespace_project_path(project.namespace, project) + + click_topmenuitem("New issue") + + expect(page).to have_content('New Issue') + expect(page).to have_content('Title') + end + + scenario 'click on New merge request shows new merge request page' do + visit namespace_project_path(project.namespace, project) + + click_topmenuitem("New merge request") + + expect(page).to have_content('New Merge Request') + expect(page).to have_content('Source branch') + expect(page).to have_content('Target branch') + end + + scenario 'click on New project snippet shows new snippet page' do + visit namespace_project_path(project.namespace, project) + + page.within '.header-content' do + find('.header-new-dropdown-toggle').trigger('click') + expect(page).to have_selector('.header-new.dropdown.open', count: 1) + find('.header-new-project-snippet a').trigger('click') + end + + expect(page).to have_content('New Snippet') + expect(page).to have_content('Title') + end + + scenario 'Click on New subgroup shows new group page' do + visit group_path(group) + + click_topmenuitem("New subgroup") + + expect(page).to have_content('Group path') + expect(page).to have_content('Group name') + end + + scenario 'Click on New project in group shows new project page' do + visit group_path(group) + + page.within '.header-content' do + find('.header-new-dropdown-toggle').trigger('click') + expect(page).to have_selector('.header-new.dropdown.open', count: 1) + find('.header-new-group-project a').trigger('click') + end + + expect(page).to have_content('Project path') + expect(page).to have_content('Project name') + end + end + + context 'used by guest user' do + before do + login_as(guest_user) + end + + scenario 'click on New issue shows new issue page' do + visit namespace_project_path(project.namespace, project) + + click_topmenuitem("New issue") + + expect(page).to have_content('New Issue') + expect(page).to have_content('Title') + end + + scenario 'has no New merge request menu item' do + visit namespace_project_path(project.namespace, project) + + hasnot_topmenuitem("New merge request") + end + + scenario 'has no New project snippet menu item' do + visit namespace_project_path(project.namespace, project) + + expect(find('.header-new.dropdown')).not_to have_selector('.header-new-project-snippet') + end + + scenario 'public project has no New Issue Button' do + visit namespace_project_path(public_project.namespace, public_project) + + hasnot_topmenuitem("New issue") + end + + scenario 'public project has no New merge request menu item' do + visit namespace_project_path(public_project.namespace, public_project) + + hasnot_topmenuitem("New merge request") + end + + scenario 'public project has no New project snippet menu item' do + visit namespace_project_path(public_project.namespace, public_project) + + expect(find('.header-new.dropdown')).not_to have_selector('.header-new-project-snippet') + end + + scenario 'has no New subgroup menu item' do + visit group_path(group) + + hasnot_topmenuitem("New subgroup") + end + + scenario 'has no New project for group menu item' do + visit group_path(group) + + expect(find('.header-new.dropdown')).not_to have_selector('.header-new-group-project') + end + end + + def click_topmenuitem(item_name) + page.within '.header-content' do + find('.header-new-dropdown-toggle').trigger('click') + expect(page).to have_selector('.header-new.dropdown.open', count: 1) + click_link item_name + end + end + + def hasnot_topmenuitem(item_name) + expect(find('.header-new.dropdown')).not_to have_content(item_name) + end +end diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb index cc25db4ad60..6afde1d0bed 100644 --- a/spec/features/groups/group_settings_spec.rb +++ b/spec/features/groups/group_settings_spec.rb @@ -52,9 +52,14 @@ feature 'Edit group settings', feature: true do given!(:project) { create(:project, group: group, path: 'project') } given(:old_project_full_path) { "/#{group.path}/#{project.path}" } given(:new_project_full_path) { "/#{new_group_path}/#{project.path}" } - - before(:context) { TestEnv.clean_test_path } - after(:example) { TestEnv.clean_test_path } + + before(:context) do + TestEnv.clean_test_path + end + + after(:example) do + TestEnv.clean_test_path + end scenario 'the project is accessible via the new path' do update_path(new_group_path) diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index 24ea7aba0cc..5737ca39b4e 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -12,7 +12,9 @@ feature 'Group', feature: true do end describe 'create a group' do - before { visit new_group_path } + before do + visit new_group_path + end describe 'with space in group path' do it 'renders new group form with validation errors' do @@ -138,7 +140,9 @@ feature 'Group', feature: true do let(:path) { edit_group_path(group) } let(:new_name) { 'new-name' } - before { visit path } + before do + visit path + end it 'saves new settings' do fill_in 'group_name', with: new_name diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb index e0b2404e60a..18102146b5f 100644 --- a/spec/features/help_pages_spec.rb +++ b/spec/features/help_pages_spec.rb @@ -34,29 +34,46 @@ describe 'Help Pages', feature: true do end end - context 'in a production environment with version check enabled', js: true do + context 'in a production environment with version check enabled', :js do before do allow(Rails.env).to receive(:production?) { true } - allow(current_application_settings).to receive(:version_check_enabled) { true } + allow_any_instance_of(ApplicationSetting).to receive(:version_check_enabled) { true } allow_any_instance_of(VersionCheck).to receive(:url) { '/version-check-url' } login_as :user visit help_path end - it 'should display a version check image' do - expect(find('.js-version-status-badge')).to be_visible + it 'has a version check image' do + expect(find('.js-version-status-badge', visible: false)['src']).to end_with('/version-check-url') end - it 'should have a src url' do - expect(find('.js-version-status-badge')['src']).to match(/\/version-check-url/) + it 'hides the version check image if the image request fails' do + # We use '--load-images=yes' with poltergeist so the image fails to load + expect(find('.js-version-status-badge', visible: false)).not_to be_visible end + end - it 'should hide the version check image if the image request fails' do - # We use '--load-images=no' with poltergeist so we must trigger manually - execute_script("$('.js-version-status-badge').trigger('error');") + describe 'when help page is customized' do + before do + allow_any_instance_of(ApplicationSetting).to receive(:help_page_hide_commercial_content?) { true } + allow_any_instance_of(ApplicationSetting).to receive(:help_page_text) { "My Custom Text" } + allow_any_instance_of(ApplicationSetting).to receive(:help_page_support_url) { "http://example.com/help" } - expect(find('.js-version-status-badge', visible: false)).not_to be_visible + login_as :user + visit help_path + end + + it 'should display custom help page text' do + expect(page).to have_text "My Custom Text" + end + + it 'should hide marketing content when enabled' do + expect(page).not_to have_link "Get a support subscription" + end + + it 'should use a custom support url' do + expect(page).to have_link "See our website for getting help", href: "http://example.com/help" end end end diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb index bfe43bff10f..56c9b10e757 100644 --- a/spec/features/issuables/default_sort_order_spec.rb +++ b/spec/features/issuables/default_sort_order_spec.rb @@ -153,7 +153,9 @@ describe 'Projects > Issuables > Default sort order', feature: true do context 'when the sort in the URL is id_desc' do let(:issuable_type) { :issue } - before { visit_issues(project, sort: 'id_desc') } + before do + visit_issues(project, sort: 'id_desc') + end it 'shows the sort order as last created' do expect(find('.issues-other-filters')).to have_content('Last created') @@ -165,7 +167,9 @@ describe 'Projects > Issuables > Default sort order', feature: true do context 'when the sort in the URL is id_asc' do let(:issuable_type) { :issue } - before { visit_issues(project, sort: 'id_asc') } + before do + visit_issues(project, sort: 'id_asc') + end it 'shows the sort order as oldest created' do expect(find('.issues-other-filters')).to have_content('Oldest created') diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index e5e4ba06b5a..863f8f75cd8 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -777,17 +777,17 @@ describe 'Filter issues', js: true, feature: true do end it 'open state' do - find('.issues-state-filters a', text: 'Closed').click + find('.issues-state-filters [data-state="closed"]').click wait_for_requests - find('.issues-state-filters a', text: 'Open').click + find('.issues-state-filters [data-state="opened"]').click wait_for_requests expect(page).to have_selector('.issues-list .issue', count: 4) end it 'closed state' do - find('.issues-state-filters a', text: 'Closed').click + find('.issues-state-filters [data-state="closed"]').click wait_for_requests expect(page).to have_selector('.issues-list .issue', count: 1) @@ -795,7 +795,7 @@ describe 'Filter issues', js: true, feature: true do end it 'all state' do - find('.issues-state-filters a', text: 'All').click + find('.issues-state-filters [data-state="all"]').click wait_for_requests expect(page).to have_selector('.issues-list .issue', count: 5) diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index 8949dbcb663..96d37e33f3d 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -24,37 +24,17 @@ describe 'New/edit issue', :feature, :js do visit new_namespace_project_issue_path(project.namespace, project) end - describe 'shorten users API pagination limit' do + describe 'shorten users API pagination limit (CE)' do before do + # Using `allow_any_instance_of`/`and_wrap_original`, `original` would + # somehow refer to the very block we defined to _wrap_ that method, instead of + # the original method, resulting in infinite recurison when called. + # This is likely a bug with helper modules included into dynamically generated view classes. + # To work around this, we have to hold on to and call to the original implementation manually. + original_issue_dropdown_options = FormHelper.instance_method(:issue_dropdown_options) allow_any_instance_of(FormHelper).to receive(:issue_dropdown_options).and_wrap_original do |original, *args| - has_multiple_assignees = *args[1] - - options = { - toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data', - title: 'Select assignee', - filter: true, - dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee', - placeholder: 'Search users', - data: { - per_page: 1, - null_user: true, - current_user: true, - project_id: project.try(:id), - field_name: "issue[assignee_ids][]", - default_label: 'Assignee', - 'max-select': 1, - 'dropdown-header': 'Assignee', - multi_select: true, - 'input-meta': 'name', - 'always-show-selectbox': true - } - } - - if has_multiple_assignees - options[:title] = 'Select assignee(s)' - options[:data][:'dropdown-header'] = 'Assignee(s)' - options[:data].delete(:'max-select') - end + options = original_issue_dropdown_options.bind(original.receiver).call(*args) + options[:data][:per_page] = 2 options end @@ -74,6 +54,7 @@ describe 'New/edit issue', :feature, :js do click_link user2.name end + find('.js-assignee-search').click find('.js-dropdown-input-clear').click page.within '.dropdown-menu-user' do @@ -83,7 +64,7 @@ describe 'New/edit issue', :feature, :js do end end - describe 'single assignee' do + describe 'single assignee (CE)' do before do click_button 'Unassigned' diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb index 80f57906506..2c0a6ffd3cb 100644 --- a/spec/features/issues/note_polling_spec.rb +++ b/spec/features/issues/note_polling_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Issue notes polling', :feature, :js do + include NoteInteractionHelpers + let(:project) { create(:empty_project, :public) } let(:issue) { create(:issue, project: project) } @@ -48,7 +50,7 @@ feature 'Issue notes polling', :feature, :js do end it 'when editing but have not changed anything, and an update comes in, show the updated content in the textarea' do - find("#note_#{existing_note.id} .js-note-edit").click + click_edit_action(existing_note) expect(page).to have_field("note[note]", with: note_text) @@ -58,19 +60,18 @@ feature 'Issue notes polling', :feature, :js do end it 'when editing but you changed some things, and an update comes in, show a warning' do - find("#note_#{existing_note.id} .js-note-edit").click + click_edit_action(existing_note) expect(page).to have_field("note[note]", with: note_text) find("#note_#{existing_note.id} .js-note-text").set('something random') - update_note(existing_note, updated_text) expect(page).to have_selector(".alert") end it 'when editing but you changed some things, an update comes in, and you press cancel, show the updated content' do - find("#note_#{existing_note.id} .js-note-edit").click + click_edit_action(existing_note) expect(page).to have_field("note[note]", with: note_text) @@ -128,4 +129,12 @@ feature 'Issue notes polling', :feature, :js do note.update(note: new_text) page.execute_script('notes.refresh();') end + + def click_edit_action(note) + note_element = find("#note_#{note.id}") + + open_more_actions_dropdown(note) + + note_element.find('.js-note-edit').click + end end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index eecc565d2bd..2cff53539f3 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -246,7 +246,10 @@ describe 'Issues', feature: true do context 'with a filter on labels' do let(:label) { create(:label, project: project) } - before { create(:label_link, label: label, target: foo) } + + before do + create(:label_link, label: label, target: foo) + end it 'sorts by least recently due date by excluding nil due dates' do bar.update(due_date: nil) diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index c82e8c03343..4763f454810 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -202,10 +202,12 @@ feature 'Login', feature: true do # TODO: otp_grace_period_started_at context 'global setting' do - before(:each) { stub_application_setting(require_two_factor_authentication: true) } + before do + stub_application_setting(require_two_factor_authentication: true) + end context 'with grace period defined' do - before(:each) do + before do stub_application_setting(two_factor_grace_period: 48) login_with(user) end @@ -242,7 +244,7 @@ feature 'Login', feature: true do end context 'without grace period defined' do - before(:each) do + before do stub_application_setting(two_factor_grace_period: 0) login_with(user) end @@ -265,7 +267,7 @@ feature 'Login', feature: true do end context 'with grace period defined' do - before(:each) do + before do stub_application_setting(two_factor_grace_period: 48) login_with(user) end @@ -306,7 +308,7 @@ feature 'Login', feature: true do end context 'without grace period defined' do - before(:each) do + before do stub_application_setting(two_factor_grace_period: 0) login_with(user) end diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index 27e2d5d16f3..9409c32104b 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -85,14 +85,18 @@ feature 'Merge request conflict resolution', js: true, feature: true do context 'the conflicts are resolvable' do let(:merge_request) { create_merge_request('conflict-resolvable') } - before { visit namespace_project_merge_request_path(project.namespace, project, merge_request) } + before do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end it 'shows a link to the conflict resolution page' do expect(page).to have_link('conflicts', href: /\/conflicts\Z/) end context 'in Inline view mode' do - before { click_link('conflicts', href: /\/conflicts\Z/) } + before do + click_link('conflicts', href: /\/conflicts\Z/) + end include_examples "conflicts are resolved in Interactive mode" include_examples "conflicts are resolved in Edit inline mode" diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb index bf34c99b92a..b4327743383 100644 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -56,7 +56,7 @@ feature 'Merge request created from fork' do visit_merge_request(merge_request) page.within('.merge-request-tabs') { click_link 'Pipelines' } - page.within('table.ci-table') do + page.within('.ci-table') do expect(page).to have_content pipeline.status expect(page).to have_content pipeline.id end diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb index 854e2d1758f..e23dc2cd940 100644 --- a/spec/features/merge_requests/diff_notes_avatars_spec.rb +++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Diff note avatars', feature: true, js: true do + include NoteInteractionHelpers + let(:user) { create(:user) } let(:project) { create(:project, :public) } let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") } @@ -110,6 +112,8 @@ feature 'Diff note avatars', feature: true, js: true do end it 'removes avatar when note is deleted' do + open_more_actions_dropdown(note) + page.within find(".note-row-#{note.id}") do find('.js-note-delete').click end diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb index 1e26b3d601e..d086be70d69 100644 --- a/spec/features/merge_requests/filter_merge_requests_spec.rb +++ b/spec/features/merge_requests/filter_merge_requests_spec.rb @@ -40,13 +40,13 @@ describe 'Filter merge requests', feature: true do end it 'does not change when closed link is clicked' do - find('.issues-state-filters a', text: "Closed").click + find('.issues-state-filters [data-state="closed"]').click expect_assignee_visual_tokens() end it 'does not change when all link is clicked' do - find('.issues-state-filters a', text: "All").click + find('.issues-state-filters [data-state="all"]').click expect_assignee_visual_tokens() end @@ -73,13 +73,13 @@ describe 'Filter merge requests', feature: true do end it 'does not change when closed link is clicked' do - find('.issues-state-filters a', text: "Closed").click + find('.issues-state-filters [data-state="closed"]').click expect_milestone_visual_tokens() end it 'does not change when all link is clicked' do - find('.issues-state-filters a', text: "All").click + find('.issues-state-filters [data-state="all"]').click expect_milestone_visual_tokens() end @@ -142,11 +142,9 @@ describe 'Filter merge requests', feature: true do expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) expect_filtered_search_input_empty - input_filtered_search_keys("label:~#{label.title} ") + input_filtered_search_keys("label:~#{label.title}") expect_mr_list_count(1) - - find("#state-opened[href=\"#{URI.parse(current_url).path}?assignee_username=#{user.username}&label_name%5B%5D=#{label.title}&scope=all&state=opened\"]") end context 'assignee and label', js: true do @@ -163,13 +161,13 @@ describe 'Filter merge requests', feature: true do end it 'does not change when closed link is clicked' do - find('.issues-state-filters a', text: "Closed").click + find('.issues-state-filters [data-state="closed"]').click expect_assignee_label_visual_tokens() end it 'does not change when all link is clicked' do - find('.issues-state-filters a', text: "All").click + find('.issues-state-filters [data-state="all"]').click expect_assignee_label_visual_tokens() end diff --git a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb index c1d4d508e57..836a7b6e09a 100644 --- a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb +++ b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb @@ -18,7 +18,9 @@ feature 'Merge immediately', :feature, :js do sha: project.repository.commit('master').id) end - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end context 'when there is active pipeline for merge request' do background do diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb index 3ceb91d951d..3a11ea3c8b2 100644 --- a/spec/features/merge_requests/mini_pipeline_graph_spec.rb +++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb @@ -12,13 +12,39 @@ feature 'Mini Pipeline Graph', :js, :feature do build.run login_as(user) - visit namespace_project_merge_request_path(project.namespace, project, merge_request) + visit_merge_request + end + + def visit_merge_request(format = :html) + visit namespace_project_merge_request_path(project.namespace, project, merge_request, format: format) end it 'should display a mini pipeline graph' do expect(page).to have_selector('.mr-widget-pipeline-graph') end + context 'as json' do + let(:artifacts_file1) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } + let(:artifacts_file2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png') } + + before do + create(:ci_build, pipeline: pipeline, artifacts_file: artifacts_file1) + create(:ci_build, pipeline: pipeline, when: 'manual') + end + + it 'avoids repeated database queries' do + before = ActiveRecord::QueryRecorder.new { visit_merge_request(:json) } + + create(:ci_build, pipeline: pipeline, artifacts_file: artifacts_file2) + create(:ci_build, pipeline: pipeline, when: 'manual') + + after = ActiveRecord::QueryRecorder.new { visit_merge_request(:json) } + + expect(before.count).to eq(after.count) + expect(before.cached_count).to eq(after.cached_count) + end + end + describe 'build list toggle' do let(:toggle) do find('.mini-pipeline-graph-dropdown-toggle') diff --git a/spec/features/merge_requests/pipelines_spec.rb b/spec/features/merge_requests/pipelines_spec.rb index 4c76004cb93..744bd484a80 100644 --- a/spec/features/merge_requests/pipelines_spec.rb +++ b/spec/features/merge_requests/pipelines_spec.rb @@ -28,7 +28,7 @@ feature 'Pipelines for Merge Requests', feature: true, js: true do end wait_for_requests - expect(page).to have_selector('.pipeline-actions') + expect(page).to have_selector('.stage-cell') end end diff --git a/spec/features/merge_requests/user_posts_notes_spec.rb b/spec/features/merge_requests/user_posts_notes_spec.rb index 06de072257a..22552529b9e 100644 --- a/spec/features/merge_requests/user_posts_notes_spec.rb +++ b/spec/features/merge_requests/user_posts_notes_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe 'Merge requests > User posts notes', :js do + include NoteInteractionHelpers + let(:project) { create(:project) } let(:merge_request) do create(:merge_request, source_project: project, target_project: project) @@ -73,6 +75,8 @@ describe 'Merge requests > User posts notes', :js do describe 'editing the note' do before do find('.note').hover + open_more_actions_dropdown(note) + find('.js-note-edit').click end @@ -100,6 +104,8 @@ describe 'Merge requests > User posts notes', :js do wait_for_requests find('.note').hover + open_more_actions_dropdown(note) + find('.js-note-edit').click page.within('.current-note-edit-form') do @@ -126,6 +132,8 @@ describe 'Merge requests > User posts notes', :js do describe 'deleting an attachment' do before do find('.note').hover + open_more_actions_dropdown(note) + find('.js-note-edit').click end diff --git a/spec/features/milestones/milestones_spec.rb b/spec/features/milestones/milestones_spec.rb index b3dfd6d0e81..c8a4d23f695 100644 --- a/spec/features/milestones/milestones_spec.rb +++ b/spec/features/milestones/milestones_spec.rb @@ -37,6 +37,14 @@ describe 'Milestone draggable', feature: true, js: true do expect(issue_target).to have_selector('.issuable-row') end + + it 'assigns issue when it has been dragged to ongoing list' do + login_as(:admin) + create_and_drag_issue + + expect(@issue.reload.assignees).not_to be_empty + expect(page).to have_selector("#sortable_issue_#{@issue.iid} .assignee-icon img", count: 1) + end end context 'merge requests' do @@ -72,7 +80,7 @@ describe 'Milestone draggable', feature: true, js: true do end def create_and_drag_issue(params = {}) - create(:issue, params.merge(title: 'Foo', project: project, milestone: milestone)) + @issue = create(:issue, params.merge(title: 'Foo', project: project, milestone: milestone)) visit namespace_project_milestone_path(project.namespace, project, milestone) scroll_into_view('.milestone-content') diff --git a/spec/features/profiles/account_spec.rb b/spec/features/profiles/account_spec.rb index 05a7587f8d4..89868c737f7 100644 --- a/spec/features/profiles/account_spec.rb +++ b/spec/features/profiles/account_spec.rb @@ -31,8 +31,13 @@ feature 'Profile > Account', feature: true do given(:new_project_path) { "/#{new_username}/#{project.path}" } given(:old_project_path) { "/#{user.username}/#{project.path}" } - before(:context) { TestEnv.clean_test_path } - after(:example) { TestEnv.clean_test_path } + before(:context) do + TestEnv.clean_test_path + end + + after(:example) do + TestEnv.clean_test_path + end scenario 'the project is accessible via the new path' do update_username(new_username) diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb index 27a20e78a43..7e2e685df26 100644 --- a/spec/features/profiles/personal_access_tokens_spec.rb +++ b/spec/features/profiles/personal_access_tokens_spec.rb @@ -17,6 +17,7 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do def disallow_personal_access_token_saves! allow_any_instance_of(PersonalAccessToken).to receive(:save).and_return(false) + errors = ActiveModel::Errors.new(PersonalAccessToken.new).tap { |e| e.add(:name, "cannot be nil") } allow_any_instance_of(PersonalAccessToken).to receive(:errors).and_return(errors) end @@ -91,8 +92,11 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do context "when revocation fails" do it "displays an error message" do - disallow_personal_access_token_saves! visit profile_personal_access_tokens_path + allow_any_instance_of(PersonalAccessToken).to receive(:update!).and_return(false) + + errors = ActiveModel::Errors.new(PersonalAccessToken.new).tap { |e| e.add(:name, "cannot be nil") } + allow_any_instance_of(PersonalAccessToken).to receive(:errors).and_return(errors) click_on "Revoke" expect(active_personal_access_tokens).to have_text(personal_access_token.name) diff --git a/spec/features/projects/artifacts/file_spec.rb b/spec/features/projects/artifacts/file_spec.rb index 25c4f3c87a2..860373e531b 100644 --- a/spec/features/projects/artifacts/file_spec.rb +++ b/spec/features/projects/artifacts/file_spec.rb @@ -39,6 +39,7 @@ feature 'Artifact file', :js, feature: true do context 'JPG file' do before do + page.driver.browser.url_blacklist = [] visit_file('rails_sample.jpg') wait_for_requests diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index 82cfbfda157..71ffa352f80 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' feature 'File blob', :js, feature: true do let(:project) { create(:project, :public) } - def visit_blob(path, fragment = nil) - visit namespace_project_blob_path(project.namespace, project, File.join('master', path), anchor: fragment) + def visit_blob(path, anchor: nil, ref: 'master') + visit namespace_project_blob_path(project.namespace, project, File.join(ref, path), anchor: anchor) wait_for_requests end @@ -17,6 +17,7 @@ feature 'File blob', :js, feature: true do it 'displays the blob' do aggregate_failures do # shows highlighted Ruby code + expect(page).to have_css(".js-syntax-highlight") expect(page).to have_content("require 'fileutils'") # does not show a viewer switcher @@ -71,6 +72,7 @@ feature 'File blob', :js, feature: true do expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false) # shows highlighted Markdown code + expect(page).to have_css(".js-syntax-highlight") expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)") # shows an enabled copy button @@ -101,7 +103,7 @@ feature 'File blob', :js, feature: true do context 'visiting with a line number anchor' do before do - visit_blob('files/markdown/ruby-style-guide.md', 'L1') + visit_blob('files/markdown/ruby-style-guide.md', anchor: 'L1') end it 'displays the blob using the simple viewer' do @@ -114,6 +116,7 @@ feature 'File blob', :js, feature: true do expect(page).to have_selector('#LC1.hll') # shows highlighted Markdown code + expect(page).to have_css(".js-syntax-highlight") expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)") # shows an enabled copy button @@ -352,6 +355,37 @@ feature 'File blob', :js, feature: true do end end + context 'binary file that appears to be text in the first 1024 bytes' do + before do + visit_blob('encoding/binary-1.bin', ref: 'binary-encoding') + end + + it 'displays the blob' do + aggregate_failures do + # shows a download link + expect(page).to have_link('Download (23.8 KB)') + + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # The specs below verify an arguably incorrect result, but since we only + # learn that the file is not actually text once the text viewer content + # is loaded asynchronously, there is no straightforward way to get these + # synchronously loaded elements to display correctly. + # + # Clicking the copy button will result in nothing being copied. + # Clicking the raw button will result in the binary file being downloaded, + # as expected. + + # shows an enabled copy button, incorrectly + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + + # shows a raw button, incorrectly + expect(page).to have_link('Open raw') + end + end + end + context '.gitlab-ci.yml' do before do project.add_master(project.creator) diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb index 1a38997450d..d04c3248ead 100644 --- a/spec/features/projects/blobs/edit_spec.rb +++ b/spec/features/projects/blobs/edit_spec.rb @@ -102,7 +102,7 @@ feature 'Editing file blob', feature: true, js: true do it 'shows blob editor with same branch' do expect(page).to have_current_path(namespace_project_edit_blob_path(project.namespace, project, tree_join(branch, file_path))) - expect(find('.js-target-branch .dropdown-toggle-text').text).to eq(branch) + expect(find('.js-branch-name').value).to eq(branch) end end @@ -112,7 +112,7 @@ feature 'Editing file blob', feature: true, js: true do end it 'shows blob editor with patch branch' do - expect(find('.js-target-branch .dropdown-toggle-text').text).to eq('patch-1') + expect(find('.js-branch-name').value).to eq('patch-1') end end end @@ -128,7 +128,7 @@ feature 'Editing file blob', feature: true, js: true do it 'shows blob editor with same branch' do expect(page).to have_current_path(namespace_project_edit_blob_path(project.namespace, project, tree_join(branch, file_path))) - expect(find('.js-target-branch .dropdown-toggle-text').text).to eq(branch) + expect(find('.js-branch-name').value).to eq(branch) end end end diff --git a/spec/features/projects/blobs/user_create_spec.rb b/spec/features/projects/blobs/user_create_spec.rb deleted file mode 100644 index 4b6c55f5f44..00000000000 --- a/spec/features/projects/blobs/user_create_spec.rb +++ /dev/null @@ -1,94 +0,0 @@ -require 'spec_helper' - -feature 'New blob creation', feature: true, js: true do - include TargetBranchHelpers - - given(:user) { create(:user) } - given(:role) { :developer } - given(:project) { create(:project) } - given(:content) { 'class NextFeature\nend\n' } - - background do - login_as(user) - project.team << [user, role] - visit namespace_project_new_blob_path(project.namespace, project, 'master') - end - - def edit_file - wait_for_requests - fill_in 'file_name', with: 'feature.rb' - execute_script("ace.edit('editor').setValue('#{content}')") - end - - def commit_file - click_button 'Commit changes' - end - - context 'with default target branch' do - background do - edit_file - commit_file - end - - scenario 'creates the blob in the default branch' do - expect(page).to have_content 'master' - expect(page).to have_content 'successfully created' - expect(page).to have_content 'NextFeature' - end - end - - context 'with different target branch' do - background do - edit_file - select_branch('feature') - commit_file - end - - scenario 'creates the blob in the different branch' do - expect(page).to have_content 'feature' - expect(page).to have_content 'successfully created' - end - end - - context 'with a new target branch' do - given(:new_branch_name) { 'new-feature' } - - background do - edit_file - create_new_branch(new_branch_name) - commit_file - end - - scenario 'creates the blob in the new branch' do - expect(page).to have_content new_branch_name - expect(page).to have_content 'successfully created' - end - scenario 'returns you to the mr' do - expect(page).to have_content 'New Merge Request' - expect(page).to have_content "From #{new_branch_name} into master" - expect(page).to have_content 'Add new file' - end - end - - context 'the file already exist in the source branch' do - background do - Files::CreateService.new( - project, - user, - start_branch: 'master', - branch_name: 'master', - commit_message: 'Create file', - file_path: 'feature.rb', - file_content: content - ).execute - edit_file - commit_file - end - - scenario 'shows error message' do - expect(page).to have_content('A file with this name already exists') - expect(page).to have_content('New file') - expect(page).to have_content('NextFeature') - end - end -end diff --git a/spec/features/projects/diffs/diff_show_spec.rb b/spec/features/projects/diffs/diff_show_spec.rb new file mode 100644 index 00000000000..48b7f1e0f34 --- /dev/null +++ b/spec/features/projects/diffs/diff_show_spec.rb @@ -0,0 +1,133 @@ +require 'spec_helper' + +feature 'Diff file viewer', :js, feature: true do + let(:project) { create(:project, :public, :repository) } + + def visit_commit(sha, anchor: nil) + visit namespace_project_commit_path(project.namespace, project, sha, anchor: anchor) + + wait_for_requests + end + + context 'Ruby file' do + before do + visit_commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') + end + + it 'shows highlighted Ruby code' do + within('.diff-file[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd"]') do + expect(page).to have_css(".js-syntax-highlight") + expect(page).to have_content("def popen(cmd, path=nil)") + end + end + end + + context 'Ruby file (stored in LFS)' do + before do + project.add_master(project.creator) + + @commit_id = Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add Ruby file in LFS", + file_path: 'files/lfs/ruby.rb', + file_content: project.repository.blob_at('master', 'files/lfs/lfs_object.iso').data + ).execute[:result] + end + + context 'when LFS is enabled on the project' do + before do + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + project.update_attribute(:lfs_enabled, true) + + visit_commit(@commit_id) + end + + it 'shows an error message' do + expect(page).to have_content('This source diff could not be displayed because it is stored in LFS. You can view the blob instead.') + end + end + + context 'when LFS is disabled on the project' do + before do + visit_commit(@commit_id) + end + + it 'displays the diff' do + expect(page).to have_content('size 1575078') + end + end + end + + context 'Image file' do + before do + visit_commit('2f63565e7aac07bcdadb654e253078b727143ec4') + end + + it 'shows a rendered image' do + within('.diff-file[id="e986451b8f7397b617dbb6fffcb5539328c56921"]') do + expect(page).to have_css('img[alt="files/images/6049019_460s.jpg"]') + end + end + end + + context 'ISO file (stored in LFS)' do + context 'when LFS is enabled on the project' do + before do + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + project.update_attribute(:lfs_enabled, true) + + visit_commit('048721d90c449b244b7b4c53a9186b04330174ec') + end + + it 'shows that file was added' do + expect(page).to have_content('File added') + end + end + + context 'when LFS is disabled on the project' do + before do + visit_commit('048721d90c449b244b7b4c53a9186b04330174ec') + end + + it 'displays the diff' do + expect(page).to have_content('size 1575078') + end + end + end + + context 'ZIP file' do + before do + visit_commit('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') + end + + it 'shows that file was added' do + expect(page).to have_content('File added') + end + end + + context 'binary file that appears to be text in the first 1024 bytes' do + before do + visit_commit('7b1cf4336b528e0f3d1d140ee50cafdbc703597c') + end + + it 'shows the diff is collapsed' do + expect(page).to have_content('This diff is collapsed. Click to expand it.') + end + + context 'expanding the diff' do + before do + # We can't use `click_link` because the "link" doesn't have an `href`. + find('a.click-to-expand').click + + wait_for_requests + end + + it 'shows there is no preview' do + expect(page).to have_content('No preview for this file type') + end + end + end +end diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb index c49648f54bd..d76b5e4ef1b 100644 --- a/spec/features/projects/features_visibility_spec.rb +++ b/spec/features/projects/features_visibility_spec.rb @@ -68,9 +68,12 @@ describe 'Edit Project Settings', feature: true do end describe 'project features visibility pages' do + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + let(:job) { create(:ci_build, pipeline: pipeline) } + let(:tools) do { - builds: namespace_project_pipelines_path(project.namespace, project), + builds: namespace_project_job_path(project.namespace, project, job), issues: namespace_project_issues_path(project.namespace, project), wiki: namespace_project_wiki_path(project.namespace, project, :home), snippets: namespace_project_snippets_path(project.namespace, project), diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb index 4e5682c8636..1b680a56492 100644 --- a/spec/features/projects/group_links_spec.rb +++ b/spec/features/projects/group_links_spec.rb @@ -16,15 +16,17 @@ feature 'Project group links', :feature, :js do before do visit namespace_project_settings_members_path(project.namespace, project) + click_on 'share-with-group-tab' + select2 group.id, from: '#link_group_id' fill_in 'expires_at_groups', with: (Time.current + 4.5.days).strftime('%Y-%m-%d') page.find('body').click - click_on 'Share' + find('.btn-create').trigger('click') end it 'shows the expiration time with a warning class' do - page.within('.enabled-groups') do - expect(page).to have_content('expires in 4 days') + page.within('.project-members-groups') do + expect(page).to have_content('Expires in 4 days') expect(page).to have_selector('.text-warning') end end @@ -43,6 +45,7 @@ feature 'Project group links', :feature, :js do it 'does not show ancestors', :nested_groups do visit namespace_project_settings_members_path(project.namespace, project) + click_on 'share-with-group-tab' click_link 'Search for a group' page.within '.select2-drop' do diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 0eda46649db..31c93c75d25 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -5,10 +5,11 @@ feature 'Jobs', :feature do let(:user) { create(:user) } let(:user_access_level) { :developer } let(:project) { create(:project) } + let(:namespace) { project.namespace } let(:pipeline) { create(:ci_pipeline, project: project) } - let(:build) { create(:ci_build, :trace, pipeline: pipeline) } - let(:build2) { create(:ci_build) } + let(:job) { create(:ci_build, :trace, pipeline: pipeline) } + let(:job2) { create(:ci_build) } let(:artifacts_file) do fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') @@ -20,7 +21,7 @@ feature 'Jobs', :feature do end describe "GET /:project/jobs" do - let!(:build) { create(:ci_build, pipeline: pipeline) } + let!(:job) { create(:ci_build, pipeline: pipeline) } context "Pending scope" do before do @@ -30,30 +31,30 @@ feature 'Jobs', :feature 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 - expect(page).to have_content build.ref - expect(page).to have_content build.name + expect(page).to have_content job.short_sha + expect(page).to have_content job.ref + expect(page).to have_content job.name end end context "Running scope" do before do - build.run! + job.run! visit namespace_project_jobs_path(project.namespace, project, scope: :running) end 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 - expect(page).to have_content build.ref - expect(page).to have_content build.name + expect(page).to have_content job.short_sha + expect(page).to have_content job.ref + expect(page).to have_content job.name end end context "Finished scope" do before do - build.run! + job.run! visit namespace_project_jobs_path(project.namespace, project, scope: :finished) end @@ -72,9 +73,9 @@ feature 'Jobs', :feature 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 - expect(page).to have_content build.name + expect(page).to have_content job.short_sha + expect(page).to have_content job.ref + expect(page).to have_content job.name expect(page).not_to have_link 'Cancel running' end end @@ -96,7 +97,7 @@ feature 'Jobs', :feature do describe "POST /:project/jobs/:id/cancel_all" do before do - build.run! + job.run! visit namespace_project_jobs_path(project.namespace, project) click_link "Cancel running" end @@ -104,17 +105,23 @@ feature 'Jobs', :feature do it 'shows all necessary content' do expect(page).to have_selector('.nav-links li.active', text: 'All') expect(page).to have_content 'canceled' - expect(page).to have_content build.short_sha - expect(page).to have_content build.ref - expect(page).to have_content build.name + expect(page).to have_content job.short_sha + expect(page).to have_content job.ref + expect(page).to have_content job.name expect(page).not_to have_link 'Cancel running' end end describe "GET /:project/jobs/:id" do context "Job from project" do + let(:job) { create(:ci_build, :success, pipeline: pipeline) } + before do - visit namespace_project_job_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, job) + end + + it 'shows status name', :js do + expect(page).to have_css('.ci-status.ci-success', text: 'passed') end it 'shows commit`s data' do @@ -124,14 +131,56 @@ feature 'Jobs', :feature do expect(page).to have_content pipeline.git_author_name end - it 'shows active build' do + it 'shows active job' do expect(page).to have_selector('.build-job.active') end end + context 'when job is not running', :js do + let(:job) { create(:ci_build, :success, pipeline: pipeline) } + + before do + visit namespace_project_job_path(project.namespace, project, job) + end + + it 'shows retry button' do + expect(page).to have_link('Retry') + end + + context 'if job passed' do + it 'does not show New issue button' do + expect(page).not_to have_link('New issue') + end + end + + context 'if job failed' do + let(:job) { create(:ci_build, :failed, pipeline: pipeline) } + + before do + visit namespace_project_job_path(namespace, project, job) + end + + it 'shows New issue button' do + expect(page).to have_link('New issue') + end + + it 'links to issues/new with the title and description filled in' do + button_title = "Build Failed ##{job.id}" + job_path = namespace_project_job_path(namespace, project, job) + options = { issue: { title: button_title, description: job_path } } + + href = new_namespace_project_issue_path(namespace, project, options) + + page.within('.header-action-buttons') do + expect(find('.js-new-issue')['href']).to include(href) + end + end + end + end + context "Job from other project" do before do - visit namespace_project_job_path(project.namespace, project, build2) + visit namespace_project_job_path(project.namespace, project, job2) end it { expect(page.status_code).to eq(404) } @@ -139,8 +188,8 @@ feature 'Jobs', :feature do context "Download artifacts" do before do - build.update_attributes(artifacts_file: artifacts_file) - visit namespace_project_job_path(project.namespace, project, build) + job.update_attributes(artifacts_file: artifacts_file) + visit namespace_project_job_path(project.namespace, project, job) end it 'has button to download artifacts' do @@ -150,10 +199,10 @@ feature 'Jobs', :feature do context 'Artifacts expire date' do before do - build.update_attributes(artifacts_file: artifacts_file, - artifacts_expire_at: expire_at) + job.update_attributes(artifacts_file: artifacts_file, + artifacts_expire_at: expire_at) - visit namespace_project_job_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, job) end context 'no expire date defined' do @@ -199,7 +248,7 @@ feature 'Jobs', :feature do context "when visiting old URL" do let(:job_url) do - namespace_project_job_path(project.namespace, project, build) + namespace_project_job_path(project.namespace, project, job) end before do @@ -213,9 +262,9 @@ feature 'Jobs', :feature do feature 'Raw trace' do before do - build.run! + job.run! - visit namespace_project_job_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, job) end it do @@ -225,16 +274,16 @@ feature 'Jobs', :feature do feature 'HTML trace', :js do before do - build.run! + job.run! - visit namespace_project_job_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, job) end context 'when job has an initial trace' do it 'loads job trace' do expect(page).to have_content 'BUILD TRACE' - build.trace.write do |stream| + job.trace.write do |stream| stream.append(' and more trace', 11) end @@ -246,12 +295,12 @@ feature 'Jobs', :feature do feature 'Variables' do let(:trigger_request) { create(:ci_trigger_request_with_variables) } - let(:build) do + let(:job) do create :ci_build, pipeline: pipeline, trigger_request: trigger_request end before do - visit namespace_project_job_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, job) end it 'shows variable key and value after click', js: true do @@ -273,20 +322,20 @@ feature 'Jobs', :feature 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) } + let(:job) { create(:ci_build, :success, environment: environment.name, deployments: [deployment], pipeline: pipeline) } it 'shows a link for the job' do - visit namespace_project_job_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, job) expect(page).to have_link environment.name end end context 'job is complete and not successful' do - let(:build) { create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) } + let(:job) { create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) } it 'shows a link for the job' do - visit namespace_project_job_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, job) expect(page).to have_link environment.name end @@ -294,10 +343,10 @@ feature 'Jobs', :feature 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) } + let(:job) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } it 'shows a link to latest deployment' do - visit namespace_project_job_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, job) expect(page).to have_link('latest deployment') end @@ -305,72 +354,47 @@ feature 'Jobs', :feature do end end - describe "POST /:project/jobs/:id/cancel" do + describe "POST /:project/jobs/:id/cancel", :js do context "Job from project" do before do - build.run! - visit namespace_project_job_path(project.namespace, project, build) - click_link "Cancel" + job.run! + visit namespace_project_job_path(project.namespace, project, job) + find('.js-cancel-job').click() end it 'loads the page and shows all needed controls' do expect(page.status_code).to eq(200) - expect(page).to have_content 'canceled' expect(page).to have_content 'Retry' end end - - context "Job from other project" do - before do - build.run! - visit namespace_project_job_path(project.namespace, project, build) - page.driver.post(cancel_namespace_project_job_path(project.namespace, project, build2)) - end - - it { expect(page.status_code).to eq(404) } - end end describe "POST /:project/jobs/:id/retry" do - context "Job from project" do + context "Job from project", :js do before do - build.run! - visit namespace_project_job_path(project.namespace, project, build) - click_link 'Cancel' - page.within('.build-header') do - click_link 'Retry job' - end + job.run! + visit namespace_project_job_path(project.namespace, project, job) + find('.js-cancel-job').click() + find('.js-retry-button').trigger('click') end - it 'shows the right status and buttons' do + it 'shows the right status and buttons', :js do expect(page).to have_http_status(200) - expect(page).to have_content 'pending' page.within('aside.right-sidebar') do expect(page).to have_content 'Cancel' end end end - context "Job from other project" do - before do - build.run! - visit namespace_project_job_path(project.namespace, project, build) - click_link 'Cancel' - page.driver.post(retry_namespace_project_job_path(project.namespace, project, build2)) - end - - it { expect(page).to have_http_status(404) } - end - context "Job that current user is not allowed to retry" do before do - build.run! - build.cancel! + job.run! + job.cancel! project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) logout_direct login_with(create(:user)) - visit namespace_project_job_path(project.namespace, project, build) + visit namespace_project_job_path(project.namespace, project, job) end it 'does not show the Retry button' do @@ -383,15 +407,15 @@ feature 'Jobs', :feature do describe "GET /:project/jobs/:id/download" do before do - build.update_attributes(artifacts_file: artifacts_file) - visit namespace_project_job_path(project.namespace, project, build) + job.update_attributes(artifacts_file: artifacts_file) + visit namespace_project_job_path(project.namespace, project, job) click_link 'Download' end context "Build from other project" do before do - build2.update_attributes(artifacts_file: artifacts_file) - visit download_namespace_project_job_artifacts_path(project.namespace, project, build2) + job2.update_attributes(artifacts_file: artifacts_file) + visit download_namespace_project_job_artifacts_path(project.namespace, project, job2) end it { expect(page.status_code).to eq(404) } @@ -403,23 +427,23 @@ feature 'Jobs', :feature do context 'job from project' do before do Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' } - build.run! - visit namespace_project_job_path(project.namespace, project, build) + job.run! + visit namespace_project_job_path(project.namespace, project, job) find('.js-raw-link-controller').click() end it 'sends the right headers' do expect(page.status_code).to eq(200) expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') - expect(page.response_headers['X-Sendfile']).to eq(build.trace.send(:current_path)) + expect(page.response_headers['X-Sendfile']).to eq(job.trace.send(:current_path)) end end context 'job from other project' do before do Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' } - build2.run! - visit raw_namespace_project_job_path(project.namespace, project, build2) + job2.run! + visit raw_namespace_project_job_path(project.namespace, project, job2) end it 'sends the right headers' do @@ -434,21 +458,18 @@ feature 'Jobs', :feature do before do Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' } - build.run! - - allow_any_instance_of(Gitlab::Ci::Trace).to receive(:paths) - .and_return(paths) - - visit namespace_project_job_path(project.namespace, project, build) + job.run! end - context 'when build has trace in file', :js do - let(:paths) do - [existing_file] - end - + context 'when job has trace in file', :js do before do - find('.js-raw-link-controller').click() + allow_any_instance_of(Gitlab::Ci::Trace) + .to receive(:paths) + .and_return([existing_file]) + + visit namespace_project_job_path(namespace, project, job) + + find('.js-raw-link-controller').click end it 'sends the right headers' do @@ -458,18 +479,24 @@ feature 'Jobs', :feature do end end - context 'when job has trace in DB' do - let(:paths) { [] } + context 'when job has trace in the database', :js do + before do + allow_any_instance_of(Gitlab::Ci::Trace) + .to receive(:paths) + .and_return([]) + + visit namespace_project_job_path(namespace, project, job) + end it 'sends the right headers' do - expect(page.status_code).not_to have_selector('.js-raw-link-controller') + expect(page).not_to have_selector('.js-raw-link-controller') end end end context "when visiting old URL" do let(:raw_job_url) do - raw_namespace_project_job_path(project.namespace, project, build) + raw_namespace_project_job_path(project.namespace, project, job) end before do @@ -485,7 +512,7 @@ feature 'Jobs', :feature do describe "GET /:project/jobs/:id/trace.json" do context "Job from project" do before do - visit trace_namespace_project_job_path(project.namespace, project, build, format: :json) + visit trace_namespace_project_job_path(project.namespace, project, job, format: :json) end it { expect(page.status_code).to eq(200) } @@ -493,7 +520,7 @@ feature 'Jobs', :feature do context "Job from other project" do before do - visit trace_namespace_project_job_path(project.namespace, project, build2, format: :json) + visit trace_namespace_project_job_path(project.namespace, project, job2, format: :json) end it { expect(page.status_code).to eq(404) } @@ -503,7 +530,7 @@ feature 'Jobs', :feature do describe "GET /:project/jobs/:id/status" do context "Job from project" do before do - visit status_namespace_project_job_path(project.namespace, project, build) + visit status_namespace_project_job_path(project.namespace, project, job) end it { expect(page.status_code).to eq(200) } @@ -511,7 +538,7 @@ feature 'Jobs', :feature do context "Job from other project" do before do - visit status_namespace_project_job_path(project.namespace, project, build2) + visit status_namespace_project_job_path(project.namespace, project, job2) end it { expect(page.status_code).to eq(404) } diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index c66b9a34b86..b1f9eb15667 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -17,10 +17,10 @@ feature "New project", feature: true do expect(find_field("project_visibility_level_#{level}")).to be_checked end - it 'saves visibility level on validation error' do + it "saves visibility level #{level} on validation error" do visit new_project_path - choose(key) + choose(s_(key)) click_button('Create project') expect(find_field("project_visibility_level_#{level}")).to be_checked diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb index 317949d6b56..2d43f7a10bc 100644 --- a/spec/features/projects/pipeline_schedules_spec.rb +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -127,7 +127,7 @@ feature 'Pipeline Schedules', :feature do end it 'shows the pipeline schedule with default ref' do - page.within('.git-revision-dropdown-toggle') do + page.within('.js-target-branch-dropdown') do expect(first('.dropdown-toggle-text').text).to eq('master') end end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 36a3ddca6ef..12c5ad45baf 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -47,7 +47,9 @@ describe 'Pipeline', :feature, :js do let(:project) { create(:project) } let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id, user: user) } - before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) } + before do + visit namespace_project_pipeline_path(project.namespace, project, pipeline) + end it 'shows the pipeline graph' do expect(page).to have_selector('.pipeline-visualization') @@ -164,7 +166,9 @@ describe 'Pipeline', :feature, :js do it { expect(page).not_to have_content('retried') } context 'when retrying' do - before { find('.js-retry-button').trigger('click') } + before do + find('.js-retry-button').trigger('click') + end it { expect(page).not_to have_content('Retry') } end @@ -174,7 +178,9 @@ describe 'Pipeline', :feature, :js do it { expect(page).not_to have_selector('.ci-canceled') } context 'when canceling' do - before { click_on 'Cancel running' } + before do + click_on 'Cancel running' + end it { expect(page).not_to have_content('Cancel running') } end @@ -226,7 +232,9 @@ describe 'Pipeline', :feature, :js do it { expect(page).not_to have_content('retried') } context 'when retrying' do - before { find('.js-retry-button').trigger('click') } + before do + find('.js-retry-button').trigger('click') + end it { expect(page).not_to have_content('Retry') } end @@ -236,7 +244,9 @@ describe 'Pipeline', :feature, :js do it { expect(page).not_to have_selector('.ci-canceled') } context 'when canceling' do - before { click_on 'Cancel running' } + before do + click_on 'Cancel running' + end it { expect(page).not_to have_content('Cancel running') } end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 05c2bf350f1..db2d1a100a5 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -149,7 +149,9 @@ describe 'Pipelines', :feature, :js do create(:ci_pipeline, :invalid, project: project) end - before { visit_project_pipelines } + before do + visit_project_pipelines + end it 'contains badge that indicates errors' do expect(page).to have_content 'yaml invalid' @@ -171,10 +173,12 @@ describe 'Pipelines', :feature, :js do commands: 'test') end - before { visit_project_pipelines } + before do + visit_project_pipelines + end it 'has a dropdown with play button' do - expect(page).to have_selector('.dropdown-toggle.btn.btn-default .icon-play') + expect(page).to have_selector('.dropdown-new.btn.btn-default .icon-play') end it 'has link to the manual action' do @@ -204,7 +208,9 @@ describe 'Pipelines', :feature, :js do stage: 'test') end - before { visit_project_pipelines } + before do + visit_project_pipelines + end it 'is cancelable' do expect(page).to have_selector('.js-pipelines-cancel-button') @@ -215,7 +221,9 @@ describe 'Pipelines', :feature, :js do end context 'when canceling' do - before { find('.js-pipelines-cancel-button').trigger('click') } + before do + find('.js-pipelines-cancel-button').trigger('click') + end it 'indicates that pipeline was canceled' do expect(page).not_to have_selector('.js-pipelines-cancel-button') @@ -255,7 +263,9 @@ describe 'Pipelines', :feature, :js do stage: 'test') end - before { visit_project_pipelines } + before do + visit_project_pipelines + end it 'has artifats' do expect(page).to have_selector('.build-artifacts') @@ -284,7 +294,9 @@ describe 'Pipelines', :feature, :js do stage: 'test') end - before { visit_project_pipelines } + before do + visit_project_pipelines + end it { expect(page).not_to have_selector('.build-artifacts') } end @@ -297,7 +309,9 @@ describe 'Pipelines', :feature, :js do stage: 'test') end - before { visit_project_pipelines } + before do + visit_project_pipelines + end it { expect(page).not_to have_selector('.build-artifacts') } end @@ -310,7 +324,9 @@ describe 'Pipelines', :feature, :js do name: 'build') end - before { visit_project_pipelines } + before do + visit_project_pipelines + end it 'should render a mini pipeline graph' do expect(page).to have_selector('.js-mini-pipeline-graph') @@ -437,7 +453,9 @@ describe 'Pipelines', :feature, :js do end context 'with gitlab-ci.yml' do - before { stub_ci_pipeline_to_return_yaml_file } + before do + stub_ci_pipeline_to_return_yaml_file + end it 'creates a new pipeline' do expect { click_on 'Create pipeline' } @@ -448,7 +466,9 @@ describe 'Pipelines', :feature, :js do end context 'without gitlab-ci.yml' do - before { click_on 'Create pipeline' } + before do + click_on 'Create pipeline' + end it { expect(page).to have_content('Missing .gitlab-ci.yml file') } end diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb index 11dcab4d737..2a9b32ea07e 100644 --- a/spec/features/projects/project_settings_spec.rb +++ b/spec/features/projects/project_settings_spec.rb @@ -58,8 +58,13 @@ describe 'Edit Project Settings', feature: true do # Not using empty project because we need a repo to exist let(:project) { create(:project, namespace: user.namespace, name: 'gitlabhq') } - before(:context) { TestEnv.clean_test_path } - after(:example) { TestEnv.clean_test_path } + before(:context) do + TestEnv.clean_test_path + end + + after(:example) do + TestEnv.clean_test_path + end specify 'the project is accessible via the new path' do rename_project(project, path: 'bar') @@ -96,9 +101,17 @@ describe 'Edit Project Settings', feature: true do let!(:project) { create(:project, namespace: user.namespace, name: 'gitlabhq') } let!(:group) { create(:group) } - before(:context) { TestEnv.clean_test_path } - before(:example) { group.add_owner(user) } - after(:example) { TestEnv.clean_test_path } + before(:context) do + TestEnv.clean_test_path + end + + before(:example) do + group.add_owner(user) + end + + after(:example) do + TestEnv.clean_test_path + end specify 'the project is accessible via the new path' do transfer_project(project, group) diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb new file mode 100644 index 00000000000..4cc38c5286e --- /dev/null +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' + +feature 'Repository settings', feature: true do + let(:project) { create(:project_empty_repo) } + let(:user) { create(:user) } + let(:role) { :developer } + + background do + project.team << [user, role] + login_as(user) + end + + context 'for developer' do + given(:role) { :developer } + + scenario 'is not allowed to view' do + visit namespace_project_settings_repository_path(project.namespace, project) + + expect(page.status_code).to eq(404) + end + end + + context 'for master' do + given(:role) { :master } + + context 'Deploy Keys', js: true do + let(:private_deploy_key) { create(:deploy_key, title: 'private_deploy_key', public: false) } + let(:public_deploy_key) { create(:another_deploy_key, title: 'public_deploy_key', public: true) } + let(:new_ssh_key) { attributes_for(:key)[:key] } + + scenario 'get list of keys' do + project.deploy_keys << private_deploy_key + project.deploy_keys << public_deploy_key + + visit namespace_project_settings_repository_path(project.namespace, project) + + expect(page.status_code).to eq(200) + expect(page).to have_content('private_deploy_key') + expect(page).to have_content('public_deploy_key') + end + + scenario 'add a new deploy key' do + visit namespace_project_settings_repository_path(project.namespace, project) + + fill_in 'deploy_key_title', with: 'new_deploy_key' + fill_in 'deploy_key_key', with: new_ssh_key + check 'deploy_key_can_push' + click_button 'Add key' + + expect(page).to have_content('new_deploy_key') + expect(page).to have_content('Write access allowed') + end + + scenario 'edit an existing deploy key' do + project.deploy_keys << private_deploy_key + visit namespace_project_settings_repository_path(project.namespace, project) + + find('li', text: private_deploy_key.title).click_link('Edit') + + fill_in 'deploy_key_title', with: 'updated_deploy_key' + check 'deploy_key_can_push' + click_button 'Save changes' + + expect(page).to have_content('updated_deploy_key') + expect(page).to have_content('Write access allowed') + end + + scenario 'remove an existing deploy key' do + project.deploy_keys << private_deploy_key + visit namespace_project_settings_repository_path(project.namespace, project) + + find('li', text: private_deploy_key.title).click_button('Remove') + + expect(page).not_to have_content(private_deploy_key.title) + end + end + end +end diff --git a/spec/features/projects/snippets/create_snippet_spec.rb b/spec/features/projects/snippets/create_snippet_spec.rb new file mode 100644 index 00000000000..5ac1ca45c74 --- /dev/null +++ b/spec/features/projects/snippets/create_snippet_spec.rb @@ -0,0 +1,86 @@ +require 'rails_helper' + +feature 'Create Snippet', :js, feature: true do + include DropzoneHelper + + let(:user) { create(:user) } + let(:project) { create(:project, :repository, :public) } + + def fill_form + fill_in 'project_snippet_title', with: 'My Snippet Title' + fill_in 'project_snippet_description', with: 'My Snippet **Description**' + page.within('.file-editor') do + find('.ace_editor').native.send_keys('Hello World!') + end + end + + context 'when a user is authenticated' do + before do + project.team << [user, :master] + login_as(user) + + visit namespace_project_snippets_path(project.namespace, project) + + click_on('New snippet') + end + + it 'creates a new snippet' do + fill_form + click_button('Create snippet') + wait_for_requests + + expect(page).to have_content('My Snippet Title') + expect(page).to have_content('Hello World!') + page.within('.snippet-header .description') do + expect(page).to have_content('My Snippet Description') + expect(page).to have_selector('strong') + end + end + + it 'uploads a file when dragging into textarea' do + fill_form + dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') + + expect(page.find_field("project_snippet_description").value).to have_content('banana_sample') + + click_button('Create snippet') + wait_for_requests + + link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] + expect(link).to match(%r{/#{Regexp.escape(project.full_path) }/uploads/\h{32}/banana_sample\.gif\z}) + end + + it 'creates a snippet when all reuiqred fields are filled in after validation failing' do + fill_in 'project_snippet_title', with: 'My Snippet Title' + click_button('Create snippet') + + expect(page).to have_selector('#error_explanation') + + fill_form + dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') + + click_button('Create snippet') + wait_for_requests + + expect(page).to have_content('My Snippet Title') + expect(page).to have_content('Hello World!') + page.within('.snippet-header .description') do + expect(page).to have_content('My Snippet Description') + expect(page).to have_selector('strong') + end + link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] + expect(link).to match(%r{/#{Regexp.escape(project.full_path) }/uploads/\h{32}/banana_sample\.gif\z}) + end + end + + context 'when a user is not authenticated' do + it 'shows a public snippet on the index page but not the New snippet button' do + snippet = create(:project_snippet, :public, project: project) + + visit namespace_project_snippets_path(project.namespace, project) + + expect(page).to have_content(snippet.title) + expect(page).not_to have_content('New snippet') + end + end +end diff --git a/spec/features/projects/user_create_dir_spec.rb b/spec/features/projects/user_create_dir_spec.rb index 5dfdc465d7d..aeb7e0b7c33 100644 --- a/spec/features/projects/user_create_dir_spec.rb +++ b/spec/features/projects/user_create_dir_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' feature 'New directory creation', feature: true, js: true do - include TargetBranchHelpers - given(:user) { create(:user) } given(:role) { :developer } given(:project) { create(:project) } @@ -36,23 +34,11 @@ feature 'New directory creation', feature: true, js: true do end end - context 'with different target branch' do - background do - select_branch('feature') - create_directory - end - - scenario 'creates the directory in the different branch' do - expect(page).to have_content 'feature' - expect(page).to have_content 'The directory has been successfully created' - end - end - context 'with a new target branch' do given(:new_branch_name) { 'new-feature' } background do - create_new_branch(new_branch_name) + fill_in :branch_name, with: new_branch_name create_directory end diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb index 49d7ef09e64..94f6bb16730 100644 --- a/spec/features/projects/wiki/markdown_preview_spec.rb +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -14,11 +14,12 @@ feature 'Projects > Wiki > User previews markdown changes', feature: true, js: t background do project.team << [user, :master] + WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute + login_as(user) visit namespace_project_path(project.namespace, project) find('.shortcuts-wiki').trigger('click') - WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute end context "while creating a new wiki page" do diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index 667895bffa5..aa9164dd979 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -4,7 +4,9 @@ feature 'Protected Branches', feature: true, js: true do let(:user) { create(:user, :admin) } let(:project) { create(:project, :repository) } - before { login_as(user) } + before do + login_as(user) + end def set_protected_branch_name(branch_name) find(".js-protected-branch-select").trigger('click') diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb index 66236dbc7fc..63a20585776 100644 --- a/spec/features/protected_tags_spec.rb +++ b/spec/features/protected_tags_spec.rb @@ -4,7 +4,9 @@ feature 'Projected Tags', feature: true, js: true do let(:user) { create(:user, :admin) } let(:project) { create(:project, :repository) } - before { login_as(user) } + before do + login_as(user) + end def set_protected_tag_name(tag_name) find(".js-protected-tag-select").click diff --git a/spec/features/reportable_note/commit_spec.rb b/spec/features/reportable_note/commit_spec.rb new file mode 100644 index 00000000000..39b1c4acf52 --- /dev/null +++ b/spec/features/reportable_note/commit_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe 'Reportable note on commit', :feature, :js do + include RepoHelpers + + let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + project.add_master(user) + login_as user + end + + context 'a normal note' do + let!(:note) { create(:note_on_commit, commit_id: sample_commit.id, project: project) } + + before do + visit namespace_project_commit_path(project.namespace, project, sample_commit.id) + end + + it_behaves_like 'reportable note' + end + + context 'a diff note' do + let!(:note) { create(:diff_note_on_commit, commit_id: sample_commit.id, project: project) } + + before do + visit namespace_project_commit_path(project.namespace, project, sample_commit.id) + end + + it_behaves_like 'reportable note' + end +end diff --git a/spec/features/reportable_note/issue_spec.rb b/spec/features/reportable_note/issue_spec.rb new file mode 100644 index 00000000000..5f526818994 --- /dev/null +++ b/spec/features/reportable_note/issue_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe 'Reportable note on issue', :feature, :js do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + let!(:note) { create(:note_on_issue, noteable: issue, project: project) } + + before do + project.add_master(user) + login_as user + + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it_behaves_like 'reportable note' +end diff --git a/spec/features/reportable_note/merge_request_spec.rb b/spec/features/reportable_note/merge_request_spec.rb new file mode 100644 index 00000000000..6d053d26626 --- /dev/null +++ b/spec/features/reportable_note/merge_request_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe 'Reportable note on merge request', :feature, :js do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:merge_request) { create(:merge_request, source_project: project) } + + before do + project.add_master(user) + login_as user + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + context 'a normal note' do + let!(:note) { create(:note_on_merge_request, noteable: merge_request, project: project) } + + it_behaves_like 'reportable note' + end + + context 'a diff note' do + let!(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) } + + it_behaves_like 'reportable note' + end +end diff --git a/spec/features/reportable_note/snippets_spec.rb b/spec/features/reportable_note/snippets_spec.rb new file mode 100644 index 00000000000..3f1e0cf9097 --- /dev/null +++ b/spec/features/reportable_note/snippets_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe 'Reportable note on snippets', :feature, :js do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + + before do + project.add_master(user) + login_as user + end + + describe 'on project snippet' do + let(:snippet) { create(:project_snippet, :public, project: project, author: user) } + let!(:note) { create(:note_on_project_snippet, noteable: snippet, project: project) } + + before do + visit namespace_project_snippet_path(project.namespace, project, snippet) + end + + it_behaves_like 'reportable note' + end + + describe 'on personal snippet' do + let(:snippet) { create(:personal_snippet, :public, author: user) } + let!(:note) { create(:note_on_personal_snippet, noteable: snippet, author: user) } + + before do + visit snippet_path(snippet) + end + + it_behaves_like 'reportable note' + end +end diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index 0e1cc9a0f73..e87d52f5c8f 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -4,7 +4,10 @@ describe "Runners" do include GitlabRoutingHelper let(:user) { create(:user) } - before { login_as(user) } + + before do + login_as(user) + end describe "specific runners" do before do @@ -127,7 +130,9 @@ describe "Runners" do end context 'when runner has tags' do - before { runner.update_attribute(:tag_list, ['tag']) } + before do + runner.update_attribute(:tag_list, ['tag']) + end scenario 'user wants to prevent runner from running untagged job' do visit runners_path(project) diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 7834807b1f1..89d4f536b20 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -83,7 +83,9 @@ describe "Search", feature: true do let(:project) { create(:project, :repository) } let(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'Bug here') } - before { note.update_attributes(commit_id: 12345678) } + before do + note.update_attributes(commit_id: 12345678) + end it 'finds comment' do visit namespace_project_path(project.namespace, project) diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 2a2655bbdb5..f33406a40a7 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -337,7 +337,9 @@ describe "Internal Project Access", feature: true do subject { namespace_project_jobs_path(project.namespace, project) } context "when allowed for public and internal" do - before { project.update(public_builds: true) } + before do + project.update(public_builds: true) + end it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } @@ -351,7 +353,9 @@ describe "Internal Project Access", feature: true do end context "when disallowed for public and internal" do - before { project.update(public_builds: false) } + before do + project.update(public_builds: false) + end it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } @@ -371,7 +375,9 @@ describe "Internal Project Access", feature: true do subject { namespace_project_job_path(project.namespace, project, build.id) } context "when allowed for public and internal" do - before { project.update(public_builds: true) } + before do + project.update(public_builds: true) + end it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } @@ -385,7 +391,9 @@ describe "Internal Project Access", feature: true do end context "when disallowed for public and internal" do - before { project.update(public_builds: false) } + before do + project.update(public_builds: false) + end it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 35d5163941e..16a1331b2f3 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -157,7 +157,9 @@ describe "Public Project Access", feature: true do subject { namespace_project_jobs_path(project.namespace, project) } context "when allowed for public" do - before { project.update(public_builds: true) } + before do + project.update(public_builds: true) + end it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } @@ -171,7 +173,9 @@ describe "Public Project Access", feature: true do end context "when disallowed for public" do - before { project.update(public_builds: false) } + before do + project.update(public_builds: false) + end it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } @@ -191,7 +195,9 @@ describe "Public Project Access", feature: true do subject { namespace_project_job_path(project.namespace, project, build.id) } context "when allowed for public" do - before { project.update(public_builds: true) } + before do + project.update(public_builds: true) + end it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } @@ -205,7 +211,9 @@ describe "Public Project Access", feature: true do end context "when disallowed for public" do - before { project.update(public_builds: false) } + before do + project.update(public_builds: false) + end it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb index d7b6dda4946..5d6d1e79af2 100644 --- a/spec/features/signup_spec.rb +++ b/spec/features/signup_spec.rb @@ -3,7 +3,9 @@ require 'spec_helper' feature 'Signup', feature: true do describe 'signup with no errors' do context "when sending confirmation email" do - before { stub_application_setting(send_user_confirmation_email: true) } + before do + stub_application_setting(send_user_confirmation_email: true) + end it 'creates the user account and sends a confirmation email' do user = build(:user) @@ -23,7 +25,9 @@ feature 'Signup', feature: true do end context "when not sending confirmation email" do - before { stub_application_setting(send_user_confirmation_email: false) } + before do + stub_application_setting(send_user_confirmation_email: false) + end it 'creates the user account and goes to dashboard' do user = build(:user) diff --git a/spec/features/snippets/create_snippet_spec.rb b/spec/features/snippets/create_snippet_spec.rb index 31a2d4ae984..ddd31ede064 100644 --- a/spec/features/snippets/create_snippet_spec.rb +++ b/spec/features/snippets/create_snippet_spec.rb @@ -1,24 +1,93 @@ require 'rails_helper' feature 'Create Snippet', :js, feature: true do + include DropzoneHelper + before do login_as :user visit new_snippet_path end - scenario 'Authenticated user creates a snippet' do + def fill_form fill_in 'personal_snippet_title', with: 'My Snippet Title' + fill_in 'personal_snippet_description', with: 'My Snippet **Description**' page.within('.file-editor') do find('.ace_editor').native.send_keys 'Hello World!' end + end - click_button 'Create snippet' + scenario 'Authenticated user creates a snippet' do + fill_form + + click_button('Create snippet') wait_for_requests expect(page).to have_content('My Snippet Title') + page.within('.snippet-header .description') do + expect(page).to have_content('My Snippet Description') + expect(page).to have_selector('strong') + end expect(page).to have_content('Hello World!') end + scenario 'previews a snippet with file' do + fill_in 'personal_snippet_description', with: 'My Snippet' + dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') + find('.js-md-preview-button').click + + page.within('#new_personal_snippet .md-preview') do + expect(page).to have_content('My Snippet') + + link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] + expect(link).to match(%r{/uploads/temp/\h{32}/banana_sample\.gif\z}) + + visit(link) + expect(page.status_code).to eq(200) + end + end + + scenario 'uploads a file when dragging into textarea' do + fill_form + + dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') + + expect(page.find_field("personal_snippet_description").value).to have_content('banana_sample') + + click_button('Create snippet') + wait_for_requests + + link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] + expect(link).to match(%r{/uploads/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z}) + + visit(link) + expect(page.status_code).to eq(200) + end + + scenario 'validation fails for the first time' do + fill_in 'personal_snippet_title', with: 'My Snippet Title' + click_button('Create snippet') + + expect(page).to have_selector('#error_explanation') + + fill_form + dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') + + click_button('Create snippet') + wait_for_requests + + expect(page).to have_content('My Snippet Title') + page.within('.snippet-header .description') do + expect(page).to have_content('My Snippet Description') + expect(page).to have_selector('strong') + end + expect(page).to have_content('Hello World!') + link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] + expect(link).to match(%r{/uploads/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z}) + + visit(link) + expect(page.status_code).to eq(200) + end + scenario 'Authenticated user creates a snippet with + in filename' do fill_in 'personal_snippet_title', with: 'My Snippet Title' page.within('.file-editor') do diff --git a/spec/features/snippets/edit_snippet_spec.rb b/spec/features/snippets/edit_snippet_spec.rb new file mode 100644 index 00000000000..89ae593db88 --- /dev/null +++ b/spec/features/snippets/edit_snippet_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +feature 'Edit Snippet', :js, feature: true do + include DropzoneHelper + + let(:file_name) { 'test.rb' } + let(:content) { 'puts "test"' } + + let(:user) { create(:user) } + let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content, author: user) } + + before do + login_as(user) + + visit edit_snippet_path(snippet) + wait_for_requests + end + + it 'updates the snippet' do + fill_in 'personal_snippet_title', with: 'New Snippet Title' + + click_button('Save changes') + wait_for_requests + + expect(page).to have_content('New Snippet Title') + end + + it 'updates the snippet with files attached' do + dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') + expect(page.find_field("personal_snippet_description").value).to have_content('banana_sample') + + click_button('Save changes') + wait_for_requests + + link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] + expect(link).to match(%r{/uploads/personal_snippet/#{snippet.id}/\h{32}/banana_sample\.gif\z}) + end +end diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb index f7afc174019..44b0c89fac7 100644 --- a/spec/features/snippets/notes_on_personal_snippets_spec.rb +++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe 'Comments on personal snippets', :js, feature: true do + include NoteInteractionHelpers + let!(:user) { create(:user) } let!(:snippet) { create(:personal_snippet, :public) } let!(:snippet_notes) do @@ -22,6 +24,8 @@ describe 'Comments on personal snippets', :js, feature: true do it 'contains notes for a snippet with correct action icons' do expect(page).to have_selector('#notes-list li', count: 2) + open_more_actions_dropdown(snippet_notes[0]) + # comment authored by current user page.within("#notes-list li#note_#{snippet_notes[0].id}") do expect(page).to have_content(snippet_notes[0].note) @@ -29,6 +33,8 @@ describe 'Comments on personal snippets', :js, feature: true do expect(page).to have_selector('.note-emoji-button') end + open_more_actions_dropdown(snippet_notes[1]) + page.within("#notes-list li#note_#{snippet_notes[1].id}") do expect(page).to have_content(snippet_notes[1].note) expect(page).not_to have_selector('.js-note-delete') @@ -68,6 +74,8 @@ describe 'Comments on personal snippets', :js, feature: true do context 'when editing a note' do it 'changes the text' do + open_more_actions_dropdown(snippet_notes[0]) + page.within("#notes-list li#note_#{snippet_notes[0].id}") do click_on 'Edit comment' end @@ -89,8 +97,10 @@ describe 'Comments on personal snippets', :js, feature: true do context 'when deleting a note' do it 'removes the note from the snippet detail page' do + open_more_actions_dropdown(snippet_notes[0]) + page.within("#notes-list li#note_#{snippet_notes[0].id}") do - click_on 'Remove comment' + click_on 'Delete comment' end wait_for_requests diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index 563e65d3cc5..51b1b8e2328 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -144,7 +144,9 @@ feature 'Task Lists', feature: true do describe 'nested tasks', js: true do let(:issue) { create(:issue, description: nested_tasks_markdown, author: user, project: project) } - before { visit_issue(project, issue) } + before do + visit_issue(project, issue) + end it 'renders' do expect(page).to have_selector('ul.task-list', count: 2) diff --git a/spec/features/todos/todos_sorting_spec.rb b/spec/features/todos/todos_sorting_spec.rb index 4d5bd476301..f012d250887 100644 --- a/spec/features/todos/todos_sorting_spec.rb +++ b/spec/features/todos/todos_sorting_spec.rb @@ -8,7 +8,9 @@ describe "Dashboard > User sorts todos", feature: true do let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) } let(:label_3) { create(:label, title: 'label_3', project: project, priority: 3) } - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end context 'sort options' do let(:issue_1) { create(:issue, title: 'issue_1', project: project) } diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index bb4b2aed0e3..feb2fe8a7d1 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -333,29 +333,6 @@ describe 'Dashboard Todos', feature: true do end end - context 'User have large number of todos' do - before do - create_list(:todo, 101, :mentioned, user: user, project: project, target: issue, author: author) - - login_as(user) - visit dashboard_todos_path - end - - it 'shows 99+ for count >= 100 in notification' do - expect(page).to have_selector('.todos-count', text: '99+') - end - - it 'shows exact number in To do tab' do - expect(page).to have_selector('.todos-pending .badge', text: '101') - end - - it 'shows exact number for count < 100' do - 3.times { first('.js-done-todo').click } - - expect(page).to have_selector('.todos-count', text: '98') - end - end - context 'User has a Build Failed todo' do let!(:todo) { create(:todo, :build_failed, user: user, project: project, author: author) } diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb index c1ae6db00c6..2ea9992173d 100644 --- a/spec/features/triggers_spec.rb +++ b/spec/features/triggers_spec.rb @@ -5,7 +5,10 @@ feature 'Triggers', feature: true, js: true do let(:user) { create(:user) } let(:user2) { create(:user) } let(:guest_user) { create(:user) } - before { login_as(user) } + + before do + login_as(user) + end before do @project = create(:empty_project) diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb index 2fed8067042..dc21637967f 100644 --- a/spec/features/u2f_spec.rb +++ b/spec/features/u2f_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do - before { allow_any_instance_of(U2fHelper).to receive(:inject_u2f_api?).and_return(true) } + before do + allow_any_instance_of(U2fHelper).to receive(:inject_u2f_api?).and_return(true) + end def manage_two_factor_authentication click_on 'Manage two-factor authentication' @@ -28,7 +30,9 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do end describe 'when 2FA via OTP is disabled' do - before { user.update_attribute(:otp_required_for_login, false) } + before do + user.update_attribute(:otp_required_for_login, false) + end it 'does not allow registering a new device' do visit profile_account_path diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb index 8509551ce4a..0a8db15c75f 100644 --- a/spec/features/unsubscribe_links_spec.rb +++ b/spec/features/unsubscribe_links_spec.rb @@ -56,7 +56,9 @@ describe 'Unsubscribe links', feature: true do end context 'when logged in' do - before { login_as(recipient) } + before do + login_as(recipient) + end it 'unsubscribes from the issue when visiting the link from the email body' do visit body_link diff --git a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb index f88a515f7fc..d9d6f2e2382 100644 --- a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb +++ b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb @@ -18,7 +18,7 @@ feature 'User uploads avatar to group', feature: true do visit group_path(group) - expect(page).to have_selector(%Q(img[src$="/uploads/group/avatar/#{group.id}/dk.png"])) + expect(page).to have_selector(%Q(img[src$="/uploads/system/group/avatar/#{group.id}/dk.png"])) # Cheating here to verify something that isn't user-facing, but is important expect(group.reload.avatar.file).to exist diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb index 0dfd29045e5..eb8dbd76aab 100644 --- a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb +++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb @@ -16,7 +16,7 @@ feature 'User uploads avatar to profile', feature: true do visit user_path(user) - expect(page).to have_selector(%Q(img[src$="/uploads/user/avatar/#{user.id}/dk.png"])) + expect(page).to have_selector(%Q(img[src$="/uploads/system/user/avatar/#{user.id}/dk.png"])) # Cheating here to verify something that isn't user-facing, but is important expect(user.reload.avatar.file).to exist diff --git a/spec/features/user_can_display_performance_bar_spec.rb b/spec/features/user_can_display_performance_bar_spec.rb new file mode 100644 index 00000000000..c2842255b86 --- /dev/null +++ b/spec/features/user_can_display_performance_bar_spec.rb @@ -0,0 +1,81 @@ +require 'rails_helper' + +describe 'User can display performacne bar', :js do + shared_examples 'performance bar is disabled' do + it 'does not show the performance bar by default' do + expect(page).not_to have_css('#peek') + end + + context 'when user press `pb`' do + before do + find('body').native.send_keys('pb') + end + + it 'does not show the performance bar by default' do + expect(page).not_to have_css('#peek') + end + end + end + + shared_examples 'performance bar is enabled' do + it 'does not show the performance bar by default' do + expect(page).not_to have_css('#peek') + end + + context 'when user press `pb`' do + before do + find('body').native.send_keys('pb') + end + + it 'does not show the performance bar by default' do + expect(page).not_to have_css('#peek') + end + end + end + + context 'when user is logged-out' do + before do + visit root_path + end + + context 'when the gitlab_performance_bar feature is disabled' do + before do + Feature.disable('gitlab_performance_bar') + end + + it_behaves_like 'performance bar is disabled' + end + + context 'when the gitlab_performance_bar feature is enabled' do + before do + Feature.enable('gitlab_performance_bar') + end + + it_behaves_like 'performance bar is disabled' + end + end + + context 'when user is logged-in' do + before do + login_as :user + + visit root_path + end + + context 'when the gitlab_performance_bar feature is disabled' do + before do + Feature.disable('gitlab_performance_bar') + end + + it_behaves_like 'performance bar is disabled' + end + + context 'when the gitlab_performance_bar feature is enabled' do + before do + Feature.enable('gitlab_performance_bar') + end + + it_behaves_like 'performance bar is enabled' + end + end +end diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb index fbe078bd136..c241dae12cf 100644 --- a/spec/features/users_spec.rb +++ b/spec/features/users_spec.rb @@ -45,7 +45,9 @@ feature 'Users', feature: true, js: true do end describe 'redirect alias routes' do - before { user } + before do + expect(user).to be_persisted + end scenario '/u/user1 redirects to user page' do visit '/u/user1' diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 96151689359..8f2d60f2f1b 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -148,7 +148,9 @@ describe IssuesFinder do let(:params) { { label_name: [label.title, label2.title].join(',') } } let(:label2) { create(:label, project: project2) } - before { create(:label_link, label: label2, target: issue2) } + before do + create(:label_link, label: label2, target: issue2) + end it 'returns the unique issues with any of those labels' do expect(issues).to contain_exactly(issue2) diff --git a/spec/finders/personal_access_tokens_finder_spec.rb b/spec/finders/personal_access_tokens_finder_spec.rb index fd92664ca24..3f22b3a253d 100644 --- a/spec/finders/personal_access_tokens_finder_spec.rb +++ b/spec/finders/personal_access_tokens_finder_spec.rb @@ -25,49 +25,65 @@ describe PersonalAccessTokensFinder do end describe 'without impersonation' do - before { params[:impersonation] = false } + before do + params[:impersonation] = false + end it { is_expected.to contain_exactly(active_personal_access_token, revoked_personal_access_token, expired_personal_access_token) } describe 'with active state' do - before { params[:state] = 'active' } + before do + params[:state] = 'active' + end it { is_expected.to contain_exactly(active_personal_access_token) } end describe 'with inactive state' do - before { params[:state] = 'inactive' } + before do + params[:state] = 'inactive' + end it { is_expected.to contain_exactly(revoked_personal_access_token, expired_personal_access_token) } end end describe 'with impersonation' do - before { params[:impersonation] = true } + before do + params[:impersonation] = true + end it { is_expected.to contain_exactly(active_impersonation_token, revoked_impersonation_token, expired_impersonation_token) } describe 'with active state' do - before { params[:state] = 'active' } + before do + params[:state] = 'active' + end it { is_expected.to contain_exactly(active_impersonation_token) } end describe 'with inactive state' do - before { params[:state] = 'inactive' } + before do + params[:state] = 'inactive' + end it { is_expected.to contain_exactly(revoked_impersonation_token, expired_impersonation_token) } end end describe 'with active state' do - before { params[:state] = 'active' } + before do + params[:state] = 'active' + end it { is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token) } end describe 'with inactive state' do - before { params[:state] = 'inactive' } + before do + params[:state] = 'inactive' + end it do is_expected.to contain_exactly(expired_personal_access_token, revoked_personal_access_token, @@ -81,7 +97,9 @@ describe PersonalAccessTokensFinder do it { is_expected.to eq(active_personal_access_token) } describe 'with impersonation' do - before { params[:impersonation] = true } + before do + params[:impersonation] = true + end it { is_expected.to be_nil } end @@ -93,7 +111,9 @@ describe PersonalAccessTokensFinder do it { is_expected.to eq(active_personal_access_token) } describe 'with impersonation' do - before { params[:impersonation] = true } + before do + params[:impersonation] = true + end it { is_expected.to be_nil } end @@ -109,7 +129,9 @@ describe PersonalAccessTokensFinder do let!(:other_user_expired_impersonation_token) { create(:personal_access_token, :expired, :impersonation, user: user2) } let!(:other_user_revoked_impersonation_token) { create(:personal_access_token, :revoked, :impersonation, user: user2) } - before { params[:user] = user } + before do + params[:user] = user + end it do is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token, @@ -118,49 +140,65 @@ describe PersonalAccessTokensFinder do end describe 'without impersonation' do - before { params[:impersonation] = false } + before do + params[:impersonation] = false + end it { is_expected.to contain_exactly(active_personal_access_token, revoked_personal_access_token, expired_personal_access_token) } describe 'with active state' do - before { params[:state] = 'active' } + before do + params[:state] = 'active' + end it { is_expected.to contain_exactly(active_personal_access_token) } end describe 'with inactive state' do - before { params[:state] = 'inactive' } + before do + params[:state] = 'inactive' + end it { is_expected.to contain_exactly(revoked_personal_access_token, expired_personal_access_token) } end end describe 'with impersonation' do - before { params[:impersonation] = true } + before do + params[:impersonation] = true + end it { is_expected.to contain_exactly(active_impersonation_token, revoked_impersonation_token, expired_impersonation_token) } describe 'with active state' do - before { params[:state] = 'active' } + before do + params[:state] = 'active' + end it { is_expected.to contain_exactly(active_impersonation_token) } end describe 'with inactive state' do - before { params[:state] = 'inactive' } + before do + params[:state] = 'inactive' + end it { is_expected.to contain_exactly(revoked_impersonation_token, expired_impersonation_token) } end end describe 'with active state' do - before { params[:state] = 'active' } + before do + params[:state] = 'active' + end it { is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token) } end describe 'with inactive state' do - before { params[:state] = 'inactive' } + before do + params[:state] = 'inactive' + end it do is_expected.to contain_exactly(expired_personal_access_token, revoked_personal_access_token, @@ -174,7 +212,9 @@ describe PersonalAccessTokensFinder do it { is_expected.to eq(active_personal_access_token) } describe 'with impersonation' do - before { params[:impersonation] = true } + before do + params[:impersonation] = true + end it { is_expected.to be_nil } end @@ -186,7 +226,9 @@ describe PersonalAccessTokensFinder do it { is_expected.to eq(active_personal_access_token) } describe 'with impersonation' do - before { params[:impersonation] = true } + before do + params[:impersonation] = true + end it { is_expected.to be_nil } end diff --git a/spec/finders/personal_projects_finder_spec.rb b/spec/finders/personal_projects_finder_spec.rb index e0e17af681a..304b0fb67fb 100644 --- a/spec/finders/personal_projects_finder_spec.rb +++ b/spec/finders/personal_projects_finder_spec.rb @@ -32,7 +32,9 @@ describe PersonalProjectsFinder do end context 'external' do - before { current_user.update_attributes(external: true) } + before do + current_user.update_attributes(external: true) + end it { is_expected.to eq([private_project, public_project]) } end diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb index f2aeda241c1..2b19cda35b0 100644 --- a/spec/finders/pipelines_finder_spec.rb +++ b/spec/finders/pipelines_finder_spec.rb @@ -170,7 +170,7 @@ describe PipelinesFinder do context 'when order_by and sort are specified' do context 'when order_by user_id' do let(:params) { { order_by: 'user_id', sort: 'asc' } } - let!(:pipelines) { create_list(:ci_pipeline, 2, project: project, user: create(:user)) } + let!(:pipelines) { Array.new(2) { create(:ci_pipeline, project: project, user: create(:user)) } } it 'sorts as user_id: :asc' do is_expected.to match_array(pipelines) diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb index f7e7e733cf7..8be447418b0 100644 --- a/spec/finders/todos_finder_spec.rb +++ b/spec/finders/todos_finder_spec.rb @@ -6,7 +6,9 @@ describe TodosFinder do let(:project) { create(:empty_project) } let(:finder) { described_class } - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end describe '#sort' do context 'by date' do diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json index 11a4caf6628..622a1e40d07 100644 --- a/spec/fixtures/api/schemas/list.json +++ b/spec/fixtures/api/schemas/list.json @@ -10,7 +10,7 @@ "id": { "type": "integer" }, "list_type": { "type": "string", - "enum": ["label", "closed"] + "enum": ["backlog", "label", "closed"] }, "label": { "type": ["object", "null"], diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 785fb724132..cc7f889b927 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -1,3 +1,4 @@ +# coding: utf-8 require 'spec_helper' describe ApplicationHelper do @@ -58,13 +59,13 @@ describe ApplicationHelper do describe 'project_icon' do it 'returns an url for the avatar' do project = create(:empty_project, avatar: File.open(uploaded_image_temp_path)) - avatar_url = "/uploads/project/avatar/#{project.id}/banana_sample.gif" + avatar_url = "/uploads/system/project/avatar/#{project.id}/banana_sample.gif" expect(helper.project_icon(project.full_path).to_s). to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />" allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host) - avatar_url = "#{gitlab_host}/uploads/project/avatar/#{project.id}/banana_sample.gif" + avatar_url = "#{gitlab_host}/uploads/system/project/avatar/#{project.id}/banana_sample.gif" expect(helper.project_icon(project.full_path).to_s). to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />" @@ -84,12 +85,12 @@ describe ApplicationHelper do it 'returns an url for the avatar' do user = create(:user, avatar: File.open(uploaded_image_temp_path)) - avatar_url = "/uploads/user/avatar/#{user.id}/banana_sample.gif" + avatar_url = "/uploads/system/user/avatar/#{user.id}/banana_sample.gif" expect(helper.avatar_icon(user.email).to_s).to match(avatar_url) allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host) - avatar_url = "#{gitlab_host}/uploads/user/avatar/#{user.id}/banana_sample.gif" + avatar_url = "#{gitlab_host}/uploads/system/user/avatar/#{user.id}/banana_sample.gif" expect(helper.avatar_icon(user.email).to_s).to match(avatar_url) end @@ -102,7 +103,7 @@ describe ApplicationHelper do user = create(:user, avatar: File.open(uploaded_image_temp_path)) expect(helper.avatar_icon(user.email).to_s). - to match("/gitlab/uploads/user/avatar/#{user.id}/banana_sample.gif") + to match("/gitlab/uploads/system/user/avatar/#{user.id}/banana_sample.gif") end it 'calls gravatar_icon when no User exists with the given email' do @@ -116,7 +117,7 @@ describe ApplicationHelper do user = create(:user, avatar: File.open(uploaded_image_temp_path)) expect(helper.avatar_icon(user).to_s). - to match("/uploads/user/avatar/#{user.id}/banana_sample.gif") + to match("/uploads/system/user/avatar/#{user.id}/banana_sample.gif") end end end @@ -256,4 +257,24 @@ describe ApplicationHelper do it { expect(helper.active_when(true)).to eq('active') } it { expect(helper.active_when(false)).to eq(nil) } end + + describe '#support_url' do + context 'when alternate support url is specified' do + let(:alternate_url) { 'http://company.example.com/getting-help' } + + before do + allow(current_application_settings).to receive(:help_page_support_url) { alternate_url } + end + + it 'returns the alternate support url' do + expect(helper.support_url).to eq(alternate_url) + end + end + + context 'when alternate support url is not specified' do + it 'builds the support url from the promo_url' do + expect(helper.support_url).to eq(helper.promo_url + '/getting-help/') + end + end + end end diff --git a/spec/helpers/blame_helper_spec.rb b/spec/helpers/blame_helper_spec.rb new file mode 100644 index 00000000000..b4368516d83 --- /dev/null +++ b/spec/helpers/blame_helper_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe BlameHelper do + describe '#get_age_map_start_date' do + let(:dates) do + [Time.zone.local(2014, 3, 17, 0, 0, 0), + Time.zone.local(2011, 11, 2, 0, 0, 0), + Time.zone.local(2015, 7, 9, 0, 0, 0), + Time.zone.local(2013, 2, 24, 0, 0, 0), + Time.zone.local(2010, 9, 22, 0, 0, 0)] + end + let(:blame_groups) do + [ + { commit: double(committed_date: dates[0]) }, + { commit: double(committed_date: dates[1]) }, + { commit: double(committed_date: dates[2]) } + ] + end + + it 'returns the earliest date from a blame group' do + project = double(created_at: dates[3]) + + duration = helper.age_map_duration(blame_groups, project) + + expect(duration[:started_days_ago]).to eq((duration[:now] - dates[1]).to_i / 1.day) + end + + it 'returns the earliest date from a project' do + project = double(created_at: dates[4]) + + duration = helper.age_map_duration(blame_groups, project) + + expect(duration[:started_days_ago]).to eq((duration[:now] - dates[4]).to_i / 1.day) + end + end + + describe '#age_map_class' do + let(:dates) do + [Time.zone.local(2014, 3, 17, 0, 0, 0)] + end + let(:blame_groups) do + [ + { commit: double(committed_date: dates[0]) } + ] + end + let(:duration) do + project = double(created_at: dates[0]) + helper.age_map_duration(blame_groups, project) + end + + it 'returns blame-commit-age-9 when oldest' do + expect(helper.age_map_class(dates[0], duration)).to eq 'blame-commit-age-9' + end + + it 'returns blame-commit-age-0 class when newest' do + expect(helper.age_map_class(duration[:now], duration)).to eq 'blame-commit-age-0' + end + end +end diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index a74615e07f9..0ac030d3171 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -8,7 +8,7 @@ describe DiffHelper do let(:commit) { project.commit(sample_commit.id) } let(:diffs) { commit.raw_diffs } let(:diff) { diffs.first } - let(:diff_refs) { [commit.parent, commit] } + let(:diff_refs) { commit.diff_refs } let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) } describe 'diff_view' do @@ -207,4 +207,41 @@ describe DiffHelper do expect(output).not_to have_css 'td:nth-child(3)' end end + + context 'viewer related' do + let(:viewer) { diff_file.simple_viewer } + + before do + assign(:project, project) + end + + describe '#diff_render_error_reason' do + context 'for error :too_large' do + before do + expect(viewer).to receive(:render_error).and_return(:too_large) + end + + it 'returns an error message' do + expect(helper.diff_render_error_reason(viewer)).to eq('it is too large') + end + end + + context 'for error :server_side_but_stored_externally' do + before do + expect(viewer).to receive(:render_error).and_return(:server_side_but_stored_externally) + expect(diff_file).to receive(:external_storage).and_return(:lfs) + end + + it 'returns an error message' do + expect(helper.diff_render_error_reason(viewer)).to eq('it is stored in LFS') + end + end + end + + describe '#diff_render_error_options' do + it 'includes a "view the blob" link' do + expect(helper.diff_render_error_options(viewer)).to include(/view the blob/) + end + end + end end diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb index cd112dbb2fb..c68e4f56b05 100644 --- a/spec/helpers/emails_helper_spec.rb +++ b/spec/helpers/emails_helper_spec.rb @@ -52,7 +52,7 @@ describe EmailsHelper do ) expect(header_logo).to eq( - %{<img style="height: 50px" src="/uploads/appearance/header_logo/#{appearance.id}/dk.png" alt="Dk" />} + %{<img style="height: 50px" src="/uploads/system/appearance/header_logo/#{appearance.id}/dk.png" alt="Dk" />} ) end end diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index c8b0d86425f..0337afa4452 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -9,7 +9,7 @@ describe GroupsHelper do group.avatar = fixture_file_upload(avatar_file_path) group.save! expect(group_icon(group.path).to_s). - to match("/uploads/group/avatar/#{group.id}/banana_sample.gif") + to match("/uploads/system/group/avatar/#{group.id}/banana_sample.gif") end it 'gives default avatar_icon when no avatar is present' do diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb index 355a4845afb..cc861af8533 100644 --- a/spec/helpers/notes_helper_spec.rb +++ b/spec/helpers/notes_helper_spec.rb @@ -256,4 +256,14 @@ describe NotesHelper do expect(helper.form_resources).to eq([@project.namespace, @project, @note]) end end + + describe '#noteable_note_url' do + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + let(:note) { create(:note_on_issue, noteable: issue, project: project) } + + it 'returns the noteable url with an anchor to the note' do + expect(noteable_note_url(note)).to match("/#{project.namespace.path}/#{project.path}/issues/#{issue.iid}##{dom_id(note)}") + end + end end diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb index 9d5f009ebe1..9ecaabc04ed 100644 --- a/spec/helpers/notifications_helper_spec.rb +++ b/spec/helpers/notifications_helper_spec.rb @@ -12,5 +12,11 @@ describe NotificationsHelper do describe 'notification_title' do it { expect(notification_title(:watch)).to match('Watch') } it { expect(notification_title(:mention)).to match('On mention') } + it { expect(notification_title(:global)).to match('Global') } + end + + describe '#notification_event_name' do + it { expect(notification_event_name(:success_pipeline)).to match('Successful pipeline') } + it { expect(notification_event_name(:failed_pipeline)).to match('Failed pipeline') } end end diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb index 2cc0b40b2d0..dff2784f21f 100644 --- a/spec/helpers/page_layout_helper_spec.rb +++ b/spec/helpers/page_layout_helper_spec.rb @@ -60,7 +60,7 @@ describe PageLayoutHelper do %w(project user group).each do |type| context "with @#{type} assigned" do it "uses #{type.titlecase} avatar if available" do - object = double(avatar_url: 'http://example.com/uploads/avatar.png') + object = double(avatar_url: 'http://example.com/uploads/system/avatar.png') assign(type, object) expect(helper.page_image).to eq object.avatar_url diff --git a/spec/helpers/profiles_helper_spec.rb b/spec/helpers/profiles_helper_spec.rb new file mode 100644 index 00000000000..b33b3f3a228 --- /dev/null +++ b/spec/helpers/profiles_helper_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +describe ProfilesHelper do + describe '#email_provider_label' do + it "returns nil for users without external email" do + user = create(:user) + allow(helper).to receive(:current_user).and_return(user) + + expect(helper.email_provider_label).to be_nil + end + + it "returns omniauth provider label for users with external email" do + stub_cas_omniauth_provider + cas_user = create(:omniauth_user, provider: 'cas3', external_email: true, email_provider: 'cas3') + allow(helper).to receive(:current_user).and_return(cas_user) + + expect(helper.email_provider_label).to eq('CAS') + end + + it "returns 'LDAP' for users with external email but no email provider" do + ldap_user = create(:omniauth_user, external_email: true) + allow(helper).to receive(:current_user).and_return(ldap_user) + + expect(helper.email_provider_label).to eq('LDAP') + end + end + + def stub_cas_omniauth_provider + provider = OpenStruct.new( + 'name' => 'cas3', + 'label' => 'CAS' + ) + + stub_omniauth_setting(providers: [provider]) + end +end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index a695621b87a..9a4086725d2 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -250,7 +250,9 @@ describe ProjectsHelper do end context "when project is private" do - before { project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE) } + before do + project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end it "shows only allowed options" do helper.instance_variable_set(:@project, project) @@ -300,4 +302,37 @@ describe ProjectsHelper do expect(helper.send(:visibility_select_options, project, Gitlab::VisibilityLevel::PRIVATE)).to include('Private') end end + + describe '#get_project_nav_tabs' do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + before do + allow(helper).to receive(:can?) { true } + end + + subject do + helper.send(:get_project_nav_tabs, project, user) + end + + context 'when builds feature is enabled' do + before do + allow(project).to receive(:builds_enabled?).and_return(true) + end + + it "does include pipelines tab" do + is_expected.to include(:pipelines) + end + end + + context 'when builds feature is disabled' do + before do + allow(project).to receive(:builds_enabled?).and_return(false) + end + + it "do not include pipelines tab" do + is_expected.not_to include(:pipelines) + end + end + end end diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb index 50060a0925d..18a41ca24e3 100644 --- a/spec/helpers/todos_helper_spec.rb +++ b/spec/helpers/todos_helper_spec.rb @@ -1,6 +1,19 @@ require "spec_helper" describe TodosHelper do + describe '#todos_count_format' do + it 'shows fuzzy count for 100 or more items' do + expect(helper.todos_count_format(100)).to eq '99+' + expect(helper.todos_count_format(1000)).to eq '99+' + end + + it 'shows exact count for 99 or fewer items' do + expect(helper.todos_count_format(99)).to eq '99' + expect(helper.todos_count_format(50)).to eq '50' + expect(helper.todos_count_format(1)).to eq '1' + end + end + describe '#todo_projects_options' do let(:projects) { create_list(:empty_project, 3) } let(:user) { create(:user) } diff --git a/spec/helpers/u2f_helper_spec.rb b/spec/helpers/u2f_helper_spec.rb new file mode 100644 index 00000000000..0d65b4fe0b8 --- /dev/null +++ b/spec/helpers/u2f_helper_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe U2fHelper do + describe 'when not on mobile' do + it 'does not inject u2f on chrome 40' do + device = double(mobile?: false) + browser = double(chrome?: true, opera?: false, version: 40, device: device) + allow(helper).to receive(:browser).and_return(browser) + expect(helper.inject_u2f_api?).to eq false + end + + it 'injects u2f on chrome 41' do + device = double(mobile?: false) + browser = double(chrome?: true, opera?: false, version: 41, device: device) + allow(helper).to receive(:browser).and_return(browser) + expect(helper.inject_u2f_api?).to eq true + end + + it 'does not inject u2f on opera 39' do + device = double(mobile?: false) + browser = double(chrome?: false, opera?: true, version: 39, device: device) + allow(helper).to receive(:browser).and_return(browser) + expect(helper.inject_u2f_api?).to eq false + end + + it 'injects u2f on opera 40' do + device = double(mobile?: false) + browser = double(chrome?: false, opera?: true, version: 40, device: device) + allow(helper).to receive(:browser).and_return(browser) + expect(helper.inject_u2f_api?).to eq true + end + end + + describe 'when on mobile' do + it 'does not inject u2f on chrome 41' do + device = double(mobile?: true) + browser = double(chrome?: true, opera?: false, version: 41, device: device) + allow(helper).to receive(:browser).and_return(browser) + expect(helper.inject_u2f_api?).to eq false + end + + it 'does not inject u2f on opera 40' do + device = double(mobile?: true) + browser = double(chrome?: false, opera?: true, version: 40, device: device) + allow(helper).to receive(:browser).and_return(browser) + expect(helper.inject_u2f_api?).to eq false + end + end +end diff --git a/spec/initializers/8_metrics_spec.rb b/spec/initializers/8_metrics_spec.rb index 570754621f3..a507d7f7f2b 100644 --- a/spec/initializers/8_metrics_spec.rb +++ b/spec/initializers/8_metrics_spec.rb @@ -7,6 +7,7 @@ describe 'instrument_classes', lib: true do before do allow(config).to receive(:instrument_method) allow(config).to receive(:instrument_methods) + allow(config).to receive(:instrument_instance_method) allow(config).to receive(:instrument_instance_methods) end diff --git a/spec/javascripts/blob/create_branch_dropdown_spec.js b/spec/javascripts/blob/create_branch_dropdown_spec.js deleted file mode 100644 index 6dbaa47c544..00000000000 --- a/spec/javascripts/blob/create_branch_dropdown_spec.js +++ /dev/null @@ -1,106 +0,0 @@ -import '~/gl_dropdown'; -import '~/blob/create_branch_dropdown'; -import '~/blob/target_branch_dropdown'; - -describe('CreateBranchDropdown', () => { - const fixtureTemplate = 'static/target_branch_dropdown.html.raw'; - // selectors - const createBranchSel = '.js-new-branch-btn'; - const backBtnSel = '.dropdown-menu-back'; - const cancelBtnSel = '.js-cancel-branch-btn'; - const branchNameSel = '#new_branch_name'; - const branchName = 'new_name'; - let dropdown; - - function createDropdown() { - const dropdownEl = document.querySelector('.js-project-branches-dropdown'); - const projectBranches = getJSONFixture('project_branches.json'); - dropdown = new gl.TargetBranchDropDown(dropdownEl); - dropdown.cachedRefs = projectBranches; - return dropdown; - } - - function createBranchBtn() { - return document.querySelector(createBranchSel); - } - - function backBtn() { - return document.querySelector(backBtnSel); - } - - function cancelBtn() { - return document.querySelector(cancelBtnSel); - } - - function branchNameEl() { - return document.querySelector(branchNameSel); - } - - function changeBranchName(text) { - branchNameEl().value = text; - branchNameEl().dispatchEvent(new Event('change')); - } - - preloadFixtures(fixtureTemplate); - - beforeEach(() => { - loadFixtures(fixtureTemplate); - createDropdown(); - }); - - it('disable submit when branch name is empty', () => { - expect(createBranchBtn()).toBeDisabled(); - }); - - it('enable submit when branch name is present', () => { - changeBranchName(branchName); - - expect(createBranchBtn()).not.toBeDisabled(); - }); - - it('resets the form when cancel btn is clicked and triggers dropdownback', () => { - const spyBackEvent = spyOnEvent(backBtnSel, 'click'); - changeBranchName(branchName); - - cancelBtn().click(); - - expect(branchNameEl()).toHaveValue(''); - expect(spyBackEvent).toHaveBeenTriggered(); - }); - - it('resets the form when back btn is clicked', () => { - changeBranchName(branchName); - - backBtn().click(); - - expect(branchNameEl()).toHaveValue(''); - }); - - describe('new branch creation', () => { - beforeEach(() => { - changeBranchName(branchName); - }); - it('sets the new branch name and updates the dropdown', () => { - spyOn(dropdown, 'setNewBranch'); - - createBranchBtn().click(); - - expect(dropdown.setNewBranch).toHaveBeenCalledWith(branchName); - }); - - it('resets the form', () => { - createBranchBtn().click(); - - expect(branchNameEl()).toHaveValue(''); - }); - - it('is triggered with enter keypress', () => { - spyOn(dropdown, 'setNewBranch'); - const enterEvent = new Event('keydown'); - enterEvent.which = 13; - branchNameEl().dispatchEvent(enterEvent); - - expect(dropdown.setNewBranch).toHaveBeenCalledWith(branchName); - }); - }); -}); diff --git a/spec/javascripts/blob/target_branch_dropdown_spec.js b/spec/javascripts/blob/target_branch_dropdown_spec.js deleted file mode 100644 index 99c9537d2ec..00000000000 --- a/spec/javascripts/blob/target_branch_dropdown_spec.js +++ /dev/null @@ -1,118 +0,0 @@ -import '~/gl_dropdown'; -import '~/blob/create_branch_dropdown'; -import '~/blob/target_branch_dropdown'; - -describe('TargetBranchDropdown', () => { - const fixtureTemplate = 'static/target_branch_dropdown.html.raw'; - let dropdown; - - function createDropdown() { - const projectBranches = getJSONFixture('project_branches.json'); - const dropdownEl = document.querySelector('.js-project-branches-dropdown'); - dropdown = new gl.TargetBranchDropDown(dropdownEl); - dropdown.cachedRefs = projectBranches; - dropdown.refreshData(); - return dropdown; - } - - function submitBtn() { - return document.querySelector('button[type="submit"]'); - } - - function searchField() { - return document.querySelector('.dropdown-page-one .dropdown-input-field'); - } - - function element() { - return document.querySelectorAll('div.dropdown-content li a'); - } - - function elementAtIndex(index) { - return element()[index]; - } - - function clickElementAtIndex(index) { - elementAtIndex(index).click(); - } - - preloadFixtures(fixtureTemplate); - - beforeEach(() => { - loadFixtures(fixtureTemplate); - createDropdown(); - }); - - it('disable submit when branch is not selected', () => { - document.querySelector('input[name="target_branch"]').value = null; - clickElementAtIndex(1); - - expect(submitBtn().getAttribute('disabled')).toEqual(''); - }); - - it('enable submit when a branch is selected', () => { - clickElementAtIndex(1); - - expect(submitBtn().getAttribute('disabled')).toBe(null); - }); - - it('triggers change.branch event on a branch click', () => { - spyOnEvent(dropdown.$dropdown, 'change.branch'); - clickElementAtIndex(0); - - expect('change.branch').toHaveBeenTriggeredOn(dropdown.$dropdown); - }); - - describe('dropdownData', () => { - it('cache the refs', () => { - const refs = dropdown.cachedRefs; - dropdown.cachedRefs = null; - - dropdown.dropdownData(refs); - - expect(dropdown.cachedRefs).toEqual(refs); - }); - - it('returns the Branches with the newBranch and defaultBranch', () => { - const refs = dropdown.cachedRefs; - dropdown.branchInput.value = 'master'; - dropdown.newBranch = { id: 'new_branch', text: 'new_branch', title: 'new_branch' }; - - const branches = dropdown.dropdownData(refs).Branches; - - expect(branches.length).toEqual(4); - expect(branches[0]).toEqual(dropdown.newBranch); - expect(branches[1]).toEqual({ id: 'master', text: 'master', title: 'master' }); - expect(branches[2]).toEqual({ id: 'development', text: 'development', title: 'development' }); - expect(branches[3]).toEqual({ id: 'staging', text: 'staging', title: 'staging' }); - }); - }); - - describe('setNewBranch', () => { - it('adds the new branch and select it', () => { - const branchName = 'new_branch'; - - dropdown.setNewBranch(branchName); - - expect(elementAtIndex(0)).toHaveClass('is-active'); - expect(elementAtIndex(0)).toContainHtml(branchName); - }); - - it("doesn't add a new branch if already exists in the list", () => { - const branchName = elementAtIndex(0).text; - const initialLength = element().length; - - dropdown.setNewBranch(branchName); - - expect(element().length).toEqual(initialLength); - }); - - it('clears the search filter', () => { - const branchName = elementAtIndex(0).text; - searchField().value = 'searching'; - - dropdown.setNewBranch(branchName); - - expect(searchField().value).toEqual(''); - }); - }); -}); diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js new file mode 100644 index 00000000000..c4e8966ad6c --- /dev/null +++ b/spec/javascripts/boards/components/board_spec.js @@ -0,0 +1,112 @@ +import Vue from 'vue'; +import '~/boards/services/board_service'; +import '~/boards/components/board'; +import '~/boards/models/list'; + +describe('Board component', () => { + let vm; + let el; + + beforeEach((done) => { + loadFixtures('boards/show.html.raw'); + + el = document.createElement('div'); + document.body.appendChild(el); + + // eslint-disable-next-line no-undef + gl.boardService = new BoardService('/', '/', 1); + + vm = new gl.issueBoards.Board({ + propsData: { + boardId: '1', + disabled: false, + issueLinkBase: '/', + rootPath: '/', + // eslint-disable-next-line no-undef + list: new List({ + id: 1, + position: 0, + title: 'test', + list_type: 'backlog', + }), + }, + }).$mount(el); + + Vue.nextTick(done); + }); + + afterEach(() => { + vm.$destroy(); + + // remove the component from the DOM + document.querySelector('.board').remove(); + + localStorage.removeItem(`boards.${vm.boardId}.${vm.list.type}.expanded`); + }); + + it('board is expandable when list type is backlog', () => { + expect( + vm.$el.classList.contains('is-expandable'), + ).toBe(true); + }); + + it('board is expandable when list type is closed', (done) => { + vm.list.type = 'closed'; + + Vue.nextTick(() => { + expect( + vm.$el.classList.contains('is-expandable'), + ).toBe(true); + + done(); + }); + }); + + it('board is not expandable when list type is label', (done) => { + vm.list.type = 'label'; + vm.list.isExpandable = false; + + Vue.nextTick(() => { + expect( + vm.$el.classList.contains('is-expandable'), + ).toBe(false); + + done(); + }); + }); + + it('collapses when clicking header', (done) => { + vm.$el.querySelector('.board-header').click(); + + Vue.nextTick(() => { + expect( + vm.$el.classList.contains('is-collapsed'), + ).toBe(true); + + done(); + }); + }); + + it('created sets isExpanded to true from localStorage', (done) => { + vm.$el.querySelector('.board-header').click(); + + return Vue.nextTick() + .then(() => { + expect( + vm.$el.classList.contains('is-collapsed'), + ).toBe(true); + + // call created manually + vm.$options.created[0].call(vm); + + return Vue.nextTick(); + }) + .then(() => { + expect( + vm.$el.classList.contains('is-collapsed'), + ).toBe(true); + + done(); + }); + }); +}); diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js b/spec/javascripts/bootstrap_linked_tabs_spec.js index a27dc48b3fd..93dc60d59fe 100644 --- a/spec/javascripts/bootstrap_linked_tabs_spec.js +++ b/spec/javascripts/bootstrap_linked_tabs_spec.js @@ -1,15 +1,6 @@ import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs'; (() => { - // TODO: remove this hack! - // PhantomJS causes spyOn to panic because replaceState isn't "writable" - let phantomjs; - try { - phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable; - } catch (err) { - phantomjs = false; - } - describe('Linked Tabs', () => { preloadFixtures('static/linked_tabs.html.raw'); @@ -19,9 +10,7 @@ import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs'; describe('when is initialized', () => { beforeEach(() => { - if (!phantomjs) { - spyOn(window.history, 'replaceState').and.callFake(function () {}); - } + spyOn(window.history, 'replaceState').and.callFake(function () {}); }); it('should activate the tab correspondent to the given action', () => { @@ -47,7 +36,7 @@ import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs'; describe('on click', () => { it('should change the url according to the clicked tab', () => { - const historySpy = !phantomjs && spyOn(history, 'replaceState').and.callFake(() => {}); + const historySpy = spyOn(history, 'replaceState').and.callFake(() => {}); const linkedTabs = new LinkedTabs({ action: 'show', diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js index 461908f3fde..be90dbdd88a 100644 --- a/spec/javascripts/build_spec.js +++ b/spec/javascripts/build_spec.js @@ -58,7 +58,7 @@ describe('Build', () => { it('displays the remove date correctly', () => { const removeDateElement = document.querySelector('.js-artifacts-remove'); - expect(removeDateElement.innerText.trim()).toBe('1 year'); + expect(removeDateElement.innerText.trim()).toBe('1 year remaining'); }); }); @@ -132,23 +132,6 @@ describe('Build', () => { expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/); expect($('#build-trace .js-build-output').text()).toMatch(/Different/); }); - - it('reloads the page when the build is done', () => { - spyOn(gl.utils, 'visitUrl'); - const deferred = $.Deferred(); - - spyOn($, 'ajax').and.returnValue(deferred.promise()); - deferred.resolve({ - html: '<span>Final</span>', - status: 'passed', - append: true, - complete: true, - }); - - this.build = new Build(); - - expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL); - }); }); describe('truncated information', () => { diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js index 44a4386b250..ace95000468 100644 --- a/spec/javascripts/commits_spec.js +++ b/spec/javascripts/commits_spec.js @@ -5,15 +5,6 @@ import '~/pager'; import '~/commits'; (() => { - // TODO: remove this hack! - // PhantomJS causes spyOn to panic because replaceState isn't "writable" - let phantomjs; - try { - phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable; - } catch (err) { - phantomjs = false; - } - describe('Commits List', () => { beforeEach(() => { setFixtures(` @@ -61,9 +52,7 @@ import '~/commits'; CommitsList.init(25); CommitsList.searchField.val(''); - if (!phantomjs) { - spyOn(history, 'replaceState').and.stub(); - } + spyOn(history, 'replaceState').and.stub(); ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => { req.success({ data: '<li>Result</li>', diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js index e347c980c78..e54ea11b08c 100644 --- a/spec/javascripts/datetime_utility_spec.js +++ b/spec/javascripts/datetime_utility_spec.js @@ -1,7 +1,27 @@ -import '~/lib/utils/datetime_utility'; +import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; (() => { describe('Date time utils', () => { + describe('timeFor', () => { + it('returns `past due` when in past', () => { + const date = new Date(); + date.setFullYear(date.getFullYear() - 1); + + expect( + gl.utils.timeFor(date), + ).toBe('Past due'); + }); + + it('returns remaining time when in the future', () => { + const date = new Date(); + date.setFullYear(date.getFullYear() + 1); + + expect( + gl.utils.timeFor(date), + ).toBe('1 year remaining'); + }); + }); + describe('get day name', () => { it('should return Sunday', () => { const day = gl.utils.getDayName(new Date('07/17/2016')); @@ -62,4 +82,13 @@ import '~/lib/utils/datetime_utility'; }); }); }); + + describe('timeIntervalInWords', () => { + it('should return string with number of minutes and seconds', () => { + expect(timeIntervalInWords(9.54)).toEqual('9 seconds'); + expect(timeIntervalInWords(1)).toEqual('1 second'); + expect(timeIntervalInWords(200)).toEqual('3 minutes 20 seconds'); + expect(timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds'); + }); + }); })(); diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js index 793ab8c451d..a4b98f6140d 100644 --- a/spec/javascripts/deploy_keys/components/key_spec.js +++ b/spec/javascripts/deploy_keys/components/key_spec.js @@ -39,9 +39,15 @@ describe('Deploy keys key', () => { ).toBe(`created ${gl.utils.getTimeago().format(deployKey.created_at)}`); }); + it('shows edit button', () => { + expect( + vm.$el.querySelectorAll('.btn')[0].textContent.trim(), + ).toBe('Edit'); + }); + it('shows remove button', () => { expect( - vm.$el.querySelector('.btn').textContent.trim(), + vm.$el.querySelectorAll('.btn')[1].textContent.trim(), ).toBe('Remove'); }); @@ -71,9 +77,15 @@ describe('Deploy keys key', () => { setTimeout(done); }); + it('shows edit button', () => { + expect( + vm.$el.querySelectorAll('.btn')[0].textContent.trim(), + ).toBe('Edit'); + }); + it('shows enable button', () => { expect( - vm.$el.querySelector('.btn').textContent.trim(), + vm.$el.querySelectorAll('.btn')[1].textContent.trim(), ).toBe('Enable'); }); @@ -82,7 +94,7 @@ describe('Deploy keys key', () => { Vue.nextTick(() => { expect( - vm.$el.querySelector('.btn').textContent.trim(), + vm.$el.querySelectorAll('.btn')[1].textContent.trim(), ).toBe('Disable'); done(); diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 9c8629ef9f0..6d00d71f145 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -97,6 +97,49 @@ describe('Filtered Search Manager', () => { }); }); + describe('searchState', () => { + beforeEach(() => { + spyOn(gl.FilteredSearchManager.prototype, 'search').and.callFake(() => {}); + }); + + it('should blur button', () => { + const e = { + currentTarget: { + blur: () => {}, + }, + }; + spyOn(e.currentTarget, 'blur').and.callThrough(); + manager.searchState(e); + + expect(e.currentTarget.blur).toHaveBeenCalled(); + }); + + it('should not call search if there is no state', () => { + const e = { + currentTarget: { + blur: () => {}, + }, + }; + + manager.searchState(e); + expect(gl.FilteredSearchManager.prototype.search).not.toHaveBeenCalled(); + }); + + it('should call search when there is state', () => { + const e = { + currentTarget: { + blur: () => {}, + dataset: { + state: 'opened', + }, + }, + }; + + manager.searchState(e); + expect(gl.FilteredSearchManager.prototype.search).toHaveBeenCalledWith('opened'); + }); + }); + describe('search', () => { const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened'; diff --git a/spec/javascripts/fixtures/boards.rb b/spec/javascripts/fixtures/boards.rb new file mode 100644 index 00000000000..d7c3dc0a235 --- /dev/null +++ b/spec/javascripts/fixtures/boards.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Projects::BoardsController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, :repository, namespace: namespace, path: 'boards-project') } + + render_views + + before(:all) do + clean_frontend_fixtures('boards/') + end + + before(:each) do + sign_in(admin) + end + + it 'boards/show.html.raw' do |example| + get(:index, + namespace_id: project.namespace, + project_id: project) + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/fixtures/project_branches.json b/spec/javascripts/fixtures/project_branches.json deleted file mode 100644 index a96a4c0c095..00000000000 --- a/spec/javascripts/fixtures/project_branches.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "master", - "development", - "staging" -] diff --git a/spec/javascripts/fixtures/target_branch_dropdown.html.haml b/spec/javascripts/fixtures/target_branch_dropdown.html.haml deleted file mode 100644 index 821fb7940a0..00000000000 --- a/spec/javascripts/fixtures/target_branch_dropdown.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -%form.js-edit-blob-form - %input{type: 'hidden', name: 'target_branch', value: 'master'} - %div - .dropdown - %button.dropdown-menu-toggle.js-project-branches-dropdown.js-target-branch{type: 'button', data: {toggle: 'dropdown', selected: 'master', field_name: 'target_branch', form_id: '.js-edit-blob-form'}} - .dropdown-menu.dropdown-menu-selectable.dropdown-menu-paging - .dropdown-page-one - .dropdown-title 'Select branch' - .dropdown-input - %input.dropdown-input-field{type: 'search', value: ''} - %i.fa.fa-search.dropdown-input-search - %i.fa.fa-times-dropdown-input-clear.js-dropdown-input-clear{role: 'button'} - .dropdown-content - .dropdown-footer - %ul.dropdown-footer-list - %li - %a.create-new-branch.dropdown-toggle-page{href: "#"} - Create new branch - .dropdown-page-two.dropdown-new-branch - %button.dropdown-title-button.dropdown-menu-back{type: 'button'} - .dropdown_title 'Create new branch' - .dropdown_content - %input#new_branch_name.default-dropdown-input{ type: "text", placeholder: "Name new branch" } - %button.btn.btn-primary.pull-left.js-new-branch-btn{ type: "button" } - Create - %button.btn.btn-default.pull-right.js-cancel-branch-btn{ type: "button" } - Cancel - %button{type: 'submit'} diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js index 3292590b9ed..10fcc590c89 100644 --- a/spec/javascripts/gl_dropdown_spec.js +++ b/spec/javascripts/gl_dropdown_spec.js @@ -185,7 +185,7 @@ import '~/lib/utils/url_utility'; expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); }); - it('should focus on input when opening for the second time', () => { + it('should focus on input when opening for the second time after transition', () => { remoteCallback(); this.dropdownContainerElement.trigger({ type: 'keyup', @@ -193,6 +193,7 @@ import '~/lib/utils/url_utility'; keyCode: ARROW_KEYS.ESC }); this.dropdownButtonElement.click(); + this.dropdownContainerElement.trigger('transitionend'); expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); }); }); @@ -201,6 +202,7 @@ import '~/lib/utils/url_utility'; it('should focus input when passing array data to drop down', () => { initDropDown.call(this, false, true); this.dropdownButtonElement.click(); + this.dropdownContainerElement.trigger('transitionend'); expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); }); }); diff --git a/spec/javascripts/gl_emoji_spec.js b/spec/javascripts/gl_emoji_spec.js index b2b46640e5b..a09e0072fa8 100644 --- a/spec/javascripts/gl_emoji_spec.js +++ b/spec/javascripts/gl_emoji_spec.js @@ -192,6 +192,9 @@ describe('gl_emoji', () => { }); describe('isFlagEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isFlagEmoji('')).toBeFalsy(); + }); it('should detect flag_ac', () => { expect(isFlagEmoji('🇦🇨')).toBeTruthy(); }); @@ -216,6 +219,9 @@ describe('gl_emoji', () => { }); describe('isKeycapEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isKeycapEmoji('')).toBeFalsy(); + }); it('should detect one(keycap)', () => { expect(isKeycapEmoji('1️⃣')).toBeTruthy(); }); @@ -231,6 +237,9 @@ describe('gl_emoji', () => { }); describe('isSkinToneComboEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isSkinToneComboEmoji('')).toBeFalsy(); + }); it('should detect hand_splayed_tone5', () => { expect(isSkinToneComboEmoji('🖐🏿')).toBeTruthy(); }); @@ -255,6 +264,9 @@ describe('gl_emoji', () => { }); describe('isHorceRacingSkinToneComboEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isHorceRacingSkinToneComboEmoji('')).toBeFalsy(); + }); it('should detect horse_racing_tone2', () => { expect(isHorceRacingSkinToneComboEmoji('🏇🏼')).toBeTruthy(); }); @@ -264,6 +276,9 @@ describe('gl_emoji', () => { }); describe('isPersonZwjEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isPersonZwjEmoji('')).toBeFalsy(); + }); it('should detect couple_mm', () => { expect(isPersonZwjEmoji('👨❤️👨')).toBeTruthy(); }); @@ -300,6 +315,22 @@ describe('gl_emoji', () => { }); describe('isEmojiUnicodeSupported', () => { + it('should gracefully handle empty string with unicode support', () => { + const isSupported = isEmojiUnicodeSupported( + { '1.0': true }, + '', + '1.0', + ); + expect(isSupported).toBeTruthy(); + }); + it('should gracefully handle empty string without unicode support', () => { + const isSupported = isEmojiUnicodeSupported( + {}, + '', + '1.0', + ); + expect(isSupported).toBeFalsy(); + }); it('bomb(6.0) with 6.0 support', () => { const emojiKey = 'bomb'; const unicodeSupportMap = Object.assign({}, emptySupportMap, { diff --git a/spec/javascripts/groups/group_item_spec.js b/spec/javascripts/groups/group_item_spec.js new file mode 100644 index 00000000000..25e10552d95 --- /dev/null +++ b/spec/javascripts/groups/group_item_spec.js @@ -0,0 +1,102 @@ +import Vue from 'vue'; +import groupItemComponent from '~/groups/components/group_item.vue'; +import GroupsStore from '~/groups/stores/groups_store'; +import { group1 } from './mock_data'; + +describe('Groups Component', () => { + let GroupItemComponent; + let component; + let store; + let group; + + describe('group with default data', () => { + beforeEach((done) => { + GroupItemComponent = Vue.extend(groupItemComponent); + store = new GroupsStore(); + group = store.decorateGroup(group1); + + component = new GroupItemComponent({ + propsData: { + group, + }, + }).$mount(); + + Vue.nextTick(() => { + done(); + }); + }); + + afterEach(() => { + component.$destroy(); + }); + + it('should render the group item correctly', () => { + expect(component.$el.classList.contains('group-row')).toBe(true); + expect(component.$el.classList.contains('.no-description')).toBe(false); + expect(component.$el.querySelector('.number-projects').textContent).toContain(group.numberProjects); + expect(component.$el.querySelector('.number-users').textContent).toContain(group.numberUsers); + expect(component.$el.querySelector('.group-visibility')).toBeDefined(); + expect(component.$el.querySelector('.avatar-container')).toBeDefined(); + expect(component.$el.querySelector('.title').textContent).toContain(group.name); + expect(component.$el.querySelector('.access-type').textContent).toContain(group.permissions.humanGroupAccess); + expect(component.$el.querySelector('.description').textContent).toContain(group.description); + expect(component.$el.querySelector('.edit-group')).toBeDefined(); + expect(component.$el.querySelector('.leave-group')).toBeDefined(); + }); + }); + + describe('group without description', () => { + beforeEach((done) => { + GroupItemComponent = Vue.extend(groupItemComponent); + store = new GroupsStore(); + group1.description = ''; + group = store.decorateGroup(group1); + + component = new GroupItemComponent({ + propsData: { + group, + }, + }).$mount(); + + Vue.nextTick(() => { + done(); + }); + }); + + afterEach(() => { + component.$destroy(); + }); + + it('should render group item correctly', () => { + expect(component.$el.querySelector('.description').textContent).toBe(''); + expect(component.$el.classList.contains('.no-description')).toBe(false); + }); + }); + + describe('user has not access to group', () => { + beforeEach((done) => { + GroupItemComponent = Vue.extend(groupItemComponent); + store = new GroupsStore(); + group1.permissions.human_group_access = null; + group = store.decorateGroup(group1); + + component = new GroupItemComponent({ + propsData: { + group, + }, + }).$mount(); + + Vue.nextTick(() => { + done(); + }); + }); + + afterEach(() => { + component.$destroy(); + }); + + it('should not display access type', () => { + expect(component.$el.querySelector('.access-type')).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/groups/groups_spec.js b/spec/javascripts/groups/groups_spec.js new file mode 100644 index 00000000000..2a77f7259da --- /dev/null +++ b/spec/javascripts/groups/groups_spec.js @@ -0,0 +1,64 @@ +import Vue from 'vue'; +import groupFolderComponent from '~/groups/components/group_folder.vue'; +import groupItemComponent from '~/groups/components/group_item.vue'; +import groupsComponent from '~/groups/components/groups.vue'; +import GroupsStore from '~/groups/stores/groups_store'; +import { groupsData } from './mock_data'; + +describe('Groups Component', () => { + let GroupsComponent; + let store; + let component; + let groups; + + beforeEach((done) => { + Vue.component('group-folder', groupFolderComponent); + Vue.component('group-item', groupItemComponent); + + store = new GroupsStore(); + groups = store.setGroups(groupsData.groups); + + store.storePagination(groupsData.pagination); + + GroupsComponent = Vue.extend(groupsComponent); + + component = new GroupsComponent({ + propsData: { + groups: store.state.groups, + pageInfo: store.state.pageInfo, + }, + }).$mount(); + + Vue.nextTick(() => { + done(); + }); + }); + + afterEach(() => { + component.$destroy(); + }); + + describe('with data', () => { + it('should render a list of groups', () => { + expect(component.$el.classList.contains('groups-list-tree-container')).toBe(true); + expect(component.$el.querySelector('#group-12')).toBeDefined(); + expect(component.$el.querySelector('#group-1119')).toBeDefined(); + expect(component.$el.querySelector('#group-1120')).toBeDefined(); + }); + + it('should render group and its subgroup', () => { + const lists = component.$el.querySelectorAll('.group-list-tree'); + + expect(lists.length).toBe(3); // one parent and two subgroups + + expect(lists[0].querySelector('#group-1119').classList.contains('is-open')).toBe(true); + expect(lists[0].querySelector('#group-1119').classList.contains('has-subgroups')).toBe(true); + + expect(lists[2].querySelector('#group-1120').textContent).toContain(groups[1119].subGroups[1120].name); + }); + + it('should remove prefix of parent group', () => { + expect(component.$el.querySelector('#group-12 #group-1128 .title').textContent).toContain('level2 / level3 / level4'); + }); + }); +}); diff --git a/spec/javascripts/groups/mock_data.js b/spec/javascripts/groups/mock_data.js new file mode 100644 index 00000000000..b3f5d791b89 --- /dev/null +++ b/spec/javascripts/groups/mock_data.js @@ -0,0 +1,114 @@ +const group1 = { + id: '12', + name: 'level1', + path: 'level1', + description: 'foo', + visibility: 'public', + avatar_url: null, + web_url: 'http://localhost:3000/groups/level1', + group_path: '/level1', + full_name: 'level1', + full_path: 'level1', + parent_id: null, + created_at: '2017-05-15T19:01:23.670Z', + updated_at: '2017-05-15T19:01:23.670Z', + number_projects_with_delimiter: '1', + number_users_with_delimiter: '1', + has_subgroups: true, + permissions: { + human_group_access: 'Master', + }, +}; + +// This group has no direct parent, should be placed as subgroup of group1 +const group14 = { + id: 1128, + name: 'level4', + path: 'level4', + description: 'foo', + visibility: 'public', + avatar_url: null, + web_url: 'http://localhost:3000/groups/level1/level2/level3/level4', + group_path: '/level1/level2/level3/level4', + full_name: 'level1 / level2 / level3 / level4', + full_path: 'level1/level2/level3/level4', + parent_id: 1127, + created_at: '2017-05-15T19:02:01.645Z', + updated_at: '2017-05-15T19:02:01.645Z', + number_projects_with_delimiter: '1', + number_users_with_delimiter: '1', + has_subgroups: true, + permissions: { + human_group_access: 'Master', + }, +}; + +const group2 = { + id: 1119, + name: 'devops', + path: 'devops', + description: 'foo', + visibility: 'public', + avatar_url: null, + web_url: 'http://localhost:3000/groups/devops', + group_path: '/devops', + full_name: 'devops', + full_path: 'devops', + parent_id: null, + created_at: '2017-05-11T19:35:09.635Z', + updated_at: '2017-05-11T19:35:09.635Z', + number_projects_with_delimiter: '1', + number_users_with_delimiter: '1', + has_subgroups: true, + permissions: { + human_group_access: 'Master', + }, +}; + +const group21 = { + id: 1120, + name: 'chef', + path: 'chef', + description: 'foo', + visibility: 'public', + avatar_url: null, + web_url: 'http://localhost:3000/groups/devops/chef', + group_path: '/devops/chef', + full_name: 'devops / chef', + full_path: 'devops/chef', + parent_id: 1119, + created_at: '2017-05-11T19:51:04.060Z', + updated_at: '2017-05-11T19:51:04.060Z', + number_projects_with_delimiter: '1', + number_users_with_delimiter: '1', + has_subgroups: true, + permissions: { + human_group_access: 'Master', + }, +}; + +const groupsData = { + groups: [group1, group14, group2, group21], + pagination: { + Date: 'Mon, 22 May 2017 22:31:52 GMT', + 'X-Prev-Page': '1', + 'X-Content-Type-Options': 'nosniff', + 'X-Total': '31', + 'Transfer-Encoding': 'chunked', + 'X-Runtime': '0.611144', + 'X-Xss-Protection': '1; mode=block', + 'X-Request-Id': 'f5db8368-3ce5-4aa4-89d2-a125d9dead09', + 'X-Ua-Compatible': 'IE=edge', + 'X-Per-Page': '20', + Link: '<http://localhost:3000/dashboard/groups.json?page=1&per_page=20>; rel="prev", <http://localhost:3000/dashboard/groups.json?page=1&per_page=20>; rel="first", <http://localhost:3000/dashboard/groups.json?page=2&per_page=20>; rel="last"', + 'X-Next-Page': '', + Etag: 'W/"a82f846947136271cdb7d55d19ef33d2"', + 'X-Frame-Options': 'DENY', + 'Content-Type': 'application/json; charset=utf-8', + 'Cache-Control': 'max-age=0, private, must-revalidate', + 'X-Total-Pages': '2', + 'X-Page': '2', + }, +}; + +export { groupsData, group1 }; diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 59c006aa0af..2ccc4f16192 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -126,7 +126,7 @@ describe('Issuable output', () => { describe('updateIssuable', () => { it('fetches new data after update', (done) => { - spyOn(vm.service, 'getData'); + spyOn(vm.service, 'getData').and.callThrough(); spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { resolve({ json() { diff --git a/spec/javascripts/jobs/header_spec.js b/spec/javascripts/jobs/header_spec.js new file mode 100644 index 00000000000..c7179b3e03d --- /dev/null +++ b/spec/javascripts/jobs/header_spec.js @@ -0,0 +1,63 @@ +import Vue from 'vue'; +import headerComponent from '~/jobs/components/header.vue'; + +describe('Job details header', () => { + let HeaderComponent; + let vm; + let props; + + beforeEach(() => { + HeaderComponent = Vue.extend(headerComponent); + + const threeWeeksAgo = new Date(); + threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21); + + props = { + job: { + status: { + group: 'failed', + icon: 'ci-status-failed', + label: 'failed', + text: 'failed', + details_path: 'path', + }, + id: 123, + created_at: threeWeeksAgo.toISOString(), + user: { + web_url: 'path', + name: 'Foo', + username: 'foobar', + email: 'foo@bar.com', + avatar_url: 'link', + }, + retry_path: 'path', + new_issue_path: 'path', + }, + isLoading: false, + }; + + vm = new HeaderComponent({ propsData: props }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render provided job information', () => { + expect( + vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(), + ).toEqual('failed Job #123 triggered 3 weeks ago by Foo'); + }); + + it('should render retry link', () => { + expect( + vm.$el.querySelector('.js-retry-button').getAttribute('href'), + ).toEqual(props.job.retry_path); + }); + + it('should render new issue link', () => { + expect( + vm.$el.querySelector('.js-new-issue').getAttribute('href'), + ).toEqual(props.job.new_issue_path); + }); +}); diff --git a/spec/javascripts/jobs/job_details_mediator_spec.js b/spec/javascripts/jobs/job_details_mediator_spec.js new file mode 100644 index 00000000000..1d7fa7e12fc --- /dev/null +++ b/spec/javascripts/jobs/job_details_mediator_spec.js @@ -0,0 +1,43 @@ +import Vue from 'vue'; +import JobMediator from '~/jobs/job_details_mediator'; +import job from './mock_data'; + +describe('JobMediator', () => { + let mediator; + + beforeEach(() => { + mediator = new JobMediator({ endpoint: 'foo' }); + }); + + it('should set defaults', () => { + expect(mediator.store).toBeDefined(); + expect(mediator.service).toBeDefined(); + expect(mediator.options).toEqual({ endpoint: 'foo' }); + expect(mediator.state.isLoading).toEqual(false); + }); + + describe('request and store data', () => { + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify(job), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptor, interceptor); + }); + + it('should store received data', (done) => { + mediator.fetchJob(); + + setTimeout(() => { + expect(mediator.store.state.job).toEqual(job); + done(); + }, 0); + }); + }); +}); diff --git a/spec/javascripts/jobs/job_store_spec.js b/spec/javascripts/jobs/job_store_spec.js new file mode 100644 index 00000000000..d00faf29d1e --- /dev/null +++ b/spec/javascripts/jobs/job_store_spec.js @@ -0,0 +1,26 @@ +import JobStore from '~/jobs/stores/job_store'; +import job from './mock_data'; + +describe('Job Store', () => { + let store; + + beforeEach(() => { + store = new JobStore(); + }); + + it('should set defaults', () => { + expect(store.state.job).toEqual({}); + }); + + describe('storeJob', () => { + it('should store empty object if none is provided', () => { + store.storeJob(); + expect(store.state.job).toEqual({}); + }); + + it('should store provided argument', () => { + store.storeJob(job); + expect(store.state.job).toEqual(job); + }); + }); +}); diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js new file mode 100644 index 00000000000..17e4ef26b2c --- /dev/null +++ b/spec/javascripts/jobs/mock_data.js @@ -0,0 +1,123 @@ +const threeWeeksAgo = new Date(); +threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21); + +export default { + id: 4757, + name: 'test', + build_path: '/root/ci-mock/-/jobs/4757', + retry_path: '/root/ci-mock/-/jobs/4757/retry', + cancel_path: '/root/ci-mock/-/jobs/4757/cancel', + new_issue_path: '/root/ci-mock/issues/new', + playable: false, + created_at: threeWeeksAgo.toISOString(), + updated_at: threeWeeksAgo.toISOString(), + finished_at: threeWeeksAgo.toISOString(), + queued: 9.54, + status: { + icon: 'icon_status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/-/jobs/4757', + favicon: '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + action: { + icon: 'icon_action_retry', + title: 'Retry', + path: '/root/ci-mock/-/jobs/4757/retry', + method: 'post', + }, + }, + coverage: 20, + erased_at: threeWeeksAgo.toISOString(), + duration: 6.785563, + tags: ['tag'], + user: { + name: 'Root', + username: 'root', + id: 1, + state: 'active', + avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + web_url: 'http://localhost:3000/root', + }, + erase_path: '/root/ci-mock/-/jobs/4757/erase', + artifacts: [null], + runner: { + id: 1, + description: 'local ci runner', + edit_path: '/root/ci-mock/runners/1/edit', + }, + pipeline: { + id: 140, + user: { + name: 'Root', + username: 'root', + id: 1, + state: 'active', + avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + web_url: 'http://localhost:3000/root', + }, + active: false, + coverage: null, + source: 'unknown', + created_at: '2017-05-24T09:59:58.634Z', + updated_at: '2017-06-01T17:32:00.062Z', + path: '/root/ci-mock/pipelines/140', + flags: { + latest: true, + stuck: false, + yaml_errors: false, + retryable: false, + cancelable: false, + }, + details: { + status: { + icon: 'icon_status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/pipelines/140', + favicon: '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + }, + duration: 6, + finished_at: '2017-06-01T17:32:00.042Z', + }, + ref: { + name: 'abc', + path: '/root/ci-mock/commits/abc', + tag: false, + branch: true, + }, + commit: { + id: 'c58647773a6b5faf066d4ad6ff2c9fbba5f180f6', + short_id: 'c5864777', + title: 'Add new file', + created_at: '2017-05-24T10:59:52.000+01:00', + parent_ids: ['798e5f902592192afaba73f4668ae30e56eae492'], + message: 'Add new file', + author_name: 'Root', + author_email: 'admin@example.com', + authored_date: '2017-05-24T10:59:52.000+01:00', + committer_name: 'Root', + committer_email: 'admin@example.com', + committed_date: '2017-05-24T10:59:52.000+01:00', + author: { + name: 'Root', + username: 'root', + id: 1, + state: 'active', + avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + web_url: 'http://localhost:3000/root', + }, + author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + commit_url: 'http://localhost:3000/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6', + commit_path: '/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6', + }, + }, + merge_request: { + iid: 2, + path: '/root/ci-mock/merge_requests/2', + }, + raw_path: '/root/ci-mock/builds/4757/raw', +}; diff --git a/spec/javascripts/jobs/sidebar_detail_row_spec.js b/spec/javascripts/jobs/sidebar_detail_row_spec.js new file mode 100644 index 00000000000..3ac65709c4a --- /dev/null +++ b/spec/javascripts/jobs/sidebar_detail_row_spec.js @@ -0,0 +1,40 @@ +import Vue from 'vue'; +import sidebarDetailRow from '~/jobs/components/sidebar_detail_row.vue'; + +describe('Sidebar detail row', () => { + let SidebarDetailRow; + let vm; + + beforeEach(() => { + SidebarDetailRow = Vue.extend(sidebarDetailRow); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render no title', () => { + vm = new SidebarDetailRow({ + propsData: { + value: 'this is the value', + }, + }).$mount(); + + expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('this is the value'); + }); + + beforeEach(() => { + vm = new SidebarDetailRow({ + propsData: { + title: 'this is the title', + value: 'this is the value', + }, + }).$mount(); + }); + + it('should render provided title and value', () => { + expect( + vm.$el.textContent.replace(/\s+/g, ' ').trim(), + ).toEqual('this is the title: this is the value'); + }); +}); diff --git a/spec/javascripts/jobs/sidebar_details_block_spec.js b/spec/javascripts/jobs/sidebar_details_block_spec.js new file mode 100644 index 00000000000..95532ef5382 --- /dev/null +++ b/spec/javascripts/jobs/sidebar_details_block_spec.js @@ -0,0 +1,111 @@ +import Vue from 'vue'; +import sidebarDetailsBlock from '~/jobs/components/sidebar_details_block.vue'; +import job from './mock_data'; + +describe('Sidebar details block', () => { + let SidebarComponent; + let vm; + + function trimWhitespace(element) { + return element.textContent.replace(/\s+/g, ' ').trim(); + } + + beforeEach(() => { + SidebarComponent = Vue.extend(sidebarDetailsBlock); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('when it is loading', () => { + it('should render a loading spinner', () => { + vm = new SidebarComponent({ + propsData: { + job: {}, + isLoading: true, + }, + }).$mount(); + + expect(vm.$el.querySelector('.fa-spinner')).toBeDefined(); + }); + }); + + beforeEach(() => { + vm = new SidebarComponent({ + propsData: { + job, + isLoading: false, + }, + }).$mount(); + }); + + describe('actions', () => { + it('should render link to new issue', () => { + expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(job.new_issue_path); + expect(vm.$el.querySelector('.js-new-issue').textContent.trim()).toEqual('New issue'); + }); + + it('should render link to retry job', () => { + expect(vm.$el.querySelector('.js-retry-job').getAttribute('href')).toEqual(job.retry_path); + }); + + it('should render link to cancel job', () => { + expect(vm.$el.querySelector('.js-cancel-job').getAttribute('href')).toEqual(job.cancel_path); + }); + }); + + describe('information', () => { + it('should render merge request link', () => { + expect( + trimWhitespace(vm.$el.querySelector('.js-job-mr')), + ).toEqual('Merge Request: !2'); + + expect( + vm.$el.querySelector('.js-job-mr a').getAttribute('href'), + ).toEqual(job.merge_request.path); + }); + + it('should render job duration', () => { + expect( + trimWhitespace(vm.$el.querySelector('.js-job-duration')), + ).toEqual('Duration: 6 seconds'); + }); + + it('should render erased date', () => { + expect( + trimWhitespace(vm.$el.querySelector('.js-job-erased')), + ).toEqual('Erased: 3 weeks ago'); + }); + + it('should render finished date', () => { + expect( + trimWhitespace(vm.$el.querySelector('.js-job-finished')), + ).toEqual('Finished: 3 weeks ago'); + }); + + it('should render queued date', () => { + expect( + trimWhitespace(vm.$el.querySelector('.js-job-queued')), + ).toEqual('Queued: 9 seconds'); + }); + + it('should render runner ID', () => { + expect( + trimWhitespace(vm.$el.querySelector('.js-job-runner')), + ).toEqual('Runner: #1'); + }); + + it('should render coverage', () => { + expect( + trimWhitespace(vm.$el.querySelector('.js-job-coverage')), + ).toEqual('Coverage: 20%'); + }); + + it('should render tags', () => { + expect( + trimWhitespace(vm.$el.querySelector('.js-job-tags')), + ).toEqual('Tags: tag'); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index e3938a77680..52cf217c25f 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -150,6 +150,14 @@ import '~/lib/utils/common_utils'; const value = gl.utils.getParameterByName('fakeParameter'); expect(value).toBe(null); }); + + it('should return valid paramentes if URL is provided', () => { + let value = gl.utils.getParameterByName('foo', 'http://cocteau.twins/?foo=bar'); + expect(value).toBe('bar'); + + value = gl.utils.getParameterByName('manan', 'http://cocteau.twins/?foo=bar&manan=canchu'); + expect(value).toBe('canchu'); + }); }); describe('gl.utils.normalizedHeaders', () => { diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 7b910282cc8..9916d2c1e21 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -12,15 +12,6 @@ import '~/notes'; import 'vendor/jquery.scrollTo'; (function () { - // TODO: remove this hack! - // PhantomJS causes spyOn to panic because replaceState isn't "writable" - var phantomjs; - try { - phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable; - } catch (err) { - phantomjs = false; - } - describe('MergeRequestTabs', function () { var stubLocation = {}; var setLocation = function (stubs) { @@ -37,11 +28,9 @@ import 'vendor/jquery.scrollTo'; this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation }); setLocation(); - if (!phantomjs) { - this.spies = { - history: spyOn(window.history, 'replaceState').and.callFake(function () {}) - }; - } + this.spies = { + history: spyOn(window.history, 'replaceState').and.callFake(function () {}) + }; }); afterEach(function () { @@ -208,11 +197,9 @@ import 'vendor/jquery.scrollTo'; pathname: '/foo/bar/merge_requests/1' }); newState = this.subject('commits'); - if (!phantomjs) { - expect(this.spies.history).toHaveBeenCalledWith({ - url: newState - }, document.title, newState); - } + expect(this.spies.history).toHaveBeenCalledWith({ + url: newState + }, document.title, newState); }); it('treats "show" like "notes"', function () { diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 24335614e09..c6f218e4dac 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -126,6 +126,7 @@ import '~/notes'; const deferred = $.Deferred(); spyOn($, 'ajax').and.returnValue(deferred.promise()); spyOn(this.notes, 'revertNoteEditForm'); + spyOn(this.notes, 'setupNewNote'); $('.js-comment-button').click(); deferred.resolve(noteEntity); @@ -136,6 +137,46 @@ import '~/notes'; this.notes.updateNote(updatedNote, $targetNote); expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote); + expect(this.notes.setupNewNote).toHaveBeenCalled(); + }); + }); + + describe('updateNoteTargetSelector', () => { + const hash = 'note_foo'; + let $note; + + beforeEach(() => { + $note = $(`<div id="${hash}"></div>`); + spyOn($note, 'filter').and.callThrough(); + spyOn($note, 'toggleClass').and.callThrough(); + }); + + it('sets target when hash matches', () => { + spyOn(gl.utils, 'getLocationHash'); + gl.utils.getLocationHash.and.returnValue(hash); + + Notes.updateNoteTargetSelector($note); + + expect($note.filter).toHaveBeenCalledWith(`#${hash}`); + expect($note.toggleClass).toHaveBeenCalledWith('target', true); + }); + + it('unsets target when hash does not match', () => { + spyOn(gl.utils, 'getLocationHash'); + gl.utils.getLocationHash.and.returnValue('note_doesnotexist'); + + Notes.updateNoteTargetSelector($note); + + expect($note.toggleClass).toHaveBeenCalledWith('target', false); + }); + + it('unsets target when there is not a hash fragment anymore', () => { + spyOn(gl.utils, 'getLocationHash'); + gl.utils.getLocationHash.and.returnValue(null); + + Notes.updateNoteTargetSelector($note); + + expect($note.toggleClass).toHaveBeenCalledWith('target', null); }); }); @@ -189,9 +230,13 @@ import '~/notes'; Notes.isUpdatedNote.and.returnValue(true); const $note = $('<div>'); $notesList.find.and.returnValue($note); + const $newNote = $(note.html); + Notes.animateUpdateNote.and.returnValue($newNote); + Notes.prototype.renderNote.call(notes, note, null, $notesList); expect(Notes.animateUpdateNote).toHaveBeenCalledWith(note.html, $note); + expect(notes.setupNewNote).toHaveBeenCalledWith($newNote); }); describe('while editing', () => { @@ -378,6 +423,23 @@ import '~/notes'; }); }); + describe('putEditFormInPlace', () => { + it('should call gl.GLForm with GFM parameter passed through', () => { + spyOn(gl, 'GLForm'); + + const $el = jasmine.createSpyObj('$form', ['find', 'closest']); + $el.find.and.returnValue($('<div>')); + $el.closest.and.returnValue($('<div>')); + + Notes.prototype.putEditFormInPlace.call({ + getEditFormSelector: () => '', + enableGFM: true + }, $el); + + expect(gl.GLForm).toHaveBeenCalledWith(jasmine.any(Object), true); + }); + }); + describe('postComment & updateComment', () => { const sampleComment = 'foo'; const updatedComment = 'bar'; @@ -461,6 +523,45 @@ import '~/notes'; }); }); + describe('update comment with script tags', () => { + const sampleComment = '<script></script>'; + const updatedComment = '<script></script>'; + const note = { + id: 1234, + html: `<li class="note note-row-1234 timeline-entry" id="note_1234"> + <div class="note-text">${sampleComment}</div> + </li>`, + note: sampleComment, + valid: true + }; + let $form; + let $notesContainer; + + beforeEach(() => { + this.notes = new Notes('', []); + window.gon.current_username = 'root'; + window.gon.current_user_fullname = 'Administrator'; + $form = $('form.js-main-target-form'); + $notesContainer = $('ul.main-notes-list'); + $form.find('textarea.js-note-text').html(sampleComment); + }); + + it('should not render a script tag', () => { + const deferred = $.Deferred(); + spyOn($, 'ajax').and.returnValue(deferred.promise()); + $('.js-comment-button').click(); + + deferred.resolve(note); + const $noteEl = $notesContainer.find(`#note_${note.id}`); + $noteEl.find('.js-note-edit').click(); + $noteEl.find('textarea.js-note-text').html(updatedComment); + $noteEl.find('.js-comment-save-button').click(); + + const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`).find('.js-task-list-container'); + expect($updatedNoteEl.find('.note-text').text().trim()).toEqual(''); + }); + }); + describe('getFormData', () => { let $form; let sampleComment; diff --git a/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js b/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js index 845b371d90c..56c57d94798 100644 --- a/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js +++ b/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js @@ -95,7 +95,7 @@ describe('Interval Pattern Input Component', function () { describe('User Actions', function () { beforeEach(function () { - // For an unknown reason, Phantom.js doesn't trigger click events + // For an unknown reason, some browsers do not propagate click events // on radio buttons in a way Vue can register. So, we have to mount // to a fixture. setFixtures('<div id="my-mount"></div>'); diff --git a/spec/javascripts/pipelines/nav_controls_spec.js b/spec/javascripts/pipelines/nav_controls_spec.js index 601eebce38a..f1697840fcd 100644 --- a/spec/javascripts/pipelines/nav_controls_spec.js +++ b/spec/javascripts/pipelines/nav_controls_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import navControlsComp from '~/pipelines/components/nav_controls'; +import navControlsComp from '~/pipelines/components/nav_controls.vue'; describe('Pipelines Nav Controls', () => { let NavControlsComponent; diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js index 594a9856d2c..3c4b20a5f06 100644 --- a/spec/javascripts/pipelines/pipeline_url_spec.js +++ b/spec/javascripts/pipelines/pipeline_url_spec.js @@ -19,7 +19,7 @@ describe('Pipeline Url Component', () => { }, }).$mount(); - expect(component.$el.tagName).toEqual('TD'); + expect(component.$el.getAttribute('class')).toContain('table-section'); }); it('should render a link the provided path and id', () => { @@ -94,7 +94,7 @@ describe('Pipeline Url Component', () => { }, }).$mount(); - expect(component.$el.querySelector('.js-pipeline-url-lastest').textContent).toContain('latest'); + expect(component.$el.querySelector('.js-pipeline-url-latest').textContent).toContain('latest'); expect(component.$el.querySelector('.js-pipeline-url-yaml').textContent).toContain('yaml invalid'); expect(component.$el.querySelector('.js-pipeline-url-stuck').textContent).toContain('stuck'); }); diff --git a/spec/javascripts/pipelines/pipelines_actions_spec.js b/spec/javascripts/pipelines/pipelines_actions_spec.js index c89dacbcd93..8a58b77f1e3 100644 --- a/spec/javascripts/pipelines/pipelines_actions_spec.js +++ b/spec/javascripts/pipelines/pipelines_actions_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import pipelinesActionsComp from '~/pipelines/components/pipelines_actions'; +import pipelinesActionsComp from '~/pipelines/components/pipelines_actions.vue'; describe('Pipelines Actions dropdown', () => { let component; diff --git a/spec/javascripts/pipelines/pipelines_artifacts_spec.js b/spec/javascripts/pipelines/pipelines_artifacts_spec.js index 9724b63d957..acb67d0ec21 100644 --- a/spec/javascripts/pipelines/pipelines_artifacts_spec.js +++ b/spec/javascripts/pipelines/pipelines_artifacts_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import artifactsComp from '~/pipelines/components/pipelines_artifacts'; +import artifactsComp from '~/pipelines/components/pipelines_artifacts.vue'; describe('Pipelines Artifacts dropdown', () => { let component; diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js index 3a56156358b..c30abb2edb0 100644 --- a/spec/javascripts/pipelines/pipelines_spec.js +++ b/spec/javascripts/pipelines/pipelines_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import pipelinesComp from '~/pipelines/pipelines'; +import pipelinesComp from '~/pipelines/components/pipelines.vue'; import Store from '~/pipelines/stores/pipelines_store'; describe('Pipelines', () => { diff --git a/spec/javascripts/pipelines/time_ago_spec.js b/spec/javascripts/pipelines/time_ago_spec.js index 24581e8c672..42b34c82f89 100644 --- a/spec/javascripts/pipelines/time_ago_spec.js +++ b/spec/javascripts/pipelines/time_ago_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import timeAgo from '~/pipelines/components/time_ago'; +import timeAgo from '~/pipelines/components/time_ago.vue'; describe('Timeago component', () => { let TimeAgo; diff --git a/spec/javascripts/pipelines_spec.js b/spec/javascripts/pipelines_spec.js index 81ac589f4e6..c08a73851be 100644 --- a/spec/javascripts/pipelines_spec.js +++ b/spec/javascripts/pipelines_spec.js @@ -1,10 +1,5 @@ import Pipelines from '~/pipelines'; -// Fix for phantomJS -if (!Element.prototype.matches && Element.prototype.webkitMatchesSelector) { - Element.prototype.matches = Element.prototype.webkitMatchesSelector; -} - describe('Pipelines', () => { preloadFixtures('static/pipeline_graph.html.raw'); diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 13827a26571..2c34402576b 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -51,7 +51,6 @@ if (process.env.BABEL_ENV === 'coverage') { './environments/environments_bundle.js', './filtered_search/filtered_search_bundle.js', './graphs/graphs_bundle.js', - './issuable/issuable_bundle.js', './issuable/time_tracking/time_tracking_bundle.js', './main.js', './merge_conflicts/merge_conflicts_bundle.js', diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js index 050170a54e9..1c3188cdda2 100644 --- a/spec/javascripts/vue_shared/components/commit_spec.js +++ b/spec/javascripts/vue_shared/components/commit_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import commitComp from '~/vue_shared/components/commit'; +import commitComp from '~/vue_shared/components/commit.vue'; describe('Commit component', () => { let props; @@ -22,7 +22,7 @@ describe('Commit component', () => { shortSha: 'b7836edd', title: 'Commit message', author: { - avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png', + avatar_url: 'https://gitlab.com/uploads/system/user/avatar/300478/avatar.png', web_url: 'https://gitlab.com/jschatz1', path: '/jschatz1', username: 'jschatz1', @@ -45,7 +45,7 @@ describe('Commit component', () => { shortSha: 'b7836edd', title: 'Commit message', author: { - avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png', + avatar_url: 'https://gitlab.com/uploads/system/user/avatar/300478/avatar.png', web_url: 'https://gitlab.com/jschatz1', path: '/jschatz1', username: 'jschatz1', diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js index 2b51c89f311..b4553acb341 100644 --- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js +++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js @@ -43,6 +43,7 @@ describe('Header CI Component', () => { isLoading: false, }, ], + hasSidebarButton: true, }; vm = new HeaderCi({ @@ -86,8 +87,12 @@ describe('Header CI Component', () => { vm.actions[0].isLoading = true; Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toEqual(''); + expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toBeFalsy(); done(); }); }); + + it('should render sidebar toggle button', () => { + expect(vm.$el.querySelector('.js-sidebar-build-toggle')).toBeDefined(); + }); }); diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js index 67419cfcbea..9475ee28a03 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js +++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import tableRowComp from '~/vue_shared/components/pipelines_table_row'; +import tableRowComp from '~/vue_shared/components/pipelines_table_row.vue'; describe('Pipelines Table Row', () => { const jsonFixtureName = 'pipelines/pipelines.json'; @@ -34,7 +34,7 @@ describe('Pipelines Table Row', () => { it('should render a table row', () => { component = buildComponent(pipeline); - expect(component.$el).toEqual('TR'); + expect(component.$el.getAttribute('class')).toContain('gl-responsive-table-row'); }); describe('status column', () => { @@ -44,13 +44,13 @@ describe('Pipelines Table Row', () => { it('should render a pipeline link', () => { expect( - component.$el.querySelector('td.commit-link a').getAttribute('href'), + component.$el.querySelector('.table-section.commit-link a').getAttribute('href'), ).toEqual(pipeline.path); }); it('should render status text', () => { expect( - component.$el.querySelector('td.commit-link a').textContent, + component.$el.querySelector('.table-section.commit-link a').textContent, ).toContain(pipeline.details.status.text); }); }); @@ -62,24 +62,24 @@ describe('Pipelines Table Row', () => { it('should render a pipeline link', () => { expect( - component.$el.querySelector('td:nth-child(2) a').getAttribute('href'), + component.$el.querySelector('.table-section:nth-child(2) a').getAttribute('href'), ).toEqual(pipeline.path); }); it('should render pipeline ID', () => { expect( - component.$el.querySelector('td:nth-child(2) a > span').textContent, + component.$el.querySelector('.table-section:nth-child(2) a > span').textContent, ).toEqual(`#${pipeline.id}`); }); describe('when a user is provided', () => { it('should render user information', () => { expect( - component.$el.querySelector('td:nth-child(2) a:nth-child(3)').getAttribute('href'), + component.$el.querySelector('.table-section:nth-child(2) a:nth-child(3)').getAttribute('href'), ).toEqual(pipeline.user.path); expect( - component.$el.querySelector('td:nth-child(2) img').getAttribute('data-original-title'), + component.$el.querySelector('.table-section:nth-child(2) img').getAttribute('data-original-title'), ).toEqual(pipeline.user.name); }); }); @@ -142,7 +142,7 @@ describe('Pipelines Table Row', () => { it('should render an icon for each stage', () => { expect( - component.$el.querySelectorAll('td:nth-child(4) .js-builds-dropdown-button').length, + component.$el.querySelectorAll('.table-section:nth-child(4) .js-builds-dropdown-button').length, ).toEqual(pipeline.details.stages.length); }); }); @@ -154,7 +154,7 @@ describe('Pipelines Table Row', () => { it('should render the provided actions', () => { expect( - component.$el.querySelectorAll('td:nth-child(6) ul li').length, + component.$el.querySelectorAll('.table-section:nth-child(6) ul li').length, ).toEqual(pipeline.details.manual_actions.length); }); }); diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_spec.js index 6cc178b8f1d..4c35d702004 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_spec.js +++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import pipelinesTableComp from '~/vue_shared/components/pipelines_table'; +import pipelinesTableComp from '~/vue_shared/components/pipelines_table.vue'; import '~/lib/utils/datetime_utility'; describe('Pipelines Table', () => { @@ -32,16 +32,14 @@ describe('Pipelines Table', () => { }); it('should render a table', () => { - expect(component.$el).toEqual('TABLE'); + expect(component.$el.getAttribute('class')).toContain('ci-table'); }); it('should render table head with correct columns', () => { - expect(component.$el.querySelector('th.js-pipeline-status').textContent).toEqual('Status'); - expect(component.$el.querySelector('th.js-pipeline-info').textContent).toEqual('Pipeline'); - expect(component.$el.querySelector('th.js-pipeline-commit').textContent).toEqual('Commit'); - expect(component.$el.querySelector('th.js-pipeline-stages').textContent).toEqual('Stages'); - expect(component.$el.querySelector('th.js-pipeline-date').textContent).toEqual(''); - expect(component.$el.querySelector('th.js-pipeline-actions').textContent).toEqual(''); + expect(component.$el.querySelector('.table-section.js-pipeline-status').textContent.trim()).toEqual('Status'); + expect(component.$el.querySelector('.table-section.js-pipeline-info').textContent.trim()).toEqual('Pipeline'); + expect(component.$el.querySelector('.table-section.js-pipeline-commit').textContent.trim()).toEqual('Commit'); + expect(component.$el.querySelector('.table-section.js-pipeline-stages').textContent.trim()).toEqual('Stages'); }); }); @@ -53,7 +51,7 @@ describe('Pipelines Table', () => { service: {}, }, }).$mount(); - expect(component.$el.querySelectorAll('tbody tr').length).toEqual(0); + expect(component.$el.querySelectorAll('.commit.gl-responsive-table-row').length).toEqual(0); }); }); @@ -67,7 +65,7 @@ describe('Pipelines Table', () => { }, }).$mount(); - expect(component.$el.querySelectorAll('tbody tr').length).toEqual(1); + expect(component.$el.querySelectorAll('.commit.gl-responsive-table-row').length).toEqual(1); }); }); }); diff --git a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js index bf28019ef24..f3b4adc0b70 100644 --- a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js +++ b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js @@ -22,7 +22,7 @@ describe('Time ago with tooltip component', () => { }).$mount(); expect(vm.$el.tagName).toEqual('TIME'); - expect(vm.$el.classList.contains('js-timeago')).toEqual(true); + expect(vm.$el.classList.contains('js-vue-timeago')).toEqual(true); expect( vm.$el.getAttribute('data-original-title'), ).toEqual(gl.utils.formatDate('2017-05-08T14:57:39.781Z')); @@ -44,17 +44,6 @@ describe('Time ago with tooltip component', () => { expect(vm.$el.getAttribute('data-placement')).toEqual('bottom'); }); - it('should render short format class', () => { - vm = new TimeagoTooltip({ - propsData: { - time: '2017-05-08T14:57:39.781Z', - shortFormat: true, - }, - }).$mount(); - - expect(vm.$el.classList.contains('js-short-timeago')).toEqual(true); - }); - it('should render provided html class', () => { vm = new TimeagoTooltip({ propsData: { diff --git a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb index 27684882435..787c2372c5b 100644 --- a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb @@ -47,16 +47,7 @@ describe Banzai::Filter::AbstractReferenceFilter do end end - context 'with RequestStore enabled' do - before do - RequestStore.begin! - end - - after do - RequestStore.end! - RequestStore.clear! - end - + context 'with RequestStore enabled', :request_store do it 'returns a list of Projects for a list of paths' do expect(filter.find_projects_for_paths([project.path_with_namespace])). to eq([project]) diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb index fbf7a461fa5..76cefe112fb 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb @@ -82,7 +82,9 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do context 'with RequestStore enabled' do let(:reference_filter) { HTML::Pipeline.new([described_class]) } - before { allow(RequestStore).to receive(:active?).and_return(true) } + before do + allow(RequestStore).to receive(:active?).and_return(true) + end it 'queries the collection on the first call' do expect_any_instance_of(Project).to receive(:default_issues_tracker?).once.and_call_original diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb index 7c4a0f32c7b..97504aebed5 100644 --- a/spec/lib/banzai/filter/redactor_filter_spec.rb +++ b/spec/lib/banzai/filter/redactor_filter_spec.rb @@ -39,7 +39,9 @@ describe Banzai::Filter::RedactorFilter, lib: true do end context 'valid projects' do - before { allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(true) } + before do + allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(true) + end it 'allows permitted Project references' do user = create(:user) @@ -54,7 +56,9 @@ describe Banzai::Filter::RedactorFilter, lib: true do end context 'invalid projects' do - before { allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(false) } + before do + allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(false) + end it 'removes unpermitted references' do user = create(:user) diff --git a/spec/lib/banzai/issuable_extractor_spec.rb b/spec/lib/banzai/issuable_extractor_spec.rb index e5d332efb08..866297f94a9 100644 --- a/spec/lib/banzai/issuable_extractor_spec.rb +++ b/spec/lib/banzai/issuable_extractor_spec.rb @@ -29,16 +29,7 @@ describe Banzai::IssuableExtractor, lib: true do expect(result).to eq(issue_link => issue, merge_request_link => merge_request) end - describe 'caching' do - before do - RequestStore.begin! - end - - after do - RequestStore.end! - RequestStore.clear! - end - + describe 'caching', :request_store do it 'saves records to cache' do extractor.extract([issue_link, merge_request_link]) diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb index d5746107ee1..76fab93821a 100644 --- a/spec/lib/banzai/reference_parser/base_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb @@ -30,7 +30,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do it 'checks if user can read the resource' do link['data-project'] = project.id.to_s - expect(subject).to receive(:can_read_reference?).with(user, project) + expect(subject).to receive(:can_read_reference?).with(user, project, link) subject.nodes_visible_to_user(user, [link]) end @@ -114,7 +114,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do expect(hash).to eq({ link => user }) end - it 'returns an empty Hash when entry does not exist in the database' do + it 'returns an empty Hash when entry does not exist in the database', :request_store do link = double(:link) expect(link).to receive(:has_attribute?). diff --git a/spec/lib/banzai/reference_parser/commit_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_parser_spec.rb index 412ffa77c36..583ce63a8ab 100644 --- a/spec/lib/banzai/reference_parser/commit_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/commit_parser_spec.rb @@ -10,7 +10,9 @@ describe Banzai::ReferenceParser::CommitParser, lib: true do describe '#nodes_visible_to_user' do context 'when the link has a data-issue attribute' do - before { link['data-commit'] = 123 } + before do + link['data-commit'] = 123 + end it_behaves_like "referenced feature visibility", "repository" end diff --git a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb index 96e55b0997a..8c0f5d7df97 100644 --- a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb @@ -10,7 +10,9 @@ describe Banzai::ReferenceParser::CommitRangeParser, lib: true do describe '#nodes_visible_to_user' do context 'when the link has a data-issue attribute' do - before { link['data-commit-range'] = '123..456' } + before do + link['data-commit-range'] = '123..456' + end it_behaves_like "referenced feature visibility", "repository" end diff --git a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb index 0af36776a54..d212bbac619 100644 --- a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb @@ -10,7 +10,9 @@ describe Banzai::ReferenceParser::ExternalIssueParser, lib: true do describe '#nodes_visible_to_user' do context 'when the link has a data-issue attribute' do - before { link['data-external-issue'] = 123 } + before do + link['data-external-issue'] = 123 + end levels = [ProjectFeature::DISABLED, ProjectFeature::PRIVATE, ProjectFeature::ENABLED] diff --git a/spec/lib/banzai/reference_parser/label_parser_spec.rb b/spec/lib/banzai/reference_parser/label_parser_spec.rb index 8c540d35ddd..ddd699f3c25 100644 --- a/spec/lib/banzai/reference_parser/label_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/label_parser_spec.rb @@ -11,7 +11,9 @@ describe Banzai::ReferenceParser::LabelParser, lib: true do describe '#nodes_visible_to_user' do context 'when the link has a data-issue attribute' do - before { link['data-label'] = label.id.to_s } + before do + link['data-label'] = label.id.to_s + end it_behaves_like "referenced feature visibility", "issues", "merge_requests" end diff --git a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb index 2d4d589ae34..72d4f3bc18e 100644 --- a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb @@ -11,7 +11,9 @@ describe Banzai::ReferenceParser::MilestoneParser, lib: true do describe '#nodes_visible_to_user' do context 'when the link has a data-issue attribute' do - before { link['data-milestone'] = milestone.id.to_s } + before do + link['data-milestone'] = milestone.id.to_s + end it_behaves_like "referenced feature visibility", "issues", "merge_requests" end diff --git a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb index d217a775802..620875ece20 100644 --- a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb @@ -4,20 +4,199 @@ describe Banzai::ReferenceParser::SnippetParser, lib: true do include ReferenceParserHelpers let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } - let(:snippet) { create(:snippet, project: project) } + let(:external_user) { create(:user, :external) } + let(:project_member) { create(:user) } + subject { described_class.new(project, user) } let(:link) { empty_html_link } + def visible_references(snippet_visibility, user = nil) + snippet = create(:project_snippet, snippet_visibility, project: project) + link['data-project'] = project.id.to_s + link['data-snippet'] = snippet.id.to_s + + subject.nodes_visible_to_user(user, [link]) + end + + before do + project.add_user(project_member, :developer) + end + describe '#nodes_visible_to_user' do - context 'when the link has a data-issue attribute' do - before { link['data-snippet'] = snippet.id.to_s } + context 'when a project is public and the snippets feature is enabled for everyone' do + before do + project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::ENABLED) + end + + it 'creates a reference for guest for a public snippet' do + expect(visible_references(:public)).to eq([link]) + end + + it 'creates a reference for a regular user for a public snippet' do + expect(visible_references(:public, user)).to eq([link]) + end + + it 'creates a reference for a regular user for an internal snippet' do + expect(visible_references(:internal, user)).to eq([link]) + end + + it 'does not create a reference for an external user for an internal snippet' do + expect(visible_references(:internal, external_user)).to be_empty + end + + it 'creates a reference for a project member for a private snippet' do + expect(visible_references(:private, project_member)).to eq([link]) + end + + it 'does not create a reference for a regular user for a private snippet' do + expect(visible_references(:private, user)).to be_empty + end + end + + context 'when a project is public and the snippets feature is enabled for project team members' do + before do + project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::PRIVATE) + end + + it 'creates a reference for a project member for a public snippet' do + expect(visible_references(:public, project_member)).to eq([link]) + end + + it 'does not create a reference for guest for a public snippet' do + expect(visible_references(:public, nil)).to be_empty + end + + it 'does not create a reference for a regular user for a public snippet' do + expect(visible_references(:public, user)).to be_empty + end + + it 'creates a reference for a project member for an internal snippet' do + expect(visible_references(:internal, project_member)).to eq([link]) + end + + it 'does not create a reference for a regular user for an internal snippet' do + expect(visible_references(:internal, user)).to be_empty + end + + it 'creates a reference for a project member for a private snippet' do + expect(visible_references(:private, project_member)).to eq([link]) + end + + it 'does not create a reference for a regular user for a private snippet' do + expect(visible_references(:private, user)).to be_empty + end + end + + context 'when a project is internal and the snippets feature is enabled for everyone' do + before do + project.update_attribute(:visibility, Gitlab::VisibilityLevel::INTERNAL) + project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::ENABLED) + end + + it 'does not create a reference for guest for a public snippet' do + expect(visible_references(:public)).to be_empty + end + + it 'does not create a reference for an external user for a public snippet' do + expect(visible_references(:public, external_user)).to be_empty + end - it_behaves_like "referenced feature visibility", "snippets" + it 'creates a reference for a regular user for a public snippet' do + expect(visible_references(:public, user)).to eq([link]) + end + + it 'creates a reference for a regular user for an internal snippet' do + expect(visible_references(:internal, user)).to eq([link]) + end + + it 'does not create a reference for an external user for an internal snippet' do + expect(visible_references(:internal, external_user)).to be_empty + end + + it 'creates a reference for a project member for a private snippet' do + expect(visible_references(:private, project_member)).to eq([link]) + end + + it 'does not create a reference for a regular user for a private snippet' do + expect(visible_references(:private, user)).to be_empty + end + end + + context 'when a project is internal and the snippets feature is enabled for project team members' do + before do + project.update_attribute(:visibility, Gitlab::VisibilityLevel::INTERNAL) + project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::PRIVATE) + end + + it 'creates a reference for a project member for a public snippet' do + expect(visible_references(:public, project_member)).to eq([link]) + end + + it 'does not create a reference for guest for a public snippet' do + expect(visible_references(:public, nil)).to be_empty + end + + it 'does not create reference for a regular user for a public snippet' do + expect(visible_references(:public, user)).to be_empty + end + + it 'creates a reference for a project member for an internal snippet' do + expect(visible_references(:internal, project_member)).to eq([link]) + end + + it 'does not create a reference for a regular user for an internal snippet' do + expect(visible_references(:internal, user)).to be_empty + end + + it 'creates a reference for a project member for a private snippet' do + expect(visible_references(:private, project_member)).to eq([link]) + end + + it 'does not create reference for a regular user for a private snippet' do + expect(visible_references(:private, user)).to be_empty + end + end + + context 'when a project is private and the snippets feature is enabled for project team members' do + before do + project.update_attribute(:visibility, Gitlab::VisibilityLevel::PRIVATE) + project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::PRIVATE) + end + + it 'creates a reference for a project member for a public snippet' do + expect(visible_references(:public, project_member)).to eq([link]) + end + + it 'does not create a reference for guest for a public snippet' do + expect(visible_references(:public, nil)).to be_empty + end + + it 'does not create a reference for a regular user for a public snippet' do + expect(visible_references(:public, user)).to be_empty + end + + it 'creates a reference for a project member for an internal snippet' do + expect(visible_references(:internal, project_member)).to eq([link]) + end + + it 'does not create a reference for a regular user for an internal snippet' do + expect(visible_references(:internal, user)).to be_empty + end + + it 'creates a reference for a project member for a private snippet' do + expect(visible_references(:private, project_member)).to eq([link]) + end + + it 'does not create a reference for a regular user for a private snippet' do + expect(visible_references(:private, user)).to be_empty + end end end describe '#referenced_by' do + let(:snippet) { create(:snippet, project: project) } describe 'when the link has a data-snippet attribute' do context 'using an existing snippet ID' do it 'returns an Array of snippets' do @@ -31,7 +210,7 @@ describe Banzai::ReferenceParser::SnippetParser, lib: true do it 'returns an empty Array' do link['data-snippet'] = '' - expect(subject.referenced_by([link])).to eq([]) + expect(subject.referenced_by([link])).to be_empty end end end diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb index 592ed0d2b98..4d560667342 100644 --- a/spec/lib/banzai/reference_parser/user_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb @@ -43,18 +43,9 @@ describe Banzai::ReferenceParser::UserParser, lib: true do expect(subject.referenced_by([link])).to eq([user]) end - context 'when RequestStore is active' do + context 'when RequestStore is active', :request_store do let(:other_user) { create(:user) } - before do - RequestStore.begin! - end - - after do - RequestStore.end! - RequestStore.clear! - end - it 'does not return users from the first call in the second' do link['data-user'] = user.id.to_s diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index fe2c00bb2ca..af0e7855a9b 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' module Ci - describe GitlabCiYamlProcessor, lib: true do + describe GitlabCiYamlProcessor, :lib do + subject { described_class.new(config, path) } let(:path) { 'path' } describe 'our current .gitlab-ci.yml' do @@ -82,6 +83,67 @@ module Ci end end + describe '#stage_seeds' do + context 'when no refs policy is specified' do + let(:config) do + YAML.dump(production: { stage: 'deploy', script: 'cap prod' }, + rspec: { stage: 'test', script: 'rspec' }, + spinach: { stage: 'test', script: 'spinach' }) + end + + let(:pipeline) { create(:ci_empty_pipeline) } + + it 'correctly fabricates a stage seeds object' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 2 + expect(seeds.first.stage[:name]).to eq 'test' + expect(seeds.second.stage[:name]).to eq 'deploy' + expect(seeds.first.builds.dig(0, :name)).to eq 'rspec' + expect(seeds.first.builds.dig(1, :name)).to eq 'spinach' + expect(seeds.second.builds.dig(0, :name)).to eq 'production' + end + end + + context 'when refs policy is specified' do + let(:config) do + YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['master'] }, + spinach: { stage: 'test', script: 'spinach', only: ['tags'] }) + end + + let(:pipeline) do + create(:ci_empty_pipeline, ref: 'feature', tag: true) + end + + it 'returns stage seeds only assigned to master to master' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 1 + expect(seeds.first.stage[:name]).to eq 'test' + expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' + end + end + + context 'when source policy is specified' do + let(:config) do + YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] }, + spinach: { stage: 'test', script: 'spinach', only: ['schedules'] }) + end + + let(:pipeline) do + create(:ci_empty_pipeline, source: :schedule) + end + + it 'returns stage seeds only assigned to schedules' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 1 + expect(seeds.first.stage[:name]).to eq 'test' + expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' + end + end + end + describe "#builds_for_ref" do let(:type) { 'test' } @@ -176,26 +238,44 @@ module Ci expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) end - it "returns builds if only has a triggers keyword specified and a trigger is provided" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["triggers"] } - }) + it "returns builds if only has special keywords specified and source matches" do + possibilities = [{ keyword: 'pushes', source: 'push' }, + { keyword: 'web', source: 'web' }, + { keyword: 'triggers', source: 'trigger' }, + { keyword: 'schedules', source: 'schedule' }, + { keyword: 'api', source: 'api' }, + { keyword: 'external', source: 'external' }] - config_processor = GitlabCiYamlProcessor.new(config, path) + possibilities.each do |possibility| + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: [possibility[:keyword]] } + }) - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, true).size).to eq(1) + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1) + end end - it "does not return builds if only has a triggers keyword specified and no trigger is provided" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["triggers"] } - }) + it "does not return builds if only has special keywords specified and source doesn't match" do + possibilities = [{ keyword: 'pushes', source: 'web' }, + { keyword: 'web', source: 'push' }, + { keyword: 'triggers', source: 'schedule' }, + { keyword: 'schedules', source: 'external' }, + { keyword: 'api', source: 'trigger' }, + { keyword: 'external', source: 'api' }] - config_processor = GitlabCiYamlProcessor.new(config, path) + possibilities.each do |possibility| + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: [possibility[:keyword]] } + }) - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0) + end end it "returns builds if only has current repository path" do @@ -332,26 +412,44 @@ module Ci expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) end - it "does not return builds if except has a triggers keyword specified and a trigger is provided" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["triggers"] } - }) + it "does not return builds if except has special keywords specified and source matches" do + possibilities = [{ keyword: 'pushes', source: 'push' }, + { keyword: 'web', source: 'web' }, + { keyword: 'triggers', source: 'trigger' }, + { keyword: 'schedules', source: 'schedule' }, + { keyword: 'api', source: 'api' }, + { keyword: 'external', source: 'external' }] - config_processor = GitlabCiYamlProcessor.new(config, path) + possibilities.each do |possibility| + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: [possibility[:keyword]] } + }) + + config_processor = GitlabCiYamlProcessor.new(config, path) - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, true).size).to eq(0) + expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0) + end end - it "returns builds if except has a triggers keyword specified and no trigger is provided" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["triggers"] } - }) + it "returns builds if except has special keywords specified and source doesn't match" do + possibilities = [{ keyword: 'pushes', source: 'web' }, + { keyword: 'web', source: 'push' }, + { keyword: 'triggers', source: 'schedule' }, + { keyword: 'schedules', source: 'external' }, + { keyword: 'api', source: 'trigger' }, + { keyword: 'external', source: 'api' }] - config_processor = GitlabCiYamlProcessor.new(config, path) + possibilities.each do |possibility| + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: [possibility[:keyword]] } + }) - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1) + end end it "does not return builds if except has current repository path" do @@ -498,62 +596,117 @@ module Ci end describe "Image and service handling" do - it "returns image and service when defined" do - config = YAML.dump({ - image: "ruby:2.1", - services: ["mysql"], - before_script: ["pwd"], - rspec: { script: "rspec" } - }) + context "when extended docker configuration is used" do + it "returns image and service when defined" do + config = YAML.dump({ image: { name: "ruby:2.1" }, + services: ["mysql", { name: "docker:dind", alias: "docker" }], + before_script: ["pwd"], + rspec: { script: "rspec" } }) - config_processor = GitlabCiYamlProcessor.new(config, path) + config_processor = GitlabCiYamlProcessor.new(config, path) - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - image: "ruby:2.1", - services: ["mysql"] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + image: { name: "ruby:2.1" }, + services: [{ name: "mysql" }, { name: "docker:dind", alias: "docker" }] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end + + it "returns image and service when overridden for job" do + config = YAML.dump({ image: "ruby:2.1", + services: ["mysql"], + before_script: ["pwd"], + rspec: { image: { name: "ruby:2.5" }, + services: [{ name: "postgresql", alias: "db-pg" }, "docker:dind"], script: "rspec" } }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + image: { name: "ruby:2.5" }, + services: [{ name: "postgresql", alias: "db-pg" }, { name: "docker:dind" }] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end end - it "returns image and service when overridden for job" do - config = YAML.dump({ - image: "ruby:2.1", - services: ["mysql"], - before_script: ["pwd"], - rspec: { image: "ruby:2.5", services: ["postgresql"], script: "rspec" } - }) + context "when etended docker configuration is not used" do + it "returns image and service when defined" do + config = YAML.dump({ image: "ruby:2.1", + services: ["mysql", "docker:dind"], + before_script: ["pwd"], + rspec: { script: "rspec" } }) - config_processor = GitlabCiYamlProcessor.new(config, path) + config_processor = GitlabCiYamlProcessor.new(config, path) - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - image: "ruby:2.5", - services: ["postgresql"] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + image: { name: "ruby:2.1" }, + services: [{ name: "mysql" }, { name: "docker:dind" }] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end + + it "returns image and service when overridden for job" do + config = YAML.dump({ image: "ruby:2.1", + services: ["mysql"], + before_script: ["pwd"], + rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } }) + + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + image: { name: "ruby:2.5" }, + services: [{ name: "postgresql" }, { name: "docker:dind" }] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end end end @@ -786,8 +939,8 @@ module Ci coverage_regex: nil, tag_list: [], options: { - image: "ruby:2.1", - services: ["mysql"], + image: { name: "ruby:2.1" }, + services: [{ name: "mysql" }], artifacts: { name: "custom_name", paths: ["logs/", "binaries/"], @@ -1163,7 +1316,7 @@ EOT config = YAML.dump({ image: ["test"], rspec: { script: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image config should be a string") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image config should be a hash or a string") end it "returns errors if job name is blank" do @@ -1184,35 +1337,35 @@ EOT config = YAML.dump({ rspec: { script: "test", image: ["test"] } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:image config should be a string") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:image config should be a hash or a string") end it "returns errors if services parameter is not an array" do config = YAML.dump({ services: "test", rspec: { script: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services config should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services config should be a array") end it "returns errors if services parameter is not an array of strings" do config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services config should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "service config should be a hash or a string") end it "returns errors if job services parameter is not an array" do config = YAML.dump({ rspec: { script: "test", services: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be a array") end it "returns errors if job services parameter is not an array of strings" do config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "service config should be a hash or a string") end it "returns error if job configuration is invalid" do @@ -1226,7 +1379,7 @@ EOT config = YAML.dump({ extra: { script: 'rspec', services: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra:services config should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra:services config should be a array") end it "returns errors if there are no jobs defined" do diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index 33ab005667a..2b26a318583 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -77,7 +77,10 @@ describe ExtractsPath, lib: true do context 'without a path' do let(:params) { { ref: 'v1.0.0.atom' } } - before { assign_ref_vars } + + before do + assign_ref_vars + end it 'sets the un-suffixed version as @ref' do expect(@ref).to eq('v1.0.0') diff --git a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb index 94dcddcc30c..fc72df575be 100644 --- a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb +++ b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb @@ -40,7 +40,9 @@ describe Gitlab::Auth::UniqueIpsLimiter, :redis, lib: true do end context 'allow 2 unique ips' do - before { current_application_settings.update!(unique_ips_limit_per_user: 2) } + before do + current_application_settings.update!(unique_ips_limit_per_user: 2) + end it 'blocks user trying to login from third ip' do change_ip('ip1') diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 50bc3ef1b7c..d09da951869 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -17,7 +17,11 @@ describe Gitlab::Auth, lib: true do end it 'OPTIONAL_SCOPES contains all non-default scopes' do - expect(subject::OPTIONAL_SCOPES).to eq [:read_user, :openid] + expect(subject::OPTIONAL_SCOPES).to eq %i[read_user read_registry openid] + end + + it 'REGISTRY_SCOPES contains all registry related scopes' do + expect(subject::REGISTRY_SCOPES).to eq %i[read_registry] end end @@ -143,6 +147,13 @@ describe Gitlab::Auth, lib: true do expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, full_authentication_abilities)) end + it 'succeeds for personal access tokens with the `read_registry` scope' do + personal_access_token = create(:personal_access_token, scopes: ['read_registry']) + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') + expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, [:read_container_image])) + end + it 'succeeds if it is an impersonation token' do impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api']) @@ -150,18 +161,11 @@ describe Gitlab::Auth, lib: true do expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(impersonation_token.user, nil, :personal_token, full_authentication_abilities)) end - it 'fails for personal access tokens with other scopes' do + it 'limits abilities based on scope' do personal_access_token = create(:personal_access_token, scopes: ['read_user']) - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') - expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) - end - - it 'fails for impersonation token with other scopes' do - impersonation_token = create(:personal_access_token, scopes: ['read_user']) - - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') - expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') + expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, [])) end it 'fails if password is nil' do @@ -200,6 +204,12 @@ describe Gitlab::Auth, lib: true do expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login) expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new) end + + it 'throws an error suggesting user create a PAT when internal auth is disabled' do + allow_any_instance_of(ApplicationSetting).to receive(:signin_enabled?) { false } + + expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalTokenError) + end end describe 'find_with_user_password' do diff --git a/spec/lib/gitlab/background_migration_spec.rb b/spec/lib/gitlab/background_migration_spec.rb new file mode 100644 index 00000000000..f2073b9bcb3 --- /dev/null +++ b/spec/lib/gitlab/background_migration_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration do + describe '.steal' do + it 'steals jobs from a queue' do + queue = [double(:job, args: ['Foo', [10, 20]])] + + allow(Sidekiq::Queue).to receive(:new). + with(BackgroundMigrationWorker.sidekiq_options['queue']). + and_return(queue) + + expect(queue[0]).to receive(:delete) + + expect(described_class).to receive(:perform).with('Foo', [10, 20]) + + described_class.steal('Foo') + end + + it 'does not steal jobs for a different migration' do + queue = [double(:job, args: ['Foo', [10, 20]])] + + allow(Sidekiq::Queue).to receive(:new). + with(BackgroundMigrationWorker.sidekiq_options['queue']). + and_return(queue) + + expect(described_class).not_to receive(:perform) + + expect(queue[0]).not_to receive(:delete) + + described_class.steal('Bar') + end + end + + describe '.perform' do + it 'performs a background migration' do + instance = double(:instance) + klass = double(:klass, new: instance) + + expect(described_class).to receive(:const_get). + with('Foo'). + and_return(klass) + + expect(instance).to receive(:perform).with(10, 20) + + described_class.perform('Foo', [10, 20]) + end + end +end diff --git a/spec/lib/gitlab/backup/repository_spec.rb b/spec/lib/gitlab/backup/repository_spec.rb new file mode 100644 index 00000000000..51c1e9d657b --- /dev/null +++ b/spec/lib/gitlab/backup/repository_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe Backup::Repository, lib: true do + let(:progress) { StringIO.new } + let!(:project) { create(:empty_project) } + + before do + allow(progress).to receive(:puts) + allow(progress).to receive(:print) + + allow_any_instance_of(String).to receive(:color) do |string, _color| + string + end + + allow_any_instance_of(described_class).to receive(:progress).and_return(progress) + end + + describe '#dump' do + describe 'repo failure' do + before do + allow_any_instance_of(Repository).to receive(:empty_repo?).and_raise(Rugged::OdbError) + allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0]) + end + + it 'does not raise error' do + expect { described_class.new.dump }.not_to raise_error + end + + it 'shows the appropriate error' do + described_class.new.dump + + expect(progress).to have_received(:puts).with("Ignoring repository error and continuing backing up project: #{project.full_path} - Rugged::OdbError") + end + end + + describe 'command failure' do + before do + allow_any_instance_of(Repository).to receive(:empty_repo?).and_return(false) + allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1]) + end + + it 'shows the appropriate error' do + described_class.new.dump + + expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error") + end + end + end + + describe '#restore' do + describe 'command failure' do + before do + allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1]) + end + + it 'shows the appropriate error' do + described_class.new.restore + + expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error") + end + end + end +end diff --git a/spec/lib/gitlab/badge/build/status_spec.rb b/spec/lib/gitlab/badge/build/status_spec.rb index 3c5414701a7..6abf4ca46a9 100644 --- a/spec/lib/gitlab/badge/build/status_spec.rb +++ b/spec/lib/gitlab/badge/build/status_spec.rb @@ -29,7 +29,9 @@ describe Gitlab::Badge::Build::Status do let!(:build) { create_build(project, sha, branch) } context 'build success' do - before { build.success! } + before do + build.success! + end describe '#status' do it 'is successful' do @@ -39,7 +41,9 @@ describe Gitlab::Badge::Build::Status do end context 'build failed' do - before { build.drop! } + before do + build.drop! + end describe '#status' do it 'failed' do diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb index ec6d3e34a96..3799a324db4 100644 --- a/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb +++ b/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb @@ -4,7 +4,9 @@ describe Gitlab::ChatCommands::Presenters::IssueSearch do let(:project) { create(:empty_project) } let(:message) { subject[:text] } - before { create_list(:issue, 2, project: project) } + before do + create_list(:issue, 2, project: project) + end subject { described_class.new(project.issues).present } diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index c0c309d8179..643e590438a 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -20,7 +20,9 @@ describe Gitlab::Checks::ChangeAccess, lib: true do ).exec end - before { project.add_developer(user) } + before do + project.add_developer(user) + end context 'without failed checks' do it "doesn't raise an error" do @@ -50,7 +52,9 @@ describe Gitlab::Checks::ChangeAccess, lib: true do let!(:protected_tag) { create(:protected_tag, project: project, name: 'v*') } context 'as master' do - before { project.add_master(user) } + before do + project.add_master(user) + end context 'deletion' do let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' } diff --git a/spec/lib/gitlab/ci/build/image_spec.rb b/spec/lib/gitlab/ci/build/image_spec.rb index 382385dfd6b..773a52cdfbc 100644 --- a/spec/lib/gitlab/ci/build/image_spec.rb +++ b/spec/lib/gitlab/ci/build/image_spec.rb @@ -10,12 +10,28 @@ describe Gitlab::Ci::Build::Image do let(:image_name) { 'ruby:2.1' } let(:job) { create(:ci_build, options: { image: image_name } ) } - it 'fabricates an object of the proper class' do - is_expected.to be_kind_of(described_class) + context 'when image is defined as string' do + it 'fabricates an object of the proper class' do + is_expected.to be_kind_of(described_class) + end + + it 'populates fabricated object with the proper name attribute' do + expect(subject.name).to eq(image_name) + end end - it 'populates fabricated object with the proper name attribute' do - expect(subject.name).to eq(image_name) + context 'when image is defined as hash' do + let(:entrypoint) { '/bin/sh' } + let(:job) { create(:ci_build, options: { image: { name: image_name, entrypoint: entrypoint } } ) } + + it 'fabricates an object of the proper class' do + is_expected.to be_kind_of(described_class) + end + + it 'populates fabricated object with the proper attributes' do + expect(subject.name).to eq(image_name) + expect(subject.entrypoint).to eq(entrypoint) + end end context 'when image name is empty' do @@ -41,10 +57,39 @@ describe Gitlab::Ci::Build::Image do let(:service_image_name) { 'postgres' } let(:job) { create(:ci_build, options: { services: [service_image_name] }) } - it 'fabricates an non-empty array of objects' do - is_expected.to be_kind_of(Array) - is_expected.not_to be_empty - expect(subject.first.name).to eq(service_image_name) + context 'when service is defined as string' do + it 'fabricates an non-empty array of objects' do + is_expected.to be_kind_of(Array) + is_expected.not_to be_empty + end + + it 'populates fabricated objects with the proper name attributes' do + expect(subject.first).to be_kind_of(described_class) + expect(subject.first.name).to eq(service_image_name) + end + end + + context 'when service is defined as hash' do + let(:service_entrypoint) { '/bin/sh' } + let(:service_alias) { 'db' } + let(:service_command) { 'sleep 30' } + let(:job) do + create(:ci_build, options: { services: [{ name: service_image_name, entrypoint: service_entrypoint, + alias: service_alias, command: service_command }] }) + end + + it 'fabricates an non-empty array of objects' do + is_expected.to be_kind_of(Array) + is_expected.not_to be_empty + expect(subject.first).to be_kind_of(described_class) + end + + it 'populates fabricated objects with the proper attributes' do + expect(subject.first.name).to eq(service_image_name) + expect(subject.first.entrypoint).to eq(service_entrypoint) + expect(subject.first.alias).to eq(service_alias) + expect(subject.first.command).to eq(service_command) + end end context 'when service image name is empty' do diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb index 2ed120f356a..878b1d6b862 100644 --- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb @@ -4,7 +4,9 @@ describe Gitlab::Ci::Config::Entry::Cache do let(:entry) { described_class.new(config) } describe 'validations' do - before { entry.compose! } + before do + entry.compose! + end context 'when entry config value is correct' do let(:config) do diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb index c330e609337..3c0007f4d57 100644 --- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb @@ -3,7 +3,9 @@ require 'spec_helper' describe Gitlab::Ci::Config::Entry::Environment do let(:entry) { described_class.new(config) } - before { entry.compose! } + before do + entry.compose! + end context 'when configuration is a string' do let(:config) { 'production' } diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index 23270ad5053..293f112b2b0 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -33,7 +33,9 @@ describe Gitlab::Ci::Config::Entry::Global do end describe '#compose!' do - before { global.compose! } + before do + global.compose! + end it 'creates nodes hash' do expect(global.descendants).to be_an Array @@ -79,7 +81,9 @@ describe Gitlab::Ci::Config::Entry::Global do end context 'when composed' do - before { global.compose! } + before do + global.compose! + end describe '#errors' do it 'has no errors' do @@ -95,13 +99,13 @@ describe Gitlab::Ci::Config::Entry::Global do describe '#image_value' do it 'returns valid image' do - expect(global.image_value).to eq 'ruby:2.2' + expect(global.image_value).to eq(name: 'ruby:2.2') end end describe '#services_value' do it 'returns array of services' do - expect(global.services_value).to eq ['postgres:9.1', 'mysql:5.5'] + expect(global.services_value).to eq [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }] end end @@ -150,8 +154,8 @@ describe Gitlab::Ci::Config::Entry::Global do script: %w[rspec ls], before_script: %w(ls pwd), commands: "ls\npwd\nrspec\nls", - image: 'ruby:2.2', - services: ['postgres:9.1', 'mysql:5.5'], + image: { name: 'ruby:2.2' }, + services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], stage: 'test', cache: { key: 'k', untracked: true, paths: ['public/'] }, variables: { 'VAR' => 'value' }, @@ -161,8 +165,8 @@ describe Gitlab::Ci::Config::Entry::Global do before_script: [], script: %w[spinach], commands: 'spinach', - image: 'ruby:2.2', - services: ['postgres:9.1', 'mysql:5.5'], + image: { name: 'ruby:2.2' }, + services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], stage: 'test', cache: { key: 'k', untracked: true, paths: ['public/'] }, variables: {}, @@ -175,7 +179,9 @@ describe Gitlab::Ci::Config::Entry::Global do end context 'when most of entires not defined' do - before { global.compose! } + before do + global.compose! + end let(:hash) do { cache: { key: 'a' }, rspec: { script: %w[ls] } } @@ -218,7 +224,9 @@ describe Gitlab::Ci::Config::Entry::Global do # details. # context 'when entires specified but not defined' do - before { global.compose! } + before do + global.compose! + end let(:hash) do { variables: nil, rspec: { script: 'rspec' } } @@ -233,7 +241,9 @@ describe Gitlab::Ci::Config::Entry::Global do end context 'when configuration is not valid' do - before { global.compose! } + before do + global.compose! + end context 'when before script is not an array' do let(:hash) do @@ -297,7 +307,9 @@ describe Gitlab::Ci::Config::Entry::Global do end describe '#[]' do - before { global.compose! } + before do + global.compose! + end let(:hash) do { cache: { key: 'a' }, rspec: { script: 'ls' } } diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb index 3c99cb0a1ee..bca22e39500 100644 --- a/spec/lib/gitlab/ci/config/entry/image_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb @@ -3,43 +3,104 @@ require 'spec_helper' describe Gitlab::Ci::Config::Entry::Image do let(:entry) { described_class.new(config) } - describe 'validation' do - context 'when entry config value is correct' do - let(:config) { 'ruby:2.2' } + context 'when configuration is a string' do + let(:config) { 'ruby:2.2' } - describe '#value' do - it 'returns image string' do - expect(entry.value).to eq 'ruby:2.2' - end + describe '#value' do + it 'returns image hash' do + expect(entry.value).to eq({ name: 'ruby:2.2' }) end + end + + describe '#errors' do + it 'does not append errors' do + expect(entry.errors).to be_empty + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#image' do + it "returns image's name" do + expect(entry.name).to eq 'ruby:2.2' + end + end - describe '#errors' do - it 'does not append errors' do - expect(entry.errors).to be_empty - end + describe '#entrypoint' do + it "returns image's entrypoint" do + expect(entry.entrypoint).to be_nil end + end + end - describe '#valid?' do - it 'is valid' do - expect(entry).to be_valid - end + context 'when configuration is a hash' do + let(:config) { { name: 'ruby:2.2', entrypoint: '/bin/sh' } } + + describe '#value' do + it 'returns image hash' do + expect(entry.value).to eq(config) end end - context 'when entry value is not correct' do - let(:config) { ['ruby:2.2'] } + describe '#errors' do + it 'does not append errors' do + expect(entry.errors).to be_empty + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end - describe '#errors' do - it 'saves errors' do - expect(entry.errors) - .to include 'image config should be a string' - end + describe '#image' do + it "returns image's name" do + expect(entry.name).to eq 'ruby:2.2' end + end + + describe '#entrypoint' do + it "returns image's entrypoint" do + expect(entry.entrypoint).to eq '/bin/sh' + end + end + end + + context 'when entry value is not correct' do + let(:config) { ['ruby:2.2'] } + + describe '#errors' do + it 'saves errors' do + expect(entry.errors) + .to include 'image config should be a hash or a string' + end + end + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + end + + context 'when unexpected key is specified' do + let(:config) { { name: 'ruby:2.2', non_existing: 'test' } } + + describe '#errors' do + it 'saves errors' do + expect(entry.errors) + .to include 'image config contains unknown keys: non_existing' + end + end - describe '#valid?' do - it 'is not valid' do - expect(entry).not_to be_valid - end + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid end end end diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index 9249bb9c172..92cba689f47 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -18,7 +18,9 @@ describe Gitlab::Ci::Config::Entry::Job do end describe 'validations' do - before { entry.compose! } + before do + entry.compose! + end context 'when entry config value is correct' do let(:config) { { script: 'rspec' } } @@ -97,14 +99,16 @@ describe Gitlab::Ci::Config::Entry::Job do let(:deps) { double('deps', '[]' => unspecified) } context 'when job config overrides global config' do - before { entry.compose!(deps) } + before do + entry.compose!(deps) + end let(:config) do { script: 'rspec', image: 'some_image', cache: { key: 'test' } } end it 'overrides global config' do - expect(entry[:image].value).to eq 'some_image' + expect(entry[:image].value).to eq(name: 'some_image') expect(entry[:cache].value).to eq(key: 'test') end end @@ -125,10 +129,14 @@ describe Gitlab::Ci::Config::Entry::Job do end context 'when composed' do - before { entry.compose! } + before do + entry.compose! + end describe '#value' do - before { entry.compose! } + before do + entry.compose! + end context 'when entry is correct' do let(:config) do diff --git a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb index 7d104372ac6..c0a2b6517e3 100644 --- a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb @@ -4,7 +4,9 @@ describe Gitlab::Ci::Config::Entry::Jobs do let(:entry) { described_class.new(config) } describe 'validations' do - before { entry.compose! } + before do + entry.compose! + end context 'when entry config value is correct' do let(:config) { { rspec: { script: 'rspec' } } } @@ -48,7 +50,9 @@ describe Gitlab::Ci::Config::Entry::Jobs do end context 'when valid job entries composed' do - before { entry.compose! } + before do + entry.compose! + end let(:config) do { rspec: { script: 'rspec' }, diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb new file mode 100644 index 00000000000..7202fe525e4 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb @@ -0,0 +1,119 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Entry::Service do + let(:entry) { described_class.new(config) } + + before do + entry.compose! + end + + context 'when configuration is a string' do + let(:config) { 'postgresql:9.5' } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns valid hash' do + expect(entry.value).to include(name: 'postgresql:9.5') + end + end + + describe '#image' do + it "returns service's image name" do + expect(entry.name).to eq 'postgresql:9.5' + end + end + + describe '#alias' do + it "returns service's alias" do + expect(entry.alias).to be_nil + end + end + + describe '#command' do + it "returns service's command" do + expect(entry.command).to be_nil + end + end + end + + context 'when configuration is a hash' do + let(:config) do + { name: 'postgresql:9.5', alias: 'db', command: 'cmd', entrypoint: '/bin/sh' } + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns valid hash' do + expect(entry.value).to eq config + end + end + + describe '#image' do + it "returns service's image name" do + expect(entry.name).to eq 'postgresql:9.5' + end + end + + describe '#alias' do + it "returns service's alias" do + expect(entry.alias).to eq 'db' + end + end + + describe '#command' do + it "returns service's command" do + expect(entry.command).to eq 'cmd' + end + end + + describe '#entrypoint' do + it "returns service's entrypoint" do + expect(entry.entrypoint).to eq '/bin/sh' + end + end + end + + context 'when entry value is not correct' do + let(:config) { ['postgresql:9.5'] } + + describe '#errors' do + it 'saves errors' do + expect(entry.errors) + .to include 'service config should be a hash or a string' + end + end + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + end + + context 'when unexpected key is specified' do + let(:config) { { name: 'postgresql:9.5', non_existing: 'test' } } + + describe '#errors' do + it 'saves errors' do + expect(entry.errors) + .to include 'service config contains unknown keys: non_existing' + end + end + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/services_spec.rb b/spec/lib/gitlab/ci/config/entry/services_spec.rb index 66fad3b6b16..7c4319aee63 100644 --- a/spec/lib/gitlab/ci/config/entry/services_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/services_spec.rb @@ -3,37 +3,32 @@ require 'spec_helper' describe Gitlab::Ci::Config::Entry::Services do let(:entry) { described_class.new(config) } - describe 'validations' do - context 'when entry config value is correct' do - let(:config) { ['postgres:9.1', 'mysql:5.5'] } + before do + entry.compose! + end - describe '#value' do - it 'returns array of services as is' do - expect(entry.value).to eq config - end - end + context 'when configuration is valid' do + let(:config) { ['postgresql:9.5', { name: 'postgresql:9.1', alias: 'postgres_old' }] } - describe '#valid?' do - it 'is valid' do - expect(entry).to be_valid - end + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid end end - context 'when entry value is not correct' do - let(:config) { 'ls' } - - describe '#errors' do - it 'saves errors' do - expect(entry.errors) - .to include 'services config should be an array of strings' - end + describe '#value' do + it 'returns valid array' do + expect(entry.value).to eq([{ name: 'postgresql:9.5' }, { name: 'postgresql:9.1', alias: 'postgres_old' }]) end + end + end + + context 'when configuration is invalid' do + let(:config) { 'postgresql:9.5' } - describe '#valid?' do - it 'is not valid' do - expect(entry).not_to be_valid - end + describe '#valid?' do + it 'is invalid' do + expect(entry).not_to be_valid end end end diff --git a/spec/lib/gitlab/ci/stage/seed_spec.rb b/spec/lib/gitlab/ci/stage/seed_spec.rb new file mode 100644 index 00000000000..d7e91a5a62c --- /dev/null +++ b/spec/lib/gitlab/ci/stage/seed_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe Gitlab::Ci::Stage::Seed do + let(:pipeline) { create(:ci_empty_pipeline) } + + let(:builds) do + [{ name: 'rspec' }, { name: 'spinach' }] + end + + subject do + described_class.new(pipeline, 'test', builds) + end + + describe '#stage' do + it 'returns hash attributes of a stage' do + expect(subject.stage).to be_a Hash + expect(subject.stage).to include(:name, :project) + end + end + + describe '#builds' do + it 'returns hash attributes of all builds' do + expect(subject.builds.size).to eq 2 + expect(subject.builds).to all(include(ref: 'master')) + expect(subject.builds).to all(include(tag: false)) + expect(subject.builds).to all(include(project: pipeline.project)) + expect(subject.builds) + .to all(include(trigger_request: pipeline.trigger_requests.first)) + end + end + + describe '#user=' do + let(:user) { build(:user) } + + it 'assignes relevant pipeline attributes' do + subject.user = user + + expect(subject.builds).to all(include(user: user)) + end + end + + describe '#create!' do + it 'creates all stages and builds' do + subject.create! + + expect(pipeline.reload.stages.count).to eq 1 + expect(pipeline.reload.builds.count).to eq 2 + expect(pipeline.builds).to all(satisfy { |job| job.stage_id.present? }) + expect(pipeline.builds).to all(satisfy { |job| job.pipeline.present? }) + expect(pipeline.builds).to all(satisfy { |job| job.project.present? }) + expect(pipeline.stages) + .to all(satisfy { |stage| stage.pipeline.present? }) + expect(pipeline.stages) + .to all(satisfy { |stage| stage.project.present? }) + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb index 8ad9b7cdf07..114d2490490 100644 --- a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb +++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb @@ -47,7 +47,9 @@ describe Gitlab::Ci::Status::Build::Cancelable do describe '#has_action?' do context 'when user is allowed to update build' do - before { build.project.team << [user, :developer] } + before do + build.project.team << [user, :developer] + end it { is_expected.to have_action } end diff --git a/spec/lib/gitlab/ci/status/build/common_spec.rb b/spec/lib/gitlab/ci/status/build/common_spec.rb index 72bd7c4eb93..03d1f46b517 100644 --- a/spec/lib/gitlab/ci/status/build/common_spec.rb +++ b/spec/lib/gitlab/ci/status/build/common_spec.rb @@ -17,13 +17,17 @@ describe Gitlab::Ci::Status::Build::Common do describe '#has_details?' do context 'when user has access to read build' do - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end it { is_expected.to have_details } end context 'when user does not have access to read build' do - before { project.update(public_builds: false) } + before do + project.update(public_builds: false) + end it { is_expected.not_to have_details } end diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index 3f30b2c38f2..c8a97016f20 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -6,7 +6,9 @@ describe Gitlab::Ci::Status::Build::Factory do let(:status) { factory.fabricate! } let(:factory) { described_class.new(build, user) } - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end context 'when build is successful' do let(:build) { create(:ci_build, :success) } diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb index 0e15a5f3c6b..32b2e62e4e0 100644 --- a/spec/lib/gitlab/ci/status/build/play_spec.rb +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -28,7 +28,9 @@ describe Gitlab::Ci::Status::Build::Play do end context 'when user can not push to the branch' do - before { build.project.add_developer(user) } + before do + build.project.add_developer(user) + end it { is_expected.not_to have_action } end diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb index 2db0f8d29bd..099d873fc01 100644 --- a/spec/lib/gitlab/ci/status/build/retryable_spec.rb +++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb @@ -47,7 +47,9 @@ describe Gitlab::Ci::Status::Build::Retryable do describe '#has_action?' do context 'when user is allowed to update build' do - before { build.project.team << [user, :developer] } + before do + build.project.team << [user, :developer] + end it { is_expected.to have_action } end diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb index 8d021c35a69..23902f26b1a 100644 --- a/spec/lib/gitlab/ci/status/build/stop_spec.rb +++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb @@ -19,7 +19,9 @@ describe Gitlab::Ci::Status::Build::Stop do describe '#has_action?' do context 'when user is allowed to update build' do - before { build.project.team << [user, :developer] } + before do + build.project.team << [user, :developer] + end it { is_expected.to have_action } end diff --git a/spec/lib/gitlab/ci/status/external/common_spec.rb b/spec/lib/gitlab/ci/status/external/common_spec.rb index 5a97d98b55f..b38fbee2486 100644 --- a/spec/lib/gitlab/ci/status/external/common_spec.rb +++ b/spec/lib/gitlab/ci/status/external/common_spec.rb @@ -4,9 +4,10 @@ describe Gitlab::Ci::Status::External::Common do let(:user) { create(:user) } let(:project) { external_status.project } let(:external_target_url) { 'http://example.gitlab.com/status' } + let(:external_description) { 'my description' } let(:external_status) do - create(:generic_commit_status, target_url: external_target_url) + create(:generic_commit_status, target_url: external_target_url, description: external_description) end subject do @@ -15,13 +16,21 @@ describe Gitlab::Ci::Status::External::Common do .extend(described_class) end + describe '#label' do + it 'returns description' do + expect(subject.label).to eq external_description + end + end + describe '#has_action?' do it { is_expected.not_to have_action } end describe '#has_details?' do context 'when user has access to read commit status' do - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end it { is_expected.to have_details } end diff --git a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb index d665674bf70..f5fd31e8d03 100644 --- a/spec/lib/gitlab/ci/status/pipeline/common_spec.rb +++ b/spec/lib/gitlab/ci/status/pipeline/common_spec.rb @@ -17,7 +17,9 @@ describe Gitlab::Ci::Status::Pipeline::Common do describe '#has_details?' do context 'when user has access to read pipeline' do - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end it { is_expected.to have_details } end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 3fdafd867da..30aa463faf8 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -7,7 +7,42 @@ describe Gitlab::Database::MigrationHelpers, lib: true do ) end - before { allow(model).to receive(:puts) } + before do + allow(model).to receive(:puts) + end + + describe '#add_timestamps_with_timezone' do + before do + allow(model).to receive(:transaction_open?).and_return(false) + end + + context 'using PostgreSQL' do + before do + allow(Gitlab::Database).to receive(:postgresql?).and_return(true) + allow(model).to receive(:disable_statement_timeout) + end + + it 'adds "created_at" and "updated_at" fields with the "datetime_with_timezone" data type' do + expect(model).to receive(:add_column).with(:foo, :created_at, :datetime_with_timezone, { null: false }) + expect(model).to receive(:add_column).with(:foo, :updated_at, :datetime_with_timezone, { null: false }) + + model.add_timestamps_with_timezone(:foo) + end + end + + context 'using MySQL' do + before do + allow(Gitlab::Database).to receive(:postgresql?).and_return(false) + end + + it 'adds "created_at" and "updated_at" fields with "datetime_with_timezone" data type' do + expect(model).to receive(:add_column).with(:foo, :created_at, :datetime_with_timezone, { null: false }) + expect(model).to receive(:add_column).with(:foo, :updated_at, :datetime_with_timezone, { null: false }) + + model.add_timestamps_with_timezone(:foo) + end + end + end describe '#add_concurrent_index' do context 'outside a transaction' do diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 9b1d66a1b1c..26e5d73d333 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -53,14 +53,18 @@ describe Gitlab::Database, lib: true do describe '.nulls_last_order' do context 'when using PostgreSQL' do - before { expect(described_class).to receive(:postgresql?).and_return(true) } + before do + expect(described_class).to receive(:postgresql?).and_return(true) + end it { expect(described_class.nulls_last_order('column', 'ASC')).to eq 'column ASC NULLS LAST'} it { expect(described_class.nulls_last_order('column', 'DESC')).to eq 'column DESC NULLS LAST'} end context 'when using MySQL' do - before { expect(described_class).to receive(:postgresql?).and_return(false) } + before do + expect(described_class).to receive(:postgresql?).and_return(false) + end it { expect(described_class.nulls_last_order('column', 'ASC')).to eq 'column IS NULL, column ASC'} it { expect(described_class.nulls_last_order('column', 'DESC')).to eq 'column DESC'} @@ -69,14 +73,18 @@ describe Gitlab::Database, lib: true do describe '.nulls_first_order' do context 'when using PostgreSQL' do - before { expect(described_class).to receive(:postgresql?).and_return(true) } + before do + expect(described_class).to receive(:postgresql?).and_return(true) + end it { expect(described_class.nulls_first_order('column', 'ASC')).to eq 'column ASC NULLS FIRST'} it { expect(described_class.nulls_first_order('column', 'DESC')).to eq 'column DESC NULLS FIRST'} end context 'when using MySQL' do - before { expect(described_class).to receive(:postgresql?).and_return(false) } + before do + expect(described_class).to receive(:postgresql?).and_return(false) + end it { expect(described_class.nulls_first_order('column', 'ASC')).to eq 'column ASC'} it { expect(described_class.nulls_first_order('column', 'DESC')).to eq 'column IS NULL, column DESC'} diff --git a/spec/lib/gitlab/diff/diff_refs_spec.rb b/spec/lib/gitlab/diff/diff_refs_spec.rb new file mode 100644 index 00000000000..a8173558c00 --- /dev/null +++ b/spec/lib/gitlab/diff/diff_refs_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe Gitlab::Diff::DiffRefs, lib: true do + let(:project) { create(:project, :repository) } + + describe '#compare_in' do + context 'with diff refs for the initial commit' do + let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') } + subject { commit.diff_refs } + + it 'returns an appropriate comparison' do + compare = subject.compare_in(project) + + expect(compare.diff_refs).to eq(subject) + end + end + + context 'with diff refs for a commit' do + let(:commit) { project.commit('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } + subject { commit.diff_refs } + + it 'returns an appropriate comparison' do + compare = subject.compare_in(project) + + expect(compare.diff_refs).to eq(subject) + end + end + + context 'with diff refs for a comparison through the base' do + subject do + described_class.new( + start_sha: '0b4bc9a49b562e85de7cc9e834518ea6828729b9', # feature + base_sha: 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f', + head_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a' # master + ) + end + + it 'returns an appropriate comparison' do + compare = subject.compare_in(project) + + expect(compare.diff_refs).to eq(subject) + end + end + + context 'with diff refs for a straight comparison' do + subject do + described_class.new( + start_sha: '0b4bc9a49b562e85de7cc9e834518ea6828729b9', # feature + base_sha: '0b4bc9a49b562e85de7cc9e834518ea6828729b9', + head_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a' # master + ) + end + + it 'returns an appropriate comparison' do + compare = subject.compare_in(project) + + expect(compare.diff_refs).to eq(subject) + end + end + end +end diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb index f2bc15d39d7..d81774c8b8f 100644 --- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb @@ -5,15 +5,7 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiff do let(:diff_files) { described_class.new(merge_request.merge_request_diff, diff_options: nil).diff_files } it 'does not highlight binary files' do - allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(double("text?" => false)) - - expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines) - - diff_files - end - - it 'does not highlight file if blob is not accessable' do - allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(nil) + allow_any_instance_of(Gitlab::Diff::File).to receive(:text?).and_return(false) expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines) @@ -21,7 +13,7 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiff do end it 'does not files marked as undiffable in .gitattributes' do - allow_any_instance_of(Repository).to receive(:diffable?).and_return(false) + allow_any_instance_of(Gitlab::Diff::File).to receive(:diffable?).and_return(false) expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines) diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index 050689b7c9a..f289131cc3a 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Diff::File, lib: true do let(:project) { create(:project, :repository) } let(:commit) { project.commit(sample_commit.id) } let(:diff) { commit.raw_diffs.first } - let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) } + let(:diff_file) { described_class.new(diff, diff_refs: commit.diff_refs, repository: project.repository) } describe '#diff_lines' do let(:diff_lines) { diff_file.diff_lines } @@ -63,11 +63,334 @@ describe Gitlab::Diff::File, lib: true do end end - describe '#blob' do + describe '#new_blob' do it 'returns blob of new commit' do - data = diff_file.blob.data + data = diff_file.new_blob.data expect(data).to include('raise RuntimeError, "System commands must be given as an array of strings"') end end + + describe '#diffable?' do + let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') } + let(:diffs) { commit.diffs } + + before do + info_dir_path = File.join(project.repository.path_to_repo, 'info') + + FileUtils.mkdir(info_dir_path) unless File.exist?(info_dir_path) + File.write(File.join(info_dir_path, 'attributes'), "*.md -diff\n") + end + + it "returns true for files that do not have attributes" do + diff_file = diffs.diff_file_with_new_path('LICENSE') + expect(diff_file.diffable?).to be_truthy + end + + it "returns false for files that have been marked as not being diffable in attributes" do + diff_file = diffs.diff_file_with_new_path('README.md') + expect(diff_file.diffable?).to be_falsey + end + end + + describe '#content_changed?' do + context 'when created' do + let(:commit) { project.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') } + + it 'returns false' do + expect(diff_file.content_changed?).to be_falsey + end + end + + context 'when deleted' do + let(:commit) { project.commit('d59c60028b053793cecfb4022de34602e1a9218e') } + let(:diff_file) { commit.diffs.diff_file_with_old_path('files/js/commit.js.coffee') } + + it 'returns false' do + expect(diff_file.content_changed?).to be_falsey + end + end + + context 'when renamed' do + let(:commit) { project.commit('6907208d755b60ebeacb2e9dfea74c92c3449a1f') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/js/commit.coffee') } + + before do + allow(diff_file.new_blob).to receive(:id).and_return(diff_file.old_blob.id) + end + + it 'returns false' do + expect(diff_file.content_changed?).to be_falsey + end + end + + context 'when content changed' do + context 'when binary' do + let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') } + + it 'returns true' do + expect(diff_file.content_changed?).to be_truthy + end + end + + context 'when not binary' do + let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') } + + it 'returns true' do + expect(diff_file.content_changed?).to be_truthy + end + end + end + end + + describe '#simple_viewer' do + context 'when the file is not diffable' do + before do + allow(diff_file).to receive(:diffable?).and_return(false) + end + + it 'returns a Not Diffable viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::NotDiffable) + end + end + + context 'when the content changed' do + context 'when the file represented by the diff file is binary' do + before do + allow(diff_file).to receive(:raw_binary?).and_return(true) + end + + it 'returns a No Preview viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::NoPreview) + end + end + + context 'when the diff file old and new blob types are different' do + before do + allow(diff_file).to receive(:different_type?).and_return(true) + end + + it 'returns a No Preview viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::NoPreview) + end + end + + context 'when the file represented by the diff file is text-based' do + it 'returns a text viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::Text) + end + end + end + + context 'when created' do + let(:commit) { project.commit('913c66a37b4a45b9769037c55c2d238bd0942d2e') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') } + + before do + allow(diff_file).to receive(:content_changed?).and_return(nil) + end + + context 'when the file represented by the diff file is binary' do + before do + allow(diff_file).to receive(:raw_binary?).and_return(true) + end + + it 'returns an Added viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::Added) + end + end + + context 'when the diff file old and new blob types are different' do + before do + allow(diff_file).to receive(:different_type?).and_return(true) + end + + it 'returns an Added viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::Added) + end + end + + context 'when the file represented by the diff file is text-based' do + it 'returns a text viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::Text) + end + end + end + + context 'when deleted' do + let(:commit) { project.commit('d59c60028b053793cecfb4022de34602e1a9218e') } + let(:diff_file) { commit.diffs.diff_file_with_old_path('files/js/commit.js.coffee') } + + before do + allow(diff_file).to receive(:content_changed?).and_return(nil) + end + + context 'when the file represented by the diff file is binary' do + before do + allow(diff_file).to receive(:raw_binary?).and_return(true) + end + + it 'returns a Deleted viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::Deleted) + end + end + + context 'when the diff file old and new blob types are different' do + before do + allow(diff_file).to receive(:different_type?).and_return(true) + end + + it 'returns a Deleted viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::Deleted) + end + end + + context 'when the file represented by the diff file is text-based' do + it 'returns a text viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::Text) + end + end + end + + context 'when renamed' do + let(:commit) { project.commit('6907208d755b60ebeacb2e9dfea74c92c3449a1f') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/js/commit.coffee') } + + before do + allow(diff_file).to receive(:content_changed?).and_return(nil) + end + + it 'returns a Renamed viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::Renamed) + end + end + + context 'when mode changed' do + before do + allow(diff_file).to receive(:content_changed?).and_return(nil) + allow(diff_file).to receive(:mode_changed?).and_return(true) + end + + it 'returns a Mode Changed viewer' do + expect(diff_file.simple_viewer).to be_a(DiffViewer::ModeChanged) + end + end + end + + describe '#rich_viewer' do + let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') } + + context 'when the diff file has a matching viewer' do + context 'when the diff file content did not change' do + before do + allow(diff_file).to receive(:content_changed?).and_return(false) + end + + it 'returns nil' do + expect(diff_file.rich_viewer).to be_nil + end + end + + context 'when the diff file is not diffable' do + before do + allow(diff_file).to receive(:diffable?).and_return(false) + end + + it 'returns nil' do + expect(diff_file.rich_viewer).to be_nil + end + end + + context 'when the diff file old and new blob types are different' do + before do + allow(diff_file).to receive(:different_type?).and_return(true) + end + + it 'returns nil' do + expect(diff_file.rich_viewer).to be_nil + end + end + + context 'when the diff file has an external storage error' do + before do + allow(diff_file).to receive(:external_storage_error?).and_return(true) + end + + it 'returns nil' do + expect(diff_file.rich_viewer).to be_nil + end + end + + context 'when everything is right' do + it 'returns the viewer' do + expect(diff_file.rich_viewer).to be_a(DiffViewer::Image) + end + end + end + + context 'when the diff file does not have a matching viewer' do + let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') } + + it 'returns nil' do + expect(diff_file.rich_viewer).to be_nil + end + end + end + + describe '#rendered_as_text?' do + context 'when the simple viewer is text-based' do + let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') } + + context 'when ignoring errors' do + context 'when the viewer has render errors' do + before do + diff_file.diff.too_large! + end + + it 'returns true' do + expect(diff_file.rendered_as_text?).to be_truthy + end + end + + context "when the viewer doesn't have render errors" do + it 'returns true' do + expect(diff_file.rendered_as_text?).to be_truthy + end + end + end + + context 'when not ignoring errors' do + context 'when the viewer has render errors' do + before do + diff_file.diff.too_large! + end + + it 'returns false' do + expect(diff_file.rendered_as_text?(ignore_errors: false)).to be_falsey + end + end + + context "when the viewer doesn't have render errors" do + it 'returns true' do + expect(diff_file.rendered_as_text?(ignore_errors: false)).to be_truthy + end + end + end + end + + context 'when the simple viewer is binary' do + let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') } + + it 'returns false' do + expect(diff_file.rendered_as_text?).to be_falsey + end + end + end end diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index 7095104d75c..b3d46e69ccb 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -381,6 +381,54 @@ describe Gitlab::Diff::Position, lib: true do end end + describe "position for a file in a straight comparison" do + let(:diff_refs) do + Gitlab::Diff::DiffRefs.new( + start_sha: '0b4bc9a49b562e85de7cc9e834518ea6828729b9', # feature + base_sha: '0b4bc9a49b562e85de7cc9e834518ea6828729b9', + head_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a' # master + ) + end + + subject do + described_class.new( + old_path: "files/ruby/feature.rb", + new_path: "files/ruby/feature.rb", + old_line: 3, + new_line: nil, + diff_refs: diff_refs + ) + end + + describe "#diff_file" do + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.deleted_file?).to be true + expect(diff_file.old_path).to eq(subject.old_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line(project.repository) + + expect(diff_line.removed?).to be true + expect(diff_line.old_line).to eq(subject.old_line) + expect(diff_line.text).to eq("- puts 'bar'") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 0, subject.old_line) + + expect(subject.line_code(project.repository)).to eq(line_code) + end + end + end + describe "#to_json" do let(:hash) do { diff --git a/spec/lib/gitlab/etag_caching/middleware_spec.rb b/spec/lib/gitlab/etag_caching/middleware_spec.rb index 24df04e985a..4acf4f047f1 100644 --- a/spec/lib/gitlab/etag_caching/middleware_spec.rb +++ b/spec/lib/gitlab/etag_caching/middleware_spec.rb @@ -15,13 +15,13 @@ describe Gitlab::EtagCaching::Middleware do end it 'does not add ETag header' do - _, headers, _ = middleware.call(build_env(path, if_none_match)) + _, headers, _ = middleware.call(build_request(path, if_none_match)) expect(headers['ETag']).to be_nil end it 'passes status code from app' do - status, _, _ = middleware.call(build_env(path, if_none_match)) + status, _, _ = middleware.call(build_request(path, if_none_match)) expect(status).to eq app_status_code end @@ -39,7 +39,7 @@ describe Gitlab::EtagCaching::Middleware do expect_any_instance_of(Gitlab::EtagCaching::Store) .to receive(:touch).and_return('123') - middleware.call(build_env(path, if_none_match)) + middleware.call(build_request(path, if_none_match)) end context 'when If-None-Match header was specified' do @@ -51,7 +51,7 @@ describe Gitlab::EtagCaching::Middleware do expect(Gitlab::Metrics).to receive(:add_event) .with(:etag_caching_key_not_found, endpoint: 'issue_notes') - middleware.call(build_env(path, if_none_match)) + middleware.call(build_request(path, if_none_match)) end end end @@ -65,7 +65,7 @@ describe Gitlab::EtagCaching::Middleware do end it 'returns this value as header' do - _, headers, _ = middleware.call(build_env(path, if_none_match)) + _, headers, _ = middleware.call(build_request(path, if_none_match)) expect(headers['ETag']).to eq 'W/"123"' end @@ -82,17 +82,17 @@ describe Gitlab::EtagCaching::Middleware do it 'does not call app' do expect(app).not_to receive(:call) - middleware.call(build_env(path, if_none_match)) + middleware.call(build_request(path, if_none_match)) end it 'returns status code 304' do - status, _, _ = middleware.call(build_env(path, if_none_match)) + status, _, _ = middleware.call(build_request(path, if_none_match)) expect(status).to eq 304 end it 'returns empty body' do - _, _, body = middleware.call(build_env(path, if_none_match)) + _, _, body = middleware.call(build_request(path, if_none_match)) expect(body).to be_empty end @@ -103,7 +103,7 @@ describe Gitlab::EtagCaching::Middleware do expect(Gitlab::Metrics).to receive(:add_event) .with(:etag_caching_cache_hit, endpoint: 'issue_notes') - middleware.call(build_env(path, if_none_match)) + middleware.call(build_request(path, if_none_match)) end context 'when polling is disabled' do @@ -113,7 +113,7 @@ describe Gitlab::EtagCaching::Middleware do end it 'returns status code 429' do - status, _, _ = middleware.call(build_env(path, if_none_match)) + status, _, _ = middleware.call(build_request(path, if_none_match)) expect(status).to eq 429 end @@ -131,7 +131,7 @@ describe Gitlab::EtagCaching::Middleware do it 'calls app' do expect(app).to receive(:call).and_return([app_status_code, {}, ['body']]) - middleware.call(build_env(path, if_none_match)) + middleware.call(build_request(path, if_none_match)) end it 'tracks "etag_caching_resource_changed" event' do @@ -142,7 +142,7 @@ describe Gitlab::EtagCaching::Middleware do expect(Gitlab::Metrics).to receive(:add_event) .with(:etag_caching_resource_changed, endpoint: 'issue_notes') - middleware.call(build_env(path, if_none_match)) + middleware.call(build_request(path, if_none_match)) end end @@ -160,7 +160,26 @@ describe Gitlab::EtagCaching::Middleware do expect(Gitlab::Metrics).to receive(:add_event) .with(:etag_caching_header_missing, endpoint: 'issue_notes') - middleware.call(build_env(path, if_none_match)) + middleware.call(build_request(path, if_none_match)) + end + end + + context 'when GitLab instance is using a relative URL' do + before do + mock_app_response + end + + it 'uses full path as cache key' do + env = { + 'PATH_INFO' => enabled_path, + 'SCRIPT_NAME' => '/relative-gitlab' + } + + expect_any_instance_of(Gitlab::EtagCaching::Store) + .to receive(:get).with("/relative-gitlab#{enabled_path}") + .and_return(nil) + + middleware.call(env) end end @@ -173,10 +192,7 @@ describe Gitlab::EtagCaching::Middleware do .to receive(:get).and_return(value) end - def build_env(path, if_none_match) - { - 'PATH_INFO' => path, - 'HTTP_IF_NONE_MATCH' => if_none_match - } + def build_request(path, if_none_match) + { 'PATH_INFO' => path, 'HTTP_IF_NONE_MATCH' => if_none_match } end end diff --git a/spec/lib/gitlab/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb index 269798c7c9e..f69cb502ca6 100644 --- a/spec/lib/gitlab/etag_caching/router_spec.rb +++ b/spec/lib/gitlab/etag_caching/router_spec.rb @@ -2,115 +2,91 @@ require 'spec_helper' describe Gitlab::EtagCaching::Router do it 'matches issue notes endpoint' do - env = build_env( + result = described_class.match( '/my-group/and-subgroup/here-comes-the-project/noteable/issue/1/notes' ) - result = described_class.match(env) - expect(result).to be_present expect(result.name).to eq 'issue_notes' end it 'matches issue title endpoint' do - env = build_env( + result = described_class.match( '/my-group/my-project/issues/123/realtime_changes' ) - result = described_class.match(env) - expect(result).to be_present expect(result.name).to eq 'issue_title' end it 'matches project pipelines endpoint' do - env = build_env( + result = described_class.match( '/my-group/my-project/pipelines.json' ) - result = described_class.match(env) - expect(result).to be_present expect(result.name).to eq 'project_pipelines' end it 'matches commit pipelines endpoint' do - env = build_env( + result = described_class.match( '/my-group/my-project/commit/aa8260d253a53f73f6c26c734c72fdd600f6e6d4/pipelines.json' ) - result = described_class.match(env) - expect(result).to be_present expect(result.name).to eq 'commit_pipelines' end it 'matches new merge request pipelines endpoint' do - env = build_env( + result = described_class.match( '/my-group/my-project/merge_requests/new.json' ) - result = described_class.match(env) - expect(result).to be_present expect(result.name).to eq 'new_merge_request_pipelines' end it 'matches merge request pipelines endpoint' do - env = build_env( + result = described_class.match( '/my-group/my-project/merge_requests/234/pipelines.json' ) - result = described_class.match(env) - expect(result).to be_present expect(result.name).to eq 'merge_request_pipelines' end it 'matches build endpoint' do - env = build_env( + result = described_class.match( '/my-group/my-project/builds/234.json' ) - result = described_class.match(env) - expect(result).to be_present expect(result.name).to eq 'project_build' end it 'does not match blob with confusing name' do - env = build_env( + result = described_class.match( '/my-group/my-project/blob/master/pipelines.json' ) - result = described_class.match(env) - expect(result).to be_blank end it 'matches the environments path' do - env = build_env( + result = described_class.match( '/my-group/my-project/environments.json' ) - result = described_class.match(env) expect(result).to be_present - expect(result.name).to eq 'environments' end it 'matches pipeline#show endpoint' do - env = build_env( + result = described_class.match( '/my-group/my-project/pipelines/2.json' ) - result = described_class.match(env) - expect(result).to be_present expect(result.name).to eq 'project_pipeline' end - - def build_env(path) - { 'PATH_INFO' => path } - end end diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb index 5d416c9eec3..eaec699ad90 100644 --- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb @@ -6,7 +6,9 @@ describe Gitlab::Gfm::ReferenceRewriter do let(:new_project) { create(:empty_project, name: 'new-project') } let(:user) { create(:user) } - before { old_project.team << [user, :reporter] } + before do + old_project.team << [user, :reporter] + end describe '#rewrite' do subject do diff --git a/spec/lib/gitlab/git/compare_spec.rb b/spec/lib/gitlab/git/compare_spec.rb index 7c45071ec45..4c9f4a28f32 100644 --- a/spec/lib/gitlab/git/compare_spec.rb +++ b/spec/lib/gitlab/git/compare_spec.rb @@ -2,8 +2,8 @@ require "spec_helper" describe Gitlab::Git::Compare, seed_helper: true do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } - let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, false) } - let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, true) } + let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: false) } + let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: true) } describe '#commits' do subject do diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 26215381cc4..eee4c9eab6d 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -16,7 +16,9 @@ describe Gitlab::Git::Repository, seed_helper: true do describe '#root_ref' do context 'with gitaly disabled' do - before { allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false) } + before do + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false) + end it 'calls #discover_default_branch' do expect(repository).to receive(:discover_default_branch) @@ -25,8 +27,13 @@ describe Gitlab::Git::Repository, seed_helper: true do end context 'with gitaly enabled' do - before { stub_gitaly } - after { Gitlab::GitalyClient.clear_stubs! } + before do + stub_gitaly + end + + after do + Gitlab::GitalyClient.clear_stubs! + end it 'gets the branch name from GitalyClient' do expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) @@ -120,8 +127,13 @@ describe Gitlab::Git::Repository, seed_helper: true do it { is_expected.not_to include("branch-from-space") } context 'with gitaly enabled' do - before { stub_gitaly } - after { Gitlab::GitalyClient.clear_stubs! } + before do + stub_gitaly + end + + after do + Gitlab::GitalyClient.clear_stubs! + end it 'gets the branch names from GitalyClient' do expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) @@ -158,8 +170,13 @@ describe Gitlab::Git::Repository, seed_helper: true do it { is_expected.not_to include("v5.0.0") } context 'with gitaly enabled' do - before { stub_gitaly } - after { Gitlab::GitalyClient.clear_stubs! } + before do + stub_gitaly + end + + after do + Gitlab::GitalyClient.clear_stubs! + end it 'gets the tag names from GitalyClient' do expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) @@ -1235,47 +1252,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe '#diffable' do - info_dir_path = attributes_path = File.join(SEED_STORAGE_PATH, TEST_REPO_PATH, 'info') - attributes_path = File.join(info_dir_path, 'attributes') - - before(:all) do - FileUtils.mkdir(info_dir_path) unless File.exist?(info_dir_path) - File.write(attributes_path, "*.md -diff\n") - end - - it "should return true for files which are text and do not have attributes" do - blob = Gitlab::Git::Blob.find( - repository, - '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', - 'LICENSE' - ) - expect(repository.diffable?(blob)).to be_truthy - end - - it "should return false for binary files which do not have attributes" do - blob = Gitlab::Git::Blob.find( - repository, - '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', - 'files/images/logo-white.png' - ) - expect(repository.diffable?(blob)).to be_falsey - end - - it "should return false for text files which have been marked as not being diffable in attributes" do - blob = Gitlab::Git::Blob.find( - repository, - '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', - 'README.md' - ) - expect(repository.diffable?(blob)).to be_falsey - end - - after(:all) do - FileUtils.rm_rf(info_dir_path) - end - end - describe '#tag_exists?' do it 'returns true for an existing tag' do tag = repository.tag_names.first @@ -1321,8 +1297,13 @@ describe Gitlab::Git::Repository, seed_helper: true do end context 'with gitaly enabled' do - before { stub_gitaly } - after { Gitlab::GitalyClient.clear_stubs! } + before do + stub_gitaly + end + + after do + Gitlab::GitalyClient.clear_stubs! + end it 'gets the branches from GitalyClient' do expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches). diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 36d1d777583..3dcc20c48e8 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -60,7 +60,9 @@ describe Gitlab::GitAccess, lib: true do let(:actor) { deploy_key } context 'when the DeployKey has access to the project' do - before { deploy_key.projects << project } + before do + deploy_key.projects << project + end it 'allows pull access' do expect { pull_access_check }.not_to raise_error @@ -84,7 +86,9 @@ describe Gitlab::GitAccess, lib: true do context 'when actor is a User' do context 'when the User can read the project' do - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it 'allows pull access' do expect { pull_access_check }.not_to raise_error @@ -159,7 +163,9 @@ describe Gitlab::GitAccess, lib: true do end describe '#check_command_disabled!' do - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end context 'over http' do let(:protocol) { 'http' } @@ -196,7 +202,9 @@ describe Gitlab::GitAccess, lib: true do describe '#check_download_access!' do describe 'master permissions' do - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end context 'pull code' do it { expect { pull_access_check }.not_to raise_error } @@ -204,7 +212,9 @@ describe Gitlab::GitAccess, lib: true do end describe 'guest permissions' do - before { project.team << [user, :guest] } + before do + project.team << [user, :guest] + end context 'pull code' do it { expect { pull_access_check }.to raise_unauthorized('You are not allowed to download code from this project.') } @@ -253,7 +263,9 @@ describe Gitlab::GitAccess, lib: true do context 'pull code' do context 'when project is authorized' do - before { key.projects << project } + before do + key.projects << project + end it { expect { pull_access_check }.not_to raise_error } end @@ -292,7 +304,9 @@ describe Gitlab::GitAccess, lib: true do end describe 'reporter user' do - before { project.team << [user, :reporter] } + before do + project.team << [user, :reporter] + end context 'pull code' do it { expect { pull_access_check }.not_to raise_error } @@ -303,7 +317,9 @@ describe Gitlab::GitAccess, lib: true do let(:user) { create(:admin) } context 'when member of the project' do - before { project.team << [user, :reporter] } + before do + project.team << [user, :reporter] + end context 'pull code' do it { expect { pull_access_check }.not_to raise_error } @@ -328,7 +344,9 @@ describe Gitlab::GitAccess, lib: true do end describe '#check_push_access!' do - before { merge_into_protected_branch } + before do + merge_into_protected_branch + end let(:unprotected_branch) { 'unprotected_branch' } let(:changes) do @@ -457,19 +475,25 @@ describe Gitlab::GitAccess, lib: true do [%w(feature exact), ['feat*', 'wildcard']].each do |protected_branch_name, protected_branch_type| context do - before { create(:protected_branch, name: protected_branch_name, project: project) } + before do + create(:protected_branch, name: protected_branch_name, project: project) + end run_permission_checks(permissions_matrix) end context "when developers are allowed to push into the #{protected_branch_type} protected branch" do - before { create(:protected_branch, :developers_can_push, name: protected_branch_name, project: project) } + before do + create(:protected_branch, :developers_can_push, name: protected_branch_name, project: project) + end run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true })) end context "developers are allowed to merge into the #{protected_branch_type} protected branch" do - before { create(:protected_branch, :developers_can_merge, name: protected_branch_name, project: project) } + before do + create(:protected_branch, :developers_can_merge, name: protected_branch_name, project: project) + end context "when a merge request exists for the given source/target branch" do context "when the merge request is in progress" do @@ -496,13 +520,17 @@ describe Gitlab::GitAccess, lib: true do end context "when developers are allowed to push and merge into the #{protected_branch_type} protected branch" do - before { create(:protected_branch, :developers_can_merge, :developers_can_push, name: protected_branch_name, project: project) } + before do + create(:protected_branch, :developers_can_merge, :developers_can_push, name: protected_branch_name, project: project) + end run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true })) end context "when no one is allowed to push to the #{protected_branch_name} protected branch" do - before { create(:protected_branch, :no_one_can_push, name: protected_branch_name, project: project) } + before do + create(:protected_branch, :no_one_can_push, name: protected_branch_name, project: project) + end run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }, master: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }, @@ -515,7 +543,9 @@ describe Gitlab::GitAccess, lib: true do let(:authentication_abilities) { build_authentication_abilities } context 'when project is authorized' do - before { project.team << [user, :reporter] } + before do + project.team << [user, :reporter] + end it { expect { push_access_check }.to raise_unauthorized('You are not allowed to upload code for this project.') } end @@ -549,7 +579,9 @@ describe Gitlab::GitAccess, lib: true do let(:can_push) { true } context 'when project is authorized' do - before { key.projects << project } + before do + key.projects << project + end it { expect { push_access_check }.not_to raise_error } end @@ -579,7 +611,9 @@ describe Gitlab::GitAccess, lib: true do let(:can_push) { false } context 'when project is authorized' do - before { key.projects << project } + before do + key.projects << project + end it { expect { push_access_check }.to raise_unauthorized('This deploy key does not have write access to this project.') } end diff --git a/spec/lib/gitlab/gitaly_client/notifications_spec.rb b/spec/lib/gitlab/gitaly_client/notifications_spec.rb index b87dacb175b..e5c9e06a15e 100644 --- a/spec/lib/gitlab/gitaly_client/notifications_spec.rb +++ b/spec/lib/gitlab/gitaly_client/notifications_spec.rb @@ -3,12 +3,13 @@ require 'spec_helper' describe Gitlab::GitalyClient::Notifications do describe '#post_receive' do let(:project) { create(:empty_project) } - let(:repo_path) { project.repository.path_to_repo } + let(:storage_name) { project.repository_storage } + let(:relative_path) { project.path_with_namespace + '.git' } subject { described_class.new(project.repository) } it 'sends a post_receive message' do expect_any_instance_of(Gitaly::Notifications::Stub). - to receive(:post_receive).with(gitaly_request_with_repo_path(repo_path)) + to receive(:post_receive).with(gitaly_request_with_path(storage_name, relative_path)) subject.post_receive end diff --git a/spec/lib/gitlab/gitaly_client/ref_spec.rb b/spec/lib/gitlab/gitaly_client/ref_spec.rb index d8cd2dcbd2a..2ea44ef74b0 100644 --- a/spec/lib/gitlab/gitaly_client/ref_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' describe Gitlab::GitalyClient::Ref do let(:project) { create(:empty_project) } - let(:repo_path) { project.repository.path_to_repo } + let(:storage_name) { project.repository_storage } + let(:relative_path) { project.path_with_namespace + '.git' } let(:client) { described_class.new(project.repository) } before do @@ -19,7 +20,8 @@ describe Gitlab::GitalyClient::Ref do describe '#branch_names' do it 'sends a find_all_branch_names message' do expect_any_instance_of(Gitaly::Ref::Stub). - to receive(:find_all_branch_names).with(gitaly_request_with_repo_path(repo_path)). + to receive(:find_all_branch_names). + with(gitaly_request_with_path(storage_name, relative_path)). and_return([]) client.branch_names @@ -29,7 +31,8 @@ describe Gitlab::GitalyClient::Ref do describe '#tag_names' do it 'sends a find_all_tag_names message' do expect_any_instance_of(Gitaly::Ref::Stub). - to receive(:find_all_tag_names).with(gitaly_request_with_repo_path(repo_path)). + to receive(:find_all_tag_names). + with(gitaly_request_with_path(storage_name, relative_path)). and_return([]) client.tag_names @@ -39,7 +42,8 @@ describe Gitlab::GitalyClient::Ref do describe '#default_branch_name' do it 'sends a find_default_branch_name message' do expect_any_instance_of(Gitaly::Ref::Stub). - to receive(:find_default_branch_name).with(gitaly_request_with_repo_path(repo_path)). + to receive(:find_default_branch_name). + with(gitaly_request_with_path(storage_name, relative_path)). and_return(double(name: 'foo')) client.default_branch_name @@ -49,7 +53,8 @@ describe Gitlab::GitalyClient::Ref do describe '#local_branches' do it 'sends a find_local_branches message' do expect_any_instance_of(Gitaly::Ref::Stub). - to receive(:find_local_branches).with(gitaly_request_with_repo_path(repo_path)). + to receive(:find_local_branches). + with(gitaly_request_with_path(storage_name, relative_path)). and_return([]) client.local_branches diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index 95ecba67532..ce7b18b784a 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -5,7 +5,9 @@ require 'spec_helper' describe Gitlab::GitalyClient, lib: true, skip_gitaly_mock: true do describe '.stub' do # Notice that this is referring to gRPC "stubs", not rspec stubs - before { described_class.clear_stubs! } + before do + described_class.clear_stubs! + end context 'when passed a UNIX socket address' do it 'passes the address as-is to GRPC' do @@ -41,7 +43,9 @@ describe Gitlab::GitalyClient, lib: true, skip_gitaly_mock: true do let(:real_feature_name) { "gitaly_#{feature_name}" } context 'when Gitaly is disabled' do - before { allow(described_class).to receive(:enabled?).and_return(false) } + before do + allow(described_class).to receive(:enabled?).and_return(false) + end it 'returns false' do expect(described_class.feature_enabled?(feature_name)).to be(false) @@ -66,7 +70,9 @@ describe Gitlab::GitalyClient, lib: true, skip_gitaly_mock: true do end context "when the feature flag is set to disable" do - before { Feature.get(real_feature_name).disable } + before do + Feature.get(real_feature_name).disable + end it 'returns false' do expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false) @@ -74,7 +80,9 @@ describe Gitlab::GitalyClient, lib: true, skip_gitaly_mock: true do end context "when the feature flag is set to enable" do - before { Feature.get(real_feature_name).enable } + before do + Feature.get(real_feature_name).enable + end it 'returns true' do expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(true) @@ -82,7 +90,9 @@ describe Gitlab::GitalyClient, lib: true, skip_gitaly_mock: true do end context "when the feature flag is set to a percentage of time" do - before { Feature.get(real_feature_name).enable_percentage_of_time(70) } + before do + Feature.get(real_feature_name).enable_percentage_of_time(70) + end it 'bases the result on pseudo-random numbers' do expect(Random).to receive(:rand).and_return(0.3) @@ -104,7 +114,9 @@ describe Gitlab::GitalyClient, lib: true, skip_gitaly_mock: true do end context "when the feature flag is set to disable" do - before { Feature.get(real_feature_name).disable } + before do + Feature.get(real_feature_name).disable + end it 'returns false' do expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false) diff --git a/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb b/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb new file mode 100644 index 00000000000..ed757ed60d8 --- /dev/null +++ b/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb @@ -0,0 +1,41 @@ +describe Gitlab::HealthChecks::PrometheusTextFormat do + let(:metric_class) { Gitlab::HealthChecks::Metric } + subject { described_class.new } + + describe '#marshal' do + let(:sample_metrics) do + [metric_class.new('metric1', 1), + metric_class.new('metric2', 2)] + end + + it 'marshal to text with non repeating type definition' do + expected = <<-EXPECTED.strip_heredoc + # TYPE metric1 gauge + metric1 1 + # TYPE metric2 gauge + metric2 2 + EXPECTED + + expect(subject.marshal(sample_metrics)).to eq(expected) + end + + context 'metrics where name repeats' do + let(:sample_metrics) do + [metric_class.new('metric1', 1), + metric_class.new('metric1', 2), + metric_class.new('metric2', 3)] + end + + it 'marshal to text with non repeating type definition' do + expected = <<-EXPECTED.strip_heredoc + # TYPE metric1 gauge + metric1 1 + metric1 2 + # TYPE metric2 gauge + metric2 3 + EXPECTED + expect(subject.marshal(sample_metrics)).to eq(expected) + end + end + end +end diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index a20cef3b000..fdc5b484ef1 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -7,30 +7,6 @@ describe Gitlab::Highlight, lib: true do let(:repository) { project.repository } let(:commit) { project.commit(sample_commit.id) } - describe '.highlight_lines' do - let(:lines) do - Gitlab::Highlight.highlight_lines(project.repository, commit.id, 'files/ruby/popen.rb') - end - - it 'highlights all the lines properly' do - expect(lines[4]).to eq(%Q{<span id="LC5" class="line" lang="ruby"> <span class="kp">extend</span> <span class="nb">self</span></span>\n}) - expect(lines[21]).to eq(%Q{<span id="LC22" class="line" lang="ruby"> <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" lang="ruby"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n}) - end - - describe 'with CRLF' do - let(:branch) { 'crlf-diff' } - let(:blob) { repository.blob_at_branch(branch, path) } - let(:lines) do - Gitlab::Highlight.highlight_lines(project.repository, 'crlf-diff', 'files/whitespace') - end - - it 'strips extra LFs' do - expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\" lang=\"plaintext\">test </span>") - end - end - end - describe 'custom highlighting from .gitattributes' do let(:branch) { 'gitattributes' } let(:blob) { repository.blob_at_branch(branch, path) } @@ -39,7 +15,9 @@ describe Gitlab::Highlight, lib: true do Gitlab::Highlight.new(blob.path, blob.data, repository: repository) end - before { project.change_head('gitattributes') } + before do + project.change_head('gitattributes') + end describe 'basic language selection' do let(:path) { 'custom-highlighting/test.gitlab-custom' } @@ -59,6 +37,19 @@ describe Gitlab::Highlight, lib: true do end describe '#highlight' do + describe 'with CRLF' do + let(:branch) { 'crlf-diff' } + let(:path) { 'files/whitespace' } + let(:blob) { repository.blob_at_branch(branch, path) } + let(:lines) do + Gitlab::Highlight.highlight(blob.path, blob.data, repository: repository).lines + end + + it 'strips extra LFs' do + expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\" lang=\"plaintext\">test </span>") + end + end + it 'links dependencies via DependencyLinker' do expect(Gitlab::DependencyLinker).to receive(:link). with('file.name', 'Contents', anything).and_call_original diff --git a/spec/lib/gitlab/i18n_spec.rb b/spec/lib/gitlab/i18n_spec.rb index a3dbeaa3753..0dba4132101 100644 --- a/spec/lib/gitlab/i18n_spec.rb +++ b/spec/lib/gitlab/i18n_spec.rb @@ -4,7 +4,9 @@ describe Gitlab::I18n, lib: true do let(:user) { create(:user, preferred_language: 'es') } describe '.locale=' do - after { described_class.use_default_locale } + after do + described_class.use_default_locale + end it 'sets the locale based on current user preferred language' do described_class.locale = user.preferred_language diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 21296a36729..412eb33b35b 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -91,6 +91,7 @@ merge_request_diff: pipelines: - project - user +- stages - statuses - builds - trigger_requests @@ -104,9 +105,15 @@ pipelines: - artifacts - pipeline_schedule - merge_requests +stages: +- project +- pipeline +- statuses +- builds statuses: - project - pipeline +- stage - user - auto_canceled_by variables: diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 54ce8051f30..50ff6ecc1e0 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -92,6 +92,7 @@ Milestone: ProjectSnippet: - id - title +- description - content - author_id - project_id @@ -174,6 +175,7 @@ MergeRequestDiff: Ci::Pipeline: - id - project_id +- source - ref - sha - before_sha @@ -191,7 +193,13 @@ Ci::Pipeline: - lock_version - auto_canceled_by_id - pipeline_schedule_id -- source +Ci::Stage: +- id +- name +- project_id +- pipeline_id +- created_at +- updated_at CommitStatus: - id - project_id @@ -213,6 +221,7 @@ CommitStatus: - stage - trigger_request_id - stage_idx +- stage_id - tag - ref - user_id diff --git a/spec/lib/gitlab/kubernetes_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb index 91f9d06b85a..e8c599a95ee 100644 --- a/spec/lib/gitlab/kubernetes_spec.rb +++ b/spec/lib/gitlab/kubernetes_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe Gitlab::Kubernetes do + include KubernetesHelpers include described_class describe '#container_exec_url' do @@ -36,4 +37,13 @@ describe Gitlab::Kubernetes do it { expect(result.query).to match(/\Acontainer=container\+1&/) } end end + + describe '#filter_by_label' do + it 'returns matching labels' do + matching_items = [kube_pod(app: 'foo')] + items = matching_items + [kube_pod] + + expect(filter_by_label(items, app: 'foo')).to eq(matching_items) + end + end end diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/ldap/adapter_spec.rb index 563c074017a..9454878b057 100644 --- a/spec/lib/gitlab/ldap/adapter_spec.rb +++ b/spec/lib/gitlab/ldap/adapter_spec.rb @@ -74,13 +74,17 @@ describe Gitlab::LDAP::Adapter, lib: true do subject { adapter.dn_matches_filter?(:dn, :filter) } context "when the search result is non-empty" do - before { allow(adapter).to receive(:ldap_search).and_return([:foo]) } + before do + allow(adapter).to receive(:ldap_search).and_return([:foo]) + end it { is_expected.to be_truthy } end context "when the search result is empty" do - before { allow(adapter).to receive(:ldap_search).and_return([]) } + before do + allow(adapter).to receive(:ldap_search).and_return([]) + end it { is_expected.to be_falsey } end @@ -91,13 +95,17 @@ describe Gitlab::LDAP::Adapter, lib: true do context "when the search is successful" do context "and the result is non-empty" do - before { allow(ldap).to receive(:search).and_return([:foo]) } + before do + allow(ldap).to receive(:search).and_return([:foo]) + end it { is_expected.to eq [:foo] } end context "and the result is empty" do - before { allow(ldap).to receive(:search).and_return([]) } + before do + allow(ldap).to receive(:search).and_return([]) + end it { is_expected.to eq [] } end diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index f4aab429931..f0a1dd22fee 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -37,7 +37,7 @@ describe Gitlab::LDAP::User, lib: true do end it "does not mark existing ldap user as changed" do - create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain', ldap_email: true) + create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain', external_email: true, email_provider: 'ldapmain') expect(ldap_user.changed?).to be_falsey end end @@ -141,8 +141,12 @@ describe Gitlab::LDAP::User, lib: true do expect(ldap_user.gl_user.email).to eq(info[:email]) end - it "has ldap_email set to true" do - expect(ldap_user.gl_user.ldap_email?).to be(true) + it "has external_email set to true" do + expect(ldap_user.gl_user.external_email?).to be(true) + end + + it "has email_provider set to provider" do + expect(ldap_user.gl_user.email_provider).to eql 'ldapmain' end end @@ -155,8 +159,8 @@ describe Gitlab::LDAP::User, lib: true do expect(ldap_user.gl_user.temp_oauth_email?).to be(true) end - it "has ldap_email set to false" do - expect(ldap_user.gl_user.ldap_email?).to be(false) + it "has external_email set to false" do + expect(ldap_user.gl_user.external_email?).to be(false) end end end @@ -169,7 +173,9 @@ describe Gitlab::LDAP::User, lib: true do context 'signup' do context 'dont block on create' do - before { configure_block(false) } + before do + configure_block(false) + end it do ldap_user.save @@ -179,7 +185,9 @@ describe Gitlab::LDAP::User, lib: true do end context 'block on create' do - before { configure_block(true) } + before do + configure_block(true) + end it do ldap_user.save @@ -196,7 +204,9 @@ describe Gitlab::LDAP::User, lib: true do end context 'dont block on create' do - before { configure_block(false) } + before do + configure_block(false) + end it do ldap_user.save @@ -206,7 +216,9 @@ describe Gitlab::LDAP::User, lib: true do end context 'block on create' do - before { configure_block(true) } + before do + configure_block(true) + end it do ldap_user.save diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 208a8d028cd..5a87b906609 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Gitlab::Metrics do + include StubENV + describe '.settings' do it 'returns a Hash' do expect(described_class.settings).to be_an_instance_of(Hash) @@ -9,7 +11,19 @@ describe Gitlab::Metrics do describe '.enabled?' do it 'returns a boolean' do - expect([true, false].include?(described_class.enabled?)).to eq(true) + expect(described_class.enabled?).to be_in([true, false]) + end + end + + describe '.prometheus_metrics_enabled?' do + it 'returns a boolean' do + expect(described_class.prometheus_metrics_enabled?).to be_in([true, false]) + end + end + + describe '.influx_metrics_enabled?' do + it 'returns a boolean' do + expect(described_class.influx_metrics_enabled?).to be_in([true, false]) end end @@ -177,4 +191,133 @@ describe Gitlab::Metrics do end end end + + shared_examples 'prometheus metrics API' do + describe '#counter' do + subject { described_class.counter(:couter, 'doc') } + + describe '#increment' do + it 'successfully calls #increment without arguments' do + expect { subject.increment }.not_to raise_exception + end + + it 'successfully calls #increment with 1 argument' do + expect { subject.increment({}) }.not_to raise_exception + end + + it 'successfully calls #increment with 2 arguments' do + expect { subject.increment({}, 1) }.not_to raise_exception + end + end + end + + describe '#summary' do + subject { described_class.summary(:summary, 'doc') } + + describe '#observe' do + it 'successfully calls #observe with 2 arguments' do + expect { subject.observe({}, 2) }.not_to raise_exception + end + end + end + + describe '#gauge' do + subject { described_class.gauge(:gauge, 'doc') } + + describe '#set' do + it 'successfully calls #set with 2 arguments' do + expect { subject.set({}, 1) }.not_to raise_exception + end + end + end + + describe '#histogram' do + subject { described_class.histogram(:histogram, 'doc') } + + describe '#observe' do + it 'successfully calls #observe with 2 arguments' do + expect { subject.observe({}, 2) }.not_to raise_exception + end + end + end + end + + context 'prometheus metrics disabled' do + before do + allow(described_class).to receive(:prometheus_metrics_enabled?).and_return(false) + end + + it_behaves_like 'prometheus metrics API' + + describe '#null_metric' do + subject { described_class.provide_metric(:test) } + + it { is_expected.to be_a(Gitlab::Metrics::NullMetric) } + end + + describe '#counter' do + subject { described_class.counter(:counter, 'doc') } + + it { is_expected.to be_a(Gitlab::Metrics::NullMetric) } + end + + describe '#summary' do + subject { described_class.summary(:summary, 'doc') } + + it { is_expected.to be_a(Gitlab::Metrics::NullMetric) } + end + + describe '#gauge' do + subject { described_class.gauge(:gauge, 'doc') } + + it { is_expected.to be_a(Gitlab::Metrics::NullMetric) } + end + + describe '#histogram' do + subject { described_class.histogram(:histogram, 'doc') } + + it { is_expected.to be_a(Gitlab::Metrics::NullMetric) } + end + end + + context 'prometheus metrics enabled' do + let(:metrics_multiproc_dir) { Dir.mktmpdir } + + before do + stub_const('Prometheus::Client::Multiprocdir', metrics_multiproc_dir) + allow(described_class).to receive(:prometheus_metrics_enabled?).and_return(true) + end + + it_behaves_like 'prometheus metrics API' + + describe '#null_metric' do + subject { described_class.provide_metric(:test) } + + it { is_expected.to be_nil } + end + + describe '#counter' do + subject { described_class.counter(:name, 'doc') } + + it { is_expected.not_to be_a(Gitlab::Metrics::NullMetric) } + end + + describe '#summary' do + subject { described_class.summary(:name, 'doc') } + + it { is_expected.not_to be_a(Gitlab::Metrics::NullMetric) } + end + + describe '#gauge' do + subject { described_class.gauge(:name, 'doc') } + + it { is_expected.not_to be_a(Gitlab::Metrics::NullMetric) } + end + + describe '#histogram' do + subject { described_class.histogram(:name, 'doc') } + + it { is_expected.not_to be_a(Gitlab::Metrics::NullMetric) } + end + end end diff --git a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb index 168090d5b5c..88107536c9e 100644 --- a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb +++ b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb @@ -6,7 +6,9 @@ describe Gitlab::Middleware::RailsQueueDuration do let(:env) { {} } let(:transaction) { double(:transaction) } - before { expect(app).to receive(:call).with(env).and_return('yay') } + before do + expect(app).to receive(:call).with(env).and_return('yay') + end describe '#call' do it 'calls the app when metrics are disabled' do @@ -15,7 +17,9 @@ describe Gitlab::Middleware::RailsQueueDuration do end context 'when metrics are enabled' do - before { allow(Gitlab::Metrics).to receive(:current_transaction).and_return(transaction) } + before do + allow(Gitlab::Metrics).to receive(:current_transaction).and_return(transaction) + end it 'calls the app when metrics are enabled but no timing header is found' do expect(middleware.call(env)).to eq('yay') diff --git a/spec/lib/gitlab/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/o_auth/auth_hash_spec.rb index 8aaeb5779d3..19ab17419fc 100644 --- a/spec/lib/gitlab/o_auth/auth_hash_spec.rb +++ b/spec/lib/gitlab/o_auth/auth_hash_spec.rb @@ -55,7 +55,9 @@ describe Gitlab::OAuth::AuthHash, lib: true do end context 'email not provided' do - before { info_hash.delete(:email) } + before do + info_hash.delete(:email) + end it 'generates a temp email' do expect( auth_hash.email).to start_with('temp-email-for-oauth') @@ -63,7 +65,9 @@ describe Gitlab::OAuth::AuthHash, lib: true do end context 'username not provided' do - before { info_hash.delete(:nickname) } + before do + info_hash.delete(:nickname) + end it 'takes the first part of the email as username' do expect(auth_hash.username).to eql 'onur.kucuk_ABC-123' @@ -71,7 +75,9 @@ describe Gitlab::OAuth::AuthHash, lib: true do end context 'name not provided' do - before { info_hash.delete(:name) } + before do + info_hash.delete(:name) + end it 'concats first and lastname as the name' do expect(auth_hash.name).to eql name_utf8 diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 828c953197d..ea29cb9caf1 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -28,11 +28,11 @@ describe Gitlab::OAuth::User, lib: true do end end - describe '#save' do - def stub_omniauth_config(messages) - allow(Gitlab.config.omniauth).to receive_messages(messages) - end + def stub_omniauth_config(messages) + allow(Gitlab.config.omniauth).to receive_messages(messages) + end + describe '#save' do def stub_ldap_config(messages) allow(Gitlab::LDAP::Config).to receive_messages(messages) end @@ -112,7 +112,9 @@ describe Gitlab::OAuth::User, lib: true do end context 'with new allow_single_sign_on enabled syntax' do - before { stub_omniauth_config(allow_single_sign_on: ['twitter']) } + before do + stub_omniauth_config(allow_single_sign_on: ['twitter']) + end it "creates a user from Omniauth" do oauth_user.save @@ -125,7 +127,9 @@ describe Gitlab::OAuth::User, lib: true do end context "with old allow_single_sign_on enabled syntax" do - before { stub_omniauth_config(allow_single_sign_on: true) } + before do + stub_omniauth_config(allow_single_sign_on: true) + end it "creates a user from Omniauth" do oauth_user.save @@ -138,14 +142,20 @@ describe Gitlab::OAuth::User, lib: true do end context 'with new allow_single_sign_on disabled syntax' do - before { stub_omniauth_config(allow_single_sign_on: []) } + before do + stub_omniauth_config(allow_single_sign_on: []) + end + it 'throws an error' do expect{ oauth_user.save }.to raise_error StandardError end end context 'with old allow_single_sign_on disabled (Default)' do - before { stub_omniauth_config(allow_single_sign_on: false) } + before do + stub_omniauth_config(allow_single_sign_on: false) + end + it 'throws an error' do expect{ oauth_user.save }.to raise_error StandardError end @@ -153,21 +163,30 @@ describe Gitlab::OAuth::User, lib: true do end context "with auto_link_ldap_user disabled (default)" do - before { stub_omniauth_config(auto_link_ldap_user: false) } + before do + stub_omniauth_config(auto_link_ldap_user: false) + end + include_examples "to verify compliance with allow_single_sign_on" end context "with auto_link_ldap_user enabled" do - before { stub_omniauth_config(auto_link_ldap_user: true) } + before do + stub_omniauth_config(auto_link_ldap_user: true) + end context "and no LDAP provider defined" do - before { stub_ldap_config(providers: []) } + before do + stub_ldap_config(providers: []) + end include_examples "to verify compliance with allow_single_sign_on" end context "and at least one LDAP provider is defined" do - before { stub_ldap_config(providers: %w(ldapmain)) } + before do + stub_ldap_config(providers: %w(ldapmain)) + end context "and a corresponding LDAP person" do before do @@ -238,7 +257,9 @@ describe Gitlab::OAuth::User, lib: true do end context "and no corresponding LDAP person" do - before { allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil) } + before do + allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil) + end include_examples "to verify compliance with allow_single_sign_on" end @@ -248,11 +269,16 @@ describe Gitlab::OAuth::User, lib: true do describe 'blocking' do let(:provider) { 'twitter' } - before { stub_omniauth_config(allow_single_sign_on: ['twitter']) } + + before do + stub_omniauth_config(allow_single_sign_on: ['twitter']) + end context 'signup with omniauth only' do context 'dont block on create' do - before { stub_omniauth_config(block_auto_created_users: false) } + before do + stub_omniauth_config(block_auto_created_users: false) + end it do oauth_user.save @@ -262,7 +288,9 @@ describe Gitlab::OAuth::User, lib: true do end context 'block on create' do - before { stub_omniauth_config(block_auto_created_users: true) } + before do + stub_omniauth_config(block_auto_created_users: true) + end it do oauth_user.save @@ -284,7 +312,9 @@ describe Gitlab::OAuth::User, lib: true do context "and no account for the LDAP user" do context 'dont block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } + before do + allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) + end it do oauth_user.save @@ -294,7 +324,9 @@ describe Gitlab::OAuth::User, lib: true do end context 'block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } + before do + allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) + end it do oauth_user.save @@ -308,7 +340,9 @@ describe Gitlab::OAuth::User, lib: true do let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } context 'dont block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } + before do + allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) + end it do oauth_user.save @@ -318,7 +352,9 @@ describe Gitlab::OAuth::User, lib: true do end context 'block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } + before do + allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) + end it do oauth_user.save @@ -336,7 +372,9 @@ describe Gitlab::OAuth::User, lib: true do end context 'dont block on create' do - before { stub_omniauth_config(block_auto_created_users: false) } + before do + stub_omniauth_config(block_auto_created_users: false) + end it do oauth_user.save @@ -346,7 +384,9 @@ describe Gitlab::OAuth::User, lib: true do end context 'block on create' do - before { stub_omniauth_config(block_auto_created_users: true) } + before do + stub_omniauth_config(block_auto_created_users: true) + end it do oauth_user.save @@ -356,7 +396,9 @@ describe Gitlab::OAuth::User, lib: true do end context 'dont block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } + before do + allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) + end it do oauth_user.save @@ -366,7 +408,9 @@ describe Gitlab::OAuth::User, lib: true do end context 'block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } + before do + allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) + end it do oauth_user.save @@ -377,4 +421,40 @@ describe Gitlab::OAuth::User, lib: true do end end end + + describe 'updating email' do + let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } + + before do + stub_omniauth_config(sync_email_from_provider: 'my-provider') + end + + context "when provider sets an email" do + it "updates the user email" do + expect(gl_user.email).to eq(info_hash[:email]) + end + + it "has external_email set to true" do + expect(gl_user.external_email?).to be(true) + end + + it "has email_provider set to provider" do + expect(gl_user.email_provider).to eql 'my-provider' + end + end + + context "when provider doesn't set an email" do + before do + info_hash.delete(:email) + end + + it "does not update the user email" do + expect(gl_user.email).not_to eq(info_hash[:email]) + end + + it "has external_email set to false" do + expect(gl_user.external_email?).to be(false) + end + end + end end diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb index 8b77c925705..593aa5038ad 100644 --- a/spec/lib/gitlab/redis_spec.rb +++ b/spec/lib/gitlab/redis_spec.rb @@ -108,11 +108,18 @@ describe Gitlab::Redis do end describe '.with' do - before { clear_pool } - after { clear_pool } + before do + clear_pool + end + + after do + clear_pool + end context 'when running not on sidekiq workers' do - before { allow(Sidekiq).to receive(:server?).and_return(false) } + before do + allow(Sidekiq).to receive(:server?).and_return(false) + end it 'instantiates a connection pool with size 5' do expect(ConnectionPool).to receive(:new).with(size: 5).and_call_original diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb index b106d156b75..a4d2367b72a 100644 --- a/spec/lib/gitlab/saml/user_spec.rb +++ b/spec/lib/gitlab/saml/user_spec.rb @@ -31,11 +31,17 @@ describe Gitlab::Saml::User, lib: true do allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } }) end - before { stub_basic_saml_config } + before do + stub_basic_saml_config + end describe 'account exists on server' do - before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) } + before do + stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) + end + let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') } + context 'and should bind with SAML' do it 'adds the SAML identity to the existing user' do saml_user.save @@ -57,7 +63,10 @@ describe Gitlab::Saml::User, lib: true do end end - before { stub_saml_group_config(%w(Interns)) } + before do + stub_saml_group_config(%w(Interns)) + end + context 'are defined but the user does not belong there' do it 'does not mark the user as external' do saml_user.save @@ -80,7 +89,9 @@ describe Gitlab::Saml::User, lib: true do describe 'no account exists on server' do shared_examples 'to verify compliance with allow_single_sign_on' do context 'with allow_single_sign_on enabled' do - before { stub_omniauth_config(allow_single_sign_on: ['saml']) } + before do + stub_omniauth_config(allow_single_sign_on: ['saml']) + end it 'creates a user from SAML' do saml_user.save @@ -93,14 +104,20 @@ describe Gitlab::Saml::User, lib: true do end context 'with allow_single_sign_on default (["saml"])' do - before { stub_omniauth_config(allow_single_sign_on: ['saml']) } + before do + stub_omniauth_config(allow_single_sign_on: ['saml']) + end + it 'does not throw an error' do expect{ saml_user.save }.not_to raise_error end end context 'with allow_single_sign_on disabled' do - before { stub_omniauth_config(allow_single_sign_on: false) } + before do + stub_omniauth_config(allow_single_sign_on: false) + end + it 'throws an error' do expect{ saml_user.save }.to raise_error StandardError end @@ -128,15 +145,22 @@ describe Gitlab::Saml::User, lib: true do end context 'with auto_link_ldap_user disabled (default)' do - before { stub_omniauth_config({ auto_link_ldap_user: false, auto_link_saml_user: false, allow_single_sign_on: ['saml'] }) } + before do + stub_omniauth_config({ auto_link_ldap_user: false, auto_link_saml_user: false, allow_single_sign_on: ['saml'] }) + end + include_examples 'to verify compliance with allow_single_sign_on' end context 'with auto_link_ldap_user enabled' do - before { stub_omniauth_config({ auto_link_ldap_user: true, auto_link_saml_user: false }) } + before do + stub_omniauth_config({ auto_link_ldap_user: true, auto_link_saml_user: false }) + end context 'and at least one LDAP provider is defined' do - before { stub_ldap_config(providers: %w(ldapmain)) } + before do + stub_ldap_config(providers: %w(ldapmain)) + end context 'and a corresponding LDAP person' do before do @@ -239,11 +263,15 @@ describe Gitlab::Saml::User, lib: true do end describe 'blocking' do - before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) } + before do + stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) + end context 'signup with SAML only' do context 'dont block on create' do - before { stub_omniauth_config(block_auto_created_users: false) } + before do + stub_omniauth_config(block_auto_created_users: false) + end it 'does not block the user' do saml_user.save @@ -253,7 +281,9 @@ describe Gitlab::Saml::User, lib: true do end context 'block on create' do - before { stub_omniauth_config(block_auto_created_users: true) } + before do + stub_omniauth_config(block_auto_created_users: true) + end it 'blocks user' do saml_user.save @@ -270,7 +300,9 @@ describe Gitlab::Saml::User, lib: true do end context 'dont block on create' do - before { stub_omniauth_config(block_auto_created_users: false) } + before do + stub_omniauth_config(block_auto_created_users: false) + end it do saml_user.save @@ -280,7 +312,9 @@ describe Gitlab::Saml::User, lib: true do end context 'block on create' do - before { stub_omniauth_config(block_auto_created_users: true) } + before do + stub_omniauth_config(block_auto_created_users: true) + end it do saml_user.save diff --git a/spec/lib/gitlab/serializer/pagination_spec.rb b/spec/lib/gitlab/serializer/pagination_spec.rb index 519eb1b274f..1bc6536439e 100644 --- a/spec/lib/gitlab/serializer/pagination_spec.rb +++ b/spec/lib/gitlab/serializer/pagination_spec.rb @@ -22,7 +22,9 @@ describe Gitlab::Serializer::Pagination do let(:params) { { page: 1, per_page: 2 } } context 'when a multiple resources are present in relation' do - before { create_list(:user, 3) } + before do + create_list(:user, 3) + end it 'correctly paginates the resource' do expect(subject.count).to be 2 diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb index 329d1d74970..bf45c8d16d6 100644 --- a/spec/lib/gitlab/template/issue_template_spec.rb +++ b/spec/lib/gitlab/template/issue_template_spec.rb @@ -52,7 +52,10 @@ describe Gitlab::Template::IssueTemplate do context 'when repo is bare or empty' do let(:empty_project) { create(:empty_project) } - before { empty_project.add_user(user, Gitlab::Access::MASTER) } + + before do + empty_project.add_user(user, Gitlab::Access::MASTER) + end it "returns empty array" do templates = subject.by_category('', empty_project) @@ -77,7 +80,9 @@ describe Gitlab::Template::IssueTemplate do context "when repo is empty" do let(:empty_project) { create(:empty_project) } - before { empty_project.add_user(user, Gitlab::Access::MASTER) } + before do + empty_project.add_user(user, Gitlab::Access::MASTER) + end it "raises file not found" do issue_template = subject.new('.gitlab/issue_templates/not_existent.md', empty_project) diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb index 2b0056d9bab..8479f92c8df 100644 --- a/spec/lib/gitlab/template/merge_request_template_spec.rb +++ b/spec/lib/gitlab/template/merge_request_template_spec.rb @@ -52,7 +52,10 @@ describe Gitlab::Template::MergeRequestTemplate do context 'when repo is bare or empty' do let(:empty_project) { create(:empty_project) } - before { empty_project.add_user(user, Gitlab::Access::MASTER) } + + before do + empty_project.add_user(user, Gitlab::Access::MASTER) + end it "returns empty array" do templates = subject.by_category('', empty_project) @@ -77,7 +80,9 @@ describe Gitlab::Template::MergeRequestTemplate do context "when repo is empty" do let(:empty_project) { create(:empty_project) } - before { empty_project.add_user(user, Gitlab::Access::MASTER) } + before do + empty_project.add_user(user, Gitlab::Access::MASTER) + end it "raises file not found" do issue_template = subject.new('.gitlab/merge_request_templates/not_existent.md', empty_project) diff --git a/spec/lib/gitlab/uploads_transfer_spec.rb b/spec/lib/gitlab/uploads_transfer_spec.rb new file mode 100644 index 00000000000..109559bb01c --- /dev/null +++ b/spec/lib/gitlab/uploads_transfer_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe Gitlab::UploadsTransfer do + it 'leaves avatar uploads where they are' do + project_with_avatar = create(:empty_project, :with_avatar) + + described_class.new.rename_namespace('project', 'project-renamed') + + expect(File.exist?(project_with_avatar.avatar.path)).to be_truthy + end +end diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index 3fe8cf43934..e8a37e8d77b 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -97,6 +97,17 @@ describe Gitlab::UrlBuilder, lib: true do end end + context 'on a PersonalSnippet' do + it 'returns a proper URL' do + personal_snippet = create(:personal_snippet) + note = build_stubbed(:note_on_personal_snippet, noteable: personal_snippet) + + url = described_class.build(note) + + expect(url).to eq "#{Settings.gitlab['url']}/snippets/#{note.noteable_id}#note_#{note.id}" + end + end + context 'on another object' do it 'returns a proper URL' do project = build_stubbed(:empty_project) diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index b1999409170..ad19998dff4 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -212,7 +212,7 @@ describe Gitlab::Workhorse, lib: true do it 'includes a Repository param' do repo_param = { Repository: { - path: repo_path, + path: '', # deprecated field; grpc automatically creates it anyway storage_name: 'default', relative_path: project.full_path + '.git' } } diff --git a/spec/lib/json_web_token/rsa_token_spec.rb b/spec/lib/json_web_token/rsa_token_spec.rb index 18726754517..e7022bd06f8 100644 --- a/spec/lib/json_web_token/rsa_token_spec.rb +++ b/spec/lib/json_web_token/rsa_token_spec.rb @@ -15,11 +15,15 @@ describe JSONWebToken::RSAToken do let(:rsa_token) { described_class.new(nil) } let(:rsa_encoded) { rsa_token.encoded } - before { allow_any_instance_of(described_class).to receive(:key).and_return(rsa_key) } + before do + allow_any_instance_of(described_class).to receive(:key).and_return(rsa_key) + end context 'token' do context 'for valid key to be validated' do - before { rsa_token['key'] = 'value' } + before do + rsa_token['key'] = 'value' + end subject { JWT.decode(rsa_encoded, rsa_key) } diff --git a/spec/lib/json_web_token/token_spec.rb b/spec/lib/json_web_token/token_spec.rb index 3d955e4d774..d7e7560d962 100644 --- a/spec/lib/json_web_token/token_spec.rb +++ b/spec/lib/json_web_token/token_spec.rb @@ -3,7 +3,10 @@ describe JSONWebToken::Token do context 'custom parameters' do let(:value) { 'value' } - before { token[:key] = value } + + before do + token[:key] = value + end it { expect(token[:key]).to eq(value) } it { expect(token.payload).to include(key: value) } diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index ec6f6c42eac..980b24370d0 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -130,8 +130,13 @@ describe Notify do end context 'with a preferred language' do - before { Gitlab::I18n.locale = :es } - after { Gitlab::I18n.use_default_locale } + before do + Gitlab::I18n.locale = :es + end + + after do + Gitlab::I18n.use_default_locale + end it 'always generates the email using the default language' do is_expected.to have_body_text('foo, bar, and baz') @@ -581,7 +586,9 @@ describe Notify do let(:project) { create(:project, :repository) } let(:commit) { project.commit } - before(:each) { allow(note).to receive(:noteable).and_return(commit) } + before do + allow(note).to receive(:noteable).and_return(commit) + end subject { described_class.note_commit_email(recipient.id, note.id) } @@ -603,7 +610,10 @@ describe Notify do describe 'on a merge request' do let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:note_on_merge_request_path) { namespace_project_merge_request_path(project.namespace, project, merge_request, anchor: "note_#{note.id}") } - before(:each) { allow(note).to receive(:noteable).and_return(merge_request) } + + before do + allow(note).to receive(:noteable).and_return(merge_request) + end subject { described_class.note_merge_request_email(recipient.id, note.id) } @@ -625,7 +635,10 @@ describe Notify do describe 'on an issue' do let(:issue) { create(:issue, project: project) } let(:note_on_issue_path) { namespace_project_issue_path(project.namespace, project, issue, anchor: "note_#{note.id}") } - before(:each) { allow(note).to receive(:noteable).and_return(issue) } + + before do + allow(note).to receive(:noteable).and_return(issue) + end subject { described_class.note_issue_email(recipient.id, note.id) } @@ -687,7 +700,9 @@ describe Notify do let(:commit) { project.commit } let(:note) { create(:discussion_note_on_commit, commit_id: commit.id, project: project, author: note_author) } - before(:each) { allow(note).to receive(:noteable).and_return(commit) } + before do + allow(note).to receive(:noteable).and_return(commit) + end subject { described_class.note_commit_email(recipient.id, note.id) } @@ -711,7 +726,10 @@ describe Notify do let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:note) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project, author: note_author) } let(:note_on_merge_request_path) { namespace_project_merge_request_path(project.namespace, project, merge_request, anchor: "note_#{note.id}") } - before(:each) { allow(note).to receive(:noteable).and_return(merge_request) } + + before do + allow(note).to receive(:noteable).and_return(merge_request) + end subject { described_class.note_merge_request_email(recipient.id, note.id) } @@ -735,7 +753,10 @@ describe Notify do let(:issue) { create(:issue, project: project) } let(:note) { create(:discussion_note_on_issue, noteable: issue, project: project, author: note_author) } let(:note_on_issue_path) { namespace_project_issue_path(project.namespace, project, issue, anchor: "note_#{note.id}") } - before(:each) { allow(note).to receive(:noteable).and_return(issue) } + + before do + allow(note).to receive(:noteable).and_return(issue) + end subject { described_class.note_issue_email(recipient.id, note.id) } diff --git a/spec/migrations/README.md b/spec/migrations/README.md new file mode 100644 index 00000000000..05d4f35db72 --- /dev/null +++ b/spec/migrations/README.md @@ -0,0 +1,87 @@ +# Testing migrations + +In order to reliably test a migration, we need to test it against a database +schema that this migration has been written for. In order to achieve that we +have some _migration helpers_ and RSpec test tag, called `:migration`. + +If you want to write a test for a migration consider adding `:migration` tag to +the test signature, like `describe SomeMigrationClass, :migration`. + +## How does it work? + +Adding a `:migration` tag to a test signature injects a few before / after +hooks to the test. + +The most important change is that adding a `:migration` tag adds a `before` +hook that will revert all migrations to the point that a migration under test +is not yet migrated. + +In other words, our custom RSpec hooks will find a previous migration, and +migrate the database **down** to the previous migration version. + +With this approach you can test a migration against a database schema that this +migration has been written for. + +Use `migrate!` helper to run the migration that is under test. + +The `after` hook will migrate the database **up** and reinstitutes the latest +schema version, so that the process does not affect subsequent specs and +ensures proper isolation. + +## Available helpers + +Use `table` helper to create a temporary `ActiveRecord::Base` derived model +for a table. + +Use `migrate!` helper to run the migration that is under test. It will not only +run migration, but will also bump the schema version in the `schema_migrations` +table. It is necessary because in the `after` hook we trigger the rest of +the migrations, and we need to know where to start. + +See `spec/support/migrations_helpers.rb` for all the available helpers. + +## An example + +```ruby +require 'spec_helper' + +# Load a migration class. + +require Rails.root.join('db', 'post_migrate', '20170526185842_migrate_pipeline_stages.rb') + +describe MigratePipelineStages, :migration do + + # Create test data - pipeline and CI/CD jobs. + + let(:jobs) { table(:ci_builds) } + let(:stages) { table(:ci_stages) } + let(:pipelines) { table(:ci_pipelines) } + let(:projects) { table(:projects) } + + before do + projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1') + pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a') + jobs.create!(id: 1, commit_id: 1, project_id: 123, stage_idx: 2, stage: 'build') + jobs.create!(id: 2, commit_id: 1, project_id: 123, stage_idx: 1, stage: 'test') + end + + # Test the migration. + + it 'correctly migrates pipeline stages' do + expect(stages.count).to be_zero + + migrate! + + expect(stages.count).to eq 2 + expect(stages.all.pluck(:name)).to match_array %w[test build] + end +end +``` + +## Best practices + +1. Use only one test example per migration unless there is a good reason to +use more. +1. Note that this type of tests do not run within the transaction, we use +a truncation database cleanup strategy. Do not depend on transaction being +present. diff --git a/spec/migrations/clean_upload_symlinks_spec.rb b/spec/migrations/clean_upload_symlinks_spec.rb new file mode 100644 index 00000000000..cecb3ddac53 --- /dev/null +++ b/spec/migrations/clean_upload_symlinks_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170406111121_clean_upload_symlinks.rb') + +describe CleanUploadSymlinks do + let(:migration) { described_class.new } + let(:test_dir) { File.join(Rails.root, "tmp", "tests", "move_uploads_test") } + let(:uploads_dir) { File.join(test_dir, "public", "uploads") } + let(:new_uploads_dir) { File.join(uploads_dir, "system") } + let(:original_path) { File.join(new_uploads_dir, 'user') } + let(:symlink_path) { File.join(uploads_dir, 'user') } + + before do + FileUtils.remove_dir(test_dir) if File.directory?(test_dir) + FileUtils.mkdir_p(uploads_dir) + allow(migration).to receive(:base_directory).and_return(test_dir) + allow(migration).to receive(:say) + end + + describe "#up" do + before do + FileUtils.mkdir_p(original_path) + FileUtils.ln_s(original_path, symlink_path) + end + + it 'removes the symlink' do + migration.up + + expect(File.symlink?(symlink_path)).to be(false) + end + end + + describe '#down' do + before do + FileUtils.mkdir_p(File.join(original_path)) + FileUtils.touch(File.join(original_path, 'dummy.file')) + end + + it 'creates a symlink' do + expected_path = File.join(symlink_path, "dummy.file") + migration.down + + expect(File.exist?(expected_path)).to be(true) + expect(File.symlink?(symlink_path)).to be(true) + end + end +end diff --git a/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb b/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb new file mode 100644 index 00000000000..1396d12e5a9 --- /dev/null +++ b/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb @@ -0,0 +1,118 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170607121233_convert_custom_notification_settings_to_columns') + +describe ConvertCustomNotificationSettingsToColumns, :migration do + let(:settings_params) do + [ + { level: 0, events: [:new_note] }, # disabled, single event + { level: 3, events: [:new_issue, :reopen_issue, :close_issue, :reassign_issue] }, # global, multiple events + { level: 5, events: described_class::EMAIL_EVENTS }, # custom, all events + { level: 5, events: [] } # custom, no events + ] + end + + let(:notification_settings_before) do + settings_params.map do |params| + events = {} + + params[:events].each do |event| + events[event] = true + end + + user = create(:user) + create_params = { user_id: user.id, level: params[:level], events: events } + notification_setting = described_class::NotificationSetting.create(create_params) + + [notification_setting, params] + end + end + + let(:notification_settings_after) do + settings_params.map do |params| + events = {} + + params[:events].each do |event| + events[event] = true + end + + user = create(:user) + create_params = events.merge(user_id: user.id, level: params[:level]) + notification_setting = described_class::NotificationSetting.create(create_params) + + [notification_setting, params] + end + end + + describe '#up' do + it 'migrates all settings where a custom event is enabled, even if they are not currently using the custom level' do + notification_settings_before + + described_class.new.up + + notification_settings_before.each do |(notification_setting, params)| + notification_setting.reload + + expect(notification_setting.read_attribute_before_type_cast(:events)).to be_nil + expect(notification_setting.level).to eq(params[:level]) + + described_class::EMAIL_EVENTS.each do |event| + # We don't set the others to false, just let them default to nil + expected = params[:events].include?(event) || nil + + expect(notification_setting.read_attribute(event)).to eq(expected) + end + end + end + end + + describe '#down' do + it 'creates a custom events hash for all settings where at least one event is enabled' do + notification_settings_after + + described_class.new.down + + notification_settings_after.each do |(notification_setting, params)| + notification_setting.reload + + expect(notification_setting.level).to eq(params[:level]) + + if params[:events].empty? + # We don't migrate empty settings + expect(notification_setting.events).to eq({}) + else + described_class::EMAIL_EVENTS.each do |event| + expected = params[:events].include?(event) + + expect(notification_setting.events[event]).to eq(expected) + expect(notification_setting.read_attribute(event)).to be_nil + end + end + end + end + + it 'reverts the database to the state it was in before' do + notification_settings_before + + described_class.new.up + described_class.new.down + + notification_settings_before.each do |(notification_setting, params)| + notification_setting.reload + + expect(notification_setting.level).to eq(params[:level]) + + if params[:events].empty? + # We don't migrate empty settings + expect(notification_setting.events).to eq({}) + else + described_class::EMAIL_EVENTS.each do |event| + expected = params[:events].include?(event) + + expect(notification_setting.events[event]).to eq(expected) + expect(notification_setting.read_attribute(event)).to be_nil + end + end + end + end + end +end diff --git a/spec/migrations/migrate_build_stage_reference_spec.rb b/spec/migrations/migrate_build_stage_reference_spec.rb new file mode 100644 index 00000000000..80b321860c2 --- /dev/null +++ b/spec/migrations/migrate_build_stage_reference_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170526185921_migrate_build_stage_reference.rb') + +describe MigrateBuildStageReference, :migration do + ## + # Create test data - pipeline and CI/CD jobs. + # + + let(:jobs) { table(:ci_builds) } + let(:stages) { table(:ci_stages) } + let(:pipelines) { table(:ci_pipelines) } + let(:projects) { table(:projects) } + + before do + # Create projects + # + projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1') + projects.create!(id: 456, name: 'gitlab2', path: 'gitlab2') + + # Create CI/CD pipelines + # + pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a') + pipelines.create!(id: 2, project_id: 456, ref: 'feature', sha: '21a3deb') + + # Create CI/CD jobs + # + jobs.create!(id: 1, commit_id: 1, project_id: 123, stage_idx: 2, stage: 'build') + jobs.create!(id: 2, commit_id: 1, project_id: 123, stage_idx: 2, stage: 'build') + jobs.create!(id: 3, commit_id: 1, project_id: 123, stage_idx: 1, stage: 'test') + jobs.create!(id: 4, commit_id: 1, project_id: 123, stage_idx: 3, stage: 'deploy') + jobs.create!(id: 5, commit_id: 2, project_id: 456, stage_idx: 2, stage: 'test:2') + jobs.create!(id: 6, commit_id: 2, project_id: 456, stage_idx: 1, stage: 'test:1') + jobs.create!(id: 7, commit_id: 2, project_id: 456, stage_idx: 1, stage: 'test:1') + jobs.create!(id: 8, commit_id: 3, project_id: 789, stage_idx: 3, stage: 'deploy') + + # Create CI/CD stages + # + stages.create(id: 101, pipeline_id: 1, project_id: 123, name: 'test') + stages.create(id: 102, pipeline_id: 1, project_id: 123, name: 'build') + stages.create(id: 103, pipeline_id: 1, project_id: 123, name: 'deploy') + stages.create(id: 104, pipeline_id: 2, project_id: 456, name: 'test:1') + stages.create(id: 105, pipeline_id: 2, project_id: 456, name: 'test:2') + stages.create(id: 106, pipeline_id: 2, project_id: 456, name: 'deploy') + end + + it 'correctly migrate build stage references' do + expect(jobs.where(stage_id: nil).count).to eq 8 + + migrate! + + expect(jobs.where(stage_id: nil).count).to eq 1 + + expect(jobs.find(1).stage_id).to eq 102 + expect(jobs.find(2).stage_id).to eq 102 + expect(jobs.find(3).stage_id).to eq 101 + expect(jobs.find(4).stage_id).to eq 103 + expect(jobs.find(5).stage_id).to eq 105 + expect(jobs.find(6).stage_id).to eq 104 + expect(jobs.find(7).stage_id).to eq 104 + expect(jobs.find(8).stage_id).to eq nil + end +end diff --git a/spec/migrations/migrate_pipeline_stages_spec.rb b/spec/migrations/migrate_pipeline_stages_spec.rb new file mode 100644 index 00000000000..c47f2bb8ff9 --- /dev/null +++ b/spec/migrations/migrate_pipeline_stages_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170526185842_migrate_pipeline_stages.rb') + +describe MigratePipelineStages, :migration do + ## + # Create test data - pipeline and CI/CD jobs. + # + + let(:jobs) { table(:ci_builds) } + let(:stages) { table(:ci_stages) } + let(:pipelines) { table(:ci_pipelines) } + let(:projects) { table(:projects) } + + before do + # Create projects + # + projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1') + projects.create!(id: 456, name: 'gitlab2', path: 'gitlab2') + + # Create CI/CD pipelines + # + pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a') + pipelines.create!(id: 2, project_id: 456, ref: 'feature', sha: '21a3deb') + + # Create CI/CD jobs + # + jobs.create!(id: 1, commit_id: 1, project_id: 123, stage_idx: 2, stage: 'build') + jobs.create!(id: 2, commit_id: 1, project_id: 123, stage_idx: 2, stage: 'build') + jobs.create!(id: 3, commit_id: 1, project_id: 123, stage_idx: 1, stage: 'test') + jobs.create!(id: 4, commit_id: 1, project_id: 123, stage_idx: 1, stage: 'test') + jobs.create!(id: 5, commit_id: 1, project_id: 123, stage_idx: 3, stage: 'deploy') + jobs.create!(id: 6, commit_id: 2, project_id: 456, stage_idx: 3, stage: 'deploy') + jobs.create!(id: 7, commit_id: 2, project_id: 456, stage_idx: 2, stage: 'test:2') + jobs.create!(id: 8, commit_id: 2, project_id: 456, stage_idx: 1, stage: 'test:1') + jobs.create!(id: 9, commit_id: 2, project_id: 456, stage_idx: 1, stage: 'test:1') + jobs.create!(id: 10, commit_id: 2, project_id: 456, stage_idx: 2, stage: 'test:2') + jobs.create!(id: 11, commit_id: 3, project_id: 456, stage_idx: 3, stage: 'deploy') + jobs.create!(id: 12, commit_id: 2, project_id: 789, stage_idx: 3, stage: 'deploy') + end + + it 'correctly migrates pipeline stages' do + expect(stages.count).to be_zero + + migrate! + + expect(stages.count).to eq 6 + expect(stages.all.pluck(:name)) + .to match_array %w[test build deploy test:1 test:2 deploy] + expect(stages.where(pipeline_id: 1).order(:id).pluck(:name)) + .to eq %w[test build deploy] + expect(stages.where(pipeline_id: 2).order(:id).pluck(:name)) + .to eq %w[test:1 test:2 deploy] + expect(stages.where(pipeline_id: 3).count).to be_zero + expect(stages.where(project_id: 789).count).to be_zero + end +end diff --git a/spec/migrations/move_uploads_to_system_dir_spec.rb b/spec/migrations/move_uploads_to_system_dir_spec.rb new file mode 100644 index 00000000000..37d66452447 --- /dev/null +++ b/spec/migrations/move_uploads_to_system_dir_spec.rb @@ -0,0 +1,68 @@ +require "spec_helper" +require Rails.root.join("db", "migrate", "20170316163845_move_uploads_to_system_dir.rb") + +describe MoveUploadsToSystemDir do + let(:migration) { described_class.new } + let(:test_dir) { File.join(Rails.root, "tmp", "move_uploads_test") } + let(:uploads_dir) { File.join(test_dir, "public", "uploads") } + let(:new_uploads_dir) { File.join(uploads_dir, "system") } + + before do + FileUtils.remove_dir(test_dir) if File.directory?(test_dir) + FileUtils.mkdir_p(uploads_dir) + allow(migration).to receive(:base_directory).and_return(test_dir) + allow(migration).to receive(:say) + end + + describe "#up" do + before do + FileUtils.mkdir_p(File.join(uploads_dir, 'user')) + FileUtils.touch(File.join(uploads_dir, 'user', 'dummy.file')) + end + + it 'moves the directory to the new path' do + expected_path = File.join(new_uploads_dir, 'user', 'dummy.file') + + migration.up + + expect(File.exist?(expected_path)).to be(true) + end + + it 'creates a symlink in the old location' do + symlink_path = File.join(uploads_dir, 'user') + expected_path = File.join(symlink_path, 'dummy.file') + + migration.up + + expect(File.exist?(expected_path)).to be(true) + expect(File.symlink?(symlink_path)).to be(true) + end + end + + describe "#down" do + before do + FileUtils.mkdir_p(File.join(new_uploads_dir, 'user')) + FileUtils.touch(File.join(new_uploads_dir, 'user', 'dummy.file')) + end + + it 'moves the directory to the old path' do + expected_path = File.join(uploads_dir, 'user', 'dummy.file') + + migration.down + + expect(File.exist?(expected_path)).to be(true) + end + + it 'removes the symlink if it existed' do + FileUtils.ln_s(File.join(new_uploads_dir, 'user'), File.join(uploads_dir, 'user')) + + directory = File.join(uploads_dir, 'user') + expected_path = File.join(directory, 'dummy.file') + + migration.down + + expect(File.exist?(expected_path)).to be(true) + expect(File.symlink?(directory)).to be(false) + end + end +end diff --git a/spec/migrations/rename_more_reserved_project_names_spec.rb b/spec/migrations/rename_more_reserved_project_names_spec.rb index 36e82729c23..4bd8d4ac0d1 100644 --- a/spec/migrations/rename_more_reserved_project_names_spec.rb +++ b/spec/migrations/rename_more_reserved_project_names_spec.rb @@ -17,7 +17,9 @@ describe RenameMoreReservedProjectNames, truncate: true do describe '#up' do context 'when project repository exists' do - before { project.create_repository } + before do + project.create_repository + end context 'when no exception is raised' do it 'renames project with reserved names' do diff --git a/spec/migrations/rename_reserved_project_names_spec.rb b/spec/migrations/rename_reserved_project_names_spec.rb index 4fb7ed36884..05e021c2e32 100644 --- a/spec/migrations/rename_reserved_project_names_spec.rb +++ b/spec/migrations/rename_reserved_project_names_spec.rb @@ -17,7 +17,9 @@ describe RenameReservedProjectNames, truncate: true do describe '#up' do context 'when project repository exists' do - before { project.create_repository } + before do + project.create_repository + end context 'when no exception is raised' do it 'renames project with reserved names' do diff --git a/spec/migrations/rename_system_namespaces_spec.rb b/spec/migrations/rename_system_namespaces_spec.rb new file mode 100644 index 00000000000..626a6005838 --- /dev/null +++ b/spec/migrations/rename_system_namespaces_spec.rb @@ -0,0 +1,254 @@ +require "spec_helper" +require Rails.root.join("db", "migrate", "20170316163800_rename_system_namespaces.rb") + +describe RenameSystemNamespaces, truncate: true do + let(:migration) { described_class.new } + let(:test_dir) { File.join(Rails.root, "tmp", "tests", "rename_namespaces_test") } + let(:uploads_dir) { File.join(test_dir, "public", "uploads") } + let(:system_namespace) do + namespace = build(:namespace, path: "system") + namespace.save(validate: false) + namespace + end + + def save_invalid_routable(routable) + routable.__send__(:prepare_route) + routable.save(validate: false) + end + + before do + FileUtils.remove_dir(test_dir) if File.directory?(test_dir) + FileUtils.mkdir_p(uploads_dir) + FileUtils.remove_dir(TestEnv.repos_path) if File.directory?(TestEnv.repos_path) + allow(migration).to receive(:say) + allow(migration).to receive(:uploads_dir).and_return(uploads_dir) + end + + describe "#system_namespace" do + it "only root namespaces called with path `system`" do + system_namespace + system_namespace_with_parent = build(:namespace, path: 'system', parent: create(:namespace)) + system_namespace_with_parent.save(validate: false) + + expect(migration.system_namespace.id).to eq(system_namespace.id) + end + end + + describe "#up" do + before do + system_namespace + end + + it "doesn't break if there are no namespaces called system" do + Namespace.delete_all + + migration.up + end + + it "renames namespaces called system" do + migration.up + + expect(system_namespace.reload.path).to eq("system0") + end + + it "renames the route to the namespace" do + migration.up + + expect(system_namespace.reload.full_path).to eq("system0") + end + + it "renames the route for projects of the namespace" do + project = build(:project, path: "project-path", namespace: system_namespace) + save_invalid_routable(project) + + migration.up + + expect(project.route.reload.path).to eq("system0/project-path") + end + + it "doesn't touch routes of namespaces that look like system" do + namespace = create(:group, path: 'systemlookalike') + project = create(:project, namespace: namespace, path: 'the-project') + + migration.up + + expect(project.route.reload.path).to eq('systemlookalike/the-project') + expect(namespace.route.reload.path).to eq('systemlookalike') + end + + it "moves the the repository for a project in the namespace" do + project = build(:project, namespace: system_namespace, path: "system-project") + save_invalid_routable(project) + TestEnv.copy_repo(project, + bare_repo: TestEnv.factory_repo_path_bare, + refs: TestEnv::BRANCH_SHA) + expected_repo = File.join(TestEnv.repos_path, "system0", "system-project.git") + + migration.up + + expect(File.directory?(expected_repo)).to be(true) + end + + it "moves the uploads for the namespace" do + allow(migration).to receive(:move_namespace_folders).with(Settings.pages.path, "system", "system0") + expect(migration).to receive(:move_namespace_folders).with(uploads_dir, "system", "system0") + + migration.up + end + + it "moves the pages for the namespace" do + allow(migration).to receive(:move_namespace_folders).with(uploads_dir, "system", "system0") + expect(migration).to receive(:move_namespace_folders).with(Settings.pages.path, "system", "system0") + + migration.up + end + + describe "clears the markdown cache for projects in the system namespace" do + let!(:project) do + project = build(:project, namespace: system_namespace) + save_invalid_routable(project) + project + end + + it 'removes description_html from projects' do + migration.up + + expect(project.reload.description_html).to be_nil + end + + it 'removes issue descriptions' do + issue = create(:issue, project: project, description_html: 'Issue description') + + migration.up + + expect(issue.reload.description_html).to be_nil + end + + it 'removes merge request descriptions' do + merge_request = create(:merge_request, + source_project: project, + target_project: project, + description_html: 'MergeRequest description') + + migration.up + + expect(merge_request.reload.description_html).to be_nil + end + + it 'removes note html' do + note = create(:note, + project: project, + noteable: create(:issue, project: project), + note_html: 'note description') + + migration.up + + expect(note.reload.note_html).to be_nil + end + + it 'removes milestone description' do + milestone = create(:milestone, + project: project, + description_html: 'milestone description') + + migration.up + + expect(milestone.reload.description_html).to be_nil + end + end + + context "system namespace -> subgroup -> system0 project" do + it "updates the route of the project correctly" do + subgroup = build(:group, path: "subgroup", parent: system_namespace) + save_invalid_routable(subgroup) + project = build(:project, path: "system0", namespace: subgroup) + save_invalid_routable(project) + + migration.up + + expect(project.route.reload.path).to eq("system0/subgroup/system0") + end + end + end + + describe "#move_repositories" do + let(:namespace) { create(:group, name: "hello-group") } + it "moves a project for a namespace" do + create(:project, namespace: namespace, path: "hello-project") + expected_path = File.join(TestEnv.repos_path, "bye-group", "hello-project.git") + + migration.move_repositories(namespace, "hello-group", "bye-group") + + expect(File.directory?(expected_path)).to be(true) + end + + it "moves a namespace in a subdirectory correctly" do + child_namespace = create(:group, name: "sub-group", parent: namespace) + create(:project, namespace: child_namespace, path: "hello-project") + + expected_path = File.join(TestEnv.repos_path, "hello-group", "renamed-sub-group", "hello-project.git") + + migration.move_repositories(child_namespace, "hello-group/sub-group", "hello-group/renamed-sub-group") + + expect(File.directory?(expected_path)).to be(true) + end + + it "moves a parent namespace with subdirectories" do + child_namespace = create(:group, name: "sub-group", parent: namespace) + create(:project, namespace: child_namespace, path: "hello-project") + expected_path = File.join(TestEnv.repos_path, "renamed-group", "sub-group", "hello-project.git") + + migration.move_repositories(child_namespace, "hello-group", "renamed-group") + + expect(File.directory?(expected_path)).to be(true) + end + end + + describe "#move_namespace_folders" do + it "moves a namespace with files" do + source = File.join(uploads_dir, "parent-group", "sub-group") + FileUtils.mkdir_p(source) + destination = File.join(uploads_dir, "parent-group", "moved-group") + FileUtils.touch(File.join(source, "test.txt")) + expected_file = File.join(destination, "test.txt") + + migration.move_namespace_folders(uploads_dir, File.join("parent-group", "sub-group"), File.join("parent-group", "moved-group")) + + expect(File.exist?(expected_file)).to be(true) + end + + it "moves a parent namespace uploads" do + source = File.join(uploads_dir, "parent-group", "sub-group") + FileUtils.mkdir_p(source) + destination = File.join(uploads_dir, "moved-parent", "sub-group") + FileUtils.touch(File.join(source, "test.txt")) + expected_file = File.join(destination, "test.txt") + + migration.move_namespace_folders(uploads_dir, "parent-group", "moved-parent") + + expect(File.exist?(expected_file)).to be(true) + end + end + + describe "#child_ids_for_parent" do + it "collects child ids for all levels" do + parent = create(:group) + first_child = create(:group, parent: parent) + second_child = create(:group, parent: parent) + third_child = create(:group, parent: second_child) + all_ids = [parent.id, first_child.id, second_child.id, third_child.id] + + collected_ids = migration.child_ids_for_parent(parent, ids: [parent.id]) + + expect(collected_ids).to contain_exactly(*all_ids) + end + end + + describe "#remove_last_ocurrence" do + it "removes only the last occurance of a string" do + input = "this/is/system/namespace/with/system" + + expect(migration.remove_last_occurrence(input, "system")).to eq("this/is/system/namespace/with/") + end + end +end diff --git a/spec/migrations/update_upload_paths_to_system_spec.rb b/spec/migrations/update_upload_paths_to_system_spec.rb new file mode 100644 index 00000000000..7df44515424 --- /dev/null +++ b/spec/migrations/update_upload_paths_to_system_spec.rb @@ -0,0 +1,53 @@ +require "spec_helper" +require Rails.root.join("db", "post_migrate", "20170317162059_update_upload_paths_to_system.rb") + +describe UpdateUploadPathsToSystem do + let(:migration) { described_class.new } + + before do + allow(migration).to receive(:say) + end + + describe "#uploads_to_switch_to_new_path" do + it "contains only uploads with the old path for the correct models" do + _upload_for_other_type = create(:upload, model: create(:ci_pipeline), path: "uploads/ci_pipeline/avatar.jpg") + _upload_with_system_path = create(:upload, model: create(:empty_project), path: "uploads/system/project/avatar.jpg") + _upload_with_other_path = create(:upload, model: create(:empty_project), path: "thelongsecretforafileupload/avatar.jpg") + old_upload = create(:upload, model: create(:empty_project), path: "uploads/project/avatar.jpg") + group_upload = create(:upload, model: create(:group), path: "uploads/group/avatar.jpg") + + expect(Upload.where(migration.uploads_to_switch_to_new_path)).to contain_exactly(old_upload, group_upload) + end + end + + describe "#uploads_to_switch_to_old_path" do + it "contains only uploads with the new path for the correct models" do + _upload_for_other_type = create(:upload, model: create(:ci_pipeline), path: "uploads/ci_pipeline/avatar.jpg") + upload_with_system_path = create(:upload, model: create(:empty_project), path: "uploads/system/project/avatar.jpg") + _upload_with_other_path = create(:upload, model: create(:empty_project), path: "thelongsecretforafileupload/avatar.jpg") + _old_upload = create(:upload, model: create(:empty_project), path: "uploads/project/avatar.jpg") + + expect(Upload.where(migration.uploads_to_switch_to_old_path)).to contain_exactly(upload_with_system_path) + end + end + + describe "#up", truncate: true do + it "updates old upload records to the new path" do + old_upload = create(:upload, model: create(:empty_project), path: "uploads/project/avatar.jpg") + + migration.up + + expect(old_upload.reload.path).to eq("uploads/system/project/avatar.jpg") + end + end + + describe "#down", truncate: true do + it "updates the new system patsh to the old paths" do + new_upload = create(:upload, model: create(:empty_project), path: "uploads/system/project/avatar.jpg") + + migration.down + + expect(new_upload.reload.path).to eq("uploads/project/avatar.jpg") + end + end +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index fa229542f70..166a4474abf 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -78,7 +78,9 @@ describe ApplicationSetting, models: true do # Upgraded databases will have this sort of content context 'repository_storages is a String, not an Array' do - before { setting.__send__(:raw_write_attribute, :repository_storages, 'default') } + before do + setting.__send__(:raw_write_attribute, :repository_storages, 'default') + end it { expect(setting.repository_storages_before_type_cast).to eq('default') } it { expect(setting.repository_storages).to eq(['default']) } diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb index f19e1af65a6..e1193e0d19a 100644 --- a/spec/models/blob_spec.rb +++ b/spec/models/blob_spec.rb @@ -199,6 +199,14 @@ describe Blob do end end + describe '#file_type' do + it 'returns the file type' do + blob = fake_blob(path: 'README.md') + + expect(blob.file_type).to eq(:readme) + end + end + describe '#simple_viewer' do context 'when the blob is empty' do it 'returns an empty viewer' do diff --git a/spec/models/blob_viewer/base_spec.rb b/spec/models/blob_viewer/base_spec.rb index d56379eb59d..574438838d8 100644 --- a/spec/models/blob_viewer/base_spec.rb +++ b/spec/models/blob_viewer/base_spec.rb @@ -106,9 +106,9 @@ describe BlobViewer::Base, model: true do end describe '#render_error' do - context 'when expanded' do + context 'when the blob is expanded' do before do - viewer.expanded = true + blob.expand! end context 'when the blob size is larger than the size limit' do diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb index 219db365a91..333f4139a96 100644 --- a/spec/models/broadcast_message_spec.rb +++ b/spec/models/broadcast_message_spec.rb @@ -21,22 +21,29 @@ describe BroadcastMessage, models: true do end describe '.current' do - it "returns last message if time match" do + it 'returns message if time match' do message = create(:broadcast_message) - expect(BroadcastMessage.current).to eq message + expect(BroadcastMessage.current).to include(message) end - it "returns nil if time not come" do + it 'returns multiple messages if time match' do + message1 = create(:broadcast_message) + message2 = create(:broadcast_message) + + expect(BroadcastMessage.current).to contain_exactly(message1, message2) + end + + it 'returns empty list if time not come' do create(:broadcast_message, :future) - expect(BroadcastMessage.current).to be_nil + expect(BroadcastMessage.current).to be_empty end - it "returns nil if time has passed" do + it 'returns empty list if time has passed' do create(:broadcast_message, :expired) - expect(BroadcastMessage.current).to be_nil + expect(BroadcastMessage.current).to be_empty end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index b0716e04d3d..3816422fec6 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -21,6 +21,18 @@ describe Ci::Build, :models do it { is_expected.to respond_to(:has_trace?) } it { is_expected.to respond_to(:trace) } + describe '.manual_actions' do + let!(:manual_but_created) { create(:ci_build, :manual, status: :created, pipeline: pipeline) } + let!(:manual_but_succeeded) { create(:ci_build, :manual, status: :success, pipeline: pipeline) } + let!(:manual_action) { create(:ci_build, :manual, pipeline: pipeline) } + + subject { described_class.manual_actions } + + it { is_expected.to include(manual_action) } + it { is_expected.to include(manual_but_succeeded) } + it { is_expected.not_to include(manual_but_created) } + end + describe '#actionize' do context 'when build is a created' do before do @@ -95,12 +107,18 @@ describe Ci::Build, :models do it { is_expected.to be_truthy } context 'is expired' do - before { build.update(artifacts_expire_at: Time.now - 7.days) } + before do + build.update(artifacts_expire_at: Time.now - 7.days) + end + it { is_expected.to be_falsy } end context 'is not expired' do - before { build.update(artifacts_expire_at: Time.now + 7.days) } + before do + build.update(artifacts_expire_at: Time.now + 7.days) + end + it { is_expected.to be_truthy } end end @@ -110,13 +128,17 @@ describe Ci::Build, :models do subject { build.artifacts_expired? } context 'is expired' do - before { build.update(artifacts_expire_at: Time.now - 7.days) } + before do + build.update(artifacts_expire_at: Time.now - 7.days) + end it { is_expected.to be_truthy } end context 'is not expired' do - before { build.update(artifacts_expire_at: Time.now + 7.days) } + before do + build.update(artifacts_expire_at: Time.now + 7.days) + end it { is_expected.to be_falsey } end @@ -141,7 +163,9 @@ describe Ci::Build, :models do context 'when artifacts_expire_at is specified' do let(:expire_at) { Time.now + 7.days } - before { build.artifacts_expire_at = expire_at } + before do + build.artifacts_expire_at = expire_at + end it { is_expected.to be_within(5).of(expire_at - Time.now) } end @@ -926,6 +950,10 @@ describe Ci::Build, :models do context 'when other build is retried' do let!(:retried_build) { Ci::Build.retry(other_build, user) } + before do + retried_build.success + end + it 'returns a retried build' do is_expected.to contain_exactly(retried_build) end @@ -1071,7 +1099,9 @@ describe Ci::Build, :models do describe '#has_expiring_artifacts?' do context 'when artifacts have expiration date set' do - before { build.update(artifacts_expire_at: 1.day.from_now) } + before do + build.update(artifacts_expire_at: 1.day.from_now) + end it 'has expiring artifacts' do expect(build).to have_expiring_artifacts @@ -1079,7 +1109,9 @@ describe Ci::Build, :models do end context 'when artifacts do not have expiration date set' do - before { build.update(artifacts_expire_at: nil) } + before do + build.update(artifacts_expire_at: nil) + end it 'does not have expiring artifacts' do expect(build).not_to have_expiring_artifacts diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/legacy_stage_spec.rb index 8f6ab908987..d43c33d3807 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/legacy_stage_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::Stage, models: true do +describe Ci::LegacyStage, :models do let(:stage) { build(:ci_stage) } let(:pipeline) { stage.pipeline } let(:stage_name) { stage.name } @@ -55,6 +55,17 @@ describe Ci::Stage, models: true do expect(stage.groups.map(&:name)) .to eq %w[aaaaa rspec spinach] end + + context 'when a name is nil on legacy pipelines' do + before do + pipeline.builds.first.update_attribute(:name, nil) + end + + it 'returns an array of three groups' do + expect(stage.groups.map(&:name)) + .to eq ['', 'aaaaa', 'rspec', 'spinach'] + end + end end describe '#statuses_count' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index ae1b01b76ab..e86cbe8498a 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -224,8 +224,19 @@ describe Ci::Pipeline, models: true do status: 'success') end - describe '#stages' do - subject { pipeline.stages } + describe '#stage_seeds' do + let(:pipeline) do + create(:ci_pipeline, config: { rspec: { script: 'rake' } }) + end + + it 'returns preseeded stage seeds object' do + expect(pipeline.stage_seeds).to all(be_a Gitlab::Ci::Stage::Seed) + expect(pipeline.stage_seeds.count).to eq 1 + end + end + + describe '#legacy_stages' do + subject { pipeline.legacy_stages } context 'stages list' do it 'returns ordered list of stages' do @@ -274,7 +285,7 @@ describe Ci::Pipeline, models: true do end it 'populates stage with correct number of warnings' do - deploy_stage = pipeline.stages.third + deploy_stage = pipeline.legacy_stages.third expect(deploy_stage).not_to receive(:statuses) expect(deploy_stage).to have_warnings @@ -288,22 +299,22 @@ describe Ci::Pipeline, models: true do end end - describe '#stages_name' do + describe '#stages_names' do it 'returns a valid names of stages' do - expect(pipeline.stages_name).to eq(%w(build test deploy)) + expect(pipeline.stages_names).to eq(%w(build test deploy)) end end end - describe '#stage' do - subject { pipeline.stage('test') } + describe '#legacy_stage' do + subject { pipeline.legacy_stage('test') } context 'with status in stage' do before do create(:commit_status, pipeline: pipeline, stage: 'test') end - it { expect(subject).to be_a Ci::Stage } + it { expect(subject).to be_a Ci::LegacyStage } it { expect(subject.name).to eq 'test' } it { expect(subject.statuses).not_to be_empty } end @@ -524,6 +535,20 @@ describe Ci::Pipeline, models: true do end end + describe '#has_stage_seeds?' do + context 'when pipeline has stage seeds' do + subject { build(:ci_pipeline_with_one_job) } + + it { is_expected.to have_stage_seeds } + end + + context 'when pipeline does not have stage seeds' do + subject { create(:ci_pipeline_without_jobs) } + + it { is_expected.not_to have_stage_seeds } + end + end + describe '#has_warnings?' do subject { pipeline.has_warnings? } @@ -1131,7 +1156,9 @@ describe Ci::Pipeline, models: true do end context 'when pipeline is not stuck' do - before { create(:ci_runner, :shared, :online) } + before do + create(:ci_runner, :shared, :online) + end it 'is not stuck' do expect(pipeline).not_to be_stuck diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 72f83d63224..6056d78da4e 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -20,8 +20,8 @@ describe Commit, models: true do end it 'caches the author' do + allow(RequestStore).to receive(:active?).and_return(true) user = create(:user, email: commit.author_email) - expect(RequestStore).to receive(:active?).twice.and_return(true) expect_any_instance_of(Commit).to receive(:find_author_by_any_email).and_call_original expect(commit.author).to eq(user) @@ -67,11 +67,11 @@ describe Commit, models: true do expect(commit.title).to eq("--no commit message") end - it "truncates a message without a newline at 80 characters" do + it 'truncates a message without a newline at natural break to 80 characters' do message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.' allow(commit).to receive(:safe_message).and_return(message) - expect(commit.title).to eq("#{message[0..79]}…") + expect(commit.title).to eq('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis…') end it "truncates a message with a newline before 80 characters at the newline" do @@ -113,6 +113,28 @@ eos end end + describe 'description' do + it 'returns description of commit message if title less than 100 characters' do + message = <<eos +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. +Vivamus egestas lacinia lacus, sed rutrum mauris. +eos + + allow(commit).to receive(:safe_message).and_return(message) + expect(commit.description).to eq('Vivamus egestas lacinia lacus, sed rutrum mauris.') + end + + it 'returns full commit message if commit title more than 100 characters' do + message = <<eos +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris. +Vivamus egestas lacinia lacus, sed rutrum mauris. +eos + + allow(commit).to receive(:safe_message).and_return(message) + expect(commit.description).to eq(message) + end + end + describe "delegation" do subject { commit } @@ -184,19 +206,25 @@ eos it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy } context 'commit has no description' do - before { allow(commit).to receive(:description?).and_return(false) } + before do + allow(commit).to receive(:description?).and_return(false) + end it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy } end context "another_commit's description does not revert commit" do - before { allow(commit).to receive(:description).and_return("Foo Bar") } + before do + allow(commit).to receive(:description).and_return("Foo Bar") + end it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy } end context "another_commit's description reverts commit" do - before { allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar") } + before do + allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar") + end it { expect(commit.reverts_commit?(another_commit, user)).to be_truthy } end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index c50b8bf7b13..9262ce08987 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -31,7 +31,10 @@ describe CommitStatus, :models do describe '#author' do subject { commit_status.author } - before { commit_status.author = User.new } + + before do + commit_status.author = User.new + end it { is_expected.to eq(commit_status.user) } end @@ -50,14 +53,18 @@ describe CommitStatus, :models do subject { commit_status.started? } context 'without started_at' do - before { commit_status.started_at = nil } + before do + commit_status.started_at = nil + end it { is_expected.to be_falsey } end %w[running success failed].each do |status| context "if commit status is #{status}" do - before { commit_status.status = status } + before do + commit_status.status = status + end it { is_expected.to be_truthy } end @@ -65,7 +72,9 @@ describe CommitStatus, :models do %w[pending canceled].each do |status| context "if commit status is #{status}" do - before { commit_status.status = status } + before do + commit_status.status = status + end it { is_expected.to be_falsey } end @@ -77,7 +86,9 @@ describe CommitStatus, :models do %w[pending running].each do |state| context "if commit_status.status is #{state}" do - before { commit_status.status = state } + before do + commit_status.status = state + end it { is_expected.to be_truthy } end @@ -85,7 +96,9 @@ describe CommitStatus, :models do %w[success failed canceled].each do |state| context "if commit_status.status is #{state}" do - before { commit_status.status = state } + before do + commit_status.status = state + end it { is_expected.to be_falsey } end @@ -97,7 +110,9 @@ describe CommitStatus, :models do %w[success failed canceled].each do |state| context "if commit_status.status is #{state}" do - before { commit_status.status = state } + before do + commit_status.status = state + end it { is_expected.to be_truthy } end @@ -105,7 +120,9 @@ describe CommitStatus, :models do %w[pending running].each do |state| context "if commit_status.status is #{state}" do - before { commit_status.status = state } + before do + commit_status.status = state + end it { is_expected.to be_falsey } end @@ -271,7 +288,9 @@ describe CommitStatus, :models do subject { commit_status.before_sha } context 'when no before_sha is set for pipeline' do - before { pipeline.before_sha = nil } + before do + pipeline.before_sha = nil + end it 'returns blank sha' do is_expected.to eq(Gitlab::Git::BLANK_SHA) @@ -280,7 +299,10 @@ describe CommitStatus, :models do context 'for before_sha set for pipeline' do let(:value) { '1234' } - before { pipeline.before_sha = value } + + before do + pipeline.before_sha = value + end it 'returns the set value' do is_expected.to eq(value) diff --git a/spec/models/concerns/access_requestable_spec.rb b/spec/models/concerns/access_requestable_spec.rb index 4829ef17a20..97b7e48bb3c 100644 --- a/spec/models/concerns/access_requestable_spec.rb +++ b/spec/models/concerns/access_requestable_spec.rb @@ -14,7 +14,9 @@ describe AccessRequestable do let(:group) { create(:group, :public, :access_requestable) } let(:user) { create(:user) } - before { group.request_access(user) } + before do + group.request_access(user) + end it { expect(group.requesters.exists?(user_id: user)).to be_truthy } end @@ -32,7 +34,9 @@ describe AccessRequestable do let(:project) { create(:empty_project, :public, :access_requestable) } let(:user) { create(:user) } - before { project.request_access(user) } + before do + project.request_access(user) + end it { expect(project.requesters.exists?(user_id: user)).to be_truthy } end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 27890e33b49..1a9bda64191 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -200,7 +200,9 @@ describe Issuable do let(:project) { issue.project } context 'user is not a participant in the issue' do - before { allow(issue).to receive(:participants).with(user).and_return([]) } + before do + allow(issue).to receive(:participants).with(user).and_return([]) + end it 'returns false when no subcription exists' do expect(issue.subscribed?(user, project)).to be_falsey @@ -220,7 +222,9 @@ describe Issuable do end context 'user is a participant in the issue' do - before { allow(issue).to receive(:participants).with(user).and_return([user]) } + before do + allow(issue).to receive(:participants).with(user).and_return([user]) + end it 'returns false when no subcription exists' do expect(issue.subscribed?(user, project)).to be_truthy @@ -252,7 +256,9 @@ describe Issuable do end context "issue is assigned" do - before { issue.assignees << user } + before do + issue.assignees << user + end it "returns correct hook data" do expect(data[:assignees].first).to eq(user.hook_attrs) @@ -276,7 +282,9 @@ describe Issuable do context 'issue has labels' do let(:labels) { [create(:label), create(:label)] } - before { issue.update_attribute(:labels, labels)} + before do + issue.update_attribute(:labels, labels) + end it 'includes labels in the hook data' do expect(data[:labels]).to eq(labels.map(&:hook_attrs)) diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb index e382c7120de..e2a29e0ae70 100644 --- a/spec/models/concerns/mentionable_spec.rb +++ b/spec/models/concerns/mentionable_spec.rb @@ -61,7 +61,9 @@ describe Issue, "Mentionable" do end context 'when the current user can see the issue' do - before { private_project.team << [user, Gitlab::Access::DEVELOPER] } + before do + private_project.team << [user, Gitlab::Access::DEVELOPER] + end it 'includes the reference' do expect(referenced_issues(user)).to contain_exactly(private_issue, public_issue) diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb index a0765a264cf..808247ebfd5 100644 --- a/spec/models/concerns/reactive_caching_spec.rb +++ b/spec/models/concerns/reactive_caching_spec.rb @@ -40,7 +40,10 @@ describe ReactiveCaching, caching: true do let(:instance) { CacheTest.new(666, &calculation) } describe '#with_reactive_cache' do - before { stub_reactive_cache } + before do + stub_reactive_cache + end + subject(:go!) { instance.result } context 'when cache is empty' do @@ -60,12 +63,17 @@ describe ReactiveCaching, caching: true do end context 'when the cache is full' do - before { stub_reactive_cache(instance, 4) } + before do + stub_reactive_cache(instance, 4) + end it { is_expected.to eq(2) } context 'and expired' do - before { invalidate_reactive_cache(instance) } + before do + invalidate_reactive_cache(instance) + end + it { is_expected.to be_nil } end end @@ -84,7 +92,9 @@ describe ReactiveCaching, caching: true do subject(:go!) { instance.exclusively_update_reactive_cache! } context 'when the lease is free and lifetime is not exceeded' do - before { stub_reactive_cache(instance, "preexisting") } + before do + stub_reactive_cache(instance, "preexisting") + end it 'takes and releases the lease' do expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return("000000") @@ -106,7 +116,10 @@ describe ReactiveCaching, caching: true do end context 'and #calculate_reactive_cache raises an exception' do - before { stub_reactive_cache(instance, "preexisting") } + before do + stub_reactive_cache(instance, "preexisting") + end + let(:calculation) { -> { raise "foo"} } it 'leaves the cache untouched' do diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index 0e10d91836d..65f05121b40 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -122,16 +122,7 @@ describe Group, 'Routable' do it { expect(group.full_path).to eq(group.path) } it { expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}") } - context 'with RequestStore active' do - before do - RequestStore.begin! - end - - after do - RequestStore.end! - RequestStore.clear! - end - + context 'with RequestStore active', :request_store do it 'does not load the route table more than once' do expect(group).to receive(:uncached_full_path).once.and_call_original diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index 4b0bfa43abf..882afeccfc6 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -49,7 +49,10 @@ describe ApplicationSetting, 'TokenAuthenticatable' do end context 'token is generated' do - before { subject.send("reset_#{token_field}!") } + before do + subject.send("reset_#{token_field}!") + end + it 'persists a new token' do expect(subject.send(:read_attribute, token_field)).to be_a String end diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index d15ff9e5ffd..eba0175c54c 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -131,7 +131,7 @@ describe Deployment, models: true do end context 'with other actions' do - let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) } + let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') } context 'when matching action is defined' do let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_other_app') } @@ -159,7 +159,7 @@ describe Deployment, models: true do context 'when matching action is defined' do let(:build) { create(:ci_build) } let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_app') } - let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) } + let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') } it { is_expected.to be_truthy } end diff --git a/spec/models/diff_viewer/base_spec.rb b/spec/models/diff_viewer/base_spec.rb new file mode 100644 index 00000000000..3755f4a56f3 --- /dev/null +++ b/spec/models/diff_viewer/base_spec.rb @@ -0,0 +1,150 @@ +require 'spec_helper' + +describe DiffViewer::Base, model: true do + include FakeBlobHelpers + + let(:project) { create(:project, :repository) } + let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') } + + let(:viewer_class) do + Class.new(described_class) do + include DiffViewer::ServerSide + + self.extensions = %w(jpg) + self.binary = true + self.collapse_limit = 1.megabyte + self.size_limit = 5.megabytes + end + end + + let(:viewer) { viewer_class.new(diff_file) } + + describe '.can_render?' do + context 'when the extension is supported' do + let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') } + + context 'when the binaryness matches' do + it 'returns true' do + expect(viewer_class.can_render?(diff_file)).to be_truthy + end + end + + context 'when the binaryness does not match' do + before do + allow(diff_file.old_blob).to receive(:binary?).and_return(false) + allow(diff_file.new_blob).to receive(:binary?).and_return(false) + end + + it 'returns false' do + expect(viewer_class.can_render?(diff_file)).to be_falsey + end + end + end + + context 'when the file type is supported' do + let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('LICENSE') } + + before do + viewer_class.file_types = %i(license) + viewer_class.binary = false + end + + context 'when the binaryness matches' do + it 'returns true' do + expect(viewer_class.can_render?(diff_file)).to be_truthy + end + end + + context 'when the binaryness does not match' do + before do + allow(diff_file.old_blob).to receive(:binary?).and_return(true) + allow(diff_file.new_blob).to receive(:binary?).and_return(true) + end + + it 'returns false' do + expect(viewer_class.can_render?(diff_file)).to be_falsey + end + end + end + + context 'when the extension and file type are not supported' do + it 'returns false' do + expect(viewer_class.can_render?(diff_file)).to be_falsey + end + end + + context 'when the file was renamed and only the old blob is supported' do + let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') } + + before do + allow(diff_file).to receive(:renamed_file?).and_return(true) + allow(diff_file.new_blob).to receive(:extension).and_return('jpeg') + end + + it 'returns false' do + expect(viewer_class.can_render?(diff_file)).to be_falsey + end + end + end + + describe '#collapsed?' do + context 'when the combined blob size is larger than the collapse limit' do + before do + allow(diff_file.old_blob).to receive(:raw_size).and_return(512.kilobytes) + allow(diff_file.new_blob).to receive(:raw_size).and_return(513.kilobytes) + end + + it 'returns true' do + expect(viewer.collapsed?).to be_truthy + end + end + + context 'when the combined blob size is smaller than the collapse limit' do + it 'returns false' do + expect(viewer.collapsed?).to be_falsey + end + end + end + + describe '#too_large?' do + context 'when the combined blob size is larger than the size limit' do + before do + allow(diff_file.old_blob).to receive(:raw_size).and_return(2.megabytes) + allow(diff_file.new_blob).to receive(:raw_size).and_return(4.megabytes) + end + + it 'returns true' do + expect(viewer.too_large?).to be_truthy + end + end + + context 'when the blob size is smaller than the size limit' do + it 'returns false' do + expect(viewer.too_large?).to be_falsey + end + end + end + + describe '#render_error' do + context 'when the combined blob size is larger than the size limit' do + before do + allow(diff_file.old_blob).to receive(:raw_size).and_return(2.megabytes) + allow(diff_file.new_blob).to receive(:raw_size).and_return(4.megabytes) + end + + it 'returns :too_large' do + expect(viewer.render_error).to eq(:too_large) + end + end + + context 'when the combined blob size is smaller than the size limit' do + it 'returns nil' do + expect(viewer.render_error).to be_nil + end + end + end +end diff --git a/spec/models/diff_viewer/server_side_spec.rb b/spec/models/diff_viewer/server_side_spec.rb new file mode 100644 index 00000000000..2d926e06936 --- /dev/null +++ b/spec/models/diff_viewer/server_side_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe DiffViewer::ServerSide, model: true do + let(:project) { create(:project, :repository) } + let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') } + + let(:viewer_class) do + Class.new(DiffViewer::Base) do + include DiffViewer::ServerSide + end + end + + subject { viewer_class.new(diff_file) } + + describe '#prepare!' do + it 'loads all diff file data' do + expect(diff_file.old_blob).to receive(:load_all_data!) + expect(diff_file.new_blob).to receive(:load_all_data!) + + subject.prepare! + end + end + + describe '#render_error' do + context 'when the diff file is stored externally' do + before do + allow(diff_file).to receive(:stored_externally?).and_return(true) + end + + it 'return :server_side_but_stored_externally' do + expect(subject.render_error).to eq(:server_side_but_stored_externally) + end + end + end +end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index f711065a0ab..f36c165ef7d 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -170,7 +170,7 @@ describe Environment, models: true do context 'when matching action is defined' do let(:build) { create(:ci_build) } let!(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } - let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) } + let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') } context 'when environment is available' do before do diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb index f4c3e6d503f..152e97e09bf 100644 --- a/spec/models/generic_commit_status_spec.rb +++ b/spec/models/generic_commit_status_spec.rb @@ -19,7 +19,10 @@ describe GenericCommitStatus, models: true do describe '#context' do subject { generic_commit_status.context } - before { generic_commit_status.context = 'my_context' } + + before do + generic_commit_status.context = 'my_context' + end it { is_expected.to eq(generic_commit_status.name) } end @@ -39,7 +42,9 @@ describe GenericCommitStatus, models: true do end context 'when user has ability to see datails' do - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end it 'details path points to an external URL' do expect(status).to have_details diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 316bf153660..449b7c2f7d7 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -143,14 +143,20 @@ describe Group, models: true do describe '#add_user' do let(:user) { create(:user) } - before { group.add_user(user, GroupMember::MASTER) } + + before do + group.add_user(user, GroupMember::MASTER) + end it { expect(group.group_members.masters.map(&:user)).to include(user) } end describe '#add_users' do let(:user) { create(:user) } - before { group.add_users([user.id], GroupMember::GUEST) } + + before do + group.add_users([user.id], GroupMember::GUEST) + end it "updates the group permission" do expect(group.group_members.guests.map(&:user)).to include(user) @@ -162,7 +168,10 @@ describe Group, models: true do describe '#avatar_type' do let(:user) { create(:user) } - before { group.add_user(user, GroupMember::MASTER) } + + before do + group.add_user(user, GroupMember::MASTER) + end it "is true if avatar is image" do group.update_attribute(:avatar, 'uploads/avatar.png') @@ -179,10 +188,12 @@ describe Group, models: true do let!(:group) { create(:group, :access_requestable, :with_avatar) } let(:user) { create(:user) } let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" } - let(:avatar_path) { "/uploads/group/avatar/#{group.id}/dk.png" } + let(:avatar_path) { "/uploads/system/group/avatar/#{group.id}/dk.png" } context 'when avatar file is uploaded' do - before { group.add_master(user) } + before do + group.add_master(user) + end it 'shows correct avatar url' do expect(group.avatar_url).to eq(avatar_path) @@ -222,7 +233,9 @@ describe Group, models: true do end describe '#has_owner?' do - before { @members = setup_group_members(group) } + before do + @members = setup_group_members(group) + end it { expect(group.has_owner?(@members[:owner])).to be_truthy } it { expect(group.has_owner?(@members[:master])).to be_falsey } @@ -233,7 +246,9 @@ describe Group, models: true do end describe '#has_master?' do - before { @members = setup_group_members(group) } + before do + @members = setup_group_members(group) + end it { expect(group.has_master?(@members[:owner])).to be_falsey } it { expect(group.has_master?(@members[:master])).to be_truthy } diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index bb4e70db2e9..12e7d646382 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -245,7 +245,9 @@ describe Issue, models: true do let(:project) { create(:empty_project) } let(:issue) { create(:issue, project: project) } - before { project.team << [user, :reporter] } + before do + project.team << [user, :reporter] + end it { is_expected.to eq true } @@ -259,12 +261,18 @@ describe Issue, models: true do let(:to_project) { create(:empty_project) } context 'destination project allowed' do - before { to_project.team << [user, :reporter] } + before do + to_project.team << [user, :reporter] + end + it { is_expected.to eq true } end context 'destination project not allowed' do - before { to_project.team << [user, :guest] } + before do + to_project.team << [user, :guest] + end + it { is_expected.to eq false } end end @@ -549,7 +557,9 @@ describe Issue, models: true do end context 'when the user is the project owner' do - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it 'returns true for a regular issue' do issue = build(:issue, project: project) diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index ed9fde57bf7..25f7062860b 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -36,7 +36,9 @@ describe MergeRequestDiff, models: true do end context 'when the raw diffs are empty' do - before { mr_diff.update_attributes(st_diffs: '') } + before do + mr_diff.update_attributes(st_diffs: '') + end it 'returns an empty DiffCollection' do expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection) @@ -45,7 +47,9 @@ describe MergeRequestDiff, models: true do end context 'when the raw diffs have invalid content' do - before { mr_diff.update_attributes(st_diffs: ["--broken-diff"]) } + before do + mr_diff.update_attributes(st_diffs: ["--broken-diff"]) + end it 'returns an empty DiffCollection' do expect(mr_diff.raw_diffs.to_a).to be_empty diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 060754fab63..cd2f11dec96 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -892,7 +892,9 @@ describe MergeRequest, models: true do end context 'when broken' do - before { allow(subject).to receive(:broken?) { true } } + before do + allow(subject).to receive(:broken?) { true } + end it 'becomes unmergeable' do expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged') @@ -944,7 +946,9 @@ describe MergeRequest, models: true do end context 'when not open' do - before { subject.close } + before do + subject.close + end it 'returns false' do expect(subject.mergeable_state?).to be_falsey @@ -952,7 +956,9 @@ describe MergeRequest, models: true do end context 'when working in progress' do - before { subject.title = 'WIP MR' } + before do + subject.title = 'WIP MR' + end it 'returns false' do expect(subject.mergeable_state?).to be_falsey @@ -960,7 +966,9 @@ describe MergeRequest, models: true do end context 'when broken' do - before { allow(subject).to receive(:broken?) { true } } + before do + allow(subject).to receive(:broken?) { true } + end it 'returns false' do expect(subject.mergeable_state?).to be_falsey diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 0e74f1ab1bd..145c7ad5770 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -43,6 +43,12 @@ describe Namespace, models: true do end end + context "is case insensitive" do + let(:group) { build(:group, path: "System") } + + it { expect(group).not_to be_valid } + end + context 'top-level group' do let(:group) { build(:group, path: 'tree') } @@ -178,8 +184,8 @@ describe Namespace, models: true do let(:parent) { create(:group, name: 'parent', path: 'parent') } let(:child) { create(:group, name: 'child', path: 'child', parent: parent) } let!(:project) { create(:project_empty_repo, path: 'the-project', namespace: child) } - let(:uploads_dir) { File.join(CarrierWave.root, 'uploads') } - let(:pages_dir) { TestEnv.pages_path } + let(:uploads_dir) { File.join(CarrierWave.root, FileUploader.base_dir) } + let(:pages_dir) { File.join(TestEnv.pages_path) } before do FileUtils.mkdir_p(File.join(uploads_dir, 'parent', 'child', 'the-project')) diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 7a01cef9b4b..d4d4fc86343 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -26,14 +26,18 @@ describe Note, models: true do it { is_expected.to validate_presence_of(:project) } context 'when note is on commit' do - before { allow(subject).to receive(:for_commit?).and_return(true) } + before do + allow(subject).to receive(:for_commit?).and_return(true) + end it { is_expected.to validate_presence_of(:commit_id) } it { is_expected.not_to validate_presence_of(:noteable_id) } end context 'when note is not on commit' do - before { allow(subject).to receive(:for_commit?).and_return(false) } + before do + allow(subject).to receive(:for_commit?).and_return(false) + end it { is_expected.not_to validate_presence_of(:commit_id) } it { is_expected.to validate_presence_of(:noteable_id) } diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb index d58673413c8..cc235ad467e 100644 --- a/spec/models/notification_setting_spec.rb +++ b/spec/models/notification_setting_spec.rb @@ -55,4 +55,34 @@ RSpec.describe NotificationSetting, type: :model do expect(user.notification_settings.for_projects.map(&:project)).to all(have_attributes(pending_delete: false)) end end + + describe 'event_enabled?' do + before do + subject.update!(user: create(:user)) + end + + context 'for an event with a matching column name' do + before do + subject.update!(events: { new_note: true }.to_json) + end + + it 'returns the value of the column' do + subject.update!(new_note: false) + + expect(subject.event_enabled?(:new_note)).to be(false) + end + + context 'when the column has a nil value' do + it 'returns the value from the events hash' do + expect(subject.event_enabled?(:new_note)).to be(false) + end + end + end + + context 'for an event without a matching column name' do + it 'returns false' do + expect(subject.event_enabled?(:foo_event)).to be(false) + end + end + end end diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb index 823623d96fa..fa781195608 100644 --- a/spec/models/personal_access_token_spec.rb +++ b/spec/models/personal_access_token_spec.rb @@ -35,6 +35,16 @@ describe PersonalAccessToken, models: true do end end + describe 'revoke!' do + let(:active_personal_access_token) { create(:personal_access_token) } + + it 'revokes the token' do + active_personal_access_token.revoke! + + expect(active_personal_access_token.revoked?).to be true + end + end + context "validations" do let(:personal_access_token) { build(:personal_access_token) } @@ -51,11 +61,17 @@ describe PersonalAccessToken, models: true do expect(personal_access_token).to be_valid end - it "rejects creating a token with non-API scopes" do + it "allows creating a token with read_registry scope" do + personal_access_token.scopes = [:read_registry] + + expect(personal_access_token).to be_valid + end + + it "rejects creating a token with unavailable scopes" do personal_access_token.scopes = [:openid, :api] expect(personal_access_token).not_to be_valid - expect(personal_access_token.errors[:scopes].first).to eq "can only contain API scopes" + expect(personal_access_token.errors[:scopes].first).to eq "can only contain available scopes" end end end diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb index 4014d6129ee..e62fd69e567 100644 --- a/spec/models/project_services/bamboo_service_spec.rb +++ b/spec/models/project_services/bamboo_service_spec.rb @@ -24,7 +24,9 @@ describe BambooService, models: true, caching: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:build_key) } it { is_expected.to validate_presence_of(:bamboo_url) } @@ -60,7 +62,9 @@ describe BambooService, models: true, caching: true do end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:build_key) } it { is_expected.not_to validate_presence_of(:bamboo_url) } diff --git a/spec/models/project_services/bugzilla_service_spec.rb b/spec/models/project_services/bugzilla_service_spec.rb index 739cc72b2ff..5f17bbde390 100644 --- a/spec/models/project_services/bugzilla_service_spec.rb +++ b/spec/models/project_services/bugzilla_service_spec.rb @@ -8,7 +8,9 @@ describe BugzillaService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:project_url) } it { is_expected.to validate_presence_of(:issues_url) } @@ -19,7 +21,9 @@ describe BugzillaService, models: true do end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:project_url) } it { is_expected.not_to validate_presence_of(:issues_url) } diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb index 05b602d8106..dd529597067 100644 --- a/spec/models/project_services/buildkite_service_spec.rb +++ b/spec/models/project_services/buildkite_service_spec.rb @@ -23,7 +23,9 @@ describe BuildkiteService, models: true, caching: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:project_url) } it { is_expected.to validate_presence_of(:token) } @@ -31,7 +33,9 @@ describe BuildkiteService, models: true, caching: true do end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:project_url) } it { is_expected.not_to validate_presence_of(:token) } diff --git a/spec/models/project_services/campfire_service_spec.rb b/spec/models/project_services/campfire_service_spec.rb index 953e664fb66..de55627dd27 100644 --- a/spec/models/project_services/campfire_service_spec.rb +++ b/spec/models/project_services/campfire_service_spec.rb @@ -8,13 +8,17 @@ describe CampfireService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:token) } end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:token) } end diff --git a/spec/models/project_services/chat_message/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb index 4ca1b8aa7b7..17355c1e6f1 100644 --- a/spec/models/project_services/chat_message/wiki_page_message_spec.rb +++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb @@ -23,7 +23,9 @@ describe ChatMessage::WikiPageMessage, models: true do context 'without markdown' do describe '#pretext' do context 'when :action == "create"' do - before { args[:object_attributes][:action] = 'create' } + before do + args[:object_attributes][:action] = 'create' + end it 'returns a message that a new wiki page was created' do expect(subject.pretext).to eq( @@ -33,7 +35,9 @@ describe ChatMessage::WikiPageMessage, models: true do end context 'when :action == "update"' do - before { args[:object_attributes][:action] = 'update' } + before do + args[:object_attributes][:action] = 'update' + end it 'returns a message that a wiki page was updated' do expect(subject.pretext).to eq( @@ -47,7 +51,9 @@ describe ChatMessage::WikiPageMessage, models: true do let(:color) { '#345' } context 'when :action == "create"' do - before { args[:object_attributes][:action] = 'create' } + before do + args[:object_attributes][:action] = 'create' + end it 'returns the attachment for a new wiki page' do expect(subject.attachments).to eq([ @@ -60,7 +66,9 @@ describe ChatMessage::WikiPageMessage, models: true do end context 'when :action == "update"' do - before { args[:object_attributes][:action] = 'update' } + before do + args[:object_attributes][:action] = 'update' + end it 'returns the attachment for an updated wiki page' do expect(subject.attachments).to eq([ @@ -81,7 +89,9 @@ describe ChatMessage::WikiPageMessage, models: true do describe '#pretext' do context 'when :action == "create"' do - before { args[:object_attributes][:action] = 'create' } + before do + args[:object_attributes][:action] = 'create' + end it 'returns a message that a new wiki page was created' do expect(subject.pretext).to eq( @@ -90,7 +100,9 @@ describe ChatMessage::WikiPageMessage, models: true do end context 'when :action == "update"' do - before { args[:object_attributes][:action] = 'update' } + before do + args[:object_attributes][:action] = 'update' + end it 'returns a message that a wiki page was updated' do expect(subject.pretext).to eq( @@ -101,7 +113,9 @@ describe ChatMessage::WikiPageMessage, models: true do describe '#attachments' do context 'when :action == "create"' do - before { args[:object_attributes][:action] = 'create' } + before do + args[:object_attributes][:action] = 'create' + end it 'returns the attachment for a new wiki page' do expect(subject.attachments).to eq('Wiki page description') @@ -109,7 +123,9 @@ describe ChatMessage::WikiPageMessage, models: true do end context 'when :action == "update"' do - before { args[:object_attributes][:action] = 'update' } + before do + args[:object_attributes][:action] = 'update' + end it 'returns the attachment for an updated wiki page' do expect(subject.attachments).to eq('Wiki page description') @@ -119,7 +135,9 @@ describe ChatMessage::WikiPageMessage, models: true do describe '#activity' do context 'when :action == "create"' do - before { args[:object_attributes][:action] = 'create' } + before do + args[:object_attributes][:action] = 'create' + end it 'returns the attachment for a new wiki page' do expect(subject.activity).to eq({ @@ -132,7 +150,9 @@ describe ChatMessage::WikiPageMessage, models: true do end context 'when :action == "update"' do - before { args[:object_attributes][:action] = 'update' } + before do + args[:object_attributes][:action] = 'update' + end it 'returns the attachment for an updated wiki page' do expect(subject.activity).to eq({ diff --git a/spec/models/project_services/custom_issue_tracker_service_spec.rb b/spec/models/project_services/custom_issue_tracker_service_spec.rb index 63320931e76..9e574762232 100644 --- a/spec/models/project_services/custom_issue_tracker_service_spec.rb +++ b/spec/models/project_services/custom_issue_tracker_service_spec.rb @@ -8,7 +8,9 @@ describe CustomIssueTrackerService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:project_url) } it { is_expected.to validate_presence_of(:issues_url) } @@ -19,7 +21,9 @@ describe CustomIssueTrackerService, models: true do end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:project_url) } it { is_expected.not_to validate_presence_of(:issues_url) } diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb index 044737c6026..1400175427f 100644 --- a/spec/models/project_services/drone_ci_service_spec.rb +++ b/spec/models/project_services/drone_ci_service_spec.rb @@ -10,7 +10,9 @@ describe DroneCiService, models: true, caching: true do describe 'validations' do context 'active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:token) } it { is_expected.to validate_presence_of(:drone_url) } @@ -18,7 +20,9 @@ describe DroneCiService, models: true, caching: true do end context 'inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:token) } it { is_expected.not_to validate_presence_of(:drone_url) } diff --git a/spec/models/project_services/emails_on_push_service_spec.rb b/spec/models/project_services/emails_on_push_service_spec.rb index e6f78898c82..d9b7010e5e5 100644 --- a/spec/models/project_services/emails_on_push_service_spec.rb +++ b/spec/models/project_services/emails_on_push_service_spec.rb @@ -3,13 +3,17 @@ require 'spec_helper' describe EmailsOnPushService do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:recipients) } end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:recipients) } end diff --git a/spec/models/project_services/external_wiki_service_spec.rb b/spec/models/project_services/external_wiki_service_spec.rb index bdeea1db1e3..291fc645a1c 100644 --- a/spec/models/project_services/external_wiki_service_spec.rb +++ b/spec/models/project_services/external_wiki_service_spec.rb @@ -9,14 +9,18 @@ describe ExternalWikiService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:external_wiki_url) } it_behaves_like 'issue tracker service URL attribute', :external_wiki_url end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:external_wiki_url) } end diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb index a97e8c6e4ce..56ace04dd58 100644 --- a/spec/models/project_services/flowdock_service_spec.rb +++ b/spec/models/project_services/flowdock_service_spec.rb @@ -8,13 +8,17 @@ describe FlowdockService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:token) } end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:token) } end diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb index a13fbae03eb..65c9e714bd1 100644 --- a/spec/models/project_services/gemnasium_service_spec.rb +++ b/spec/models/project_services/gemnasium_service_spec.rb @@ -8,14 +8,18 @@ describe GemnasiumService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:token) } it { is_expected.to validate_presence_of(:api_key) } end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:token) } it { is_expected.not_to validate_presence_of(:api_key) } diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 1200ae7eb22..c7c8e9651ab 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -8,13 +8,17 @@ describe HipchatService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:token) } end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:token) } end diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb index d5a16226d9d..a5c4938b54e 100644 --- a/spec/models/project_services/irker_service_spec.rb +++ b/spec/models/project_services/irker_service_spec.rb @@ -10,13 +10,17 @@ describe IrkerService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:recipients) } end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:recipients) } end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 0ee050196e4..e2b8226124f 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -10,7 +10,9 @@ describe JiraService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:url) } it { is_expected.to validate_presence_of(:project_key) } @@ -18,7 +20,9 @@ describe JiraService, models: true do end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:url) } end diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 0dcf4a4b5d6..858ad595dbf 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -7,31 +7,15 @@ describe KubernetesService, models: true, caching: true do let(:project) { build_stubbed(:kubernetes_project) } let(:service) { project.kubernetes_service } - # We use Kubeclient to interactive with the Kubernetes API. It will - # GET /api/v1 for a list of resources the API supports. This must be stubbed - # in addition to any other HTTP requests we expect it to perform. - let(:discovery_url) { service.api_url + '/api/v1' } - let(:discovery_response) { { body: kube_discovery_body.to_json } } - - let(:pods_url) { service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods" } - let(:pods_response) { { body: kube_pods_body(kube_pod).to_json } } - - def stub_kubeclient_discover - WebMock.stub_request(:get, discovery_url).to_return(discovery_response) - end - - def stub_kubeclient_pods - stub_kubeclient_discover - WebMock.stub_request(:get, pods_url).to_return(pods_response) - end - describe "Associations" do it { is_expected.to belong_to :project } end describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.not_to validate_presence_of(:namespace) } it { is_expected.to validate_presence_of(:api_url) } @@ -66,7 +50,9 @@ describe KubernetesService, models: true, caching: true do end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:api_url) } it { is_expected.not_to validate_presence_of(:token) } @@ -87,7 +73,9 @@ describe KubernetesService, models: true, caching: true do end context 'as template' do - before { subject.template = true } + before do + subject.template = true + end it 'sets the namespace to the default' do expect(kube_namespace).not_to be_nil @@ -96,7 +84,9 @@ describe KubernetesService, models: true, caching: true do end context 'with associated project' do - before { subject.project = project } + before do + subject.project = project + end it 'sets the namespace to the default' do expect(kube_namespace).not_to be_nil @@ -111,6 +101,34 @@ describe KubernetesService, models: true, caching: true do it "returns the default namespace" do is_expected.to eq(service.send(:default_namespace)) end + + context 'when namespace is specified' do + before do + service.namespace = 'my-namespace' + end + + it "returns the user-namespace" do + is_expected.to eq('my-namespace') + end + end + + context 'when service is not assigned to project' do + before do + service.project = nil + end + + it "does not return namespace" do + is_expected.to be_nil + end + end + end + + describe '#actual_namespace' do + subject { service.actual_namespace } + + it "returns the default namespace" do + is_expected.to eq(service.send(:default_namespace)) + end context 'when namespace is specified' do before do @@ -134,6 +152,8 @@ describe KubernetesService, models: true, caching: true do end describe '#test' do + let(:discovery_url) { 'https://kubernetes.example.com/api/v1' } + before do stub_kubeclient_discover end @@ -142,7 +162,8 @@ describe KubernetesService, models: true, caching: true do let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' } it 'tests with the prefix' do - service.api_url = 'https://kubernetes.example.com/prefix/' + service.api_url = 'https://kubernetes.example.com/prefix' + stub_kubeclient_discover expect(service.test[:success]).to be_truthy expect(WebMock).to have_requested(:get, discovery_url).once @@ -170,9 +191,9 @@ describe KubernetesService, models: true, caching: true do end context 'failure' do - let(:discovery_response) { { status: 404 } } - it 'fails to read the discovery endpoint' do + WebMock.stub_request(:get, service.api_url + '/api/v1').to_return(status: 404) + expect(service.test[:success]).to be_falsy expect(WebMock).to have_requested(:get, discovery_url).once end @@ -188,7 +209,9 @@ describe KubernetesService, models: true, caching: true do end context 'namespace is provided' do - before { subject.namespace = 'my-project' } + before do + subject.namespace = 'my-project' + end it 'sets the variables' do expect(subject.predefined_variables).to include( @@ -258,27 +281,36 @@ describe KubernetesService, models: true, caching: true do end describe '#calculate_reactive_cache' do - before { stub_kubeclient_pods } subject { service.calculate_reactive_cache } context 'when service is inactive' do - before { service.active = false } + before do + service.active = false + end it { is_expected.to be_nil } end context 'when kubernetes responds with valid pods' do + before do + stub_kubeclient_pods + end + it { is_expected.to eq(pods: [kube_pod]) } end - context 'when kubernetes responds with 500' do - let(:pods_response) { { status: 500 } } + context 'when kubernetes responds with 500s' do + before do + stub_kubeclient_pods(status: 500) + end it { expect { subject }.to raise_error(KubeException) } end - context 'when kubernetes responds with 404' do - let(:pods_response) { { status: 404 } } + context 'when kubernetes responds with 404s' do + before do + stub_kubeclient_pods(status: 404) + end it { is_expected.to eq(pods: []) } end diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb index facc034f69c..bd50a2d1470 100644 --- a/spec/models/project_services/microsoft_teams_service_spec.rb +++ b/spec/models/project_services/microsoft_teams_service_spec.rb @@ -11,14 +11,18 @@ describe MicrosoftTeamsService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:webhook) } it_behaves_like 'issue tracker service URL attribute', :webhook end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:webhook) } end diff --git a/spec/models/project_services/pivotaltracker_service_spec.rb b/spec/models/project_services/pivotaltracker_service_spec.rb index a76e909d04d..f4c1a9c94b6 100644 --- a/spec/models/project_services/pivotaltracker_service_spec.rb +++ b/spec/models/project_services/pivotaltracker_service_spec.rb @@ -8,13 +8,17 @@ describe PivotaltrackerService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:token) } end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:token) } end diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb index b761567ad76..c914b32f7a4 100644 --- a/spec/models/project_services/prometheus_service_spec.rb +++ b/spec/models/project_services/prometheus_service_spec.rb @@ -14,13 +14,17 @@ describe PrometheusService, models: true, caching: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:api_url) } end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:api_url) } end diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb index a7e7594a7d5..9171d9604ee 100644 --- a/spec/models/project_services/pushover_service_spec.rb +++ b/spec/models/project_services/pushover_service_spec.rb @@ -8,7 +8,9 @@ describe PushoverService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:api_key) } it { is_expected.to validate_presence_of(:user_key) } @@ -16,7 +18,9 @@ describe PushoverService, models: true do end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:api_key) } it { is_expected.not_to validate_presence_of(:user_key) } diff --git a/spec/models/project_services/redmine_service_spec.rb b/spec/models/project_services/redmine_service_spec.rb index 0a7b237a051..6631d9040b1 100644 --- a/spec/models/project_services/redmine_service_spec.rb +++ b/spec/models/project_services/redmine_service_spec.rb @@ -8,7 +8,9 @@ describe RedmineService, models: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:project_url) } it { is_expected.to validate_presence_of(:issues_url) } @@ -19,7 +21,9 @@ describe RedmineService, models: true do end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:project_url) } it { is_expected.not_to validate_presence_of(:issues_url) } diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb index 77b18e1c7d0..7349eb4149a 100644 --- a/spec/models/project_services/teamcity_service_spec.rb +++ b/spec/models/project_services/teamcity_service_spec.rb @@ -24,7 +24,9 @@ describe TeamcityService, models: true, caching: true do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:build_type) } it { is_expected.to validate_presence_of(:teamcity_url) } @@ -60,7 +62,9 @@ describe TeamcityService, models: true, caching: true do end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:build_type) } it { is_expected.not_to validate_presence_of(:teamcity_url) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 3ed52d42f86..63333b7af1f 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -812,7 +812,7 @@ describe Project, models: true do context 'when avatar file is uploaded' do let(:project) { create(:empty_project, :with_avatar) } - let(:avatar_path) { "/uploads/project/avatar/#{project.id}/dk.png" } + let(:avatar_path) { "/uploads/system/project/avatar/#{project.id}/dk.png" } let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" } it 'shows correct url' do @@ -1005,13 +1005,17 @@ describe Project, models: true do subject { project.shared_runners_enabled } context 'are enabled' do - before { stub_application_setting(shared_runners_enabled: true) } + before do + stub_application_setting(shared_runners_enabled: true) + end it { is_expected.to be_truthy } end context 'are disabled' do - before { stub_application_setting(shared_runners_enabled: false) } + before do + stub_application_setting(shared_runners_enabled: false) + end it { is_expected.to be_falsey } end @@ -1107,7 +1111,9 @@ describe Project, models: true do subject { project.pages_deployed? } context 'if public folder does exist' do - before { allow(Dir).to receive(:exist?).with(project.public_pages_path).and_return(true) } + before do + allow(Dir).to receive(:exist?).with(project.public_pages_path).and_return(true) + end it { is_expected.to be_truthy } end @@ -1365,7 +1371,9 @@ describe Project, models: true do subject { project.container_registry_url } - before { stub_container_registry_config(**registry_settings) } + before do + stub_container_registry_config(**registry_settings) + end context 'for enabled registry' do let(:registry_settings) do @@ -1389,7 +1397,9 @@ describe Project, models: true do let(:project) { create(:empty_project) } context 'when container registry is enabled' do - before { stub_container_registry_config(enabled: true) } + before do + stub_container_registry_config(enabled: true) + end context 'when tags are present for multi-level registries' do before do @@ -1427,7 +1437,9 @@ describe Project, models: true do end context 'when container registry is disabled' do - before { stub_container_registry_config(enabled: false) } + before do + stub_container_registry_config(enabled: false) + end it 'should not have image tags' do expect(project).not_to have_container_registry_tags @@ -1945,7 +1957,9 @@ describe Project, models: true do describe '#parent_changed?' do let(:project) { create(:empty_project) } - before { project.namespace_id = 7 } + before do + project.namespace_id = 7 + end it { expect(project.parent_changed?).to be_truthy } end diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 362565506e5..ea3cd5fe10a 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -240,7 +240,9 @@ describe ProjectTeam, models: true do it { expect(project.team.max_member_access(requester.id)).to eq(Gitlab::Access::NO_ACCESS) } context 'but share_with_group_lock is true' do - before { project.namespace.update(share_with_group_lock: true) } + before do + project.namespace.update(share_with_group_lock: true) + end it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::NO_ACCESS) } it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::NO_ACCESS) } @@ -389,16 +391,7 @@ describe ProjectTeam, models: true do end describe '#max_member_access_for_user_ids' do - context 'with RequestStore enabled' do - before do - RequestStore.begin! - end - - after do - RequestStore.end! - RequestStore.clear! - end - + context 'with RequestStore enabled', :request_store do include_examples 'max member access for users' def access_levels(users) diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 224067f58dd..3f5f4eea4a1 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -8,7 +8,10 @@ describe ProjectWiki, models: true do let(:project_wiki) { ProjectWiki.new(project, user) } subject { project_wiki } - before { project_wiki.wiki } + + before do + project_wiki.wiki + end describe "#path_with_namespace" do it "returns the project path with namespace with the .wiki extension" do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 718b7d5e86b..a6d4d92c450 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1905,19 +1905,43 @@ describe Repository, models: true do end describe '#is_ancestor?' do - context 'Gitaly is_ancestor feature enabled' do - let(:commit) { repository.commit } - let(:ancestor) { commit.parents.first } + let(:commit) { repository.commit } + let(:ancestor) { commit.parents.first } + context 'with Gitaly enabled' do + it 'it is an ancestor' do + expect(repository.is_ancestor?(ancestor.id, commit.id)).to eq(true) + end + + it 'it is not an ancestor' do + expect(repository.is_ancestor?(commit.id, ancestor.id)).to eq(false) + end + + it 'returns false on nil-values' do + expect(repository.is_ancestor?(nil, commit.id)).to eq(false) + expect(repository.is_ancestor?(ancestor.id, nil)).to eq(false) + expect(repository.is_ancestor?(nil, nil)).to eq(false) + end + end + + context 'with Gitaly disabled' do before do - allow(Gitlab::GitalyClient).to receive(:enabled?).and_return(true) - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(true) + allow(Gitlab::GitalyClient).to receive(:enabled?).and_return(false) + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(false) end - it "asks Gitaly server if it's an ancestor" do - expect_any_instance_of(Gitlab::GitalyClient::Commit).to receive(:is_ancestor).with(ancestor.id, commit.id) + it 'it is an ancestor' do + expect(repository.is_ancestor?(ancestor.id, commit.id)).to eq(true) + end + + it 'it is not an ancestor' do + expect(repository.is_ancestor?(commit.id, ancestor.id)).to eq(false) + end - repository.is_ancestor?(ancestor.id, commit.id) + it 'returns false on nil-values' do + expect(repository.is_ancestor?(nil, commit.id)).to eq(false) + expect(repository.is_ancestor?(ancestor.id, nil)).to eq(false) + expect(repository.is_ancestor?(nil, nil)).to eq(false) end end end diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb index c1fe1b06c52..1754253e0f2 100644 --- a/spec/models/route_spec.rb +++ b/spec/models/route_spec.rb @@ -9,7 +9,10 @@ describe Route, models: true do end describe 'validations' do - before { route } + before do + expect(route).to be_persisted + end + it { is_expected.to validate_presence_of(:source) } it { is_expected.to validate_presence_of(:path) } it { is_expected.to validate_uniqueness_of(:path) } @@ -59,7 +62,9 @@ describe Route, models: true do context 'path update' do context 'when route name is set' do - before { route.update_attributes(path: 'bar') } + before do + route.update_attributes(path: 'bar') + end it 'updates children routes with new path' do expect(described_class.exists?(path: 'bar')).to be_truthy diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a83726b48a0..1a1bbd60504 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -987,7 +987,7 @@ describe User, models: true do context 'when avatar file is uploaded' do let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" } - let(:avatar_path) { "/uploads/user/avatar/#{user.id}/dk.png" } + let(:avatar_path) { "/uploads/system/user/avatar/#{user.id}/dk.png" } it 'shows correct avatar url' do expect(user.avatar_url).to eq(avatar_path) @@ -1584,7 +1584,9 @@ describe User, models: true do end context 'user is member of the top group' do - before { group.add_owner(user) } + before do + group.add_owner(user) + end if Group.supports_nested_groups? it 'returns all groups' do @@ -1602,7 +1604,9 @@ describe User, models: true do end context 'user is member of the first child (internal node), branch 1', :nested_groups do - before { nested_group_1.add_owner(user) } + before do + nested_group_1.add_owner(user) + end it 'returns the groups in the hierarchy' do is_expected.to match_array [ @@ -1613,7 +1617,9 @@ describe User, models: true do end context 'user is member of the first child (internal node), branch 2', :nested_groups do - before { nested_group_2.add_owner(user) } + before do + nested_group_2.add_owner(user) + end it 'returns the groups in the hierarchy' do is_expected.to match_array [ @@ -1624,7 +1630,9 @@ describe User, models: true do end context 'user is member of the last child (leaf node)', :nested_groups do - before { nested_group_1_1.add_owner(user) } + before do + nested_group_1_1.add_owner(user) + end it 'returns the groups in the hierarchy' do is_expected.to match_array [ diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb index 3f4ce222b60..48a139d4b83 100644 --- a/spec/policies/ci/build_policy_spec.rb +++ b/spec/policies/ci/build_policy_spec.rb @@ -10,7 +10,9 @@ describe Ci::BuildPolicy, :models do end shared_context 'public pipelines disabled' do - before { project.update_attribute(:public_builds, false) } + before do + project.update_attribute(:public_builds, false) + end end describe '#rules' do @@ -54,7 +56,9 @@ describe Ci::BuildPolicy, :models do let(:project) { create(:empty_project, :public) } context 'team member is a guest' do - before { project.team << [user, :guest] } + before do + project.team << [user, :guest] + end context 'when public builds are enabled' do it 'includes ability to read build' do @@ -72,7 +76,9 @@ describe Ci::BuildPolicy, :models do end context 'team member is a reporter' do - before { project.team << [user, :reporter] } + before do + project.team << [user, :reporter] + end context 'when public builds are enabled' do it 'includes ability to read build' do diff --git a/spec/policies/deploy_key_policy_spec.rb b/spec/policies/deploy_key_policy_spec.rb new file mode 100644 index 00000000000..28e10f0bfe2 --- /dev/null +++ b/spec/policies/deploy_key_policy_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe DeployKeyPolicy, models: true do + subject { described_class.abilities(current_user, deploy_key).to_set } + + describe 'updating a deploy_key' do + context 'when a regular user' do + let(:current_user) { create(:user) } + + context 'tries to update private deploy key attached to project' do + let(:deploy_key) { create(:deploy_key, public: false) } + let(:project) { create(:project_empty_repo) } + + before do + project.add_master(current_user) + project.deploy_keys << deploy_key + end + + it { is_expected.to include(:update_deploy_key) } + end + + context 'tries to update private deploy key attached to other project' do + let(:deploy_key) { create(:deploy_key, public: false) } + let(:other_project) { create(:project_empty_repo) } + + before do + other_project.deploy_keys << deploy_key + end + + it { is_expected.not_to include(:update_deploy_key) } + end + + context 'tries to update public deploy key' do + let(:deploy_key) { create(:another_deploy_key, public: true) } + + it { is_expected.not_to include(:update_deploy_key) } + end + end + + context 'when an admin user' do + let(:current_user) { create(:user, :admin) } + + context ' tries to update private deploy key' do + let(:deploy_key) { create(:deploy_key, public: false) } + + it { is_expected.to include(:update_deploy_key) } + end + + context 'when an admin user tries to update public deploy key' do + let(:deploy_key) { create(:another_deploy_key, public: true) } + + it { is_expected.to include(:update_deploy_key) } + end + end + end +end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 0d3af1f4499..848fd547e10 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -139,6 +139,18 @@ describe ProjectPolicy, models: true do is_expected.not_to include(:read_build, :read_pipeline) end end + + context 'when builds are disabled' do + before do + project.project_feature.update( + builds_access_level: ProjectFeature::DISABLED) + end + + it do + is_expected.not_to include(:read_build) + is_expected.to include(:read_pipeline) + end + end end context 'reporter' do diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb index e1771b636b8..d2b2528c57a 100644 --- a/spec/policies/project_snippet_policy_spec.rb +++ b/spec/policies/project_snippet_policy_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe ProjectSnippetPolicy, models: true do let(:regular_user) { create(:user) } let(:external_user) { create(:user, :external) } - let(:project) { create(:empty_project) } + let(:project) { create(:empty_project, :public) } let(:author_permissions) do [ @@ -78,7 +78,9 @@ describe ProjectSnippetPolicy, models: true do context 'project team member external user' do subject { abilities(external_user, :internal) } - before { project.team << [external_user, :developer] } + before do + project.team << [external_user, :developer] + end it do is_expected.to include(:read_project_snippet) @@ -107,7 +109,7 @@ describe ProjectSnippetPolicy, models: true do end context 'snippet author' do - let(:snippet) { create(:project_snippet, :private, author: regular_user) } + let(:snippet) { create(:project_snippet, :private, author: regular_user, project: project) } subject { described_class.abilities(regular_user, snippet).to_set } @@ -120,7 +122,9 @@ describe ProjectSnippetPolicy, models: true do context 'project team member normal user' do subject { abilities(regular_user, :private) } - before { project.team << [regular_user, :developer] } + before do + project.team << [regular_user, :developer] + end it do is_expected.to include(:read_project_snippet) @@ -131,7 +135,9 @@ describe ProjectSnippetPolicy, models: true do context 'project team member external user' do subject { abilities(external_user, :private) } - before { project.team << [external_user, :developer] } + before do + project.team << [external_user, :developer] + end it do is_expected.to include(:read_project_snippet) diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb index 44720fc4448..f5a14b1d04d 100644 --- a/spec/presenters/merge_request_presenter_spec.rb +++ b/spec/presenters/merge_request_presenter_spec.rb @@ -132,6 +132,11 @@ describe MergeRequestPresenter do it 'does not present related issues links' do is_expected.not_to match("#{project.full_path}/issues/#{issue_b.iid}") end + + it 'appends status when closing issue is already closed' do + issue_a.close + is_expected.to match('(closed)') + end end describe '#mentioned_issues_links' do @@ -147,6 +152,11 @@ describe MergeRequestPresenter do it 'does not present closing issues links' do is_expected.not_to match("#{project.full_path}/issues/#{issue_a.iid}") end + + it 'appends status when mentioned issue is already closed' do + issue_b.close + is_expected.to match('(closed)') + end end describe '#assign_to_closing_issues_link' do diff --git a/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb b/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb index 6443f86b6a1..5c39e1b5f96 100644 --- a/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb +++ b/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb @@ -51,10 +51,6 @@ describe Projects::Settings::DeployKeysPresenter do expect(presenter.available_project_keys).not_to be_empty end - it 'returns false if any available_project_keys are enabled' do - expect(presenter.any_available_project_keys_enabled?).to eq(true) - end - it 'returns the available_project_keys size' do expect(presenter.available_project_keys_size).to eq(1) end diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index bbdef0aeb1b..6d822b5cb4f 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -9,7 +9,9 @@ describe API::AwardEmoji do let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) } let!(:note) { create(:note, project: project, noteable: issue) } - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do context 'on an issue' do diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 6b637a03b6f..b8ca73c321c 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -34,7 +34,9 @@ describe API::CommitStatuses do let!(:status6) { create_status(master, status: 'success') } context 'latest commit statuses' do - before { get api(get_url, reporter) } + before do + get api(get_url, reporter) + end it 'returns latest commit statuses' do expect(response).to have_http_status(200) @@ -48,7 +50,9 @@ describe API::CommitStatuses do end context 'all commit statuses' do - before { get api(get_url, reporter), all: 1 } + before do + get api(get_url, reporter), all: 1 + end it 'returns all commit statuses' do expect(response).to have_http_status(200) @@ -61,7 +65,9 @@ describe API::CommitStatuses do end context 'latest commit statuses for specific ref' do - before { get api(get_url, reporter), ref: 'develop' } + before do + get api(get_url, reporter), ref: 'develop' + end it 'returns latest commit statuses for specific ref' do expect(response).to have_http_status(200) @@ -72,7 +78,9 @@ describe API::CommitStatuses do end context 'latest commit statues for specific name' do - before { get api(get_url, reporter), name: 'coverage' } + before do + get api(get_url, reporter), name: 'coverage' + end it 'return latest commit statuses for specific name' do expect(response).to have_http_status(200) @@ -85,7 +93,9 @@ describe API::CommitStatuses do end context 'ci commit does not exist' do - before { get api(get_url, reporter) } + before do + get api(get_url, reporter) + end it 'returns empty array' do expect(response.status).to eq 200 @@ -95,7 +105,9 @@ describe API::CommitStatuses do end context "guest user" do - before { get api(get_url, guest) } + before do + get api(get_url, guest) + end it "does not return project commits" do expect(response).to have_http_status(403) @@ -103,7 +115,9 @@ describe API::CommitStatuses do end context "unauthorized user" do - before { get api(get_url) } + before do + get api(get_url) + end it "does not return project commits" do expect(response).to have_http_status(401) @@ -209,7 +223,9 @@ describe API::CommitStatuses do end context 'when status is invalid' do - before { post api(post_url, developer), state: 'invalid' } + before do + post api(post_url, developer), state: 'invalid' + end it 'does not create commit status' do expect(response).to have_http_status(400) @@ -217,7 +233,9 @@ describe API::CommitStatuses do end context 'when request without a state made' do - before { post api(post_url, developer) } + before do + post api(post_url, developer) + end it 'does not create commit status' do expect(response).to have_http_status(400) @@ -226,7 +244,10 @@ describe API::CommitStatuses do context 'when commit SHA is invalid' do let(:sha) { 'invalid_sha' } - before { post api(post_url, developer), state: 'running' } + + before do + post api(post_url, developer), state: 'running' + end it 'returns not found error' do expect(response).to have_http_status(404) @@ -248,7 +269,9 @@ describe API::CommitStatuses do end context 'reporter user' do - before { post api(post_url, reporter), state: 'running' } + before do + post api(post_url, reporter), state: 'running' + end it 'does not create commit status' do expect(response).to have_http_status(403) @@ -256,7 +279,9 @@ describe API::CommitStatuses do end context 'guest user' do - before { post api(post_url, guest), state: 'running' } + before do + post api(post_url, guest), state: 'running' + end it 'does not create commit status' do expect(response).to have_http_status(403) @@ -264,7 +289,9 @@ describe API::CommitStatuses do end context 'unauthorized user' do - before { post api(post_url) } + before do + post api(post_url) + end it 'does not create commit status' do expect(response).to have_http_status(401) diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index b0c265b6453..0dad547735d 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -9,11 +9,15 @@ describe API::Commits do let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') } let!(:another_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'another comment on a commit') } - before { project.team << [user, :reporter] } + before do + project.team << [user, :reporter] + end describe "List repository commits" do context "authorized user" do - before { project.team << [user2, :reporter] } + before do + project.team << [user2, :reporter] + end it "returns project commits" do commit = project.repository.commit @@ -514,7 +518,9 @@ describe API::Commits do describe "Get the diff of a commit" do context "authorized user" do - before { project.team << [user2, :reporter] } + before do + project.team << [user2, :reporter] + end it "returns the diff of the selected commit" do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff", user) diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 843e9862b0c..9c260f88f56 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -13,7 +13,7 @@ describe API::DeployKeys do describe 'GET /deploy_keys' do context 'when unauthenticated' do - it 'should return authentication error' do + it 'returns authentication error' do get api('/deploy_keys') expect(response.status).to eq(401) @@ -21,7 +21,7 @@ describe API::DeployKeys do end context 'when authenticated as non-admin user' do - it 'should return a 403 error' do + it 'returns a 403 error' do get api('/deploy_keys', user) expect(response.status).to eq(403) @@ -29,7 +29,7 @@ describe API::DeployKeys do end context 'when authenticated as admin' do - it 'should return all deploy keys' do + it 'returns all deploy keys' do get api('/deploy_keys', admin) expect(response.status).to eq(200) @@ -41,9 +41,11 @@ describe API::DeployKeys do end describe 'GET /projects/:id/deploy_keys' do - before { deploy_key } + before do + deploy_key + end - it 'should return array of ssh keys' do + it 'returns array of ssh keys' do get api("/projects/#{project.id}/deploy_keys", admin) expect(response).to have_http_status(200) @@ -54,14 +56,14 @@ describe API::DeployKeys do end describe 'GET /projects/:id/deploy_keys/:key_id' do - it 'should return a single key' do + it 'returns a single key' do get api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) expect(response).to have_http_status(200) expect(json_response['title']).to eq(deploy_key.title) end - it 'should return 404 Not Found with invalid ID' do + it 'returns 404 Not Found with invalid ID' do get api("/projects/#{project.id}/deploy_keys/404", admin) expect(response).to have_http_status(404) @@ -69,26 +71,26 @@ describe API::DeployKeys do end describe 'POST /projects/:id/deploy_keys' do - it 'should not create an invalid ssh key' do + it 'does not create an invalid ssh key' do post api("/projects/#{project.id}/deploy_keys", admin), { title: 'invalid key' } expect(response).to have_http_status(400) expect(json_response['error']).to eq('key is missing') end - it 'should not create a key without title' do + it 'does not create a key without title' do post api("/projects/#{project.id}/deploy_keys", admin), key: 'some key' expect(response).to have_http_status(400) expect(json_response['error']).to eq('title is missing') end - it 'should create new ssh key' do + it 'creates new ssh key' do key_attrs = attributes_for :another_key expect do post api("/projects/#{project.id}/deploy_keys", admin), key_attrs - end.to change{ project.deploy_keys.count }.by(1) + end.to change { project.deploy_keys.count }.by(1) end it 'returns an existing ssh key when attempting to add a duplicate' do @@ -117,10 +119,55 @@ describe API::DeployKeys do end end + describe 'PUT /projects/:id/deploy_keys/:key_id' do + let(:private_deploy_key) { create(:another_deploy_key, public: false) } + let(:project_private_deploy_key) do + create(:deploy_keys_project, project: project, deploy_key: private_deploy_key) + end + + it 'updates a public deploy key as admin' do + expect do + put api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin), { title: 'new title' } + end.not_to change(deploy_key, :title) + + expect(response).to have_http_status(200) + end + + it 'does not update a public deploy key as non admin' do + expect do + put api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", user), { title: 'new title' } + end.not_to change(deploy_key, :title) + + expect(response).to have_http_status(404) + end + + it 'does not update a private key with invalid title' do + project_private_deploy_key + + expect do + put api("/projects/#{project.id}/deploy_keys/#{private_deploy_key.id}", admin), { title: '' } + end.not_to change(deploy_key, :title) + + expect(response).to have_http_status(400) + end + + it 'updates a private ssh key with correct attributes' do + project_private_deploy_key + + put api("/projects/#{project.id}/deploy_keys/#{private_deploy_key.id}", admin), { title: 'new title', can_push: true } + + expect(json_response['id']).to eq(private_deploy_key.id) + expect(json_response['title']).to eq('new title') + expect(json_response['can_push']).to eq(true) + end + end + describe 'DELETE /projects/:id/deploy_keys/:key_id' do - before { deploy_key } + before do + deploy_key + end - it 'should delete existing key' do + it 'deletes existing key' do expect do delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) @@ -128,7 +175,7 @@ describe API::DeployKeys do end.to change{ project.deploy_keys.count }.by(-1) end - it 'should return 404 Not Found with invalid ID' do + it 'returns 404 Not Found with invalid ID' do delete api("/projects/#{project.id}/deploy_keys/404", admin) expect(response).to have_http_status(404) @@ -150,7 +197,7 @@ describe API::DeployKeys do end context 'when authenticated as non-admin user' do - it 'should return a 404 error' do + it 'returns a 404 error' do post api("/projects/#{project2.id}/deploy_keys/#{deploy_key.id}/enable", user) expect(response).to have_http_status(404) diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index d325c6eff9d..c5ec8be4f21 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -13,7 +13,9 @@ describe API::Files do let(:author_email) { 'user@example.org' } let(:author_name) { 'John Doe' } - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end def route(file_path = nil) "/projects/#{project.id}/repository/files/#{file_path}" diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index ed392acc607..191c60aba31 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -55,40 +55,62 @@ describe API::Helpers do subject { current_user } describe "Warden authentication" do - before { doorkeeper_guard_returns false } + before do + doorkeeper_guard_returns false + end context "with invalid credentials" do context "GET request" do - before { env['REQUEST_METHOD'] = 'GET' } + before do + env['REQUEST_METHOD'] = 'GET' + end + it { is_expected.to be_nil } end end context "with valid credentials" do - before { warden_authenticate_returns user } + before do + warden_authenticate_returns user + end context "GET request" do - before { env['REQUEST_METHOD'] = 'GET' } + before do + env['REQUEST_METHOD'] = 'GET' + end + it { is_expected.to eq(user) } end context "HEAD request" do - before { env['REQUEST_METHOD'] = 'HEAD' } + before do + env['REQUEST_METHOD'] = 'HEAD' + end + it { is_expected.to eq(user) } end context "PUT request" do - before { env['REQUEST_METHOD'] = 'PUT' } + before do + env['REQUEST_METHOD'] = 'PUT' + end + it { is_expected.to be_nil } end context "POST request" do - before { env['REQUEST_METHOD'] = 'POST' } + before do + env['REQUEST_METHOD'] = 'POST' + end + it { is_expected.to be_nil } end context "DELETE request" do - before { env['REQUEST_METHOD'] = 'DELETE' } + before do + env['REQUEST_METHOD'] = 'DELETE' + end + it { is_expected.to be_nil } end end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index cf232e7ff69..86e15d896df 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -15,21 +15,43 @@ describe API::Internal do end end - describe "GET /internal/broadcast_message" do - context "broadcast message exists" do - let!(:broadcast_message) { create(:broadcast_message, starts_at: Time.now.yesterday, ends_at: Time.now.tomorrow ) } + describe 'GET /internal/broadcast_message' do + context 'broadcast message exists' do + let!(:broadcast_message) { create(:broadcast_message, starts_at: 1.day.ago, ends_at: 1.day.from_now ) } - it do - get api("/internal/broadcast_message"), secret_token: secret_token + it 'returns one broadcast message' do + get api('/internal/broadcast_message'), secret_token: secret_token expect(response).to have_http_status(200) - expect(json_response["message"]).to eq(broadcast_message.message) + expect(json_response['message']).to eq(broadcast_message.message) end end - context "broadcast message doesn't exist" do - it do - get api("/internal/broadcast_message"), secret_token: secret_token + context 'broadcast message does not exist' do + it 'returns nothing' do + get api('/internal/broadcast_message'), secret_token: secret_token + + expect(response).to have_http_status(200) + expect(json_response).to be_empty + end + end + end + + describe 'GET /internal/broadcast_messages' do + context 'broadcast message(s) exist' do + let!(:broadcast_message) { create(:broadcast_message, starts_at: 1.day.ago, ends_at: 1.day.from_now ) } + + it 'returns active broadcast message(s)' do + get api('/internal/broadcast_messages'), secret_token: secret_token + + expect(response).to have_http_status(200) + expect(json_response[0]['message']).to eq(broadcast_message.message) + end + end + + context 'broadcast message does not exist' do + it 'returns nothing' do + get api('/internal/broadcast_messages'), secret_token: secret_token expect(response).to have_http_status(200) expect(json_response).to be_empty diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb index e5e5872dc1f..8d647eb1c7e 100644 --- a/spec/requests/api/jobs_spec.rb +++ b/spec/requests/api/jobs_spec.rb @@ -11,7 +11,7 @@ describe API::Jobs, :api do ref: project.default_branch) end - let!(:build) { create(:ci_build, pipeline: pipeline) } + let!(:job) { create(:ci_build, pipeline: pipeline) } let(:user) { create(:user) } let(:api_user) { user } @@ -42,13 +42,13 @@ describe API::Jobs, :api do end it 'returns pipeline data' do - json_build = json_response.first + json_job = json_response.first - expect(json_build['pipeline']).not_to be_empty - expect(json_build['pipeline']['id']).to eq build.pipeline.id - expect(json_build['pipeline']['ref']).to eq build.pipeline.ref - expect(json_build['pipeline']['sha']).to eq build.pipeline.sha - expect(json_build['pipeline']['status']).to eq build.pipeline.status + expect(json_job['pipeline']).not_to be_empty + expect(json_job['pipeline']['id']).to eq job.pipeline.id + expect(json_job['pipeline']['ref']).to eq job.pipeline.ref + expect(json_job['pipeline']['sha']).to eq job.pipeline.sha + expect(json_job['pipeline']['status']).to eq job.pipeline.status end context 'filter project with one scope element' do @@ -79,7 +79,7 @@ describe API::Jobs, :api do context 'unauthorized user' do let(:api_user) { nil } - it 'does not return project builds' do + it 'does not return project jobs' do expect(response).to have_http_status(401) end end @@ -105,13 +105,13 @@ describe API::Jobs, :api do end it 'returns pipeline data' do - json_build = json_response.first + json_job = json_response.first - expect(json_build['pipeline']).not_to be_empty - expect(json_build['pipeline']['id']).to eq build.pipeline.id - expect(json_build['pipeline']['ref']).to eq build.pipeline.ref - expect(json_build['pipeline']['sha']).to eq build.pipeline.sha - expect(json_build['pipeline']['status']).to eq build.pipeline.status + expect(json_job['pipeline']).not_to be_empty + expect(json_job['pipeline']['id']).to eq job.pipeline.id + expect(json_job['pipeline']['ref']).to eq job.pipeline.ref + expect(json_job['pipeline']['sha']).to eq job.pipeline.sha + expect(json_job['pipeline']['status']).to eq job.pipeline.status end context 'filter jobs with one scope element' do @@ -140,7 +140,7 @@ describe API::Jobs, :api do context 'jobs in different pipelines' do let!(:pipeline2) { create(:ci_empty_pipeline, project: project) } - let!(:build2) { create(:ci_build, pipeline: pipeline2) } + let!(:job2) { create(:ci_build, pipeline: pipeline2) } it 'excludes jobs from other pipelines' do json_response.each { |job| expect(job['pipeline']['id']).to eq(pipeline.id) } @@ -159,7 +159,7 @@ describe API::Jobs, :api do describe 'GET /projects/:id/jobs/:job_id' do before do - get api("/projects/#{project.id}/jobs/#{build.id}", api_user) + get api("/projects/#{project.id}/jobs/#{job.id}", api_user) end context 'authorized user' do @@ -169,12 +169,13 @@ describe API::Jobs, :api do end it 'returns pipeline data' do - json_build = json_response - expect(json_build['pipeline']).not_to be_empty - expect(json_build['pipeline']['id']).to eq build.pipeline.id - expect(json_build['pipeline']['ref']).to eq build.pipeline.ref - expect(json_build['pipeline']['sha']).to eq build.pipeline.sha - expect(json_build['pipeline']['status']).to eq build.pipeline.status + json_job = json_response + + expect(json_job['pipeline']).not_to be_empty + expect(json_job['pipeline']['id']).to eq job.pipeline.id + expect(json_job['pipeline']['ref']).to eq job.pipeline.ref + expect(json_job['pipeline']['sha']).to eq job.pipeline.sha + expect(json_job['pipeline']['status']).to eq job.pipeline.status end end @@ -189,11 +190,11 @@ describe API::Jobs, :api do describe 'GET /projects/:id/jobs/:job_id/artifacts' do before do - get api("/projects/#{project.id}/jobs/#{build.id}/artifacts", api_user) + get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) end context 'job with artifacts' do - let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } context 'authorized user' do let(:download_headers) do @@ -204,7 +205,7 @@ describe API::Jobs, :api do it 'returns specific job artifacts' do expect(response).to have_http_status(200) expect(response.headers).to include(download_headers) - expect(response.body).to match_file(build.artifacts_file.file.file) + expect(response.body).to match_file(job.artifacts_file.file.file) end end @@ -224,14 +225,14 @@ describe API::Jobs, :api do describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do let(:api_user) { reporter } - let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } before do - build.success + job.success end - def get_for_ref(ref = pipeline.ref, job = build.name) - get api("/projects/#{project.id}/jobs/artifacts/#{ref}/download", api_user), job: job + def get_for_ref(ref = pipeline.ref, job_name = job.name) + get api("/projects/#{project.id}/jobs/artifacts/#{ref}/download", api_user), job: job_name end context 'when not logged in' do @@ -285,7 +286,7 @@ describe API::Jobs, :api do let(:download_headers) do { 'Content-Transfer-Encoding' => 'binary', 'Content-Disposition' => - "attachment; filename=#{build.artifacts_file.filename}" } + "attachment; filename=#{job.artifacts_file.filename}" } end it { expect(response).to have_http_status(200) } @@ -321,16 +322,16 @@ describe API::Jobs, :api do end describe 'GET /projects/:id/jobs/:job_id/trace' do - let(:build) { create(:ci_build, :trace, pipeline: pipeline) } + let(:job) { create(:ci_build, :trace, pipeline: pipeline) } before do - get api("/projects/#{project.id}/jobs/#{build.id}/trace", api_user) + get api("/projects/#{project.id}/jobs/#{job.id}/trace", api_user) end context 'authorized user' do it 'returns specific job trace' do expect(response).to have_http_status(200) - expect(response.body).to eq(build.trace.raw) + expect(response.body).to eq(job.trace.raw) end end @@ -345,7 +346,7 @@ describe API::Jobs, :api do describe 'POST /projects/:id/jobs/:job_id/cancel' do before do - post api("/projects/#{project.id}/jobs/#{build.id}/cancel", api_user) + post api("/projects/#{project.id}/jobs/#{job.id}/cancel", api_user) end context 'authorized user' do @@ -375,10 +376,10 @@ describe API::Jobs, :api do end describe 'POST /projects/:id/jobs/:job_id/retry' do - let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } + let(:job) { create(:ci_build, :canceled, pipeline: pipeline) } before do - post api("/projects/#{project.id}/jobs/#{build.id}/retry", api_user) + post api("/projects/#{project.id}/jobs/#{job.id}/retry", api_user) end context 'authorized user' do @@ -410,28 +411,29 @@ describe API::Jobs, :api do describe 'POST /projects/:id/jobs/:job_id/erase' do before do - post api("/projects/#{project.id}/jobs/#{build.id}/erase", user) + post api("/projects/#{project.id}/jobs/#{job.id}/erase", user) end context 'job is erasable' do - let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) } + let(:job) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) } it 'erases job content' do expect(response).to have_http_status(201) - expect(build).not_to have_trace - expect(build.artifacts_file.exists?).to be_falsy - expect(build.artifacts_metadata.exists?).to be_falsy + expect(job).not_to have_trace + expect(job.artifacts_file.exists?).to be_falsy + expect(job.artifacts_metadata.exists?).to be_falsy end it 'updates job' do - build.reload - expect(build.erased_at).to be_truthy - expect(build.erased_by).to eq(user) + job.reload + + expect(job.erased_at).to be_truthy + expect(job.erased_by).to eq(user) end end context 'job is not erasable' do - let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) } + let(:job) { create(:ci_build, :trace, project: project, pipeline: pipeline) } it 'responds with forbidden' do expect(response).to have_http_status(403) @@ -439,25 +441,25 @@ describe API::Jobs, :api do end end - describe 'POST /projects/:id/jobs/:build_id/artifacts/keep' do + describe 'POST /projects/:id/jobs/:job_id/artifacts/keep' do before do - post api("/projects/#{project.id}/jobs/#{build.id}/artifacts/keep", user) + post api("/projects/#{project.id}/jobs/#{job.id}/artifacts/keep", user) end context 'artifacts did not expire' do - let(:build) do + let(:job) do create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days) end it 'keeps artifacts' do expect(response).to have_http_status(200) - expect(build.reload.artifacts_expire_at).to be_nil + expect(job.reload.artifacts_expire_at).to be_nil end end context 'no artifacts' do - let(:build) { create(:ci_build, project: project, pipeline: pipeline) } + let(:job) { create(:ci_build, project: project, pipeline: pipeline) } it 'responds with not found' do expect(response).to have_http_status(404) @@ -467,18 +469,18 @@ describe API::Jobs, :api do describe 'POST /projects/:id/jobs/:job_id/play' do before do - post api("/projects/#{project.id}/jobs/#{build.id}/play", api_user) + post api("/projects/#{project.id}/jobs/#{job.id}/play", api_user) end context 'on an playable job' do - let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) } + let(:job) { create(:ci_build, :manual, project: project, pipeline: pipeline) } context 'when user is authorized to trigger a manual action' 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) - expect(build.reload).to be_pending + expect(json_response['id']).to eq(job.id) + expect(job.reload).to be_pending end end @@ -487,7 +489,7 @@ describe API::Jobs, :api do let(:api_user) { create(:user) } it 'does not trigger a manual action' do - expect(build.reload).to be_manual + expect(job.reload).to be_manual expect(response).to have_http_status(404) end end @@ -496,7 +498,7 @@ describe API::Jobs, :api do let(:api_user) { reporter } it 'does not trigger a manual action' do - expect(build.reload).to be_manual + expect(job.reload).to be_manual expect(response).to have_http_status(403) end end diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb index ab957c72984..f534332ca6c 100644 --- a/spec/requests/api/keys_spec.rb +++ b/spec/requests/api/keys_spec.rb @@ -4,11 +4,9 @@ describe API::Keys do let(:user) { create(:user) } let(:admin) { create(:admin) } let(:key) { create(:key, user: user) } - let(:email) { create(:email, user: user) } + let(:email) { create(:email, user: user) } describe 'GET /keys/:uid' do - before { admin } - context 'when unauthenticated' do it 'returns authentication error' do get api("/keys/#{key.id}") diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 0c6b55c1630..f7e2f1908bb 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -339,7 +339,9 @@ describe API::Labels do end context "when user is already subscribed to label" do - before { label1.subscribe(user, project) } + before do + label1.subscribe(user, project) + end it "returns 304" do post api("/projects/#{project.id}/labels/#{label1.id}/subscribe", user) @@ -358,7 +360,9 @@ describe API::Labels do end describe "POST /projects/:id/labels/:label_id/unsubscribe" do - before { label1.subscribe(user, project) } + before do + label1.subscribe(user, project) + end context "when label_id is a label title" do it "unsubscribes from the label" do @@ -381,7 +385,9 @@ describe API::Labels do end context "when user is already unsubscribed from label" do - before { label1.unsubscribe(user, project) } + before do + label1.unsubscribe(user, project) + end it "returns 304" do post api("/projects/#{project.id}/labels/#{label1.id}/unsubscribe", user) diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index dd74351a2b1..40934c25afc 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -6,7 +6,9 @@ describe API::Milestones do let!(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') } let!(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') } - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end describe 'GET /projects/:id/milestones' do it 'returns project milestones' do diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 6afcd237c3c..03f2b5950ee 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -28,7 +28,9 @@ describe API::Notes do system: true end - before { project.team << [user, :reporter] } + before do + project.team << [user, :reporter] + end describe "GET /projects/:id/noteable/:noteable_id/notes" do context "when noteable is an Issue" do @@ -58,7 +60,9 @@ describe API::Notes do end context "and issue is confidential" do - before { ext_issue.update_attributes(confidential: true) } + before do + ext_issue.update_attributes(confidential: true) + end it "returns 404" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", user) @@ -150,7 +154,9 @@ describe API::Notes do end context "when issue is confidential" do - before { issue.update_attributes(confidential: true) } + before do + issue.update_attributes(confidential: true) + end it "returns 404" do get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{issue_note.id}", private_user) diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index 9e6957e9922..258085e503f 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -10,7 +10,9 @@ describe API::Pipelines do ref: project.default_branch, user: user) end - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end describe 'GET /projects/:id/pipelines ' do context 'authorized user' do @@ -285,7 +287,9 @@ describe API::Pipelines do describe 'POST /projects/:id/pipeline ' do context 'authorized user' do context 'with gitlab-ci.yml' do - before { stub_ci_pipeline_to_return_yaml_file } + before do + stub_ci_pipeline_to_return_yaml_file + end it 'creates and returns a new pipeline' do expect do @@ -419,7 +423,9 @@ describe API::Pipelines do context 'user without proper access rights' do let!(:reporter) { create(:user) } - before { project.team << [reporter, :reporter] } + before do + project.team << [reporter, :reporter] + end it 'rejects the action' do post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter) diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 3ab1764f5c3..4d4631322b1 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -36,11 +36,34 @@ describe API::ProjectSnippets do end end + describe 'GET /projects/:project_id/snippets/:id' do + let(:user) { create(:user) } + let(:snippet) { create(:project_snippet, :public, project: project) } + + it 'returns snippet json' do + get api("/projects/#{project.id}/snippets/#{snippet.id}", user) + + expect(response).to have_http_status(200) + + expect(json_response['title']).to eq(snippet.title) + expect(json_response['description']).to eq(snippet.description) + expect(json_response['file_name']).to eq(snippet.file_name) + end + + it 'returns 404 for invalid snippet id' do + get api("/projects/#{project.id}/snippets/1234", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Not found') + end + end + describe 'POST /projects/:project_id/snippets/' do let(:params) do { title: 'Test Title', file_name: 'test.rb', + description: 'test description', code: 'puts "hello world"', visibility: 'public' } @@ -52,6 +75,7 @@ describe API::ProjectSnippets do expect(response).to have_http_status(201) snippet = ProjectSnippet.find(json_response['id']) expect(snippet.content).to eq(params[:code]) + expect(snippet.description).to eq(params[:description]) expect(snippet.title).to eq(params[:title]) expect(snippet.file_name).to eq(params[:file_name]) expect(snippet.visibility_level).to eq(Snippet::PUBLIC) @@ -106,12 +130,14 @@ describe API::ProjectSnippets do it 'updates snippet' do new_content = 'New content' + new_description = 'New description' - put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), code: new_content + put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), code: new_content, description: new_description expect(response).to have_http_status(200) snippet.reload expect(snippet.content).to eq(new_content) + expect(snippet.description).to eq(new_description) end it 'returns 404 for invalid snippet id' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 86c57204971..d92262a4c99 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -398,6 +398,15 @@ describe API::Projects do expect(json_response['tag_list']).to eq(%w[tagFirst tagSecond]) end + it 'uploads avatar for project a project' do + project = attributes_for(:project, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')) + + post api('/projects', user), project + + project_id = json_response['id'] + expect(json_response['avatar_url']).to eq("http://localhost/uploads/system/project/avatar/#{project_id}/banana_sample.gif") + end + it 'sets a project as allowing merge even if build fails' do project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: false }) post api('/projects', user), project @@ -467,8 +476,9 @@ describe API::Projects do end describe 'POST /projects/user/:id' do - before { project } - before { admin } + before do + expect(project).to be_persisted + end it 'creates new project without path but with name and return 201' do expect { post api("/projects/user/#{user.id}", admin), name: 'Foo Project' }.to change {Project.count}.by(1) @@ -572,7 +582,9 @@ describe API::Projects do end describe "POST /projects/:id/uploads" do - before { project } + before do + project + end it "uploads the file and returns its info" do post api("/projects/#{project.id}/uploads", user), file: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") @@ -720,7 +732,9 @@ describe API::Projects do describe 'permissions' do context 'all projects' do - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it 'contains permission information' do get api("/projects", user) @@ -747,7 +761,9 @@ describe API::Projects do context 'group project' do let(:project2) { create(:empty_project, group: create(:group)) } - before { project2.group.add_owner(user) } + before do + project2.group.add_owner(user) + end it 'sets the owner and return 200' do get api("/projects/#{project2.id}", user) @@ -813,7 +829,9 @@ describe API::Projects do end describe 'GET /projects/:id/snippets' do - before { snippet } + before do + snippet + end it 'returns an array of project snippets' do get api("/projects/#{project.id}/snippets", user) @@ -870,7 +888,9 @@ describe API::Projects do end describe 'DELETE /projects/:id/snippets/:snippet_id' do - before { snippet } + before do + snippet + end it 'deletes existing project snippet' do expect do @@ -1065,14 +1085,16 @@ describe API::Projects do end describe 'PUT /projects/:id' do - before { project } - before { user } - before { user3 } - before { user4 } - before { project3 } - before { project4 } - before { project_member2 } - before { project_member } + before do + expect(project).to be_persisted + expect(user).to be_persisted + expect(user3).to be_persisted + expect(user4).to be_persisted + expect(project3).to be_persisted + expect(project4).to be_persisted + expect(project_member2).to be_persisted + expect(project_member).to be_persisted + end it 'returns 400 when nothing sent' do project_param = {} diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index be83514ed9c..d554c242916 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -190,17 +190,23 @@ describe API::Runner do pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, commands: "ls\ndate") end - before { project.runners << runner } + before do + project.runners << runner + end describe 'POST /api/v4/jobs/request' do let!(:last_update) {} let!(:new_update) { } let(:user_agent) { 'gitlab-runner 9.0.0 (9-0-stable; go1.7.4; linux/amd64)' } - before { stub_container_registry_config(enabled: false) } + before do + stub_container_registry_config(enabled: false) + end shared_examples 'no jobs available' do - before { request_job } + before do + request_job + end context 'when runner sends version in User-Agent' do context 'for stable version' do @@ -277,7 +283,9 @@ describe API::Runner do end context 'when jobs are finished' do - before { job.success } + before do + job.success + end it_behaves_like 'no jobs available' end @@ -356,8 +364,11 @@ describe API::Runner do expect(json_response['token']).to eq(job.token) expect(json_response['job_info']).to eq(expected_job_info) expect(json_response['git_info']).to eq(expected_git_info) - expect(json_response['image']).to eq({ 'name' => 'ruby:2.1' }) - expect(json_response['services']).to eq([{ 'name' => 'postgres' }]) + expect(json_response['image']).to eq({ 'name' => 'ruby:2.1', 'entrypoint' => '/bin/sh' }) + expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil, + 'alias' => nil, 'command' => nil }, + { 'name' => 'docker:dind', 'entrypoint' => '/bin/sh', + 'alias' => 'docker', 'command' => 'sleep 30' }]) expect(json_response['steps']).to eq(expected_steps) expect(json_response['artifacts']).to eq(expected_artifacts) expect(json_response['cache']).to eq(expected_cache) @@ -431,8 +442,29 @@ describe API::Runner do expect(response).to have_http_status(201) expect(json_response['id']).to eq(test_job.id) expect(json_response['dependencies'].count).to eq(2) - expect(json_response['dependencies']).to include({ 'id' => job.id, 'name' => job.name, 'token' => job.token }, - { 'id' => job2.id, 'name' => job2.name, 'token' => job2.token }) + expect(json_response['dependencies']).to include( + { 'id' => job.id, 'name' => job.name, 'token' => job.token }, + { 'id' => job2.id, 'name' => job2.name, 'token' => job2.token }) + end + end + + context 'when pipeline have jobs with artifacts' do + let!(:job) { create(:ci_build_tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } + + before do + job.success + end + + it 'returns dependent jobs' do + request_job + + expect(response).to have_http_status(201) + expect(json_response['id']).to eq(test_job.id) + expect(json_response['dependencies'].count).to eq(1) + expect(json_response['dependencies']).to include( + { 'id' => job.id, 'name' => job.name, 'token' => job.token, + 'artifacts_file' => { 'filename' => 'ci_build_artifacts.zip', 'size' => 106365 } }) end end @@ -484,10 +516,14 @@ describe API::Runner do end context 'when job has no tags' do - before { job.update(tags: []) } + before do + job.update(tags: []) + end context 'when runner is allowed to pick untagged jobs' do - before { runner.update_column(:run_untagged, true) } + before do + runner.update_column(:run_untagged, true) + end it 'picks job' do request_job @@ -497,7 +533,9 @@ describe API::Runner do end context 'when runner is not allowed to pick untagged jobs' do - before { runner.update_column(:run_untagged, false) } + before do + runner.update_column(:run_untagged, false) + end it_behaves_like 'no jobs available' end @@ -537,7 +575,9 @@ describe API::Runner do end context 'when registry is enabled' do - before { stub_container_registry_config(enabled: true, host_port: registry_url) } + before do + stub_container_registry_config(enabled: true, host_port: registry_url) + end it 'sends registry credentials key' do request_job @@ -548,7 +588,9 @@ describe API::Runner do end context 'when registry is disabled' do - before { stub_container_registry_config(enabled: false, host_port: registry_url) } + before do + stub_container_registry_config(enabled: false, host_port: registry_url) + end it 'does not send registry credentials' do request_job @@ -570,7 +612,9 @@ describe API::Runner do describe 'PUT /api/v4/jobs/:id' do let(:job) { create(:ci_build, :pending, :trace, pipeline: pipeline, runner_id: runner.id) } - before { job.run! } + before do + job.run! + end context 'when status is given' do it 'mark job as succeeded' do @@ -625,7 +669,9 @@ describe API::Runner do let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) } let(:update_interval) { 10.seconds.to_i } - before { initial_patch_the_trace } + before do + initial_patch_the_trace + end context 'when request is valid' do it 'gets correct response' do @@ -767,7 +813,9 @@ describe API::Runner do let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') } - before { job.run! } + before do + job.run! + end describe 'POST /api/v4/jobs/:id/artifacts/authorize' do context 'when using token as parameter' do @@ -873,13 +921,17 @@ describe API::Runner do end context 'when uses regular file post' do - before { upload_artifacts(file_upload, headers_with_token, false) } + before do + upload_artifacts(file_upload, headers_with_token, false) + end it_behaves_like 'successful artifacts upload' end context 'when uses accelerated file post' do - before { upload_artifacts(file_upload, headers_with_token, true) } + before do + upload_artifacts(file_upload, headers_with_token, true) + end it_behaves_like 'successful artifacts upload' end @@ -1033,7 +1085,9 @@ describe API::Runner do allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir) end - after { FileUtils.remove_entry @tmpdir } + after do + FileUtils.remove_entry @tmpdir + end it' "fails to post artifacts for outside of tmp path"' do upload_artifacts(file_upload, headers_with_token) @@ -1055,7 +1109,9 @@ describe API::Runner do describe 'GET /api/v4/jobs/:id/artifacts' do let(:token) { job.token } - before { download_artifact } + before do + download_artifact + end context 'when job has artifacts' do let(:job) { create(:ci_build, :artifacts) } diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 2398ae6219c..ede48b1c888 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -40,7 +40,10 @@ describe API::Settings, 'Settings' do plantuml_url: 'http://plantuml.example.com', default_snippet_visibility: 'internal', restricted_visibility_levels: ['public'], - default_artifacts_expire_in: '2 days' + default_artifacts_expire_in: '2 days', + help_page_text: 'custom help text', + help_page_hide_commercial_content: true, + help_page_support_url: 'http://example.com/help' expect(response).to have_http_status(200) expect(json_response['default_projects_limit']).to eq(3) expect(json_response['signin_enabled']).to be_falsey @@ -53,6 +56,9 @@ describe API::Settings, 'Settings' do expect(json_response['default_snippet_visibility']).to eq('internal') expect(json_response['restricted_visibility_levels']).to eq(['public']) expect(json_response['default_artifacts_expire_in']).to eq('2 days') + expect(json_response['help_page_text']).to eq('custom help text') + expect(json_response['help_page_hide_commercial_content']).to be_truthy + expect(json_response['help_page_support_url']).to eq('http://example.com/help') end end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index e429cddcf6a..8741cbd4e80 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -80,11 +80,33 @@ describe API::Snippets do end end + describe 'GET /snippets/:id' do + let(:snippet) { create(:personal_snippet, author: user) } + + it 'returns snippet json' do + get api("/snippets/#{snippet.id}", user) + + expect(response).to have_http_status(200) + + expect(json_response['title']).to eq(snippet.title) + expect(json_response['description']).to eq(snippet.description) + expect(json_response['file_name']).to eq(snippet.file_name) + end + + it 'returns 404 for invalid snippet id' do + get api("/snippets/1234", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Not found') + end + end + describe 'POST /snippets/' do let(:params) do { title: 'Test Title', file_name: 'test.rb', + description: 'test description', content: 'puts "hello world"', visibility: 'public' } @@ -97,6 +119,7 @@ describe API::Snippets do expect(response).to have_http_status(201) expect(json_response['title']).to eq(params[:title]) + expect(json_response['description']).to eq(params[:description]) expect(json_response['file_name']).to eq(params[:file_name]) end @@ -150,12 +173,14 @@ describe API::Snippets do it 'updates snippet' do new_content = 'New content' + new_description = 'New description' - put api("/snippets/#{snippet.id}", user), content: new_content + put api("/snippets/#{snippet.id}", user), content: new_content, description: new_description expect(response).to have_http_status(200) snippet.reload expect(snippet.content).to eq(new_content) + expect(snippet.description).to eq(new_description) end it 'returns 404 for invalid snippet id' do diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index 2eb191d6049..f65b475fe44 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -5,7 +5,9 @@ describe API::SystemHooks do let(:admin) { create(:admin) } let!(:hook) { create(:system_hook, url: "http://example.com") } - before { stub_request(:post, hook.url) } + before do + stub_request(:post, hook.url) + end describe "GET /hooks" do context "when no user" do diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb index cb55985e3f5..f8af9295842 100644 --- a/spec/requests/api/templates_spec.rb +++ b/spec/requests/api/templates_spec.rb @@ -2,14 +2,18 @@ require 'spec_helper' describe API::Templates do context 'the Template Entity' do - before { get api('/templates/gitignores/Ruby') } + before do + get api('/templates/gitignores/Ruby') + end it { expect(json_response['name']).to eq('Ruby') } it { expect(json_response['content']).to include('*.gem') } end context 'the TemplateList Entity' do - before { get api('/templates/gitignores') } + before do + get api('/templates/gitignores') + end it { expect(json_response.first['name']).not_to be_nil } it { expect(json_response.first['content']).to be_nil } @@ -47,7 +51,9 @@ describe API::Templates do end context 'the License Template Entity' do - before { get api('/templates/licenses/mit') } + before do + get api('/templates/licenses/mit') + end it 'returns a license template' do expect(json_response['key']).to eq('mit') diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index ec51b96c86b..9dc4b6972a6 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -160,7 +160,9 @@ describe API::Users do end describe "POST /users" do - before { admin } + before do + admin + end it "creates user" do expect do @@ -349,7 +351,9 @@ describe API::Users do describe "PUT /users/:id" do let!(:admin_user) { create(:admin) } - before { admin } + before do + admin + end it "updates user with new bio" do put api("/users/#{user.id}", admin), { bio: 'new test bio' } @@ -502,7 +506,9 @@ describe API::Users do end describe "POST /users/:id/keys" do - before { admin } + before do + admin + end it "does not create invalid ssh key" do post api("/users/#{user.id}/keys", admin), { title: "invalid key" } @@ -532,7 +538,9 @@ describe API::Users do end describe 'GET /user/:id/keys' do - before { admin } + before do + admin + end context 'when unauthenticated' do it 'returns authentication error' do @@ -563,7 +571,9 @@ describe API::Users do end describe 'DELETE /user/:id/keys/:key_id' do - before { admin } + before do + admin + end context 'when unauthenticated' do it 'returns authentication error' do @@ -601,7 +611,9 @@ describe API::Users do end describe "POST /users/:id/emails" do - before { admin } + before do + admin + end it "does not create invalid email" do post api("/users/#{user.id}/emails", admin), {} @@ -625,7 +637,9 @@ describe API::Users do end describe 'GET /user/:id/emails' do - before { admin } + before do + admin + end context 'when unauthenticated' do it 'returns authentication error' do @@ -662,7 +676,9 @@ describe API::Users do end describe 'DELETE /user/:id/emails/:email_id' do - before { admin } + before do + admin + end context 'when unauthenticated' do it 'returns authentication error' do @@ -708,7 +724,10 @@ describe API::Users do describe "DELETE /users/:id" do let!(:namespace) { user.namespace } let!(:issue) { create(:issue, author: user) } - before { admin } + + before do + admin + end it "deletes user" do Sidekiq::Testing.inline! { delete api("/users/#{user.id}", admin) } @@ -1068,7 +1087,10 @@ describe API::Users do end describe 'POST /users/:id/block' do - before { admin } + before do + admin + end + it 'blocks existing user' do post api("/users/#{user.id}/block", admin) expect(response).to have_http_status(201) @@ -1096,7 +1118,10 @@ describe API::Users do describe 'POST /users/:id/unblock' do let(:blocked_user) { create(:user, state: 'blocked') } - before { admin } + + before do + admin + end it 'unblocks existing user' do post api("/users/#{user.id}/unblock", admin) diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 286de277ae7..83c675792f4 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -137,6 +137,18 @@ describe Ci::API::Builds do end end end + + context 'when docker configuration options are used' do + let!(:build) { create(:ci_build, :extended_options, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + + it 'starts a build' do + register_builds info: { platform: :darwin } + + expect(response).to have_http_status(201) + expect(json_response['options']['image']).to eq('ruby:2.1') + expect(json_response['options']['services']).to eq(['postgres', 'docker:dind']) + end + end end context 'when builds are finished' do @@ -229,7 +241,9 @@ describe Ci::API::Builds do end context 'when runner is allowed to pick untagged builds' do - before { runner.update_column(:run_untagged, true) } + before do + runner.update_column(:run_untagged, true) + end it 'picks build' do register_builds @@ -455,7 +469,9 @@ describe Ci::API::Builds do let(:token) { build.token } let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => token) } - before { build.run! } + before do + build.run! + end describe "POST /builds/:id/artifacts/authorize" do context "authorizes posting artifact to running build" do @@ -511,7 +527,9 @@ describe Ci::API::Builds do end context 'authorization token is invalid' do - before { post authorize_url, { token: 'invalid', filesize: 100 } } + before do + post authorize_url, { token: 'invalid', filesize: 100 } + end it 'responds with forbidden' do expect(response).to have_http_status(403) diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb index 0b9733221d8..78b2be350cd 100644 --- a/spec/requests/ci/api/runners_spec.rb +++ b/spec/requests/ci/api/runners_spec.rb @@ -12,7 +12,9 @@ describe Ci::API::Runners do describe "POST /runners/register" do context 'when runner token is provided' do - before { post ci_api("/runners/register"), token: registration_token } + before do + post ci_api("/runners/register"), token: registration_token + end it 'creates runner with default values' do expect(response).to have_http_status 201 @@ -69,7 +71,10 @@ describe Ci::API::Runners do context 'when project token is provided' do let(:project) { FactoryGirl.create(:empty_project) } - before { post ci_api("/runners/register"), token: project.runners_token } + + before do + post ci_api("/runners/register"), token: project.runners_token + end it 'creates runner' do expect(response).to have_http_status 201 diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index f018b48ceb2..dce78faefc9 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -418,17 +418,17 @@ describe 'Git HTTP requests', lib: true do end context 'when username and password are provided' do - it 'rejects pulls with 2FA error message' do + it 'rejects pulls with personal access token error message' do download(path, user: user.username, password: user.password) do |response| expect(response).to have_http_status(:unauthorized) - expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP') + expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP') end end - it 'rejects the push attempt' do + it 'rejects the push attempt with personal access token error message' do upload(path, user: user.username, password: user.password) do |response| expect(response).to have_http_status(:unauthorized) - expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP') + expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP') end end end @@ -441,6 +441,41 @@ describe 'Git HTTP requests', lib: true do end end + context 'when internal auth is disabled' do + before do + allow_any_instance_of(ApplicationSetting).to receive(:signin_enabled?) { false } + end + + it 'rejects pulls with personal access token error message' do + download(path, user: 'foo', password: 'bar') do |response| + expect(response).to have_http_status(:unauthorized) + expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP') + end + end + + it 'rejects pushes with personal access token error message' do + upload(path, user: 'foo', password: 'bar') do |response| + expect(response).to have_http_status(:unauthorized) + expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP') + end + end + + context 'when LDAP is configured' do + before do + allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) + allow_any_instance_of(Gitlab::LDAP::Authentication). + to receive(:login).and_return(nil) + end + + it 'does not display the personal access token error message' do + upload(path, user: 'foo', password: 'bar') do |response| + expect(response).to have_http_status(:unauthorized) + expect(response.body).not_to include('You must use a personal access token with \'api\' scope for Git over HTTP') + end + end + end + end + context "when blank password attempts follow a valid login" do def attempt_login(include_password) password = include_password ? user.password : "" @@ -592,7 +627,9 @@ describe 'Git HTTP requests', lib: true do let(:path) { "/#{project.path_with_namespace}/info/refs" } context "when no params are added" do - before { get path } + before do + get path + end it "redirects to the .git suffix version" do expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs") @@ -601,7 +638,10 @@ describe 'Git HTTP requests', lib: true do context "when the upload-pack service is requested" do let(:params) { { service: 'git-upload-pack' } } - before { get path, params } + + before do + get path, params + end it "redirects to the .git suffix version" do expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}") @@ -610,7 +650,10 @@ describe 'Git HTTP requests', lib: true do context "when the receive-pack service is requested" do let(:params) { { service: 'git-receive-pack' } } - before { get path, params } + + before do + get path, params + end it "redirects to the .git suffix version" do expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}") @@ -619,7 +662,10 @@ describe 'Git HTTP requests', lib: true do context "when the params are anything else" do let(:params) { { service: 'git-implode-pack' } } - before { get path, params } + + before do + get path, params + end it "redirects to the sign-in page" do expect(response).to redirect_to(new_user_session_path) @@ -648,7 +694,7 @@ describe 'Git HTTP requests', lib: true do # Provide a dummy file in its place allow_any_instance_of(Repository).to receive(:blob_at).and_call_original allow_any_instance_of(Repository).to receive(:blob_at).with('b83d6e391c22777fca1ed3012fce84f633d7fed0', 'info/refs') do - Gitlab::Git::Blob.find(project.repository, 'master', 'bar/branch-test.txt') + Blob.decorate(Gitlab::Git::Blob.find(project.repository, 'master', 'bar/branch-test.txt'), project) end get "/#{project.path_with_namespace}/blob/master/info/refs" @@ -660,7 +706,9 @@ describe 'Git HTTP requests', lib: true do end context "when the file does not exist" do - before { get "/#{project.path_with_namespace}/blob/master/info/refs" } + before do + get "/#{project.path_with_namespace}/blob/master/info/refs" + end it "returns not found" do expect(response).to have_http_status(:not_found) diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index a3e7844b2f3..5e4cf05748e 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -6,7 +6,9 @@ describe JwtController do let(:service_name) { 'test' } let(:parameters) { { service: service_name } } - before { stub_const('JwtController::SERVICES', service_name => service_class) } + before do + stub_const('JwtController::SERVICES', service_name => service_class) + end context 'existing service' do subject! { get '/jwt/auth', parameters } @@ -41,6 +43,19 @@ describe JwtController do it { expect(response).to have_http_status(401) } end + + context 'using personal access tokens' do + let(:user) { create(:user) } + let(:pat) { create(:personal_access_token, user: user, scopes: ['read_registry']) } + let(:headers) { { authorization: credentials('personal_access_token', pat.token) } } + + subject! { get '/jwt/auth', parameters, headers } + + it 'authenticates correctly' do + expect(response).to have_http_status(200) + expect(service_class).to have_received(:new).with(nil, user, parameters) + end + end end context 'using User login' do @@ -57,7 +72,7 @@ describe JwtController do context 'without personal token' do it 'rejects the authorization attempt' do expect(response).to have_http_status(401) - expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP') + expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP') end end @@ -75,9 +90,24 @@ describe JwtController do context 'using invalid login' do let(:headers) { { authorization: credentials('invalid', 'password') } } - subject! { get '/jwt/auth', parameters, headers } + context 'when internal auth is enabled' do + it 'rejects the authorization attempt' do + get '/jwt/auth', parameters, headers + + expect(response).to have_http_status(401) + expect(response.body).not_to include('You must use a personal access token with \'api\' scope for Git over HTTP') + end + end - it { expect(response).to have_http_status(401) } + context 'when internal auth is disabled' do + it 'rejects the authorization attempt with personal access token message' do + allow_any_instance_of(ApplicationSetting).to receive(:signin_enabled?) { false } + get '/jwt/auth', parameters, headers + + expect(response).to have_http_status(401) + expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP') + end + end end end diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb index 05176c3beaa..6d1f0b24196 100644 --- a/spec/requests/openid_connect_spec.rb +++ b/spec/requests/openid_connect_spec.rb @@ -79,7 +79,7 @@ describe 'OpenID Connect requests' do 'email_verified' => true, 'website' => 'https://example.com', 'profile' => 'http://localhost/alice', - 'picture' => "http://localhost/uploads/user/avatar/#{user.id}/dk.png" + 'picture' => "http://localhost/uploads/system/user/avatar/#{user.id}/dk.png" }) end end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 54417f6b3e1..95d40138fea 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -93,13 +93,17 @@ describe 'project routing' do end context 'name with dot' do - before { allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq.keys', any_args).and_return(true) } + before do + allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq.keys', any_args).and_return(true) + end 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_by_full_path).with('gitlab/subgroup/gitlabhq', any_args).and_return(true) } + before do + allow(Project).to receive(:find_by_full_path).with('gitlab/subgroup/gitlabhq', any_args).and_return(true) + end it { expect(get('/gitlab/subgroup/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlabhq') } end @@ -201,10 +205,12 @@ describe 'project routing' do # POST /:project_id/deploy_keys(.:format) deploy_keys#create # new_project_deploy_key GET /:project_id/deploy_keys/new(.:format) deploy_keys#new # project_deploy_key GET /:project_id/deploy_keys/:id(.:format) deploy_keys#show + # edit_project_deploy_key GET /:project_id/deploy_keys/:id/edit(.:format) deploy_keys#edit + # project_deploy_key PATCH /:project_id/deploy_keys/:id(.:format) deploy_keys#update # DELETE /:project_id/deploy_keys/:id(.:format) deploy_keys#destroy describe Projects::DeployKeysController, 'routing' do it_behaves_like 'RESTful project resources' do - let(:actions) { [:index, :new, :create] } + let(:actions) { [:index, :new, :create, :edit, :update] } let(:controller) { 'deploy_keys' } end end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index a62af13cf0c..a45839b16f5 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -286,7 +286,9 @@ end describe "Groups", "routing" do let(:name) { 'complex.group-namegit' } - before { allow_any_instance_of(GroupUrlConstrainer).to receive(:matches?).and_return(true) } + before do + allow_any_instance_of(GroupUrlConstrainer).to receive(:matches?).and_return(true) + end it "to #show" do expect(get("/groups/#{name}")).to route_to('groups#show', id: name) diff --git a/spec/rubocop/cop/activerecord_serialize_spec.rb b/spec/rubocop/cop/activerecord_serialize_spec.rb index a303b16d264..5bd7e5fa926 100644 --- a/spec/rubocop/cop/activerecord_serialize_spec.rb +++ b/spec/rubocop/cop/activerecord_serialize_spec.rb @@ -10,7 +10,7 @@ describe RuboCop::Cop::ActiverecordSerialize do context 'inside the app/models directory' do it 'registers an offense when serialize is used' do - allow(cop).to receive(:in_models?).and_return(true) + allow(cop).to receive(:in_model?).and_return(true) inspect_source(cop, 'serialize :foo') @@ -23,7 +23,7 @@ describe RuboCop::Cop::ActiverecordSerialize do context 'outside the app/models directory' do it 'does nothing' do - allow(cop).to receive(:in_models?).and_return(false) + allow(cop).to receive(:in_model?).and_return(false) inspect_source(cop, 'serialize :foo') diff --git a/spec/rubocop/cop/migration/add_timestamps_spec.rb b/spec/rubocop/cop/migration/add_timestamps_spec.rb new file mode 100644 index 00000000000..18df62dec3e --- /dev/null +++ b/spec/rubocop/cop/migration/add_timestamps_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../../rubocop/cop/migration/add_timestamps' + +describe RuboCop::Cop::Migration::AddTimestamps do + include CopHelper + + subject(:cop) { described_class.new } + let(:migration_with_add_timestamps) do + %q( + class Users < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column(:users, :username, :text) + add_timestamps(:users) + end + end + ) + end + + let(:migration_without_add_timestamps) do + %q( + class Users < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column(:users, :username, :text) + end + end + ) + end + + let(:migration_with_add_timestamps_with_timezone) do + %q( + class Users < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column(:users, :username, :text) + add_timestamps_with_timezone(:users) + end + end + ) + end + + context 'in migration' do + before do + allow(cop).to receive(:in_migration?).and_return(true) + end + + it 'registers an offense when the "add_timestamps" method is used' do + inspect_source(cop, migration_with_add_timestamps) + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([7]) + end + end + + it 'does not register an offense when the "add_timestamps" method is not used' do + inspect_source(cop, migration_without_add_timestamps) + + aggregate_failures do + expect(cop.offenses.size).to eq(0) + end + end + + it 'does not register an offense when the "add_timestamps_with_timezone" method is used' do + inspect_source(cop, migration_with_add_timestamps_with_timezone) + + aggregate_failures do + expect(cop.offenses.size).to eq(0) + end + end + end + + context 'outside of migration' do + it 'registers no offense' do + inspect_source(cop, migration_with_add_timestamps) + inspect_source(cop, migration_without_add_timestamps) + inspect_source(cop, migration_with_add_timestamps_with_timezone) + + expect(cop.offenses.size).to eq(0) + end + end +end diff --git a/spec/rubocop/cop/migration/datetime_spec.rb b/spec/rubocop/cop/migration/datetime_spec.rb new file mode 100644 index 00000000000..388b086ce6a --- /dev/null +++ b/spec/rubocop/cop/migration/datetime_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../../rubocop/cop/migration/datetime' + +describe RuboCop::Cop::Migration::Datetime do + include CopHelper + + subject(:cop) { described_class.new } + let(:migration_with_datetime) do + %q( + class Users < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column(:users, :username, :text) + add_column(:users, :last_sign_in, :datetime) + end + end + ) + end + + let(:migration_without_datetime) do + %q( + class Users < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column(:users, :username, :text) + end + end + ) + end + + let(:migration_with_datetime_with_timezone) do + %q( + class Users < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column(:users, :username, :text) + add_column(:users, :last_sign_in, :datetime_with_timezone) + end + end + ) + end + + context 'in migration' do + before do + allow(cop).to receive(:in_migration?).and_return(true) + end + + it 'registers an offense when the ":datetime" data type is used' do + inspect_source(cop, migration_with_datetime) + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([7]) + end + end + + it 'does not register an offense when the ":datetime" data type is not used' do + inspect_source(cop, migration_without_datetime) + + aggregate_failures do + expect(cop.offenses.size).to eq(0) + end + end + + it 'does not register an offense when the ":datetime_with_timezone" data type is used' do + inspect_source(cop, migration_with_datetime_with_timezone) + + aggregate_failures do + expect(cop.offenses.size).to eq(0) + end + end + end + + context 'outside of migration' do + it 'registers no offense' do + inspect_source(cop, migration_with_datetime) + inspect_source(cop, migration_without_datetime) + inspect_source(cop, migration_with_datetime_with_timezone) + + expect(cop.offenses.size).to eq(0) + end + end +end diff --git a/spec/rubocop/cop/migration/timestamps_spec.rb b/spec/rubocop/cop/migration/timestamps_spec.rb new file mode 100644 index 00000000000..cdf1423d0c5 --- /dev/null +++ b/spec/rubocop/cop/migration/timestamps_spec.rb @@ -0,0 +1,99 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../../rubocop/cop/migration/timestamps' + +describe RuboCop::Cop::Migration::Timestamps do + include CopHelper + + subject(:cop) { described_class.new } + let(:migration_with_timestamps) do + %q( + class Users < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :users do |t| + t.string :username, null: false + t.timestamps null: true + t.string :password + end + end + end + ) + end + + let(:migration_without_timestamps) do + %q( + class Users < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :users do |t| + t.string :username, null: false + t.string :password + end + end + end + ) + end + + let(:migration_with_timestamps_with_timezone) do + %q( + class Users < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :users do |t| + t.string :username, null: false + t.timestamps_with_timezone null: true + t.string :password + end + end + end + ) + end + + context 'in migration' do + before do + allow(cop).to receive(:in_migration?).and_return(true) + end + + it 'registers an offense when the "timestamps" method is used' do + inspect_source(cop, migration_with_timestamps) + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([8]) + end + end + + it 'does not register an offense when the "timestamps" method is not used' do + inspect_source(cop, migration_without_timestamps) + + aggregate_failures do + expect(cop.offenses.size).to eq(0) + end + end + + it 'does not register an offense when the "timestamps_with_timezone" method is used' do + inspect_source(cop, migration_with_timestamps_with_timezone) + + aggregate_failures do + expect(cop.offenses.size).to eq(0) + end + end + end + + context 'outside of migration' do + it 'registers no offense' do + inspect_source(cop, migration_with_timestamps) + inspect_source(cop, migration_without_timestamps) + inspect_source(cop, migration_with_timestamps_with_timezone) + + expect(cop.offenses.size).to eq(0) + end + end +end diff --git a/spec/rubocop/cop/polymorphic_associations_spec.rb b/spec/rubocop/cop/polymorphic_associations_spec.rb new file mode 100644 index 00000000000..49959aa6419 --- /dev/null +++ b/spec/rubocop/cop/polymorphic_associations_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../rubocop/cop/polymorphic_associations' + +describe RuboCop::Cop::PolymorphicAssociations do + include CopHelper + + subject(:cop) { described_class.new } + + context 'inside the app/models directory' do + it 'registers an offense when polymorphic: true is used' do + allow(cop).to receive(:in_model?).and_return(true) + + inspect_source(cop, 'belongs_to :foo, polymorphic: true') + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + end + end + end + + context 'outside the app/models directory' do + it 'does nothing' do + allow(cop).to receive(:in_model?).and_return(false) + + inspect_source(cop, 'belongs_to :foo, polymorphic: true') + + expect(cop.offenses).to be_empty + end + end +end diff --git a/spec/rubocop/cop/rspec/single_line_hook_spec.rb b/spec/rubocop/cop/rspec/single_line_hook_spec.rb new file mode 100644 index 00000000000..6cf0831d3ad --- /dev/null +++ b/spec/rubocop/cop/rspec/single_line_hook_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../../rubocop/cop/rspec/single_line_hook' + +describe RuboCop::Cop::RSpec::SingleLineHook do + include CopHelper + + subject(:cop) { described_class.new } + + # Override `CopHelper#inspect_source` to always appear to be in a spec file, + # so that our RSpec-only cop actually runs + def inspect_source(*args) + super(*args, 'foo_spec.rb') + end + + it 'registers an offense for a single-line `before` block' do + inspect_source(cop, 'before { do_something }') + + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + expect(cop.highlights).to eq(['before { do_something }']) + end + + it 'registers an offense for a single-line `after` block' do + inspect_source(cop, 'after(:each) { undo_something }') + + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + expect(cop.highlights).to eq(['after(:each) { undo_something }']) + end + + it 'registers an offense for a single-line `around` block' do + inspect_source(cop, 'around { |ex| do_something_else }') + + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + expect(cop.highlights).to eq(['around { |ex| do_something_else }']) + end + + it 'ignores a multi-line `before` block' do + inspect_source(cop, ['before do', + ' do_something', + 'end']) + + expect(cop.offenses.size).to eq(0) + end + + it 'ignores a multi-line `after` block' do + inspect_source(cop, ['after(:each) do', + ' undo_something', + 'end']) + + expect(cop.offenses.size).to eq(0) + end + + it 'ignores a multi-line `around` block' do + inspect_source(cop, ['around do |ex|', + ' do_something_else', + 'end']) + + expect(cop.offenses.size).to eq(0) + end +end diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb index e2511e8968c..b92c1c28ba8 100644 --- a/spec/serializers/build_details_entity_spec.rb +++ b/spec/serializers/build_details_entity_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe BuildDetailsEntity do set(:user) { create(:admin) } - it 'inherits from BuildEntity' do - expect(described_class).to be < BuildEntity + it 'inherits from JobEntity' do + expect(described_class).to be < JobEntity end describe '#as_json' do @@ -29,7 +29,7 @@ describe BuildDetailsEntity do it 'contains the needed key value pairs' do expect(subject).to include(:coverage, :erased_at, :duration) - expect(subject).to include(:artifacts, :runner, :pipeline) + expect(subject).to include(:runner, :pipeline) expect(subject).to include(:raw_path, :merge_request) expect(subject).to include(:new_issue_path) end diff --git a/spec/serializers/deploy_key_entity_spec.rb b/spec/serializers/deploy_key_entity_spec.rb index e73fbe190ca..ed89fccc3d0 100644 --- a/spec/serializers/deploy_key_entity_spec.rb +++ b/spec/serializers/deploy_key_entity_spec.rb @@ -12,27 +12,44 @@ describe DeployKeyEntity do let(:entity) { described_class.new(deploy_key, user: user) } - it 'returns deploy keys with projects a user can read' do - expected_result = { - id: deploy_key.id, - user_id: deploy_key.user_id, - title: deploy_key.title, - fingerprint: deploy_key.fingerprint, - can_push: deploy_key.can_push, - destroyed_when_orphaned: true, - almost_orphaned: false, - created_at: deploy_key.created_at, - updated_at: deploy_key.updated_at, - projects: [ - { - id: project.id, - name: project.name, - full_path: namespace_project_path(project.namespace, project), - full_name: project.full_name - } - ] - } - - expect(entity.as_json).to eq(expected_result) + describe 'returns deploy keys with projects a user can read' do + let(:expected_result) do + { + id: deploy_key.id, + user_id: deploy_key.user_id, + title: deploy_key.title, + fingerprint: deploy_key.fingerprint, + can_push: deploy_key.can_push, + destroyed_when_orphaned: true, + almost_orphaned: false, + created_at: deploy_key.created_at, + updated_at: deploy_key.updated_at, + can_edit: false, + projects: [ + { + id: project.id, + name: project.name, + full_path: namespace_project_path(project.namespace, project), + full_name: project.full_name + } + ] + } + end + + it { expect(entity.as_json).to eq(expected_result) } + end + + describe 'returns can_edit true if user is a master of project' do + before do + project.add_master(user) + end + + it { expect(entity.as_json).to include(can_edit: true) } + end + + describe 'returns can_edit true if a user admin' do + let(:user) { create(:user, :admin) } + + it { expect(entity.as_json).to include(can_edit: true) } end end diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb index d2ad6c44702..4c52a00b442 100644 --- a/spec/serializers/environment_serializer_spec.rb +++ b/spec/serializers/environment_serializer_spec.rb @@ -62,7 +62,9 @@ describe EnvironmentSerializer do subject { serializer.represent(resource) } context 'when there is a single environment' do - before { create(:environment, name: 'staging') } + before do + create(:environment, name: 'staging') + end it 'represents one standalone environment' do expect(subject.count).to eq 1 @@ -138,7 +140,9 @@ describe EnvironmentSerializer do context 'when resource is paginatable relation' do context 'when there is a single environment object in relation' do - before { create(:environment) } + before do + create(:environment) + end it 'serializes environments' do expect(subject.first).to have_key :id @@ -146,7 +150,9 @@ describe EnvironmentSerializer do end context 'when multiple environment objects are serialized' do - before { create_list(:environment, 3) } + before do + create_list(:environment, 3) + end it 'serializes appropriate number of objects' do expect(subject.count).to be 2 diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/job_entity_spec.rb index 46d43a80ef7..5ca7bf2fcaf 100644 --- a/spec/serializers/build_entity_spec.rb +++ b/spec/serializers/job_entity_spec.rb @@ -1,24 +1,24 @@ require 'spec_helper' -describe BuildEntity do +describe JobEntity do let(:user) { create(:user) } - let(:build) { create(:ci_build, :failed) } - let(:project) { build.project } + let(:job) { create(:ci_build) } + let(:project) { job.project } let(:request) { double('request') } before do allow(request).to receive(:current_user).and_return(user) + project.add_developer(user) end let(:entity) do - described_class.new(build, request: request) + described_class.new(job, request: request) end subject { entity.as_json } - it 'contains paths to build page and retry action' do - expect(subject).to include(:build_path, :retry_path) - expect(subject[:retry_path]).not_to be_nil + it 'contains paths to job page action' do + expect(subject).to include(:build_path) end it 'does not contain sensitive information' do @@ -27,7 +27,7 @@ describe BuildEntity do end it 'contains whether it is playable' do - expect(subject[:playable]).to eq build.playable? + expect(subject[:playable]).to eq job.playable? end it 'contains timestamps' do @@ -39,18 +39,38 @@ describe BuildEntity do expect(subject[:status]).to include :icon, :favicon, :text, :label end - context 'when build is a regular job' do + context 'when job is retryable' do + before do + job.update(status: :failed) + end + + it 'contains cancel path' do + expect(subject).to include(:retry_path) + end + end + + context 'when job is cancelable' do + before do + job.update(status: :running) + end + + it 'contains cancel path' do + expect(subject).to include(:cancel_path) + end + end + + context 'when job is a regular job' do it 'does not contain path to play action' do expect(subject).not_to include(:play_path) end - it 'is not a playable job' do + it 'is not a playable build' do expect(subject[:playable]).to be false end end - context 'when build is a manual action' do - let(:build) { create(:ci_build, :manual) } + context 'when job is a manual action' do + let(:job) { create(:ci_build, :manual) } context 'when user is allowed to trigger action' do before do @@ -79,4 +99,25 @@ describe BuildEntity do end end end + + context 'when job is generic commit status' do + let(:job) { create(:generic_commit_status, target_url: 'http://google.com') } + + it 'contains paths to target action' do + expect(subject).to include(:build_path) + end + + it 'does not contain paths to other action paths' do + expect(subject).not_to include(:retry_path, :cancel_path, :play_path) + end + + it 'contains timestamps' do + expect(subject).to include(:created_at, :updated_at) + end + + it 'contains details' do + expect(subject).to include :status + expect(subject[:status]).to include :icon, :favicon, :text, :label + end + end end diff --git a/spec/serializers/pipeline_details_entity_spec.rb b/spec/serializers/pipeline_details_entity_spec.rb index 03cc5ae9b63..d28dec9592a 100644 --- a/spec/serializers/pipeline_details_entity_spec.rb +++ b/spec/serializers/pipeline_details_entity_spec.rb @@ -51,7 +51,9 @@ describe PipelineDetailsEntity do end context 'user has ability to retry pipeline' do - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end it 'retryable flag is true' do expect(subject[:flags][:retryable]).to eq true @@ -77,7 +79,9 @@ describe PipelineDetailsEntity do end context 'user has ability to cancel pipeline' do - before { project.add_developer(user) } + before do + project.add_developer(user) + end it 'cancelable flag is true' do expect(subject[:flags][:cancelable]).to eq true @@ -91,6 +95,20 @@ describe PipelineDetailsEntity do end end + context 'when pipeline has commit statuses' do + let(:pipeline) { create(:ci_empty_pipeline) } + + before do + create(:generic_commit_status, pipeline: pipeline) + end + + it 'contains stages' do + expect(subject).to include(:details) + expect(subject[:details]).to include(:stages) + expect(subject[:details][:stages].first).to include(name: 'external') + end + end + context 'when pipeline has YAML errors' do let(:pipeline) do create(:ci_pipeline, config: { rspec: { invalid: :value } }) diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb index a059c2cc736..46650f3a80d 100644 --- a/spec/serializers/pipeline_entity_spec.rb +++ b/spec/serializers/pipeline_entity_spec.rb @@ -51,7 +51,9 @@ describe PipelineEntity do end context 'user has ability to retry pipeline' do - before { project.team << [user, :developer] } + before do + project.team << [user, :developer] + end it 'contains retry path' do expect(subject[:retry_path]).to be_present @@ -77,7 +79,9 @@ describe PipelineEntity do end context 'user has ability to cancel pipeline' do - before { project.add_developer(user) } + before do + project.add_developer(user) + end it 'contains cancel path' do expect(subject[:cancel_path]).to be_present diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index 088f24eb180..44813656aff 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -69,7 +69,9 @@ describe PipelineSerializer do let(:pagination) { { page: 1, per_page: 2 } } context 'when a single pipeline object is present in relation' do - before { create(:ci_empty_pipeline) } + before do + create(:ci_empty_pipeline) + end it 'serializes pipeline relation' do expect(subject.first).to have_key :id @@ -77,7 +79,9 @@ describe PipelineSerializer do end context 'when a multiple pipeline objects are being serialized' do - before { create_list(:ci_empty_pipeline, 3) } + before do + create_list(:ci_empty_pipeline, 3) + end it 'serializes appropriate number of objects' do expect(subject.count).to be 2 @@ -102,18 +106,11 @@ describe PipelineSerializer do Ci::Pipeline::AVAILABLE_STATUSES.each do |status| create_pipeline(status) end - - RequestStore.begin! - end - - after do - RequestStore.end! - RequestStore.clear! end - it "verifies number of queries" do + it 'verifies number of queries', :request_store do recorded = ActiveRecord::QueryRecorder.new { subject } - expect(recorded.count).to be_within(1).of(60) + expect(recorded.count).to be_within(1).of(57) expect(recorded.cached_count).to eq(0) end diff --git a/spec/serializers/stage_entity_spec.rb b/spec/serializers/stage_entity_spec.rb index 64b3217b809..40e303f7b89 100644 --- a/spec/serializers/stage_entity_spec.rb +++ b/spec/serializers/stage_entity_spec.rb @@ -54,6 +54,17 @@ describe StageEntity do it 'exposes the group key' do expect(subject).to include :groups end + + context 'and contains commit status' do + before do + create(:generic_commit_status, pipeline: pipeline, stage: 'test') + end + + it 'contains commit status' do + groups = subject[:groups].map { |group| group[:name] } + expect(groups).to include('generic') + end + end end end end diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index e273dfe1552..60cb7a9440f 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -34,7 +34,9 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'for changed configuration' do - before { stub_application_setting(container_registry_token_expire_delay: expire_delay) } + before do + stub_application_setting(container_registry_token_expire_delay: expire_delay) + end it { expect(expires_at).to be_within(2.seconds).of(Time.now + expire_delay.minutes) } end @@ -117,7 +119,9 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'allow developer to push images' do - before { project.team << [current_user, :developer] } + before do + project.team << [current_user, :developer] + end let(:current_params) do { scope: "repository:#{project.path_with_namespace}:push" } @@ -128,7 +132,9 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'allow reporter to pull images' do - before { project.team << [current_user, :reporter] } + before do + project.team << [current_user, :reporter] + end context 'when pulling from root level repository' do let(:current_params) do @@ -141,7 +147,9 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'return a least of privileges' do - before { project.team << [current_user, :reporter] } + before do + project.team << [current_user, :reporter] + end let(:current_params) do { scope: "repository:#{project.path_with_namespace}:push,pull" } @@ -152,7 +160,9 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'disallow guest to pull or push images' do - before { project.team << [current_user, :guest] } + before do + project.team << [current_user, :guest] + end let(:current_params) do { scope: "repository:#{project.path_with_namespace}:pull,push" } @@ -355,7 +365,9 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do context 'for project without container registry' do let(:project) { create(:empty_project, :public, container_registry_enabled: false) } - before { project.update(container_registry_enabled: false) } + before do + project.update(container_registry_enabled: false) + end context 'disallow when pulling' do let(:current_params) do diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb index a8555f5b4a0..effa4633d13 100644 --- a/spec/services/boards/create_service_spec.rb +++ b/spec/services/boards/create_service_spec.rb @@ -14,8 +14,9 @@ describe Boards::CreateService, services: true do it 'creates the default lists' do board = service.execute - expect(board.lists.size).to eq 1 - expect(board.lists.first).to be_closed + expect(board.lists.size).to eq 2 + expect(board.lists.first).to be_backlog + expect(board.lists.last).to be_closed end end diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index c982031c791..a1e220c2322 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -13,6 +13,7 @@ describe Boards::Issues::ListService, services: true do let(:p2) { create(:label, title: 'P2', project: project, priority: 2) } let(:p3) { create(:label, title: 'P3', project: project, priority: 3) } + let!(:backlog) { create(:backlog_list, board: board) } let!(:list1) { create(:list, board: board, label: development, position: 0) } let!(:list2) { create(:list, board: board, label: testing, position: 1) } let!(:closed) { create(:closed_list, board: board) } @@ -53,12 +54,20 @@ describe Boards::Issues::ListService, services: true do expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1] end + it 'returns opened issues when listing issues from Backlog' do + params = { board_id: board.id, id: backlog.id } + + issues = described_class.new(project, user, params).execute + + expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1] + end + it 'returns closed issues when listing issues from Closed' do params = { board_id: board.id, id: closed.id } issues = described_class.new(project, user, params).execute - expect(issues).to eq [closed_issue4, closed_issue2, closed_issue5, closed_issue3, closed_issue1] + expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1] end it 'returns opened issues that have label list applied when listing issues from a label list' do diff --git a/spec/services/boards/lists/list_service_spec.rb b/spec/services/boards/lists/list_service_spec.rb index ab9fb1bc914..68140759600 100644 --- a/spec/services/boards/lists/list_service_spec.rb +++ b/spec/services/boards/lists/list_service_spec.rb @@ -1,16 +1,33 @@ require 'spec_helper' describe Boards::Lists::ListService, services: true do + let(:project) { create(:empty_project) } + let(:board) { create(:board, project: project) } + let(:label) { create(:label, project: project) } + let!(:list) { create(:list, board: board, label: label) } + let(:service) { described_class.new(project, double) } + describe '#execute' do - it "returns board's lists" do - project = create(:empty_project) - board = create(:board, project: project) - label = create(:label, project: project) - list = create(:list, board: board, label: label) + context 'when the board has a backlog list' do + let!(:backlog_list) { create(:backlog_list, board: board) } + + it 'does not create a backlog list' do + expect { service.execute(board) }.not_to change(board.lists, :count) + end + + it "returns board's lists" do + expect(service.execute(board)).to eq [backlog_list, list, board.closed_list] + end + end - service = described_class.new(project, double) + context 'when the board does not have a backlog list' do + it 'creates a backlog list' do + expect { service.execute(board) }.to change(board.lists, :count).by(1) + end - expect(service.execute(board)).to eq [list, board.closed_list] + it "returns board's lists" do + expect(service.execute(board)).to eq [board.backlog_list, list, board.closed_list] + end end end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 597c3947e71..77c07b71c68 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::CreatePipelineService, services: true do +describe Ci::CreatePipelineService, :services do let(:project) { create(:project, :repository) } let(:user) { create(:admin) } @@ -30,6 +30,7 @@ describe Ci::CreatePipelineService, services: true do it 'creates a pipeline' do expect(pipeline).to be_kind_of(Ci::Pipeline) expect(pipeline).to be_valid + expect(pipeline).to be_persisted expect(pipeline).to be_push expect(pipeline).to eq(project.pipelines.last) expect(pipeline).to have_attributes(user: user) @@ -37,6 +38,14 @@ describe Ci::CreatePipelineService, services: true do expect(pipeline.builds.first).to be_kind_of(Ci::Build) end + it 'increments the prometheus counter' do + expect(Gitlab::Metrics).to receive(:counter) + .with(:pipelines_created_count, "Pipelines created count") + .and_call_original + + pipeline + end + context 'when merge requests already exist for this source branch' do it 'updates head pipeline of each merge request' do merge_request_1 = create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project) diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index 7254e6b357a..ef9927c5969 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -25,12 +25,24 @@ describe Ci::RetryBuildService, :services do user_id auto_canceled_by_id retried].freeze shared_examples 'build duplication' do + let(:stage) do + # TODO, we still do not have factory for new stages, we will need to + # switch existing factory to persist stages, instead of using LegacyStage + # + Ci::Stage.create!(project: project, pipeline: pipeline, name: 'test') + end + let(:build) do create(:ci_build, :failed, :artifacts_expired, :erased, :queued, :coverage, :tags, :allowed_to_fail, :on_tag, - :teardown_environment, :triggered, :trace, - description: 'some build', pipeline: pipeline, - auto_canceled_by: create(:ci_empty_pipeline)) + :triggered, :trace, :teardown_environment, + description: 'my-job', stage: 'test', pipeline: pipeline, + auto_canceled_by: create(:ci_empty_pipeline)) do |build| + ## + # TODO, workaround for FactoryGirl limitation when having both + # stage (text) and stage_id (integer) columns in the table. + build.stage_id = stage.id + end end describe 'clone accessors' do diff --git a/spec/services/ci/update_build_queue_service_spec.rb b/spec/services/ci/update_build_queue_service_spec.rb index c44e6b2a48b..efefa8e8eca 100644 --- a/spec/services/ci/update_build_queue_service_spec.rb +++ b/spec/services/ci/update_build_queue_service_spec.rb @@ -9,7 +9,9 @@ describe Ci::UpdateBuildQueueService, :services do let(:runner) { create(:ci_runner) } context 'when there are runner that can pick build' do - before { build.project.runners << runner } + before do + build.project.runners << runner + end it 'ticks runner queue value' do expect { subject.execute(build) } @@ -36,7 +38,9 @@ describe Ci::UpdateBuildQueueService, :services do end context 'when there are no runners that can pick build' do - before { build.tag_list = [:docker] } + before do + build.tag_list = [:docker] + end it 'does not tick runner queue value' do expect { subject.execute(build) } diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb index 5398b5c3f7e..6cf4342ad4c 100644 --- a/spec/services/create_deployment_service_spec.rb +++ b/spec/services/create_deployment_service_spec.rb @@ -204,7 +204,9 @@ describe CreateDeploymentService, services: true do let(:merge_request) { create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: project) } context "while updating the 'first_deployed_to_production_at' time" do - before { merge_request.mark_as_merged } + before do + merge_request.mark_as_merged + end context "for merge requests merged before the current deploy" do it "sets the time if the deploy's environment is 'production'" do diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index bcb62429275..fbd9026640c 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -14,7 +14,9 @@ describe Groups::CreateService, '#execute', services: true do end context "cannot create group with restricted visibility level" do - before { allow_any_instance_of(ApplicationSetting).to receive(:restricted_visibility_levels).and_return([Gitlab::VisibilityLevel::PUBLIC]) } + before do + allow_any_instance_of(ApplicationSetting).to receive(:restricted_visibility_levels).and_return([Gitlab::VisibilityLevel::PUBLIC]) + end it { is_expected.not_to be_persisted } end @@ -25,7 +27,9 @@ describe Groups::CreateService, '#execute', services: true do let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) } context 'as group owner' do - before { group.add_owner(user) } + before do + group.add_owner(user) + end it { is_expected.to be_persisted } end diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index 6437d00e451..eb9b1670c71 100644 --- a/spec/services/issuable/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -72,7 +72,7 @@ describe Issuable::BulkUpdateService, services: true do end context "when the new assignee ID is #{IssuableFinder::NONE}" do - it "unassigns the issues" do + it 'unassigns the issues' do expect { bulk_update(merge_request, assignee_id: IssuableFinder::NONE) } .to change { merge_request.reload.assignee }.to(nil) end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index dab1a3469f7..370bd352200 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -155,7 +155,9 @@ describe Issues::CreateService, services: true do context 'issue create service' do context 'assignees' do - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it 'removes assignee when user id is invalid' do opts = { title: 'Title', description: 'Description', assignee_ids: [-1] } diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 9f8346d52bb..d1dd1466d95 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -251,12 +251,18 @@ describe Issues::MoveService, services: true do end context 'user is reporter only in new project' do - before { new_project.team << [user, :reporter] } + before do + new_project.team << [user, :reporter] + end + it { expect { move }.to raise_error(StandardError, /permissions/) } end context 'user is reporter only in old project' do - before { old_project.team << [user, :reporter] } + before do + old_project.team << [user, :reporter] + end + it { expect { move }.to raise_error(StandardError, /permissions/) } end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 5184c1d5f19..c26642f5015 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -31,6 +31,13 @@ describe Issues::UpdateService, services: true do end end + def find_notes(action) + issue + .notes + .joins(:system_note_metadata) + .where(system_note_metadata: { action: action }) + end + def update_issue(opts) described_class.new(project, user, opts).execute(issue) end @@ -288,7 +295,9 @@ describe Issues::UpdateService, services: true do end context 'when issue has the `label` label' do - before { issue.labels << label } + before do + issue.labels << label + end it 'does not send notifications for existing labels' do opts = { label_ids: [label.id, label2.id] } @@ -322,7 +331,9 @@ describe Issues::UpdateService, services: true do it { expect(issue.tasks?).to eq(true) } context 'when tasks are marked as completed' do - before { update_issue(description: "- [x] Task 1\n- [X] Task 2") } + before do + update_issue(description: "- [x] Task 1\n- [X] Task 2") + end it 'creates system note about task status change' do note1 = find_note('marked the task **Task 1** as completed') @@ -330,6 +341,9 @@ describe Issues::UpdateService, services: true do expect(note1).not_to be_nil expect(note2).not_to be_nil + + description_notes = find_notes('description') + expect(description_notes.length).to eq(1) end end @@ -345,6 +359,9 @@ describe Issues::UpdateService, services: true do expect(note1).not_to be_nil expect(note2).not_to be_nil + + description_notes = find_notes('description') + expect(description_notes.length).to eq(1) end end @@ -354,10 +371,12 @@ describe Issues::UpdateService, services: true do update_issue(description: "- [x] Task 1\n- [ ] Task 3\n- [ ] Task 2") end - it 'does not create a system note' do - note = find_note('marked the task **Task 2** as incomplete') + it 'does not create a system note for the task' do + task_note = find_note('marked the task **Task 2** as incomplete') + description_notes = find_notes('description') - expect(note).to be_nil + expect(task_note).to be_nil + expect(description_notes.length).to eq(2) end end @@ -368,9 +387,11 @@ describe Issues::UpdateService, services: true do end it 'does not create a system note referencing the position the old item' do - note = find_note('marked the task **Two** as incomplete') + task_note = find_note('marked the task **Two** as incomplete') + description_notes = find_notes('description') - expect(note).to be_nil + expect(task_note).to be_nil + expect(description_notes.length).to eq(2) end it 'does not generate a new note at all' do @@ -400,7 +421,9 @@ describe Issues::UpdateService, services: true do context 'when remove_label_ids and label_ids are passed' do let(:params) { { label_ids: [], remove_label_ids: [label.id] } } - before { issue.update_attributes(labels: [label, label3]) } + before do + issue.update_attributes(labels: [label, label3]) + end it 'ignores the label_ids parameter' do expect(result.label_ids).not_to be_empty @@ -414,7 +437,9 @@ describe Issues::UpdateService, services: true do context 'when add_label_ids and remove_label_ids are passed' do let(:params) { { add_label_ids: [label3.id], remove_label_ids: [label.id] } } - before { issue.update_attributes(labels: [label]) } + before do + issue.update_attributes(labels: [label]) + end it 'adds the passed labels' do expect(result.label_ids).to include(label3.id) diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb index 5ce8e17976b..5a05ab3ea50 100644 --- a/spec/services/members/create_service_spec.rb +++ b/spec/services/members/create_service_spec.rb @@ -5,7 +5,9 @@ describe Members::CreateService, services: true do let(:user) { create(:user) } let(:project_user) { create(:user) } - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it 'adds user to members' do params = { user_ids: project_user.id.to_s, access_level: Gitlab::Access::GUEST } diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 6f9d1208b1d..01ef52396d7 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -206,7 +206,9 @@ describe MergeRequests::BuildService, services: true do context 'branch starts with external issue IID followed by a hyphen' do let(:source_branch) { '12345-fix-issue' } - before { allow(project).to receive(:default_issues_tracker?).and_return(false) } + before do + allow(project).to receive(:default_issues_tracker?).and_return(false) + end it 'sets the title to: Resolves External Issue $issue-iid' do expect(merge_request.title).to eq('Resolve External Issue 12345') diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 2963f62cc7d..13fee953e41 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -150,7 +150,9 @@ describe MergeRequests::CreateService, services: true do context 'asssignee_id' do let(:assignee) { create(:user) } - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it 'removes assignee_id when user id is invalid' do opts = { title: 'Title', description: 'Description', assignee_id: -1 } diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb index 935f4710851..bb46e1dd9ab 100644 --- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb +++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb @@ -10,8 +10,8 @@ describe MergeRequests::MergeRequestDiffCacheService do expect(Rails.cache).to receive(:read).with(cache_key).and_return({}) expect(Rails.cache).to receive(:write).with(cache_key, anything) - allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(double("text?" => true)) - allow_any_instance_of(Repository).to receive(:diffable?).and_return(true) + allow_any_instance_of(Gitlab::Diff::File).to receive(:text?).and_return(true) + allow_any_instance_of(Gitlab::Diff::File).to receive(:diffable?).and_return(true) subject.execute(merge_request) end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index d96f819e66a..b3b188a805f 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -81,7 +81,9 @@ describe MergeRequests::MergeService, services: true do end context "when jira_issue_transition_id is not present" do - before { allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(nil) } + before do + allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(nil) + end it "does not close issue" do allow(jira_tracker).to receive_messages(jira_issue_transition_id: nil) diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index d371fc68312..fd46020bbdb 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -30,6 +30,13 @@ describe MergeRequests::UpdateService, services: true do end end + def find_notes(action) + @merge_request + .notes + .joins(:system_note_metadata) + .where(system_note_metadata: { action: action }) + end + def update_merge_request(opts) @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) @merge_request.reload @@ -349,7 +356,9 @@ describe MergeRequests::UpdateService, services: true do end context 'when issue has the `label` label' do - before { merge_request.labels << label } + before do + merge_request.labels << label + end it 'does not send notifications for existing labels' do opts = { label_ids: [label.id, label2.id] } @@ -381,12 +390,16 @@ describe MergeRequests::UpdateService, services: true do end context 'when MergeRequest has tasks' do - before { update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) } + before do + update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) + end it { expect(@merge_request.tasks?).to eq(true) } context 'when tasks are marked as completed' do - before { update_merge_request({ description: "- [x] Task 1\n- [X] Task 2" }) } + before do + update_merge_request({ description: "- [x] Task 1\n- [X] Task 2" }) + end it 'creates system note about task status change' do note1 = find_note('marked the task **Task 1** as completed') @@ -394,6 +407,9 @@ describe MergeRequests::UpdateService, services: true do expect(note1).not_to be_nil expect(note2).not_to be_nil + + description_notes = find_notes('description') + expect(description_notes.length).to eq(1) end end @@ -409,6 +425,9 @@ describe MergeRequests::UpdateService, services: true do expect(note1).not_to be_nil expect(note2).not_to be_nil + + description_notes = find_notes('description') + expect(description_notes.length).to eq(1) end end end diff --git a/spec/services/notes/slash_commands_service_spec.rb b/spec/services/notes/slash_commands_service_spec.rb index c9954dc3603..d5ffc1908a9 100644 --- a/spec/services/notes/slash_commands_service_spec.rb +++ b/spec/services/notes/slash_commands_service_spec.rb @@ -6,7 +6,9 @@ describe Notes::SlashCommandsService, services: true do let(:master) { create(:user).tap { |u| project.team << [u, :master] } } let(:assignee) { create(:user) } - before { project.team << [assignee, :master] } + before do + project.team << [assignee, :master] + end end shared_examples 'note on noteable that does not support slash commands' do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index de3bbc6b6a1..f1e00c1163b 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -1539,8 +1539,7 @@ describe NotificationService, services: true do # When resource is nil it means global notification def update_custom_notification(event, user, resource: nil, value: true) setting = user.notification_settings_for(resource) - setting.events[event] = value - setting.save + setting.update!(event => value) end def add_users_with_subscription(project, issuable) diff --git a/spec/services/pages_service_spec.rb b/spec/services/pages_service_spec.rb index aa63fe3a5c1..cf38c7c75e5 100644 --- a/spec/services/pages_service_spec.rb +++ b/spec/services/pages_service_spec.rb @@ -10,10 +10,14 @@ describe PagesService, services: true do end context 'execute asynchronously for pages job' do - before { build.name = 'pages' } + before do + build.name = 'pages' + end context 'on success' do - before { build.success } + before do + build.success + end it 'executes worker' do expect(PagesWorker).to receive(:perform_async) @@ -23,7 +27,9 @@ describe PagesService, services: true do %w(pending running failed canceled).each do |status| context "on #{status}" do - before { build.status = status } + before do + build.status = status + end it 'does not execute worker' do expect(PagesWorker).not_to receive(:perform_async) diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 3c566c04d6b..40298dcb723 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -115,7 +115,7 @@ describe Projects::CreateService, '#execute', services: true do stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) opts.merge!( - visibility_level: Gitlab::VisibilityLevel.options['Public'] + visibility_level: Gitlab::VisibilityLevel::PUBLIC ) end diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb index 0657b7e93fe..d75851134ee 100644 --- a/spec/services/projects/participants_service_spec.rb +++ b/spec/services/projects/participants_service_spec.rb @@ -13,7 +13,7 @@ describe Projects::ParticipantsService, services: true do groups = participants.groups expect(groups.size).to eq 1 - expect(groups.first[:avatar_url]).to eq("/uploads/group/avatar/#{group.id}/dk.png") + expect(groups.first[:avatar_url]).to eq("/uploads/system/group/avatar/#{group.id}/dk.png") end it 'should return an url for the avatar with relative url' do @@ -24,7 +24,7 @@ describe Projects::ParticipantsService, services: true do groups = participants.groups expect(groups.size).to eq 1 - expect(groups.first[:avatar_url]).to eq("/gitlab/uploads/group/avatar/#{group.id}/dk.png") + expect(groups.first[:avatar_url]).to eq("/gitlab/uploads/system/group/avatar/#{group.id}/dk.png") end end end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index b957517c715..5d2f4cf17fb 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -59,12 +59,16 @@ describe Projects::TransferService, services: true do context 'visibility level' do let(:internal_group) { create(:group, :internal) } - before { internal_group.add_owner(user) } + before do + internal_group.add_owner(user) + end context 'when namespace visibility level < project visibility level' do let(:public_project) { create(:project, :public, :repository, namespace: user.namespace) } - before { transfer_project(public_project, user, internal_group) } + before do + transfer_project(public_project, user, internal_group) + end it { expect(public_project.visibility_level).to eq(internal_group.visibility_level) } end @@ -72,7 +76,9 @@ describe Projects::TransferService, services: true do context 'when namespace visibility level > project visibility level' do let(:private_project) { create(:project, :private, :repository, namespace: user.namespace) } - before { transfer_project(private_project, user, internal_group) } + before do + transfer_project(private_project, user, internal_group) + end it { expect(private_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) } end diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb index e5e400ee281..c12fb1a6e53 100644 --- a/spec/services/slash_commands/interpret_service_spec.rb +++ b/spec/services/slash_commands/interpret_service_spec.rb @@ -378,13 +378,15 @@ describe SlashCommands::InterpretService, services: true do context 'assign command with multiple assignees' do let(:content) { "/assign @#{developer.username} @#{developer2.username}" } - before{ project.team << [developer2, :developer] } + before do + project.team << [developer2, :developer] + end context 'Issue' do it 'fetches assignee and populates assignee_id if content contains /assign' do _, updates = service.execute(content, issue) - expect(updates[:assignee_ids]).to match_array([developer.id, developer2.id]) + expect(updates[:assignee_ids]).to match_array([developer.id]) end end @@ -798,7 +800,11 @@ describe SlashCommands::InterpretService, services: true do context 'if the project has multiple boards' do let(:issuable) { issue } - before { create(:board, project: project) } + + before do + create(:board, project: project) + end + it_behaves_like 'empty command' end diff --git a/spec/services/spam_service_spec.rb b/spec/services/spam_service_spec.rb index 74cba8c014b..5e6e43b7a90 100644 --- a/spec/services/spam_service_spec.rb +++ b/spec/services/spam_service_spec.rb @@ -70,7 +70,9 @@ describe SpamService, services: true do end context 'when not indicated as spam by akismet' do - before { allow(AkismetService).to receive(:new).and_return(double(is_spam?: false)) } + before do + allow(AkismetService).to receive(:new).and_return(double(is_spam?: false)) + end it 'returns false' do expect(check_spam(issue, request, false)).to be_falsey diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index c499b1bb343..9295c09aefc 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -1052,7 +1052,7 @@ describe SystemNoteService, services: true do let(:action) { 'task' } end - it "posts the 'marked as a Work In Progress from commit' system note" do + it "posts the 'marked the task as complete' system note" do expect(subject.note).to eq("marked the task **task** as completed") end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 994c7dcbb46..01ac3cbd3f6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,6 +3,7 @@ SimpleCovEnv.start! ENV["RAILS_ENV"] ||= 'test' ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true' +# ENV['prometheus_multiproc_dir'] = 'tmp/prometheus_multiproc_dir_test' require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' @@ -55,6 +56,8 @@ RSpec.configure do |config| config.include StubGitlabCalls config.include StubGitlabData config.include ApiHelpers, :api + config.include Rails.application.routes.url_helpers, type: :routing + config.include MigrationsHelpers, :migration config.infer_spec_type_from_file_location! @@ -72,10 +75,18 @@ RSpec.configure do |config| TestEnv.cleanup end + config.before(:example, :request_store) do + RequestStore.begin! + end + + config.after(:example, :request_store) do + RequestStore.end! + RequestStore.clear! + end + if ENV['CI'] - # Retry only on feature specs that use JS - config.around :each, :js do |ex| - ex.run_with_retry retry: 3 + config.around(:each) do |ex| + ex.run_with_retry retry: 2 end end @@ -96,6 +107,15 @@ RSpec.configure do |config| Sidekiq.redis(&:flushall) end + config.before(:example, :migration) do + ActiveRecord::Migrator + .migrate(migrations_paths, previous_migration.version) + end + + config.after(:example, :migration) do + ActiveRecord::Migrator.migrate(migrations_paths) + end + config.around(:each, :nested_groups) do |example| example.run if Group.supports_nested_groups? end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index b8ca8f22a3d..c34e76fa72f 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -14,8 +14,10 @@ Capybara.register_driver :poltergeist do |app| js_errors: true, timeout: timeout, window_size: [1366, 768], + url_whitelist: %w[localhost 127.0.0.1], + url_blacklist: %w[.mp4 .png .gif .avi .bmp .jpg .jpeg], phantomjs_options: [ - '--load-images=no' + '--load-images=yes' ] ) end diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index 6f31828b825..7f5769209bb 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -19,6 +19,10 @@ RSpec.configure do |config| DatabaseCleaner.strategy = :truncation end + config.before(:each, :migration) do + DatabaseCleaner.strategy = :truncation + end + config.before(:each) do DatabaseCleaner.start end diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb new file mode 100644 index 00000000000..0d80c95e826 --- /dev/null +++ b/spec/support/features/reportable_note_shared_examples.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +shared_examples 'reportable note' do + include NotesHelper + + let(:comment) { find("##{ActionView::RecordIdentifier.dom_id(note)}") } + let(:more_actions_selector) { '.more-actions.dropdown' } + let(:abuse_report_path) { new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) } + + it 'has a `More actions` dropdown' do + expect(comment).to have_selector(more_actions_selector) + end + + it 'dropdown has Edit, Report and Delete links' do + dropdown = comment.find(more_actions_selector) + + dropdown.click + dropdown.find('.dropdown-menu li', match: :first) + + expect(dropdown).to have_button('Edit comment') + expect(dropdown).to have_link('Report as abuse', href: abuse_report_path) + expect(dropdown).to have_link('Delete comment', href: note_url(note, project)) + end + + it 'Report button links to a report page' do + dropdown = comment.find(more_actions_selector) + + dropdown.click + dropdown.find('.dropdown-menu li', match: :first) + + dropdown.click_link('Report as abuse') + + expect(find('#user_name')['value']).to match(note.author.username) + expect(find('#abuse_report_message')['value']).to match(noteable_note_url(note)) + end +end diff --git a/spec/support/helpers/note_interaction_helpers.rb b/spec/support/helpers/note_interaction_helpers.rb new file mode 100644 index 00000000000..551c759133c --- /dev/null +++ b/spec/support/helpers/note_interaction_helpers.rb @@ -0,0 +1,8 @@ +module NoteInteractionHelpers + def open_more_actions_dropdown(note) + note_element = find("#note_#{note.id}") + + note_element.find('.more-actions').click + note_element.find('.more-actions .dropdown-menu li', match: :first) + end +end diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb index a982b159b48..aace4b3adee 100644 --- a/spec/support/javascript_fixtures_helpers.rb +++ b/spec/support/javascript_fixtures_helpers.rb @@ -48,7 +48,7 @@ module JavaScriptFixturesHelpers link_tags = doc.css('link') link_tags.remove - scripts = doc.css("script:not([type='text/template'])") + scripts = doc.css("script:not([type='text/template']):not([type='text/x-template'])") scripts.remove fixture = doc.to_html diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb index 9280fad4ace..c92f78b324c 100644 --- a/spec/support/kubernetes_helpers.rb +++ b/spec/support/kubernetes_helpers.rb @@ -1,7 +1,26 @@ module KubernetesHelpers include Gitlab::Kubernetes - def kube_discovery_body + def kube_response(body) + { body: body.to_json } + end + + def kube_pods_response + kube_response(kube_pods_body) + end + + def stub_kubeclient_discover + WebMock.stub_request(:get, service.api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body)) + end + + def stub_kubeclient_pods(response = nil) + stub_kubeclient_discover + pods_url = service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods" + + WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response) + end + + def kube_v1_discovery_body { "kind" => "APIResourceList", "resources" => [ @@ -10,17 +29,19 @@ module KubernetesHelpers } end - def kube_pods_body(*pods) - { "kind" => "PodList", - "items" => [kube_pod] } + def kube_pods_body + { + "kind" => "PodList", + "items" => [kube_pod] + } end # This is a partial response, it will have many more elements in reality but # these are the ones we care about at the moment - def kube_pod(app: "valid-pod-label") + def kube_pod(name: "kube-pod", app: "valid-pod-label") { "metadata" => { - "name" => "kube-pod", + "name" => name, "creationTimestamp" => "2016-11-25T19:55:19Z", "labels" => { "app" => app } }, diff --git a/spec/support/matchers/gitaly_matchers.rb b/spec/support/matchers/gitaly_matchers.rb index ed14bcec9f2..ebfabcd8f24 100644 --- a/spec/support/matchers/gitaly_matchers.rb +++ b/spec/support/matchers/gitaly_matchers.rb @@ -1,5 +1,10 @@ -RSpec::Matchers.define :gitaly_request_with_repo_path do |path| - match { |actual| actual.repository.path == path } +RSpec::Matchers.define :gitaly_request_with_path do |storage_name, relative_path| + match do |actual| + repository = actual.repository + + repository.storage_name == storage_name && + repository.relative_path == relative_path + end end RSpec::Matchers.define :gitaly_request_with_params do |params| diff --git a/spec/support/migrations_helpers.rb b/spec/support/migrations_helpers.rb new file mode 100644 index 00000000000..91fbb4eaf48 --- /dev/null +++ b/spec/support/migrations_helpers.rb @@ -0,0 +1,29 @@ +module MigrationsHelpers + def table(name) + Class.new(ActiveRecord::Base) { self.table_name = name } + end + + def migrations_paths + ActiveRecord::Migrator.migrations_paths + end + + def table_exists?(name) + ActiveRecord::Base.connection.table_exists?(name) + end + + def migrations + ActiveRecord::Migrator.migrations(migrations_paths) + end + + def previous_migration + migrations.each_cons(2) do |previous, migration| + break previous if migration.name == described_class.name + end + end + + def migrate! + ActiveRecord::Migrator.up(migrations_paths) do |migration| + migration.name == described_class.name + end + end +end diff --git a/spec/support/milestone_tabs_examples.rb b/spec/support/milestone_tabs_examples.rb index 4ad8b0a16e1..70b499198bf 100644 --- a/spec/support/milestone_tabs_examples.rb +++ b/spec/support/milestone_tabs_examples.rb @@ -1,17 +1,23 @@ shared_examples 'milestone tabs' do def go(path, extra_params = {}) - params = if milestone.is_a?(GlobalMilestone) - { group_id: group.to_param, id: milestone.safe_title, title: milestone.title } - else - { namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid } - end + params = + case milestone + when DashboardMilestone + { id: milestone.safe_title, title: milestone.title } + when GroupMilestone + { group_id: group.to_param, id: milestone.safe_title, title: milestone.title } + else + { namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid } + end get path, params.merge(extra_params) end describe '#merge_requests' do context 'as html' do - before { go(:merge_requests, format: 'html') } + before do + go(:merge_requests, format: 'html') + end it 'redirects to milestone#show' do expect(response).to redirect_to(milestone_path) @@ -19,7 +25,9 @@ shared_examples 'milestone tabs' do end context 'as json' do - before { go(:merge_requests, format: 'json') } + before do + go(:merge_requests, format: 'json') + end it 'renders the merge requests tab template to a string' do expect(response).to render_template('shared/milestones/_merge_requests_tab') @@ -30,7 +38,9 @@ shared_examples 'milestone tabs' do describe '#participants' do context 'as html' do - before { go(:participants, format: 'html') } + before do + go(:participants, format: 'html') + end it 'redirects to milestone#show' do expect(response).to redirect_to(milestone_path) @@ -38,7 +48,9 @@ shared_examples 'milestone tabs' do end context 'as json' do - before { go(:participants, format: 'json') } + before do + go(:participants, format: 'json') + end it 'renders the participants tab template to a string' do expect(response).to render_template('shared/milestones/_participants_tab') @@ -49,7 +61,9 @@ shared_examples 'milestone tabs' do describe '#labels' do context 'as html' do - before { go(:labels, format: 'html') } + before do + go(:labels, format: 'html') + end it 'redirects to milestone#show' do expect(response).to redirect_to(milestone_path) @@ -57,7 +71,9 @@ shared_examples 'milestone tabs' do end context 'as json' do - before { go(:labels, format: 'json') } + before do + go(:labels, format: 'json') + end it 'renders the labels tab template to a string' do expect(response).to render_template('shared/milestones/_labels_tab') diff --git a/spec/support/reference_parser_shared_examples.rb b/spec/support/reference_parser_shared_examples.rb index 8eb74635a60..bd83cb88058 100644 --- a/spec/support/reference_parser_shared_examples.rb +++ b/spec/support/reference_parser_shared_examples.rb @@ -3,7 +3,9 @@ RSpec.shared_examples "referenced feature visibility" do |*related_features| related_features.map { |feature| (feature + "_access_level").to_sym } end - before { link['data-project'] = project.id.to_s } + before do + link['data-project'] = project.id.to_s + end context "when feature is disabled" do it "does not create reference" do @@ -13,7 +15,9 @@ RSpec.shared_examples "referenced feature visibility" do |*related_features| end context "when feature is enabled only for team members" do - before { set_features_fields_to(ProjectFeature::PRIVATE) } + before do + set_features_fields_to(ProjectFeature::PRIVATE) + end it "does not create reference for non member" do non_member = create(:user) diff --git a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb index 1dd3663b944..5e6f9f323a1 100644 --- a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb +++ b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb @@ -11,7 +11,9 @@ shared_examples 'new issuable record that supports slash commands' do let(:params) { base_params.merge(defined?(default_params) ? default_params : {}).merge(example_params) } let(:issuable) { described_class.new(project, user, params).execute } - before { project.team << [assignee, :master] } + before do + project.team << [assignee, :master] + end context 'with labels in command only' do let(:example_params) do diff --git a/spec/support/services/issuable_update_service_shared_examples.rb b/spec/support/services/issuable_update_service_shared_examples.rb index 8947f20562f..ffbce6c42bf 100644 --- a/spec/support/services/issuable_update_service_shared_examples.rb +++ b/spec/support/services/issuable_update_service_shared_examples.rb @@ -4,7 +4,9 @@ shared_examples 'issuable update service' do end context 'changing state' do - before { expect(project).to receive(:execute_hooks).once } + before do + expect(project).to receive(:execute_hooks).once + end context 'to reopened' do it 'executes hooks only once' do diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb index 7e35ebb6c97..a7deb038703 100644 --- a/spec/support/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/slack_mattermost_notifications_shared_examples.rb @@ -11,14 +11,18 @@ RSpec.shared_examples 'slack or mattermost notifications' do describe 'Validations' do context 'when service is active' do - before { subject.active = true } + before do + subject.active = true + end it { is_expected.to validate_presence_of(:webhook) } it_behaves_like 'issue tracker service URL attribute', :webhook end context 'when service is inactive' do - before { subject.active = false } + before do + subject.active = false + end it { is_expected.not_to validate_presence_of(:webhook) } end diff --git a/spec/support/target_branch_helpers.rb b/spec/support/target_branch_helpers.rb deleted file mode 100644 index 01d1c53fe6c..00000000000 --- a/spec/support/target_branch_helpers.rb +++ /dev/null @@ -1,16 +0,0 @@ -module TargetBranchHelpers - def select_branch(name) - first('button.js-target-branch').click - wait_for_requests - all('a[data-group="Branches"]').find do |el| - el.text == name - end.click - end - - def create_new_branch(name) - first('button.js-target-branch').click - click_link 'Create new branch' - fill_in 'new_branch_name', with: name - click_button 'Create' - end -end diff --git a/spec/support/unique_ip_check_shared_examples.rb b/spec/support/unique_ip_check_shared_examples.rb index 7cf5a65eeed..1986d202c4a 100644 --- a/spec/support/unique_ip_check_shared_examples.rb +++ b/spec/support/unique_ip_check_shared_examples.rb @@ -31,7 +31,9 @@ end shared_examples 'user login operation with unique ip limit' do include_context 'unique ips sign in limit' do - before { current_application_settings.update!(unique_ips_limit_per_user: 1) } + before do + current_application_settings.update!(unique_ips_limit_per_user: 1) + end it 'allows user authenticating from the same ip' do expect { operation_from_ip('ip') }.not_to raise_error @@ -47,7 +49,9 @@ end shared_examples 'user login request with unique ip limit' do |success_status = 200| include_context 'unique ips sign in limit' do - before { current_application_settings.update!(unique_ips_limit_per_user: 1) } + before do + current_application_settings.update!(unique_ips_limit_per_user: 1) + end it 'allows user authenticating from the same ip' do expect(request_from_ip('ip')).to have_http_status(success_status) diff --git a/spec/support/updating_mentions_shared_examples.rb b/spec/support/updating_mentions_shared_examples.rb index e0c59a5c280..eeec3e1d79b 100644 --- a/spec/support/updating_mentions_shared_examples.rb +++ b/spec/support/updating_mentions_shared_examples.rb @@ -2,7 +2,9 @@ RSpec.shared_examples 'updating mentions' do |service_class| let(:mentioned_user) { create(:user) } let(:service_class) { service_class } - before { project.team << [mentioned_user, :developer] } + before do + project.team << [mentioned_user, :developer] + end def update_mentionable(opts) reset_delivered_emails! @@ -15,7 +17,9 @@ RSpec.shared_examples 'updating mentions' do |service_class| end context 'in title' do - before { update_mentionable(title: mentioned_user.to_reference) } + before do + update_mentionable(title: mentioned_user.to_reference) + end it 'emails only the newly-mentioned user' do should_only_email(mentioned_user) @@ -23,7 +27,9 @@ RSpec.shared_examples 'updating mentions' do |service_class| end context 'in description' do - before { update_mentionable(description: mentioned_user.to_reference) } + before do + update_mentionable(description: mentioned_user.to_reference) + end it 'emails only the newly-mentioned user' do should_only_email(mentioned_user) diff --git a/spec/support/wait_for_requests.rb b/spec/support/wait_for_requests.rb index 05ec9026141..1cbe609c0e0 100644 --- a/spec/support/wait_for_requests.rb +++ b/spec/support/wait_for_requests.rb @@ -7,7 +7,7 @@ module WaitForRequests def block_and_wait_for_requests_complete Gitlab::Testing::RequestBlockerMiddleware.block_requests! wait_for('pending requests complete') do - Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero? + Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero? && finished_all_requests? end ensure Gitlab::Testing::RequestBlockerMiddleware.allow_requests! @@ -40,13 +40,13 @@ module WaitForRequests end def finished_all_vue_resource_requests? - page.evaluate_script('window.activeVueResources || 0').zero? + Capybara.page.evaluate_script('window.activeVueResources || 0').zero? end def finished_all_ajax_requests? - return true if page.evaluate_script('typeof jQuery === "undefined"') + return true if Capybara.page.evaluate_script('typeof jQuery === "undefined"') - page.evaluate_script('jQuery.active').zero? + Capybara.page.evaluate_script('jQuery.active').zero? end def javascript_test? diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 0ff1a988a9e..1e5f55a738a 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -241,6 +241,10 @@ describe 'gitlab:app namespace rake task' do project_a project_b + # Avoid asking gitaly about the root ref (which will fail beacuse of the + # mocked storages) + allow_any_instance_of(Repository).to receive(:empty_repo?).and_return(false) + # We only need a backup of the repositories for this test ENV["SKIP"] = "db,uploads,builds,artifacts,lfs,registry" create_backup diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index 4a636decafd..cfa6c9ca8ce 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -79,8 +79,14 @@ describe 'gitlab:gitaly namespace rake task' do describe 'storage_config' do it 'prints storage configuration in a TOML format' do config = { - 'default' => { 'path' => '/path/to/default' }, - 'nfs_01' => { 'path' => '/path/to/nfs_01' } + 'default' => { + 'path' => '/path/to/default', + 'gitaly_address' => 'unix:/path/to/my.socket' + }, + 'nfs_01' => { + 'path' => '/path/to/nfs_01', + 'gitaly_address' => 'unix:/path/to/my.socket' + } } allow(Gitlab.config.repositories).to receive(:storages).and_return(config) @@ -89,6 +95,7 @@ describe 'gitlab:gitaly namespace rake task' do expected_output = <<~TOML # Gitaly storage configuration generated from #{Gitlab.config.source} on #{Time.current.to_s(:long)} # This is in TOML format suitable for use in Gitaly's config.toml file. + socket_path = "/path/to/my.socket" [[storage]] name = "default" path = "/path/to/default" diff --git a/spec/unicorn/unicorn_spec.rb b/spec/unicorn/unicorn_spec.rb index 8518c047a47..41de94d35c2 100644 --- a/spec/unicorn/unicorn_spec.rb +++ b/spec/unicorn/unicorn_spec.rb @@ -67,8 +67,8 @@ describe 'Unicorn' do end def wait_unicorn_boot!(master_pid, ready_file) - # Unicorn should boot in under 60 seconds so 120 seconds seems like a good timeout. - timeout = 120 + # We have seen the boot timeout after 2 minutes in CI so let's set it to 5 minutes. + timeout = 5 * 60 timeout.times do return if File.exist?(ready_file) pid = Process.waitpid(master_pid, Process::WNOHANG) diff --git a/spec/uploaders/artifact_uploader_spec.rb b/spec/uploaders/artifact_uploader_spec.rb index 24e2e3a9f0e..2a3bd0e3bb2 100644 --- a/spec/uploaders/artifact_uploader_spec.rb +++ b/spec/uploaders/artifact_uploader_spec.rb @@ -17,22 +17,45 @@ describe ArtifactUploader do describe '.artifacts_upload_path' do subject { described_class.artifacts_upload_path } - + it { is_expected.to start_with(path) } it { is_expected.to end_with('tmp/uploads/') } end describe '#store_dir' do subject { uploader.store_dir } - + it { is_expected.to start_with(path) } it { is_expected.to end_with("#{job.project_id}/#{job.id}") } end describe '#cache_dir' do subject { uploader.cache_dir } - + + it { is_expected.to start_with(path) } + it { is_expected.to end_with('/tmp/cache') } + end + + describe '#work_dir' do + subject { uploader.work_dir } + it { is_expected.to start_with(path) } - it { is_expected.to end_with('tmp/cache') } + it { is_expected.to end_with('/tmp/work') } + end + + describe '#filename' do + # we need to use uploader, as this makes to use mounter + # which initialises uploader.file object + let(:uploader) { job.artifacts_file } + + subject { uploader.filename } + + it { is_expected.to be_nil } + + context 'with artifacts' do + let(:job) { create(:ci_build, :artifacts) } + + it { is_expected.not_to be_nil } + end end end diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb index ea714fb08f0..d82dbe871d5 100644 --- a/spec/uploaders/attachment_uploader_spec.rb +++ b/spec/uploaders/attachment_uploader_spec.rb @@ -3,6 +3,17 @@ require 'spec_helper' describe AttachmentUploader do let(:uploader) { described_class.new(build_stubbed(:user)) } + describe "#store_dir" do + it "stores in the system dir" do + expect(uploader.store_dir).to start_with("uploads/system/user") + end + + it "uses the old path when using object storage" do + expect(described_class).to receive(:file_storage?).and_return(false) + expect(uploader.store_dir).to start_with("uploads/user") + end + end + describe '#move_to_cache' do it 'is true' do expect(uploader.move_to_cache).to eq(true) diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb index c4d558805ab..201fe6949aa 100644 --- a/spec/uploaders/avatar_uploader_spec.rb +++ b/spec/uploaders/avatar_uploader_spec.rb @@ -3,6 +3,17 @@ require 'spec_helper' describe AvatarUploader do let(:uploader) { described_class.new(build_stubbed(:user)) } + describe "#store_dir" do + it "stores in the system dir" do + expect(uploader.store_dir).to start_with("uploads/system/user") + end + + it "uses the old path when using object storage" do + expect(described_class).to receive(:file_storage?).and_return(false) + expect(uploader.store_dir).to start_with("uploads/user") + end + end + describe '#move_to_cache' do it 'is false' do expect(uploader.move_to_cache).to eq(false) diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb new file mode 100644 index 00000000000..896cb410ed5 --- /dev/null +++ b/spec/uploaders/file_mover_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe FileMover do + let(:filename) { 'banana_sample.gif' } + let(:file) { fixture_file_upload(Rails.root.join('spec', 'fixtures', filename)) } + let(:temp_description) do + 'test ![banana_sample](/uploads/temp/secret55/banana_sample.gif) same ![banana_sample]'\ + '(/uploads/temp/secret55/banana_sample.gif)' + end + let(:temp_file_path) { File.join('secret55', filename).to_s } + let(:file_path) { File.join('uploads', 'personal_snippet', snippet.id.to_s, 'secret55', filename).to_s } + + let(:snippet) { create(:personal_snippet, description: temp_description) } + + subject { described_class.new(file_path, snippet).execute } + + describe '#execute' do + before do + expect(FileUtils).to receive(:mkdir_p).with(a_string_including(File.dirname(file_path))) + expect(FileUtils).to receive(:move).with(a_string_including(temp_file_path), a_string_including(file_path)) + allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:exists?).and_return(true) + allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:size).and_return(10) + end + + context 'when move and field update successful' do + it 'updates the description correctly' do + subject + + expect(snippet.reload.description) + .to eq( + "test ![banana_sample](/uploads/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)"\ + " same ![banana_sample](/uploads/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)" + ) + end + + it 'creates a new update record' do + expect { subject }.to change { Upload.count }.by(1) + end + end + + context 'when update_markdown fails' do + before do + expect(FileUtils).to receive(:move).with(a_string_including(file_path), a_string_including(temp_file_path)) + end + + subject { described_class.new(file_path, snippet, :non_existing_field).execute } + + it 'does not update the description' do + subject + + expect(snippet.reload.description) + .to eq( + "test ![banana_sample](/uploads/temp/secret55/banana_sample.gif)"\ + " same ![banana_sample](/uploads/temp/secret55/banana_sample.gif)" + ) + end + + it 'does not create a new update record' do + expect { subject }.not_to change { Upload.count } + end + end + end +end diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index d9113ef4095..47e9365e13d 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -15,6 +15,16 @@ describe FileUploader do end end + describe "#store_dir" do + it "stores in the namespace path" do + project = build_stubbed(:empty_project) + uploader = described_class.new(project) + + expect(uploader.store_dir).to include(project.path_with_namespace) + expect(uploader.store_dir).not_to include("system") + end + end + describe 'initialize' do it 'generates a secret if none is provided' do expect(SecureRandom).to receive(:hex).and_return('secret') diff --git a/spec/uploaders/gitlab_uploader_spec.rb b/spec/uploaders/gitlab_uploader_spec.rb index 78e9d9cf46c..a144b39f74f 100644 --- a/spec/uploaders/gitlab_uploader_spec.rb +++ b/spec/uploaders/gitlab_uploader_spec.rb @@ -53,4 +53,19 @@ describe GitlabUploader do expect(subject.move_to_store).to eq(true) end end + + describe '#cache!' do + it 'moves the file from the working directory to the cache directory' do + # One to get the work dir, the other to remove it + expect(subject).to receive(:workfile_path).exactly(2).times.and_call_original + # Test https://github.com/carrierwavesubject/carrierwave/blob/v1.0.0/lib/carrierwave/sanitized_file.rb#L200 + expect(FileUtils).to receive(:mv).with(anything, /^#{subject.work_dir}/).and_call_original + expect(FileUtils).to receive(:mv).with(/^#{subject.work_dir}/, /#{subject.cache_dir}/).and_call_original + + fixture = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg') + subject.cache!(fixture_file_upload(fixture)) + + expect(subject.file.path).to match(/#{subject.cache_dir}/) + end + end end diff --git a/spec/uploaders/lfs_object_uploader_spec.rb b/spec/uploaders/lfs_object_uploader_spec.rb index c3b72e7d677..7088bc23334 100644 --- a/spec/uploaders/lfs_object_uploader_spec.rb +++ b/spec/uploaders/lfs_object_uploader_spec.rb @@ -1,21 +1,9 @@ require 'spec_helper' describe LfsObjectUploader do - let(:uploader) { described_class.new(build_stubbed(:empty_project)) } - - describe '#cache!' do - it 'caches the file in the cache directory' do - # One to get the work dir, the other to remove it - expect(uploader).to receive(:workfile_path).exactly(2).times.and_call_original - expect(FileUtils).to receive(:mv).with(anything, /^#{uploader.work_dir}/).and_call_original - expect(FileUtils).to receive(:mv).with(/^#{uploader.work_dir}/, /^#{uploader.cache_dir}/).and_call_original - - fixture = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg') - uploader.cache!(fixture_file_upload(fixture)) - - expect(uploader.file.path).to start_with(uploader.cache_dir) - end - end + let(:lfs_object) { create(:lfs_object, :with_file) } + let(:uploader) { described_class.new(lfs_object) } + let(:path) { Gitlab.config.lfs.storage_path } describe '#move_to_cache' do it 'is true' do @@ -28,4 +16,25 @@ describe LfsObjectUploader do expect(uploader.move_to_store).to eq(true) end end + + describe '#store_dir' do + subject { uploader.store_dir } + + it { is_expected.to start_with(path) } + it { is_expected.to end_with("#{lfs_object.oid[0, 2]}/#{lfs_object.oid[2, 2]}") } + end + + describe '#cache_dir' do + subject { uploader.cache_dir } + + it { is_expected.to start_with(path) } + it { is_expected.to end_with('/tmp/cache') } + end + + describe '#work_dir' do + subject { uploader.work_dir } + + it { is_expected.to start_with(path) } + it { is_expected.to end_with('/tmp/work') } + end end diff --git a/spec/uploaders/records_uploads_spec.rb b/spec/uploaders/records_uploads_spec.rb index 5c26e334a6e..bb32ee62ccb 100644 --- a/spec/uploaders/records_uploads_spec.rb +++ b/spec/uploaders/records_uploads_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' describe RecordsUploads do - let(:uploader) do + let!(:uploader) do class RecordsUploadsExampleUploader < GitlabUploader include RecordsUploads @@ -57,6 +57,13 @@ describe RecordsUploads do uploader.store!(upload_fixture('rails_sample.jpg')) end + it 'does not create an Upload record if model is missing' do + expect_any_instance_of(RecordsUploadsExampleUploader).to receive(:model).and_return(nil) + expect(Upload).not_to receive(:record).with(uploader) + + uploader.store!(upload_fixture('rails_sample.jpg')) + end + it 'it destroys Upload records at the same path before recording' do existing = Upload.create!( path: File.join('uploads', 'rails_sample.jpg'), diff --git a/spec/views/help/index.html.haml_spec.rb b/spec/views/help/index.html.haml_spec.rb index 6b07fcfc987..1f8261cc46b 100644 --- a/spec/views/help/index.html.haml_spec.rb +++ b/spec/views/help/index.html.haml_spec.rb @@ -21,7 +21,7 @@ describe 'help/index' do render expect(rendered).to match '8.0.2' - expect(rendered).to match 'abcdefg' + expect(rendered).to have_link('abcdefg', 'https://gitlab.com/gitlab-org/gitlab-ce/commits/abcdefg') end end diff --git a/spec/views/projects/diffs/_viewer.html.haml_spec.rb b/spec/views/projects/diffs/_viewer.html.haml_spec.rb new file mode 100644 index 00000000000..32469202508 --- /dev/null +++ b/spec/views/projects/diffs/_viewer.html.haml_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' + +describe 'projects/diffs/_viewer.html.haml', :view do + include FakeBlobHelpers + + let(:project) { create(:project, :repository) } + let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') } + let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') } + + let(:viewer_class) do + Class.new(DiffViewer::Base) do + include DiffViewer::Rich + + self.partial_name = 'text' + end + end + + let(:viewer) { viewer_class.new(diff_file) } + + before do + assign(:project, project) + + controller.params[:controller] = 'projects/commit' + controller.params[:action] = 'show' + controller.params[:namespace_id] = project.namespace.to_param + controller.params[:project_id] = project.to_param + controller.params[:id] = commit.id + end + + def render_view + render partial: 'projects/diffs/viewer', locals: { viewer: viewer } + end + + context 'when there is a render error' do + before do + allow(viewer).to receive(:render_error).and_return(:too_large) + end + + it 'renders the error' do + render_view + + expect(view).to render_template('projects/diffs/_render_error') + end + end + + context 'when the viewer is collapsed' do + before do + allow(diff_file).to receive(:collapsed?).and_return(true) + end + + it 'renders the collapsed view' do + render_view + + expect(view).to render_template('projects/diffs/_collapsed') + end + end + + context 'when there is no render error' do + it 'prepares the viewer' do + expect(viewer).to receive(:prepare!) + + render_view + end + + it 'renders the viewer' do + render_view + + expect(view).to render_template('projects/diffs/viewers/_text') + end + end +end diff --git a/spec/views/projects/jobs/show.html.haml_spec.rb b/spec/views/projects/jobs/show.html.haml_spec.rb index 8f2822f5dc5..d9a7ba265f8 100644 --- a/spec/views/projects/jobs/show.html.haml_spec.rb +++ b/spec/views/projects/jobs/show.html.haml_spec.rb @@ -15,36 +15,6 @@ describe 'projects/jobs/show', :view do allow(view).to receive(:can?).and_return(true) end - describe 'job information in header' do - let(:build) do - create(:ci_build, :success, environment: 'staging') - end - - before do - render - end - - it 'shows status name' do - expect(rendered).to have_css('.ci-status.ci-success', text: 'passed') - end - - it 'does not render a link to the job' do - expect(rendered).not_to have_link('passed') - end - - it 'shows job id' do - expect(rendered).to have_css('.js-build-id', text: build.id) - end - - it 'shows a link to the pipeline' do - expect(rendered).to have_link(build.pipeline.id) - end - - it 'shows a link to the commit' do - expect(rendered).to have_link(build.pipeline.short_sha) - end - end - describe 'environment info in job view' do context 'job with latest deployment' do let(:build) do @@ -215,34 +185,6 @@ describe 'projects/jobs/show', :view do end end - context 'when job is not running' do - before do - build.success! - render - end - - it 'shows retry button' do - expect(rendered).to have_link('Retry') - end - - context 'if build passed' do - it 'does not show New issue button' do - expect(rendered).not_to have_link('New issue') - end - end - - context 'if build failed' do - before do - build.status = 'failed' - render - end - - it 'shows New issue button' do - expect(rendered).to have_link('New issue') - end - end - end - describe 'commit title in sidebar' do let(:commit_title) { project.commit.title } @@ -269,25 +211,4 @@ describe 'projects/jobs/show', :view do expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_2') end end - - describe 'New issue button' do - before do - build.status = 'failed' - render - end - - it 'links to issues/new with the title and description filled in' do - title = "Build Failed ##{build.id}" - build_url = namespace_project_job_url(project.namespace, project, build) - href = new_namespace_project_issue_path( - project.namespace, - project, - issue: { - title: title, - description: build_url - } - ) - expect(rendered).to have_link('New issue', href: href) - end - end end diff --git a/spec/workers/background_migration_worker_spec.rb b/spec/workers/background_migration_worker_spec.rb new file mode 100644 index 00000000000..0d742ae9dc7 --- /dev/null +++ b/spec/workers/background_migration_worker_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe BackgroundMigrationWorker do + describe '.perform' do + it 'performs a background migration' do + expect(Gitlab::BackgroundMigration). + to receive(:perform). + with('Foo', [10, 20]) + + described_class.new.perform('Foo', [10, 20]) + end + end +end diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb index a0ed85cc0b3..5b6b38e0f76 100644 --- a/spec/workers/emails_on_push_worker_spec.rb +++ b/spec/workers/emails_on_push_worker_spec.rb @@ -71,7 +71,9 @@ describe EmailsOnPushWorker do end context "when there are no errors in sending" do - before { perform } + before do + perform + end it "sends a mail with the correct subject" do expect(email.subject).to include('adds bar folder and branch-test text file') diff --git a/spec/workers/expire_build_artifacts_worker_spec.rb b/spec/workers/expire_build_artifacts_worker_spec.rb index 73cbadc13d9..b47b4a02a68 100644 --- a/spec/workers/expire_build_artifacts_worker_spec.rb +++ b/spec/workers/expire_build_artifacts_worker_spec.rb @@ -5,10 +5,14 @@ describe ExpireBuildArtifactsWorker do let(:worker) { described_class.new } - before { Sidekiq::Worker.clear_all } + before do + Sidekiq::Worker.clear_all + end describe '#perform' do - before { build } + before do + build + end subject! do Sidekiq::Testing.fake! { worker.perform } diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index 8c5303b61cc..f443bb2c9b4 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -23,7 +23,9 @@ describe GitGarbageCollectWorker do end shared_examples 'gc tasks' do - before { allow(subject).to receive(:bitmaps_enabled?).and_return(bitmaps_enabled) } + before do + allow(subject).to receive(:bitmaps_enabled?).and_return(bitmaps_enabled) + end it 'incremental repack adds a new packfile' do create_objects(project) diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index f4bc63bcc6a..3c93da63f2e 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -89,31 +89,30 @@ describe PostReceive do end context "does not create a Ci::Pipeline" do - before { stub_ci_pipeline_yaml_file(nil) } + before do + stub_ci_pipeline_yaml_file(nil) + end it { expect{ subject }.not_to change{ Ci::Pipeline.count } } end end - end - describe '#process_repository_update' do - let(:changes) {'123456 789012 refs/heads/tést'} - let(:fake_hook_data) do - { event_name: 'repository_update' } - end + context 'after project changes hooks' do + let(:changes) { '123456 789012 refs/heads/tést' } + let(:fake_hook_data) { Hash.new(event_name: 'repository_update') } - before do - allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner) - allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data) - # silence hooks so we can isolate - allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true) - allow(subject).to receive(:process_project_changes).and_return(true) - end + before do + allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data) + # silence hooks so we can isolate + allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true) + allow_any_instance_of(GitPushService).to receive(:execute).and_return(true) + end - it 'calls SystemHooksService' do - expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true) + it 'calls SystemHooksService' do + expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true) - subject.perform(pwd(project), key_id, base64_changes) + described_class.new.perform(project_identifier, key_id, base64_changes) + end end end diff --git a/spec/workers/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb index 8434b0c8e5b..549635f7f33 100644 --- a/spec/workers/stuck_ci_jobs_worker_spec.rb +++ b/spec/workers/stuck_ci_jobs_worker_spec.rb @@ -34,7 +34,9 @@ describe StuckCiJobsWorker do let(:status) { 'pending' } context 'when job is not stuck' do - before { allow_any_instance_of(Ci::Build).to receive(:stuck?).and_return(false) } + before do + allow_any_instance_of(Ci::Build).to receive(:stuck?).and_return(false) + end context 'when job was not updated for more than 1 day ago' do let(:updated_at) { 2.days.ago } @@ -53,7 +55,9 @@ describe StuckCiJobsWorker do end context 'when job is stuck' do - before { allow_any_instance_of(Ci::Build).to receive(:stuck?).and_return(true) } + before do + allow_any_instance_of(Ci::Build).to receive(:stuck?).and_return(true) + end context 'when job was not updated for more than 1 hour ago' do let(:updated_at) { 2.hours.ago } @@ -93,7 +97,9 @@ describe StuckCiJobsWorker do let(:status) { 'running' } let(:updated_at) { 2.days.ago } - before { job.project.update(pending_delete: true) } + before do + job.project.update(pending_delete: true) + end it 'does not drop job' do expect_any_instance_of(Ci::Build).not_to receive(:drop) diff --git a/tmp/prometheus_multiproc_dir/.gitkeep b/tmp/prometheus_multiproc_dir/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/tmp/prometheus_multiproc_dir/.gitkeep diff --git a/vendor/assets/javascripts/peek.js b/vendor/assets/javascripts/peek.js new file mode 100644 index 00000000000..f7e77de34ff --- /dev/null +++ b/vendor/assets/javascripts/peek.js @@ -0,0 +1,78 @@ +(function($) { + var fetchRequestResults, getRequestId, peekEnabled, toggleBar, updatePerformanceBar; + getRequestId = function() { + return $('#peek').data('request-id'); + }; + peekEnabled = function() { + return $('#peek').length; + }; + updatePerformanceBar = function(results) { + var key, label, data, table, html, tr, duration_td, sql_td, strong; + + Object.keys(results.data).forEach(function(key) { + Object.keys(results.data[key]).forEach(function(label) { + data = results.data[key][label]; + + if (label == 'queries') { + table = document.createElement('table'); + + for (var i = 0; i < data.length; i += 1) { + tr = document.createElement('tr'); + duration_td = document.createElement('td'); + sql_td = document.createElement('td'); + strong = document.createElement('strong'); + + strong.append(data[i]['duration'] + 'ms'); + duration_td.appendChild(strong); + tr.appendChild(duration_td); + + sql_td.appendChild(document.createTextNode(data[i]['sql'])); + tr.appendChild(sql_td); + + table.appendChild(tr); + } + + table.className = 'table'; + $("[data-defer-to=" + key + "-" + label + "]").html(table); + } else { + $("[data-defer-to=" + key + "-" + label + "]").text(results.data[key][label]); + } + }); + }); + return $(document).trigger('peek:render', [getRequestId(), results]); + }; + toggleBar = function(event) { + var wrapper; + if ($(event.target).is(':input')) { + return; + } + if (event.which === 96 && !event.metaKey) { + wrapper = $('#peek'); + if (wrapper.hasClass('disabled')) { + wrapper.removeClass('disabled'); + return document.cookie = "peek=true; path=/"; + } else { + wrapper.addClass('disabled'); + return document.cookie = "peek=false; path=/"; + } + } + }; + fetchRequestResults = function() { + return $.ajax('/-/peek/results', { + data: { + request_id: getRequestId() + }, + success: function(data, textStatus, xhr) { + return updatePerformanceBar(data); + }, + error: function(xhr, textStatus, error) {} + }); + }; + $(document).on('keypress', toggleBar); + $(document).on('peek:update', fetchRequestResults); + return $(function() { + if (peekEnabled()) { + return $(this).trigger('peek:update'); + } + }); +})(jQuery); diff --git a/vendor/assets/javascripts/peek.performance_bar.js b/vendor/assets/javascripts/peek.performance_bar.js new file mode 100644 index 00000000000..6ed86dce2f2 --- /dev/null +++ b/vendor/assets/javascripts/peek.performance_bar.js @@ -0,0 +1,182 @@ +var PerformanceBar, ajaxStart, renderPerformanceBar, updateStatus; + +PerformanceBar = (function() { + PerformanceBar.prototype.appInfo = null; + + PerformanceBar.prototype.width = null; + + PerformanceBar.formatTime = function(value) { + if (value >= 1000) { + return ((value / 1000).toFixed(3)) + "s"; + } else { + return (value.toFixed(0)) + "ms"; + } + }; + + function PerformanceBar(options) { + var k, v; + if (options == null) { + options = {}; + } + this.el = $('#peek-view-performance-bar .performance-bar'); + for (k in options) { + v = options[k]; + this[k] = v; + } + if (this.width == null) { + this.width = this.el.width(); + } + if (this.timing == null) { + this.timing = window.performance.timing; + } + } + + PerformanceBar.prototype.render = function(serverTime) { + var networkTime, perfNetworkTime; + if (serverTime == null) { + serverTime = 0; + } + this.el.empty(); + this.addBar('frontend', '#90d35b', 'domLoading', 'domInteractive'); + perfNetworkTime = this.timing.responseEnd - this.timing.requestStart; + if (serverTime && serverTime <= perfNetworkTime) { + networkTime = perfNetworkTime - serverTime; + this.addBar('latency / receiving', '#f1faff', this.timing.requestStart + serverTime, this.timing.requestStart + serverTime + networkTime); + this.addBar('app', '#90afcf', this.timing.requestStart, this.timing.requestStart + serverTime, this.appInfo); + } else { + this.addBar('backend', '#c1d7ee', 'requestStart', 'responseEnd'); + } + this.addBar('tcp / ssl', '#45688e', 'connectStart', 'connectEnd'); + this.addBar('redirect', '#0c365e', 'redirectStart', 'redirectEnd'); + this.addBar('dns', '#082541', 'domainLookupStart', 'domainLookupEnd'); + return this.el; + }; + + PerformanceBar.prototype.isLoaded = function() { + return this.timing.domInteractive; + }; + + PerformanceBar.prototype.start = function() { + return this.timing.navigationStart; + }; + + PerformanceBar.prototype.end = function() { + return this.timing.domInteractive; + }; + + PerformanceBar.prototype.total = function() { + return this.end() - this.start(); + }; + + PerformanceBar.prototype.addBar = function(name, color, start, end, info) { + var bar, left, offset, time, title, width; + if (typeof start === 'string') { + start = this.timing[start]; + } + if (typeof end === 'string') { + end = this.timing[end]; + } + if (!((start != null) && (end != null))) { + return; + } + time = end - start; + offset = start - this.start(); + left = this.mapH(offset); + width = this.mapH(time); + title = name + ": " + (PerformanceBar.formatTime(time)); + bar = $('<li></li>', { + 'data-title': title, + 'data-toggle': 'tooltip', + 'data-container': 'body' + }); + bar.css({ + width: width + "px", + left: left + "px", + background: color + }); + return this.el.append(bar); + }; + + PerformanceBar.prototype.mapH = function(offset) { + return offset * (this.width / this.total()); + }; + + return PerformanceBar; + +})(); + +renderPerformanceBar = function() { + var bar, resp, span, time; + resp = $('#peek-server_response_time'); + time = Math.round(resp.data('time') * 1000); + bar = new PerformanceBar; + bar.render(time); + span = $('<span>', { + 'data-toggle': 'tooltip', + 'data-title': 'Total navigation time for this page.', + 'data-container': 'body' + }).text(PerformanceBar.formatTime(bar.total())); + return updateStatus(span); +}; + +updateStatus = function(html) { + return $('#serverstats').html(html); +}; + +ajaxStart = null; + +$(document).on('pjax:start page:fetch turbolinks:request-start', function(event) { + return ajaxStart = event.timeStamp; +}); + +$(document).on('pjax:end page:load turbolinks:load', function(event, xhr) { + var ajaxEnd, serverTime, total; + if (ajaxStart == null) { + return; + } + ajaxEnd = event.timeStamp; + total = ajaxEnd - ajaxStart; + serverTime = xhr ? parseInt(xhr.getResponseHeader('X-Runtime')) : 0; + return setTimeout(function() { + var bar, now, span, tech; + now = new Date().getTime(); + bar = new PerformanceBar({ + timing: { + requestStart: ajaxStart, + responseEnd: ajaxEnd, + domLoading: ajaxEnd, + domInteractive: now + }, + isLoaded: function() { + return true; + }, + start: function() { + return ajaxStart; + }, + end: function() { + return now; + } + }); + bar.render(serverTime); + if ($.fn.pjax != null) { + tech = 'PJAX'; + } else { + tech = 'Turbolinks'; + } + span = $('<span>', { + 'data-toggle': 'tooltip', + 'data-title': tech + " navigation time", + 'data-container': 'body' + }).text(PerformanceBar.formatTime(total)); + updateStatus(span); + return ajaxStart = null; + }, 0); +}); + +$(function() { + if (window.performance) { + return renderPerformanceBar(); + } else { + return $('#peek-view-performance-bar').remove(); + } +}); diff --git a/vendor/assets/stylesheets/peek.scss b/vendor/assets/stylesheets/peek.scss new file mode 100644 index 00000000000..f1845fb9044 --- /dev/null +++ b/vendor/assets/stylesheets/peek.scss @@ -0,0 +1,94 @@ +//= require peek/views/performance_bar +//= require peek/views/rblineprof + +header.navbar-gitlab.with-peek { + top: 35px; +} + +#peek { + height: 35px; + background: #000; + line-height: 35px; + color: #999; + + &.disabled { + display: none; + } + + &.production { + background-color: #222; + } + + &.staging { + background-color: #291430; + } + + &.development { + background-color: #4c1210; + } + + .wrapper { + width: 800px; + margin: 0 auto; + } + + // UI Elements + .bucket { + background: #111; + display: inline-block; + padding: 4px 6px; + font-family: Consolas, "Liberation Mono", Courier, monospace; + line-height: 1; + color: #ccc; + border-radius: 3px; + box-shadow: 0 1px 0 rgba(255,255,255,.2), inset 0 1px 2px rgba(0,0,0,.25); + + .hidden { + display: none; + } + + &:hover .hidden { + display: inline; + } + } + + strong { + color: #fff; + } + + table { + strong { + color: #000; + } + } + + .view { + margin-right: 15px; + float: left; + + &:last-child { + margin-right: 0; + } + } + + .css-truncate { + &.css-truncate-target, + .css-truncate-target { + display: inline-block; + max-width: 125px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: top; + } + + &.expandable:hover .css-truncate-target, + &.expandable:hover.css-truncate-target { + max-width: 10000px !important; + } + } +} + +#modal-peek-pg-queries-content { + color: #000; +} diff --git a/yarn.lock b/yarn.lock index 1db64aead8d..b902d5235d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -882,20 +882,20 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.6" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" -body-parser@^1.12.4: - version "1.16.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.16.0.tgz#924a5e472c6229fb9d69b85a20d5f2532dec788b" +body-parser@^1.16.1: + version "1.17.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.2.tgz#f8892abc8f9e627d42aedafbca66bf5ab99104ee" dependencies: bytes "2.4.0" content-type "~1.0.2" - debug "2.6.0" + debug "2.6.7" depd "~1.1.0" - http-errors "~1.5.1" + http-errors "~1.6.1" iconv-lite "0.4.15" on-finished "~2.3.0" - qs "6.2.1" + qs "6.4.0" raw-body "~2.2.0" - type-is "~1.6.14" + type-is "~1.6.15" boom@2.x.x: version "2.10.1" @@ -1265,14 +1265,6 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concat-stream@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.0.tgz#53f7d43c51c5e43f81c8fdd03321c631be68d611" - dependencies: - inherits "~2.0.1" - readable-stream "~2.0.0" - typedarray "~0.0.5" - concat-stream@^1.4.6: version "1.6.0" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" @@ -1305,12 +1297,12 @@ connect-history-api-fallback@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.3.0.tgz#e51d17f8f0ef0db90a64fdb47de3051556e9f169" -connect@^3.3.5: - version "3.5.0" - resolved "https://registry.yarnpkg.com/connect/-/connect-3.5.0.tgz#b357525a0b4c1f50599cd983e1d9efeea9677198" +connect@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.6.2.tgz#694e8d20681bfe490282c8ab886be98f09f42fe7" dependencies: - debug "~2.2.0" - finalhandler "0.5.0" + debug "2.6.7" + finalhandler "1.0.3" parseurl "~1.3.1" utils-merge "1.0.0" @@ -1538,10 +1530,6 @@ de-indent@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" -debug@0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" - debug@2.2.0, debug@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" @@ -1554,18 +1542,18 @@ debug@2.3.3: dependencies: ms "0.7.2" -debug@2.6.0, debug@^2.1.0, debug@^2.1.1, debug@^2.2.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b" - dependencies: - ms "0.7.2" - debug@2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e" dependencies: ms "2.0.0" +debug@^2.1.0, debug@^2.1.1, debug@^2.2.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b" + dependencies: + ms "0.7.2" + decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -1778,9 +1766,9 @@ end-of-stream@1.0.0: dependencies: once "~1.3.0" -engine.io-client@1.8.2: - version "1.8.2" - resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-1.8.2.tgz#c38767547f2a7d184f5752f6f0ad501006703766" +engine.io-client@1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-1.8.3.tgz#1798ed93451246453d4c6f635d7a201fe940d5ab" dependencies: component-emitter "1.2.1" component-inherit "0.0.3" @@ -1791,7 +1779,7 @@ engine.io-client@1.8.2: parsejson "0.0.3" parseqs "0.0.5" parseuri "0.0.5" - ws "1.1.1" + ws "1.1.2" xmlhttprequest-ssl "1.5.3" yeast "0.1.2" @@ -1806,16 +1794,16 @@ engine.io-parser@1.3.2: has-binary "0.1.7" wtf-8 "1.0.0" -engine.io@1.8.2: - version "1.8.2" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-1.8.2.tgz#6b59be730b348c0125b0a4589de1c355abcf7a7e" +engine.io@1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-1.8.3.tgz#8de7f97895d20d39b85f88eeee777b2bd42b13d4" dependencies: accepts "1.3.3" base64id "1.0.0" cookie "0.3.1" debug "2.3.3" engine.io-parser "1.3.2" - ws "1.1.1" + ws "1.1.2" enhanced-resolve@^3.0.0: version "3.1.0" @@ -1884,10 +1872,6 @@ es6-promise@^3.0.2, es6-promise@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6" -es6-promise@~4.0.3: - version "4.0.5" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.0.5.tgz#7882f30adde5b240ccfa7f7d78c548330951ae42" - es6-set@~0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.4.tgz#9516b6761c2964b92ff479456233a247dc707ce8" @@ -2219,15 +2203,6 @@ extglob@^0.3.1: dependencies: is-extglob "^1.0.0" -extract-zip@~1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.5.0.tgz#92ccf6d81ef70a9fa4c1747114ccef6d8688a6c4" - dependencies: - concat-stream "1.5.0" - debug "0.7.4" - mkdirp "0.5.0" - yauzl "2.4.1" - extsprintf@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" @@ -2258,12 +2233,6 @@ faye-websocket@~0.7.3: dependencies: websocket-driver ">=0.3.6" -fd-slicer@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" - dependencies: - pend "~1.2.0" - figures@^1.3.5: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" @@ -2313,17 +2282,7 @@ fill-range@^2.1.0: repeat-element "^1.1.2" repeat-string "^1.5.2" -finalhandler@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-0.5.0.tgz#e9508abece9b6dba871a6942a1d7911b91911ac7" - dependencies: - debug "~2.2.0" - escape-html "~1.0.3" - on-finished "~2.3.0" - statuses "~1.3.0" - unpipe "~1.0.0" - -finalhandler@~1.0.3: +finalhandler@1.0.3, finalhandler@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.3.tgz#ef47e77950e999780e86022a560e3217e0d0cc89" dependencies: @@ -2407,13 +2366,11 @@ from@~0: version "0.1.7" resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" -fs-extra@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" +fs-access@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fs-access/-/fs-access-1.0.1.tgz#d6a87f262271cefebec30c553407fb995da8777a" dependencies: - graceful-fs "^4.1.2" - jsonfile "^2.1.0" - klaw "^1.0.0" + null-check "^1.0.0" fs.realpath@^1.0.0: version "1.0.0" @@ -2551,7 +2508,7 @@ got@^3.2.0: read-all-stream "^3.0.0" timed-out "^2.0.0" -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: +graceful-fs@^4.1.11, graceful-fs@^4.1.2: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -2628,13 +2585,6 @@ hash.js@^1.0.0: dependencies: inherits "^2.0.1" -hasha@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/hasha/-/hasha-2.2.0.tgz#78d7cbfc1e6d66303fe79837365984517b2f6ee1" - dependencies: - is-stream "^1.0.1" - pinkie-promise "^2.0.0" - hawk@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" @@ -2695,7 +2645,7 @@ http-deceiver@^1.2.4: version "1.2.7" resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" -http-errors@~1.5.0, http-errors@~1.5.1: +http-errors@~1.5.0: version "1.5.1" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" dependencies: @@ -2987,7 +2937,7 @@ is-resolvable@^1.0.0: dependencies: tryit "^1.0.1" -is-stream@^1.0.0, is-stream@^1.0.1: +is-stream@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -3124,9 +3074,9 @@ istanbul@^0.4.5: which "^1.1.1" wordwrap "^1.0.0" -jasmine-core@^2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.5.2.tgz#6f61bd79061e27f43e6f9355e44b3c6cab6ff297" +jasmine-core@^2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.6.3.tgz#45072950e4a42b1e322fe55c001100a465d77815" jasmine-jquery@^2.1.1: version "2.1.1" @@ -3225,12 +3175,6 @@ json5@^0.5.0, json5@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" -jsonfile@^2.1.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" - optionalDependencies: - graceful-fs "^4.1.6" - jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -3261,6 +3205,13 @@ jszip@^3.1.3: pako "~1.0.2" readable-stream "~2.0.6" +karma-chrome-launcher@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-2.1.1.tgz#216879c68ac04d8d5140e99619ba04b59afd46cf" + dependencies: + fs-access "^1.0.0" + which "^1.2.1" + karma-coverage-istanbul-reporter@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-0.2.0.tgz#5766263338adeb0026f7e4ac7a89a5f056c5642c" @@ -3277,13 +3228,6 @@ karma-mocha-reporter@^2.2.2: dependencies: chalk "1.1.3" -karma-phantomjs-launcher@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.2.tgz#19e1041498fd75563ed86730a22c1fe579fa8fb1" - dependencies: - lodash "^4.0.1" - phantomjs-prebuilt "^2.1.7" - karma-sourcemap-loader@^0.3.7: version "0.3.7" resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz#91322c77f8f13d46fed062b042e1009d4c4505d8" @@ -3300,16 +3244,16 @@ karma-webpack@^2.0.2: source-map "^0.1.41" webpack-dev-middleware "^1.0.11" -karma@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/karma/-/karma-1.4.1.tgz#41981a71d54237606b0a3ea8c58c90773f41650e" +karma@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/karma/-/karma-1.7.0.tgz#6f7a1a406446fa2e187ec95398698f4cee476269" dependencies: bluebird "^3.3.0" - body-parser "^1.12.4" + body-parser "^1.16.1" chokidar "^1.4.1" colors "^1.1.0" combine-lists "^1.0.0" - connect "^3.3.5" + connect "^3.6.0" core-js "^2.2.0" di "^0.0.1" dom-serialize "^2.2.0" @@ -3321,20 +3265,16 @@ karma@^1.4.1: lodash "^3.8.0" log4js "^0.6.31" mime "^1.3.4" - minimatch "^3.0.0" + minimatch "^3.0.2" optimist "^0.6.1" qjobs "^1.1.4" range-parser "^1.2.0" - rimraf "^2.3.3" + rimraf "^2.6.0" safe-buffer "^5.0.1" - socket.io "1.7.2" + socket.io "1.7.3" source-map "^0.5.3" - tmp "0.0.28" - useragent "^2.1.10" - -kew@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b" + tmp "0.0.31" + useragent "^2.1.12" kind-of@^3.0.2: version "3.1.0" @@ -3342,12 +3282,6 @@ kind-of@^3.0.2: dependencies: is-buffer "^1.0.2" -klaw@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" - optionalDependencies: - graceful-fs "^4.1.9" - latest-version@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-1.0.1.tgz#72cfc46e3e8d1be651e1ebb54ea9f6ea96f374bb" @@ -3556,7 +3490,7 @@ lodash@^3.8.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.0.0, lodash@^4.0.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0: +lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -3704,12 +3638,6 @@ minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" -mkdirp@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12" - dependencies: - minimist "0.0.8" - mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -3907,6 +3835,10 @@ npmlog@^4.0.1: gauge "~2.7.1" set-blocking "~2.0.0" +null-check@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd" + num2fraction@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" @@ -4165,24 +4097,6 @@ pdfjs-dist@^1.8.252: node-ensure "^0.0.0" worker-loader "^0.8.0" -pend@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" - -phantomjs-prebuilt@^2.1.7: - version "2.1.14" - resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.14.tgz#d53d311fcfb7d1d08ddb24014558f1188c516da0" - dependencies: - es6-promise "~4.0.3" - extract-zip "~1.5.0" - fs-extra "~1.0.0" - hasha "~2.2.0" - kew "~0.7.0" - progress "~1.1.8" - request "~2.79.0" - request-progress "~2.0.1" - which "~1.2.10" - pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -4518,7 +4432,7 @@ process@^0.11.0, process@~0.11.0: version "0.11.9" resolved "https://registry.yarnpkg.com/process/-/process-0.11.9.tgz#7bd5ad21aa6253e7da8682264f1e11d11c0318c1" -progress@^1.1.8, progress@~1.1.8: +progress@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" @@ -4573,10 +4487,6 @@ qjobs@^1.1.4: version "1.1.5" resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.1.5.tgz#659de9f2cf8dcc27a1481276f205377272382e73" -qs@6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.1.tgz#ce03c5ff0935bc1d9d69a9f14cbd18e568d67625" - qs@6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" @@ -4701,7 +4611,7 @@ readable-stream@^2.0.0, "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2. string_decoder "~0.10.x" util-deprecate "~1.0.1" -readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@~2.0.0, readable-stream@~2.0.6: +readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" dependencies: @@ -4855,13 +4765,7 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -request-progress@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-2.0.1.tgz#5d36bb57961c673aa5b788dbc8141fdf23b44e08" - dependencies: - throttleit "^1.0.0" - -request@^2.79.0, request@~2.79.0: +request@^2.79.0: version "2.79.0" resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" dependencies: @@ -4934,7 +4838,13 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@^2.2.8, rimraf@^2.3.3, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@~2.5.1, rimraf@~2.5.4: +rimraf@2, rimraf@^2.2.8, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@^2.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" + dependencies: + glob "^7.0.5" + +rimraf@~2.5.1, rimraf@~2.5.4: version "2.5.4" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" dependencies: @@ -5094,15 +5004,15 @@ socket.io-adapter@0.5.0: debug "2.3.3" socket.io-parser "2.3.1" -socket.io-client@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-1.7.2.tgz#39fdb0c3dd450e321b7e40cfd83612ec533dd644" +socket.io-client@1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-1.7.3.tgz#b30e86aa10d5ef3546601c09cde4765e381da377" dependencies: backo2 "1.0.2" component-bind "1.0.0" component-emitter "1.2.1" debug "2.3.3" - engine.io-client "1.8.2" + engine.io-client "1.8.3" has-binary "0.1.7" indexof "0.0.1" object-component "0.0.3" @@ -5119,16 +5029,16 @@ socket.io-parser@2.3.1: isarray "0.0.1" json3 "3.3.2" -socket.io@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-1.7.2.tgz#83bbbdf2e79263b378900da403e7843e05dc3b71" +socket.io@1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-1.7.3.tgz#b8af9caba00949e568e369f1327ea9be9ea2461b" dependencies: debug "2.3.3" - engine.io "1.8.2" + engine.io "1.8.3" has-binary "0.1.7" object-assign "4.1.0" socket.io-adapter "0.5.0" - socket.io-client "1.7.2" + socket.io-client "1.7.3" socket.io-parser "2.3.1" sockjs-client@1.0.1: @@ -5269,7 +5179,7 @@ stats-webpack-plugin@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/stats-webpack-plugin/-/stats-webpack-plugin-0.4.3.tgz#b2f618202f28dd04ab47d7ecf54ab846137b7aea" -"statuses@>= 1.3.1 < 2", statuses@~1.3.0, statuses@~1.3.1: +"statuses@>= 1.3.1 < 2", statuses@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" @@ -5449,10 +5359,6 @@ three@^0.84.0: version "0.84.0" resolved "https://registry.yarnpkg.com/three/-/three-0.84.0.tgz#95be85a55a0fa002aa625ed559130957dcffd918" -throttleit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" - through@2, through@^2.3.6, through@~2.3, through@~2.3.1: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -5481,9 +5387,9 @@ tiny-emitter@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.1.0.tgz#ab405a21ffed814a76c19739648093d70654fecb" -tmp@0.0.28, tmp@0.0.x: - version "0.0.28" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120" +tmp@0.0.31, tmp@0.0.x: + version "0.0.31" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" dependencies: os-tmpdir "~1.0.1" @@ -5541,14 +5447,14 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-is@~1.6.14, type-is@~1.6.15: +type-is@~1.6.15: version "1.6.15" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" dependencies: media-typer "0.3.0" mime-types "~2.1.15" -typedarray@^0.0.6, typedarray@~0.0.5: +typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -5653,9 +5559,9 @@ user-home@^2.0.0: dependencies: os-homedir "^1.0.0" -useragent@^2.1.10: - version "2.1.12" - resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.1.12.tgz#aa7da6cdc48bdc37ba86790871a7321d64edbaa2" +useragent@^2.1.12: + version "2.1.13" + resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.1.13.tgz#bba43e8aa24d5ceb83c2937473e102e21df74c10" dependencies: lru-cache "2.2.x" tmp "0.0.x" @@ -5883,7 +5789,7 @@ which-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" -which@^1.1.1, which@~1.2.10: +which@^1.1.1, which@^1.2.1: version "1.2.12" resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192" dependencies: @@ -5942,9 +5848,9 @@ write@^0.2.1: dependencies: mkdirp "^0.5.1" -ws@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.1.tgz#082ddb6c641e85d4bb451f03d52f06eabdb1f018" +ws@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.2.tgz#8a244fa052401e08c9886cf44a85189e1fd4067f" dependencies: options ">=0.0.5" ultron "1.0.x" @@ -6015,12 +5921,6 @@ yargs@~3.10.0: decamelize "^1.0.0" window-size "0.1.0" -yauzl@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" - dependencies: - fd-slicer "~1.0.1" - yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" |