diff options
472 files changed, 4484 insertions, 3119 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 38fb743b0c9..fed5971233d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.5-golang-1.8-git-2.13-phantomjs-2.1-node-8.x-yarn-1.0-postgresql-9.6" +image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.5-golang-1.8-git-2.13-chrome-62.0-node-8.x-yarn-1.2-postgresql-9.6" .default-cache: &default-cache key: "ruby-235-with-yarn" @@ -23,7 +23,6 @@ variables: SIMPLECOV: "true" GIT_DEPTH: "20" GIT_SUBMODULE_STRATEGY: "none" - PHANTOMJS_VERSION: "2.1.1" GET_SOURCES_ATTEMPTS: "3" KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json @@ -455,7 +454,7 @@ db:migrate:reset-mysql: variables: SETUP_DB: "false" script: - - git fetch origin v9.3.0 + - git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0 - git checkout -f FETCH_HEAD - bundle install $BUNDLE_INSTALL_FLAGS - cp config/gitlab.yml.example config/gitlab.yml @@ -551,7 +550,6 @@ karma: <<: *dedicated-runner <<: *except-docs <<: *pull-cache - image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.5-golang-1.8-git-2.13-chrome-61.0-node-8.x-yarn-1.0-postgresql-9.6" stage: test variables: BABEL_ENV: "coverage" @@ -1 +1 @@ -7.5
\ No newline at end of file +9.0.0 diff --git a/.scss-lint.yml b/.scss-lint.yml index d2c972fa9c4..16a168b7c60 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -112,7 +112,7 @@ linters: # Reports when you define the same selector twice in a single sheet. MergeableSelector: - enabled: false + enabled: true # Functions, mixins, variables, and placeholders should be declared # with all lowercase letters and hyphens instead of underscores. diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bca9944bb1..2f13eca2caf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,30 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 10.1.1 (2017-10-31) + +- [FIXED] Auto Devops kubernetes default namespace is now correctly built out of gitlab project group-name. !14642 (Mircea Danila Dumitrescu) +- [FIXED] Forbid the usage of `Redis#keys`. !14889 +- [FIXED] Make the circuitbreaker more robust by adding higher thresholds, and multiple access attempts. !14933 +- [FIXED] Only cache last push event for existing projects when pushing to a fork. !14989 +- [FIXED] Fix bug preventing secondary emails from being confirmed. !15010 +- [FIXED] Fix broken wiki pages that link to a wiki file. !15019 +- [FIXED] Don't rename paths that were freed up when upgrading. !15029 +- [FIXED] Fix bitbucket login. !15051 +- [FIXED] Update gitaly in Gitlab 10.1 to 0.43.1 for temp file cleanup. !15055 +- [FIXED] Use the correct visibility attribute for projects in system hooks. !15065 +- [FIXED] Normalize LDAP DN when looking up identity. +- [FIXED] Adds callback functions for initial request in clusters page. +- [FIXED] Fix missing Import/Export issue assignees. +- [FIXED] Allow boards as top level route. +- [FIXED] Fix widget of locked merge requests not being presented. +- [FIXED] Fix editing issue description in mobile view. +- [FIXED] Fix deletion of container registry or images returning an error. +- [FIXED] Fix the writing of invalid environment refs. +- [CHANGED] Store circuitbreaker settings in the database instead of config. !14842 +- [CHANGED] Update default disabled merge request widget message to reflect a general failure. !14960 +- [PERFORMANCE] Stop merge requests with thousands of commits from timing out. !15063 + ## 10.1.0 (2017-10-22) - [SECURITY] Use a timeout on certain git operations. !14872 @@ -194,6 +218,24 @@ entry. - creation of keys moved to services. !13331 (haseebeqx) - Add username as GL_USERNAME in hooks. +## 10.0.5 (2017-11-03) + +- [FIXED] Fix incorrect X-axis labels in Prometheus graphs. !14258 +- [FIXED] Fix `rake gitlab:incoming_email:check` and make it report the actual error. !14423 +- [FIXED] Does not check if an invariant hashed storage path exists on disk when renaming projects. !14428 +- [FIXED] Fix bottom spacing for dropdowns that open upwards. !14535 +- [FIXED] Fix the project import with issues and milestones. !14657 +- [FIXED] Fix broken Y-axis scaling in some Prometheus graphs. !14693 +- [FIXED] Fixed duplicate notifications when added multiple labels on an issue. !14798 +- [FIXED] Don't rename paths that were freed up when upgrading. !15029 +- [FIXED] Fixed issue/merge request breadcrumb titles not having links. +- [FIXED] Fix application setting to cache nil object. +- [FIXED] Fix missing Import/Export issue assignees. +- [FIXED] Allow boards as top level route. +- [FIXED] Fixed milestone breadcrumb links. +- [FIXED] Fixed merge request widget merged & closed date tooltip text. +- [FIXED] fix merge request widget status icon for failed CI. + ## 10.0.4 (2017-10-16) - [SECURITY] Move project repositories between namespaces when renaming users. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9a26bed3d29..c4e5fd842df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -104,8 +104,7 @@ the remaining issues on the GitHub issue tracker. ## I want to contribute! -If you want to contribute to GitLab, but are not sure where to start, -look for [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight]. +If you want to contribute to GitLab, [issues with the label `Accepting Merge Requests` and small weight][accepting-mrs-weight] is a great place to start. Issues with a lower weight (1 or 2) are deemed suitable for beginners. These issues will be of reasonable size and challenge, for anyone to start contributing to GitLab. @@ -324,9 +324,9 @@ group :development, :test do # Generate Fake data gem 'ffaker', '~> 2.4' - gem 'capybara', '~> 2.15.0' + gem 'capybara', '~> 2.15' gem 'capybara-screenshot', '~> 1.0.0' - gem 'poltergeist', '~> 1.9.0' + gem 'selenium-webdriver', '~> 3.5' gem 'spring', '~> 2.0.0' gem 'spring-commands-rspec', '~> 1.0.4' diff --git a/Gemfile.lock b/Gemfile.lock index 8ccf18818a6..ae145ca5f69 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -113,12 +113,13 @@ GEM mime-types (>= 1.16) cause (0.1) charlock_holmes (0.7.5) + childprocess (0.7.0) + ffi (~> 1.0, >= 1.0.11) chronic (0.10.2) chronic_duration (0.10.6) numerizer (~> 0.1.1) chunky_png (1.3.5) citrus (3.0.2) - cliver (0.3.2) coderay (1.1.1) coercible (1.0.0) descendants_tracker (~> 0.0.1) @@ -604,11 +605,6 @@ GEM pg (0.18.4) po_to_json (1.0.1) json (>= 1.6.0) - poltergeist (1.9.0) - capybara (~> 2.1) - cliver (~> 0.3.1) - multi_json (~> 1.0) - websocket-driver (>= 0.2.0) posix-spawn (0.3.13) powerpack (0.1.1) premailer (1.10.4) @@ -818,6 +814,9 @@ GEM activesupport (>= 3.1) select2-rails (3.5.9.3) thor (~> 0.14) + selenium-webdriver (3.5.0) + childprocess (~> 0.5) + rubyzip (~> 1.0) sentry-raven (2.5.3) faraday (>= 0.7.6, < 1.0) settingslogic (2.0.9) @@ -949,9 +948,6 @@ GEM hashdiff webpack-rails (0.9.10) railties (>= 3.2.0) - websocket-driver (0.6.3) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.2) wikicloth (0.8.1) builder expression_parser @@ -988,7 +984,7 @@ DEPENDENCIES browser (~> 2.2) bullet (~> 5.5.0) bundler-audit (~> 0.5.0) - capybara (~> 2.15.0) + capybara (~> 2.15) capybara-screenshot (~> 1.0.0) carrierwave (~> 1.2) charlock_holmes (~> 0.7.5) @@ -1104,7 +1100,6 @@ DEPENDENCIES peek-redis (~> 1.2.0) peek-sidekiq (~> 1.0.3) pg (~> 0.18.2) - poltergeist (~> 1.9.0) premailer-rails (~> 1.9.7) prometheus-client-mmap (~> 0.7.0.beta18) pry-byebug (~> 3.4.1) @@ -1150,6 +1145,7 @@ DEPENDENCIES scss_lint (~> 0.54.0) seed-fu (~> 2.3.5) select2-rails (~> 3.5.9) + selenium-webdriver (~> 3.5) sentry-raven (~> 2.5.3) settingslogic (~> 2.0.9) sham_rack (~> 1.3.6) diff --git a/app/assets/javascripts/clusters.js b/app/assets/javascripts/clusters.js index 661870c226c..c9fef94efea 100644 --- a/app/assets/javascripts/clusters.js +++ b/app/assets/javascripts/clusters.js @@ -1,6 +1,7 @@ /* globals Flash */ import Visibility from 'visibilityjs'; import axios from 'axios'; +import setAxiosCsrfToken from './lib/utils/axios_utils'; import Poll from './lib/utils/poll'; import { s__ } from './locale'; import initSettingsPanels from './settings_panels'; @@ -17,6 +18,7 @@ import Flash from './flash'; class ClusterService { constructor(options = {}) { this.options = options; + setAxiosCsrfToken(); } fetchData() { return axios.get(this.options.endpoint); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 5930868412b..760fb0cdf67 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -16,7 +16,7 @@ import CILintEditor from './ci_lint_editor'; import groupsSelect from './groups_select'; /* global Search */ /* global Admin */ -/* global NamespaceSelects */ +import NamespaceSelect from './namespace_select'; /* global NewCommitForm */ /* global NewBranchForm */ /* global Project */ @@ -575,7 +575,8 @@ import Diff from './diff'; new UsersSelect(); break; case 'projects': - new NamespaceSelects(); + document.querySelectorAll('.js-namespace-select') + .forEach(dropdown => new NamespaceSelect({ dropdown })); break; case 'labels': switch (path[2]) { diff --git a/app/assets/javascripts/droplab/plugins/filter.js b/app/assets/javascripts/droplab/plugins/filter.js index d6a1aadd49c..404d707cf7a 100644 --- a/app/assets/javascripts/droplab/plugins/filter.js +++ b/app/assets/javascripts/droplab/plugins/filter.js @@ -79,8 +79,6 @@ const Filter = { this.hook.trigger.addEventListener('keydown.dl', this.eventWrapper.debounceKeydown); this.hook.trigger.addEventListener('mousedown.dl', this.eventWrapper.debounceKeydown); - - this.debounceKeydown({ detail: { hook: this.hook } }); }, destroy: function destroy() { diff --git a/app/assets/javascripts/droplab/utils.js b/app/assets/javascripts/droplab/utils.js index 4da7344604e..bfe056a0fcc 100644 --- a/app/assets/javascripts/droplab/utils.js +++ b/app/assets/javascripts/droplab/utils.js @@ -30,7 +30,7 @@ const utils = { }, isDropDownParts(target) { - if (!target || target.tagName === 'HTML') return false; + if (!target || !target.hasAttribute || target.tagName === 'HTML') return false; return target.hasAttribute(DATA_TRIGGER) || target.hasAttribute(DATA_DROPDOWN); }, }; diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 7a17adcd44e..b7747ee3f83 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -119,11 +119,9 @@ export default function dropzoneInput(form) { // removeAllFiles(true) stops uploading files (if any) // and remove them from dropzone files queue. $cancelButton.on('click', (e) => { - const target = e.target.closest('.js-main-target-form').querySelector('.div-dropzone'); - e.preventDefault(); e.stopPropagation(); - Dropzone.forElement(target).removeAllFiles(true); + Dropzone.forElement($formDropzone.get(0)).removeAllFiles(true); }); // If 'error' event is fired, we store a failed files, diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 6de01fa53d0..fc0308b81ba 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -421,7 +421,11 @@ 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="gl-responsive-table-row" + :class="{ + 'js-child-row environment-child-row': model.isChildren, + 'folder-row': model.isFolder, + }" role="row"> <div class="table-section section-10" role="gridcell"> <div @@ -495,15 +499,16 @@ export default { </a> </div> - <div class="table-section section-25" role="gridcell"> + <div + v-if="!model.isFolder" + class="table-section section-25" role="gridcell"> <div - v-if="!model.isFolder" role="rowheader" class="table-mobile-header"> Commit </div> <div - v-if="!model.isFolder && hasLastDeploymentKey" + v-if="hasLastDeploymentKey" class="js-commit-component table-mobile-content"> <commit-component :tag="commitTag" @@ -514,21 +519,22 @@ export default { :author="commitAuthor"/> </div> <div - v-if="!model.isFolder && !hasLastDeploymentKey" + v-if="!hasLastDeploymentKey" class="commit-title table-mobile-content"> No deployments yet </div> </div> - <div class="table-section section-10" role="gridcell"> + <div + v-if="!model.isFolder" + class="table-section section-10" role="gridcell"> <div - v-if="!model.isFolder" role="rowheader" class="table-mobile-header"> Updated </div> <span - v-if="!model.isFolder && canShowDate" + v-if="canShowDate" class="environment-created-date-timeago table-mobile-content"> {{createdDate}} </span> diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js new file mode 100644 index 00000000000..45bff245827 --- /dev/null +++ b/app/assets/javascripts/lib/utils/axios_utils.js @@ -0,0 +1,6 @@ +import axios from 'axios'; +import csrf from './csrf'; + +export default function setAxiosCsrfToken() { + axios.defaults.headers.common[csrf.headerKey] = csrf.token; +} diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js index 5da2db063a4..1d496c64e53 100644 --- a/app/assets/javascripts/namespace_select.js +++ b/app/assets/javascripts/namespace_select.js @@ -1,85 +1,57 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, no-param-reassign, no-cond-assign, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, max-len */ import Api from './api'; +import './lib/utils/url_utility'; -(function() { - window.NamespaceSelect = (function() { - function NamespaceSelect(opts) { - this.onSelectItem = this.onSelectItem.bind(this); - var fieldName, showAny; - this.dropdown = opts.dropdown; - showAny = true; - fieldName = 'namespace_id'; - if (this.dropdown.attr('data-field-name')) { - fieldName = this.dropdown.data('fieldName'); - } - if (this.dropdown.attr('data-show-any')) { - showAny = this.dropdown.data('showAny'); - } - this.dropdown.glDropdown({ - filterable: true, - selectable: true, - filterRemote: true, - search: { - fields: ['path'] - }, - fieldName: fieldName, - toggleLabel: function(selected) { - if (selected.id == null) { - return selected.text; - } else { - return selected.kind + ": " + selected.full_path; - } - }, - data: function(term, dataCallback) { - return Api.namespaces(term, function(namespaces) { - var anyNamespace; - if (showAny) { - anyNamespace = { - text: 'Any namespace', - id: null - }; - namespaces.unshift(anyNamespace); - namespaces.splice(1, 0, 'divider'); - } - return dataCallback(namespaces); - }); - }, - text: function(namespace) { - if (namespace.id == null) { - return namespace.text; - } else { - return namespace.kind + ": " + namespace.full_path; - } - }, - renderRow: this.renderRow, - clicked: this.onSelectItem - }); - } - - NamespaceSelect.prototype.onSelectItem = function(options) { - const { e } = options; - return e.preventDefault(); - }; +export default class NamespaceSelect { + constructor(opts) { + const isFilter = opts.dropdown.dataset.isFilter === 'true'; + const fieldName = opts.dropdown.dataset.fieldName || 'namespace_id'; - return NamespaceSelect; - })(); - - window.NamespaceSelects = (function() { - function NamespaceSelects(opts) { - var ref; - if (opts == null) { - opts = {}; - } - this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-namespace-select'); - this.$dropdowns.each(function(i, dropdown) { - var $dropdown; - $dropdown = $(dropdown); - return new window.NamespaceSelect({ - dropdown: $dropdown + $(opts.dropdown).glDropdown({ + filterable: true, + selectable: true, + filterRemote: true, + search: { + fields: ['path'] + }, + fieldName: fieldName, + toggleLabel: function(selected) { + if (selected.id == null) { + return selected.text; + } else { + return selected.kind + ": " + selected.full_path; + } + }, + data: function(term, dataCallback) { + return Api.namespaces(term, function(namespaces) { + if (isFilter) { + const anyNamespace = { + text: 'Any namespace', + id: null + }; + namespaces.unshift(anyNamespace); + namespaces.splice(1, 0, 'divider'); + } + return dataCallback(namespaces); }); - }); - } - - return NamespaceSelects; - })(); -}).call(window); + }, + text: function(namespace) { + if (namespace.id == null) { + return namespace.text; + } else { + return namespace.kind + ": " + namespace.full_path; + } + }, + renderRow: this.renderRow, + clicked(options) { + if (!isFilter) { + const { e } = options; + e.preventDefault(); + } + }, + url(namespace) { + return gl.utils.mergeUrlParams({ [fieldName]: namespace.id }, window.location.href); + }, + }); + } +} diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 705bec23b53..e1ab28978e8 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -413,8 +413,9 @@ export default class Notes { return; } this.note_ids.push(noteEntity.id); + form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`); - row = form.closest('tr'); + row = (form.length || !noteEntity.discussion_line_code) ? form.closest('tr') : $(`#${noteEntity.discussion_line_code}`); if (noteEntity.on_image) { row = form; diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index 54227425d2a..547140b1a43 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -1,6 +1,6 @@ <script> - import getActionIcon from '../../../vue_shared/ci_action_icons'; import tooltip from '../../../vue_shared/directives/tooltip'; + import icon from '../../../vue_shared/components/icon.vue'; /** * Renders either a cancel, retry or play icon pointing to the given path. @@ -29,17 +29,18 @@ }, }, + components: { + icon, + }, + directives: { tooltip, }, computed: { - actionIconSvg() { - return getActionIcon(this.actionIcon); - }, - cssClass() { - return `js-${gl.text.dasherize(this.actionIcon)}`; + const actionIconDash = gl.text.dasherize(this.actionIcon); + return `${actionIconDash} js-icon-${actionIconDash}`; }, }, }; @@ -50,14 +51,9 @@ :data-method="actionMethod" :title="tooltipText" :href="link" - class="ci-action-icon-container" + class="ci-action-icon-container ci-action-icon-wrapper" + :class="cssClass" data-container="body"> - - <i - class="ci-action-icon-wrapper" - :class="cssClass" - v-html="actionIconSvg" - aria-hidden="true" - /> + <icon :name="actionIcon"/> </a> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue index 18fe1847eef..1c0944d45fc 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue @@ -1,5 +1,5 @@ <script> - import getActionIcon from '../../../vue_shared/ci_action_icons'; + import icon from '../../../vue_shared/components/icon.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; /** @@ -29,14 +29,12 @@ }, }, - directives: { - tooltip, + components: { + icon, }, - computed: { - actionIconSvg() { - return getActionIcon(this.actionIcon); - }, + directives: { + tooltip, }, }; </script> @@ -49,7 +47,7 @@ rel="nofollow" class="ci-action-icon-wrapper js-ci-status-icon" data-container="body" - v-html="actionIconSvg" aria-label="Job's action"> + <icon :name="actionIcon"/> </a> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue index 3e5d6d15909..7006d05e7b2 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -18,7 +18,7 @@ * "group": "success", * "details_path": "/root/ci-mock/builds/4256", * "action": { - * "icon": "icon_action_retry", + * "icon": "retry", * "title": "Retry", * "path": "/root/ci-mock/builds/4256/retry", * "method": "post" diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index 3933509a6f4..5dea4555515 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -19,7 +19,7 @@ * "group": "success", * "details_path": "/root/ci-mock/builds/4256", * "action": { - * "icon": "icon_action_retry", + * "icon": "retry", * "title": "Retry", * "path": "/root/ci-mock/builds/4256/retry", * "method": "post" diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index 1a7a5c2a415..ac9d9c901ca 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -14,7 +14,7 @@ */ import Flash from '../../flash'; -import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons'; +import icon from '../../vue_shared/components/icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; @@ -45,6 +45,7 @@ export default { components: { loadingIcon, + icon, }, updated() { @@ -122,8 +123,8 @@ export default { return `ci-status-icon-${this.stage.status.group}`; }, - svgIcon() { - return borderlessStatusIconEntityMap[this.stage.status.icon]; + borderlessIcon() { + return `${this.stage.status.icon}_borderless`; }, }, }; @@ -145,9 +146,10 @@ export default { aria-expanded="false"> <span - v-html="svgIcon" aria-hidden="true" :aria-label="stage.title"> + <icon + :name="borderlessIcon"/> </span> <i diff --git a/app/assets/javascripts/test_utils/index.js b/app/assets/javascripts/test_utils/index.js index 8875590f0f2..a55a338eea8 100644 --- a/app/assets/javascripts/test_utils/index.js +++ b/app/assets/javascripts/test_utils/index.js @@ -1,6 +1,8 @@ import 'core-js/es6/map'; import 'core-js/es6/set'; import simulateDrag from './simulate_drag'; +import simulateInput from './simulate_input'; // Export to global space for rspec to use window.simulateDrag = simulateDrag; +window.simulateInput = simulateInput; diff --git a/app/assets/javascripts/test_utils/simulate_input.js b/app/assets/javascripts/test_utils/simulate_input.js new file mode 100644 index 00000000000..90c1b7cb57e --- /dev/null +++ b/app/assets/javascripts/test_utils/simulate_input.js @@ -0,0 +1,23 @@ +function triggerEvents(input) { + input.dispatchEvent(new Event('keydown')); + input.dispatchEvent(new Event('keypress')); + input.dispatchEvent(new Event('input')); + input.dispatchEvent(new Event('keyup')); +} + +export default function simulateInput(target, text) { + const input = document.querySelector(target); + if (!input || !input.matches('textarea, input')) { + return false; + } + + if (text.length > 0) { + Array.prototype.forEach.call(text, (char) => { + input.value += char; + triggerEvents(input); + }); + } else { + triggerEvents(input); + } + return true; +} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js index c79b5c720eb..029832bdd27 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js @@ -1,6 +1,6 @@ import PipelineStage from '../../pipelines/components/stage.vue'; import ciIcon from '../../vue_shared/components/ci_icon.vue'; -import { statusIconEntityMap } from '../../vue_shared/ci_status_icons'; +import icon from '../../vue_shared/components/icon.vue'; export default { name: 'MRWidgetPipeline', @@ -10,6 +10,7 @@ export default { components: { 'pipeline-stage': PipelineStage, ciIcon, + icon, }, computed: { hasPipeline() { @@ -20,9 +21,6 @@ export default { return hasCI && !ciStatus; }, - svg() { - return statusIconEntityMap.icon_status_failed; - }, stageText() { return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage'; }, @@ -38,8 +36,10 @@ export default { <template v-if="hasCIError"> <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10"> <span - v-html="svg" - aria-hidden="true"></span> + aria-hidden="true"> + <icon + name="status_failed"/> + </span> </div> <div class="media-body"> Could not connect to the CI server. Please check your settings and try again diff --git a/app/assets/javascripts/vue_shared/ci_action_icons.js b/app/assets/javascripts/vue_shared/ci_action_icons.js deleted file mode 100644 index b21f0ab49fd..00000000000 --- a/app/assets/javascripts/vue_shared/ci_action_icons.js +++ /dev/null @@ -1,21 +0,0 @@ -import cancelSVG from 'icons/_icon_action_cancel.svg'; -import retrySVG from 'icons/_icon_action_retry.svg'; -import playSVG from 'icons/_icon_action_play.svg'; -import stopSVG from 'icons/_icon_action_stop.svg'; - -/** - * For the provided action returns the respective SVG - * - * @param {String} action - * @return {SVG|String} - */ -export default function getActionIcon(action) { - const icons = { - icon_action_cancel: cancelSVG, - icon_action_play: playSVG, - icon_action_retry: retrySVG, - icon_action_stop: stopSVG, - }; - - return icons[action] || ''; -} diff --git a/app/assets/javascripts/vue_shared/ci_status_icons.js b/app/assets/javascripts/vue_shared/ci_status_icons.js deleted file mode 100644 index d9d0cad38e4..00000000000 --- a/app/assets/javascripts/vue_shared/ci_status_icons.js +++ /dev/null @@ -1,43 +0,0 @@ -import BORDERLESS_CANCELED_SVG from 'icons/_icon_status_canceled_borderless.svg'; -import BORDERLESS_CREATED_SVG from 'icons/_icon_status_created_borderless.svg'; -import BORDERLESS_FAILED_SVG from 'icons/_icon_status_failed_borderless.svg'; -import BORDERLESS_MANUAL_SVG from 'icons/_icon_status_manual_borderless.svg'; -import BORDERLESS_PENDING_SVG from 'icons/_icon_status_pending_borderless.svg'; -import BORDERLESS_RUNNING_SVG from 'icons/_icon_status_running_borderless.svg'; -import BORDERLESS_SKIPPED_SVG from 'icons/_icon_status_skipped_borderless.svg'; -import BORDERLESS_SUCCESS_SVG from 'icons/_icon_status_success_borderless.svg'; -import BORDERLESS_WARNING_SVG from 'icons/_icon_status_warning_borderless.svg'; - -import CANCELED_SVG from 'icons/_icon_status_canceled.svg'; -import CREATED_SVG from 'icons/_icon_status_created.svg'; -import FAILED_SVG from 'icons/_icon_status_failed.svg'; -import MANUAL_SVG from 'icons/_icon_status_manual.svg'; -import PENDING_SVG from 'icons/_icon_status_pending.svg'; -import RUNNING_SVG from 'icons/_icon_status_running.svg'; -import SKIPPED_SVG from 'icons/_icon_status_skipped.svg'; -import SUCCESS_SVG from 'icons/_icon_status_success.svg'; -import WARNING_SVG from 'icons/_icon_status_warning.svg'; - -export const borderlessStatusIconEntityMap = { - icon_status_canceled: BORDERLESS_CANCELED_SVG, - icon_status_created: BORDERLESS_CREATED_SVG, - icon_status_failed: BORDERLESS_FAILED_SVG, - icon_status_manual: BORDERLESS_MANUAL_SVG, - icon_status_pending: BORDERLESS_PENDING_SVG, - icon_status_running: BORDERLESS_RUNNING_SVG, - icon_status_skipped: BORDERLESS_SKIPPED_SVG, - icon_status_success: BORDERLESS_SUCCESS_SVG, - icon_status_warning: BORDERLESS_WARNING_SVG, -}; - -export const statusIconEntityMap = { - icon_status_canceled: CANCELED_SVG, - icon_status_created: CREATED_SVG, - icon_status_failed: FAILED_SVG, - icon_status_manual: MANUAL_SVG, - icon_status_pending: PENDING_SVG, - icon_status_running: RUNNING_SVG, - icon_status_skipped: SKIPPED_SVG, - icon_status_success: SUCCESS_SVG, - icon_status_warning: WARNING_SVG, -}; diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue index 5b6c6e8d0b9..fc795936abf 100644 --- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue +++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue @@ -43,7 +43,6 @@ computed: { cssClass() { const className = this.status.group; - return className ? `ci-status ci-${className}` : 'ci-status'; }, }, diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index ec88119e16c..2a018f38366 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -1,5 +1,5 @@ <script> - import { statusIconEntityMap } from '../ci_status_icons'; + import icon from '../../vue_shared/components/icon.vue'; /** * Renders CI icon based on API response shared between all places where it is used. @@ -30,11 +30,11 @@ }, }, - computed: { - statusIconSvg() { - return statusIconEntityMap[this.status.icon]; - }, + components: { + icon, + }, + computed: { cssClass() { const status = this.status.group; return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`; @@ -44,7 +44,8 @@ </script> <template> <span - :class="cssClass" - v-html="statusIconSvg"> + :class="cssClass"> + <icon + :name="status.icon"/> </span> </template> diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue new file mode 100644 index 00000000000..2e5f9f1088f --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/icon.vue @@ -0,0 +1,52 @@ +<script> + +/* This is a re-usable vue component for rendering a svg sprite + icon + + Sample configuration: + + <icon + :img-src="userAvatarSrc" + :img-alt="tooltipText" + :tooltip-text="tooltipText" + tooltip-placement="top" + /> + +*/ + export default { + props: { + name: { + type: String, + required: true, + }, + + size: { + type: Number, + required: false, + default: 0, + }, + + cssClasses: { + type: String, + required: false, + default: '', + }, + }, + + computed: { + spriteHref() { + return `${gon.sprite_icons}#${this.name}`; + }, + iconSizeClass() { + return this.size ? `s${this.size}` : ''; + }, + }, + }; +</script> +<template> + <svg + :class="[iconSizeClass, cssClasses]"> + <use + v-bind="{'xlink:href':spriteHref}"/> + </svg> +</template> diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 7b1ef003bb2..c334f39f416 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -56,4 +56,4 @@ @import "framework/icons"; @import "framework/snippets"; @import "framework/memory_graph"; -@import "framework/responsive-tables"; +@import "framework/responsive_tables"; diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 8819a0c20f4..def986180fc 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -40,6 +40,10 @@ &.top-block { border-top: none; + + .container-fluid { + background-color: inherit; + } } &.middle-block { @@ -98,10 +102,6 @@ background-color: $white-light; border-top: none; } - - &.top-block .container-fluid { - background-color: inherit; - } } .sub-header-block { diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index e0e46dd73af..1bd94c0acba 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -12,15 +12,15 @@ border-left: 3px solid $border-color; color: $text-color; background: $gray-light; -} -.bs-callout h4 { - margin-top: 0; - margin-bottom: 5px; -} + h4 { + margin-top: 0; + margin-bottom: 5px; + } -.bs-callout p:last-child { - margin-bottom: 0; + p:last-child { + margin-bottom: 0; + } } /* Variations */ diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 96f9dda26c4..ea3007f5e08 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -5,32 +5,6 @@ .cgreen { color: $common-green; } .cdark { color: $common-gray-dark; } -/** COMMON CLASSES **/ -.prepend-top-0 { margin-top: 0; } -.prepend-top-5 { margin-top: 5px; } -.prepend-top-10 { margin-top: 10px; } -.prepend-top-default { margin-top: $gl-padding !important; } -.prepend-top-20 { margin-top: 20px; } -.prepend-left-4 { margin-left: 4px; } -.prepend-left-5 { margin-left: 5px; } -.prepend-left-10 { margin-left: 10px; } -.prepend-left-default { margin-left: $gl-padding; } -.prepend-left-20 { margin-left: 20px; } -.append-right-5 { margin-right: 5px; } -.append-right-8 { margin-right: 8px; } -.append-right-10 { margin-right: 10px; } -.append-right-default { margin-right: $gl-padding; } -.append-right-20 { margin-right: 20px; } -.append-bottom-0 { margin-bottom: 0; } -.append-bottom-5 { margin-bottom: 5px; } -.append-bottom-10 { margin-bottom: 10px; } -.append-bottom-15 { margin-bottom: 15px; } -.append-bottom-20 { margin-bottom: 20px; } -.append-bottom-default { margin-bottom: $gl-padding; } -.inline { display: inline-block; } -.center { text-align: center; } -.vertical-align-middle { vertical-align: middle; } - .underlined-link { text-decoration: underline; } .hint { font-style: italic; color: $hint-color; } .light { color: $common-gray; } @@ -79,6 +53,14 @@ hr { .str-truncated { @include str-truncated; + + &-60 { + @include str-truncated(60%); + } + + &-100 { + @include str-truncated(100%); + } } .block-truncated { @@ -104,10 +86,17 @@ hr { font-size: 14px; } -table a code { - position: relative; - top: -2px; - margin-right: 3px; +table { + a code { + position: relative; + top: -2px; + margin-right: 3px; + } + + td.permission-x { + background: $table-permission-x-bg !important; + text-align: center; + } } .loading { @@ -292,13 +281,6 @@ img.emoji { margin-bottom: 10px; } -table { - td.permission-x { - background: $table-permission-x-bg !important; - text-align: center; - } -} - .btn-sign-in { text-shadow: none; @@ -364,10 +346,11 @@ table { .dropzone .dz-preview .dz-progress { border-color: $border-color !important; -} -.dropzone .dz-preview .dz-progress .dz-upload { - background: $gl-success !important; + .dz-upload { + background: $gl-success !important; + } + } .dz-message { @@ -428,16 +411,6 @@ table { border-radius: $border-radius-default; } -.str-truncated { - &-60 { - @include str-truncated(60%); - } - - &-100 { - @include str-truncated(100%); - } -} - .tooltip { .tooltip-inner { word-wrap: break-word; @@ -448,3 +421,30 @@ table { pointer-events: none; opacity: .5; } + +/** COMMON CLASSES **/ +.prepend-top-0 { margin-top: 0; } +.prepend-top-5 { margin-top: 5px; } +.prepend-top-10 { margin-top: 10px; } +.prepend-top-15 { margin-top: 15px; } +.prepend-top-default { margin-top: $gl-padding !important; } +.prepend-top-20 { margin-top: 20px; } +.prepend-left-4 { margin-left: 4px; } +.prepend-left-5 { margin-left: 5px; } +.prepend-left-10 { margin-left: 10px; } +.prepend-left-default { margin-left: $gl-padding; } +.prepend-left-20 { margin-left: 20px; } +.append-right-5 { margin-right: 5px; } +.append-right-8 { margin-right: 8px; } +.append-right-10 { margin-right: 10px; } +.append-right-default { margin-right: $gl-padding; } +.append-right-20 { margin-right: 20px; } +.append-bottom-0 { margin-bottom: 0; } +.append-bottom-5 { margin-bottom: 5px; } +.append-bottom-10 { margin-bottom: 10px; } +.append-bottom-15 { margin-bottom: 15px; } +.append-bottom-20 { margin-bottom: 20px; } +.append-bottom-default { margin-bottom: $gl-padding; } +.inline { display: inline-block; } +.center { text-align: center; } +.vertical-align-middle { vertical-align: middle; } diff --git a/app/assets/stylesheets/framework/contextual-sidebar.scss b/app/assets/stylesheets/framework/contextual-sidebar.scss index fa5d3833f3e..320f458630a 100644 --- a/app/assets/stylesheets/framework/contextual-sidebar.scss +++ b/app/assets/stylesheets/framework/contextual-sidebar.scss @@ -141,15 +141,15 @@ svg { fill: $gl-text-color-secondary; } - } - .nav-item-name { - flex: 1; - } + .nav-item-name { + flex: 1; + } - li.active { - > a { - font-weight: $gl-font-weight-bold; + &.active { + > a { + font-weight: $gl-font-weight-bold; + } } } @@ -484,10 +484,7 @@ height: calc(100vh - #{$header-height}); @media (min-width: $screen-sm-min) { - height: 475px; // Needed for PhantomJS - // scss-lint:disable DuplicateProperty height: calc(100vh - 180px); - // scss-lint:enable DuplicateProperty } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 1aa53b8f8cf..08c603edd23 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -727,11 +727,11 @@ .pika-single.animate-picker.is-bound { @include set-visible; -} -.pika-single.animate-picker.is-bound.is-hidden { - @include set-invisible; - overflow: hidden; + &.is-hidden { + @include set-invisible; + overflow: hidden; + } } @mixin dropdown-item-hover { @@ -938,9 +938,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu { border-right: 0; } } -} -.projects-dropdown-container { .projects-list-frequent-container, .projects-list-search-container, { padding: 8px 0; @@ -951,11 +949,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu { .projects-list-frequent-container li.section-empty, .projects-list-search-container li.section-empty { padding: 0 15px; - } - - .section-header, - .projects-list-frequent-container li.section-empty, - .projects-list-search-container li.section-empty { color: $gl-text-color-secondary; font-size: $gl-font-size; } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 5833ef939e9..6382551fcc9 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -165,22 +165,36 @@ &:last-child { border-right: none; } - } - td.blame-commit { - padding: 5px 10px; - min-width: 400px; - max-width: 400px; - background: $gray-light; - border-left: 3px solid; + &.blame-commit { + padding: 5px 10px; + min-width: 400px; + max-width: 400px; + background: $gray-light; + border-left: 3px solid; + + .commit-row-title { + display: flex; + } + + .item-title { + flex: 1; + margin-right: 0.5em; + } + } + + &.line-numbers { + float: none; + border-left: 1px solid $blame-line-numbers-border; - .commit-row-title { - display: flex; + i { + float: none; + margin-right: 0; + } } - .item-title { - flex: 1; - margin-right: 0.5em; + &.lines { + padding: 0; } } @@ -195,20 +209,6 @@ border-left-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%); } } - - td.line-numbers { - float: none; - border-left: 1px solid $blame-line-numbers-border; - - i { - float: none; - margin-right: 0; - } - } - - td.lines { - padding: 0; - } } &.logs { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 0d80a85d521..a7333925f80 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -268,12 +268,6 @@ .filtered-search-box-input-container { flex: 1; position: relative; - // Fix PhantomJS not supporting `flex: 1;` properly. - // This is important because it can change the expected `e.target` when clicking things in tests. - // See https://gitlab.com/gitlab-org/gitlab-ce/blob/b54acba8b732688c59fe2f38510c469dc86ee499/spec/features/issues/filtered_search/visual_tokens_spec.rb#L61 - // - With `width: 100%`: `e.target` = `.tokens-container`, https://i.imgur.com/jGq7wbx.png - // - Without `width: 100%`: `e.target` = `.filtered-search`, https://i.imgur.com/cNI2CyT.png - width: 100%; min-width: 0; } @@ -469,10 +463,10 @@ word-break: break-all; } } -} -.filter-dropdown-item.droplab-item-active .btn { - @extend %filter-dropdown-item-btn-hover; + &.droplab-item-active .btn { + @extend %filter-dropdown-item-btn-hover; + } } .filter-dropdown-loading { diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 62ba74ff582..5d777f0d468 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -352,7 +352,77 @@ .header-user .dropdown-menu-nav, .header-new .dropdown-menu-nav { - margin-top: $dropdown-vertical-offset; + margin-top: 4px; +} + +.search { + margin: 4px 8px 0; + + form { + height: 32px; + border: 0; + border-radius: $border-radius-default; + transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s; + + &:hover { + box-shadow: none; + } + } + + .search-input { + color: $white-light; + background: none; + transition: color ease-in-out 0.15s; + } + + .search-input::placeholder { + transition: color ease-in-out 0.15s; + } + + .location-badge { + font-size: 12px; + margin: -4px 4px -4px -4px; + line-height: 25px; + padding: 4px 8px; + border-radius: 2px 0 0 2px; + height: 32px; + transition: border-color ease-in-out 0.15s; + } + + &.search-active { + form { + background-color: rgba($indigo-200, .3); + box-shadow: none; + + .search-input { + color: $gl-text-color; + transition: color ease-in-out 0.15s; + } + + .search-input::placeholder { + color: $gl-text-color-tertiary; + } + + .search-input-wrap { + .search-icon, + .clear-icon { + color: $gl-text-color-tertiary; + transition: color ease-in-out 0.15s; + } + } + } + + .location-badge { + background-color: $nav-badge-bg; + border-color: $border-color; + } + + .search-input-wrap { + .clear-icon { + color: $white-light; + } + } + } } .breadcrumbs { diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 69d19ea2962..cb324ccc440 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -30,10 +30,10 @@ body { .container { padding-top: 0; z-index: 5; -} -.container .content { - margin: 0; + .content { + margin: 0; + } } .navless-container { @@ -82,26 +82,26 @@ body { transition: background-color 0.15s, border-color 0.15s; background-color: $orange-500; border-color: $orange-500; - } - .alert-warning + .alert-warning { - background-color: $orange-600; - border-color: $orange-600; - } + &:only-of-type { + background-color: $orange-500; + border-color: $orange-500; + } - .alert-warning + .alert-warning + .alert-warning { - background-color: $orange-700; - border-color: $orange-700; - } + + .alert-warning { + background-color: $orange-600; + border-color: $orange-600; - .alert-warning + .alert-warning + .alert-warning + .alert-warning { - background-color: $orange-800; - border-color: $orange-800; - } + + .alert-warning { + background-color: $orange-700; + border-color: $orange-700; - .alert-warning:only-of-type { - background-color: $orange-500; - border-color: $orange-500; + + .alert-warning { + background-color: $orange-800; + border-color: $orange-800; + } + } + } } } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index d43f998cb82..511608c618c 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -299,40 +299,40 @@ ul.indent-list { } } -.group-list-tree .avatar-container.content-loading { - position: relative; +.group-list-tree { + .avatar-container.content-loading { + position: relative; - > a, - > a .avatar { - height: 100%; - border-radius: 50%; - } + > a, + > a .avatar { + height: 100%; + border-radius: 50%; + } - > a { - padding: 2px; - } + > a { + padding: 2px; - > a .avatar { - border: 2px solid $white-normal; + .avatar { + border: 2px solid $white-normal; - &.identicon { - line-height: 30px; + &.identicon { + line-height: 30px; + } + } } - } - &::after { - content: ""; - position: absolute; - height: 100%; - width: 100%; - background-color: transparent; - border: 2px outset $kdb-border; - border-radius: 50%; - animation: spin-avatar 3s infinite linear; + &::after { + content: ""; + position: absolute; + height: 100%; + width: 100%; + background-color: transparent; + border: 2px outset $kdb-border; + border-radius: 50%; + animation: spin-avatar 3s infinite linear; + } } -} -.group-list-tree { .folder-toggle-wrap { float: left; line-height: $list-text-height; diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index e3920b5d3d9..0a5a16c09b0 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -173,21 +173,8 @@ ul > li { white-space: nowrap; } -} - -@media(max-width: $screen-xs-max) { - .atwho-view-ul { - width: 350px; - } - - .atwho-view ul li { - overflow: hidden; - text-overflow: ellipsis; - } -} -// TODO: fallback to global style -.atwho-view { + // TODO: fallback to global style .atwho-view-ul { padding: 8px 1px; @@ -220,3 +207,14 @@ } } } + +@media(max-width: $screen-xs-max) { + .atwho-view-ul { + width: 350px; + } + + .atwho-view ul li { + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/app/assets/stylesheets/framework/responsive-tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss index 8e653c443cf..8b7afdbe1a5 100644 --- a/app/assets/stylesheets/framework/responsive-tables.scss +++ b/app/assets/stylesheets/framework/responsive_tables.scss @@ -3,57 +3,77 @@ max-width: #{$max + '%'}; } +.gl-responsive-table-row-layout { + width: 100%; + + @media (min-width: $screen-md-min) { + display: flex; + align-items: center; + + & > &:not(:first-child) { + margin-top: $gl-padding; + } + } +} + .gl-responsive-table-row { + @extend .gl-responsive-table-row-layout; margin-top: 10px; border: 1px solid $border-color; @media (min-width: $screen-md-min) { - padding: 15px 0; margin: 0; - display: flex; - align-items: center; + padding: $gl-padding 0; border: none; - border-bottom: 1px solid $white-normal; + + &:not(:last-child) { + border-bottom: 1px solid $white-normal; + } } +} - .table-section { - white-space: nowrap; +.gl-responsive-table-row-col-span { + flex-wrap: wrap; +} + +.table-section { + white-space: nowrap; - $section-widths: 10 15 20 25 30 40; - @each $width in $section-widths { - &.section-#{$width} { - flex: 0 0 #{$width + '%'}; + $section-widths: 10 15 20 25 30 40 100; + @each $width in $section-widths { + &.section-#{$width} { + flex: 0 0 #{$width + '%'}; - @media (min-width: $screen-md-min) { - max-width: #{$width + '%'}; - } + @media (min-width: $screen-md-min) { + max-width: #{$width + '%'}; } } + } - &:not(.table-button-footer) { - @media (max-width: $screen-sm-max) { - display: flex; - align-self: stretch; - padding: 10px; - align-items: center; - min-height: 62px; + @media (max-width: $screen-sm-max) { + display: flex; + align-self: stretch; + padding: 10px; + align-items: center; + min-height: 62px; - &:not(:first-of-type) { - border-top: 1px solid $white-normal; - } - } + &:not(:first-child) { + border-top: 1px solid $white-normal; } + } - &.section-wrap { - white-space: normal; + &.section-wrap { + white-space: normal; - @media (max-width: $screen-sm-max) { - flex-wrap: wrap; - } + @media (max-width: $screen-sm-max) { + flex-wrap: wrap; } } -} + &.section-align-top { + align-self: flex-start; + } +} .table-button-footer { @media (min-width: $screen-md-min) { @@ -61,12 +81,13 @@ } @media (max-width: $screen-sm-max) { - background-color: $gray-normal; + display: block; align-self: stretch; + min-height: 0; + background-color: $gray-normal; border-top: 1px solid $border-color; .table-action-buttons { - padding: 10px 5px; display: flex; .btn { @@ -77,7 +98,14 @@ > .external-url, > .btn { flex: 1 1 28px; - margin: 0 5px; + + &:not(:first-child) { + margin-left: 5px; + } + + &:not(:last-child) { + margin-right: 5px; + } } .dropdown-new { diff --git a/app/assets/stylesheets/framework/secondary-navigation-elements.scss b/app/assets/stylesheets/framework/secondary-navigation-elements.scss index 3fd2549b143..9e1f77e5726 100644 --- a/app/assets/stylesheets/framework/secondary-navigation-elements.scss +++ b/app/assets/stylesheets/framework/secondary-navigation-elements.scss @@ -340,11 +340,64 @@ } } -.project-item-select-holder.btn-group { - display: flex; - max-width: 350px; - overflow: hidden; - float: right; +.page-with-layout-nav { + .right-sidebar { + top: ($header-height + 1) * 2; + } + + &.page-with-sub-nav { + .right-sidebar { + top: ($header-height + 1) * 3; + + &.affix { + top: $header-height; + } + } + } +} + +.with-performance-bar .page-with-layout-nav { + .right-sidebar { + top: ($header-height + 1) * 2 + $performance-bar-height; + } + + &.page-with-sub-nav { + .right-sidebar { + top: ($header-height + 1) * 3 + $performance-bar-height; + + &.affix { + top: $header-height + $performance-bar-height; + } + } + } +} + +@media (max-width: $screen-xs-max) { + .top-area { + flex-flow: row wrap; + + .nav-controls { + $controls-margin: $btn-xs-side-margin - 2px; + flex: 0 0 100%; + + &.controls-flex { + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: center; + padding: 0 0 $gl-padding-top; + } + + .controls-item, + .controls-item-full, + .controls-item:last-child { + flex: 1 1 35%; + display: block; + width: 100%; + margin: $controls-margin; + } + } + } .new-project-item-link { white-space: nowrap; diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 621eec4f158..aa35cd9bea4 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -60,22 +60,12 @@ border-radius: $border-radius-base; border: 1px solid $dropdown-border-color; min-width: 175px; - color: $gl-text-color; - z-index: 999; + color: $gl-grayish-blue; } -.select2-drop-mask { - z-index: 998; -} - -.select2-drop.select2-drop-above.select2-drop-active { - border-top: 1px solid $dropdown-border-color; - margin-top: -6px; -} - -.select2-results li.select2-result-with-children > .select2-result-label { - font-weight: $gl-font-weight-bold; - color: $gl-text-color; +.select2-results .select2-result-label, +.select2-more-results { + padding: 10px 15px; } .select2-container-active { @@ -144,58 +134,46 @@ .select2-drop-auto-width & { padding: 15px 15px 5px; } -} -.select2-search input { - padding: 2px 25px 2px 5px; - background: $white-light image-url('select2.png'); - background-repeat: no-repeat; - background-position: right 0 bottom 6px; - border: 1px solid $input-border; - border-radius: $border-radius-default; - transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; - - &:focus { - border-color: $input-border-focus; + input { + padding: 2px 25px 2px 5px; + background: $white-light image-url('select2.png'); + background-repeat: no-repeat; + background-position: right 0 bottom 6px; + border: 1px solid $input-border; + border-radius: $border-radius-default; + transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; + + &:focus { + border-color: $input-border-focus; + } + + &.select2-active { + background-color: $white-light; + background-image: image-url('select2-spinner.gif') !important; + background-repeat: no-repeat; + background-position: right 5px center !important; + background-size: 16px 16px !important; + } } } -.select2-search input.select2-active { - background-color: $white-light; - background-image: image-url('select2-spinner.gif') !important; - background-repeat: no-repeat; - background-position: right 5px center !important; - background-size: 16px 16px !important; +.select2-results .select2-no-results, +.select2-results .select2-searching, +.select2-results .select2-ajax-error, +.select2-results .select2-selection-limit { + background: $gray-light; + display: list-item; + padding: 10px 15px; } .select2-results { margin: 0; - padding: #{$gl-padding / 2} 0; - - .select2-no-results, - .select2-searching, - .select2-ajax-error, - .select2-selection-limit { - background: transparent; - padding: #{$gl-padding / 2} $gl-padding; - } - - .select2-result-label, - .select2-more-results { - padding: #{$gl-padding / 2} $gl-padding; - } + padding: 10px 0; - .select2-highlighted { - background: transparent; + li.select2-result-with-children > .select2-result-label { + font-weight: $gl-font-weight-bold; color: $gl-text-color; - - .select2-result-label { - background: $dropdown-item-hover-bg; - } - } - - .select2-result { - padding: 0 1px; } } @@ -212,6 +190,8 @@ } .select2-highlighted { + background: $gl-link-color !important; + .group-result { .group-path { color: $white-light; diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 65b140cd7f8..c3d8f0c61a2 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -217,13 +217,31 @@ $white-gc-bg: #eaf2f5; .cp { color: $white-cp; font-weight: $gl-font-weight-bold; } .c1 { color: $white-c1; font-style: italic; } .cs { color: $white-cs; font-weight: $gl-font-weight-bold; font-style: italic; } - .gd { color: $white-gd; background-color: $white-gd-bg; } - .gd .x { color: $white-gd-x; background-color: $white-gd-x-bg; } + + .gd { + color: $white-gd; + background-color: $white-gd-bg; + + .x { + color: $white-gd-x; + background-color: $white-gd-x-bg; + } + } + .ge { font-style: italic; } .gr { color: $white-gr; } .gh { color: $white-gh; } - .gi { color: $white-gi; background-color: $white-gi-bg; } - .gi .x { color: $white-gi-x; background-color: $white-gi-x-bg; } + + .gi { + color: $white-gi; + background-color: $white-gi-bg; + + .x { + color: $white-gi-x; + background-color: $white-gi-x-bg; + } + } + .go { color: $white-go; } .gp { color: $white-gp; } .gs { font-weight: $gl-font-weight-bold; } diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss index fbe538ad1d7..658ac26fca9 100644 --- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss +++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss @@ -158,13 +158,31 @@ span.highlight_word { .cp { color: $highlighted-cp; font-weight: $gl-font-weight-bold; } .c1 { color: $highlighted-c1; font-style: italic; } .cs { color: $highlighted-cs; font-weight: $gl-font-weight-bold; font-style: italic; } -.gd { color: $highlighted-gd; background-color: $highlighted-gd-bg; } -.gd .x { color: $highlighted-gd; background-color: $highlighted-gd-x-bg; } + +.gd { + color: $highlighted-gd; + background-color: $highlighted-gd-bg; + + .x { + color: $highlighted-gd; + background-color: $highlighted-gd-x-bg; + } +} + .ge { font-style: italic; } .gr { color: $highlighted-gr; } .gh { color: $highlighted-gh; } -.gi { color: $highlighted-gi; background-color: $highlighted-gi-bg; } -.gi .x { color: $highlighted-gi; background-color: $highlighted-gi-x-bg; } + +.gi { + color: $highlighted-gi; + background-color: $highlighted-gi-bg; + + .x { + color: $highlighted-gi; + background-color: $highlighted-gi-x-bg; + } +} + .go { color: $highlighted-go; } .gp { color: $highlighted-gp; } .gs { font-weight: $gl-font-weight-bold; } diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 91296b354a7..3683afa07de 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -72,7 +72,7 @@ } .boards-list { - height: calc(100vh - 152px); + height: calc(100vh - 105px); width: 100%; padding-top: 25px; padding-bottom: 25px; @@ -81,11 +81,12 @@ overflow-x: scroll; white-space: nowrap; - @media (min-width: $screen-sm-min) { - height: 475px; // Needed for PhantomJS - // scss-lint:disable DuplicateProperty - height: calc(100vh - 222px); - // scss-lint:enable DuplicateProperty + @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + height: calc(100vh - 90px); + } + + @media (min-width: $screen-md-min) { + height: calc(100vh - 160px); min-height: 475px; } } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 50ec5110bf1..46978be8ba0 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -68,18 +68,18 @@ &.affix { top: $header-height; - } - // with sidebar - &.affix.sidebar-expanded { - right: 306px; - left: 16px; - } + // with sidebar + &.sidebar-expanded { + right: 306px; + left: 16px; + } - // without sidebar - &.affix.sidebar-collapsed { - right: 16px; - left: 16px; + // without sidebar + &.sidebar-collapsed { + right: 16px; + left: 16px; + } } &.affix-top { @@ -333,8 +333,10 @@ svg { position: relative; - top: 2px; + top: 3px; margin-right: 3px; + width: 14px; + height: 14px; } } @@ -348,9 +350,10 @@ svg { position: relative; - top: 2px; + top: 3px; margin-right: 3px; - height: 13px; + height: 14px; + width: 14px; } a { @@ -369,7 +372,7 @@ .build-job { position: relative; - .fa-arrow-right { + .icon-arrow-right { position: absolute; left: 15px; top: 20px; @@ -379,7 +382,7 @@ &.active { font-weight: $gl-font-weight-bold; - .fa-arrow-right { + .icon-arrow-right { display: block; } } @@ -392,8 +395,7 @@ background-color: $row-hover; } - .fa-refresh { - font-size: 13px; + .icon-retry { margin-left: 3px; } } diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index 8d6f30e3b84..5c91579c69c 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -2,8 +2,4 @@ .clipboard-addon { background-color: $white-light; } - - .alert-block { - margin-bottom: 10px; - } } diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 2a92673d9fa..82d9be29201 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -22,6 +22,11 @@ } } } + + svg { + width: 136px; + height: 136px; + } } .col-headers { @@ -155,11 +160,6 @@ } } - .landing svg { - width: 136px; - height: 136px; - } - .fa-spinner { font-size: 28px; position: relative; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 09f831dcb29..faa3d1fb4d5 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -380,6 +380,10 @@ } } } + + .line_content { + white-space: pre-wrap; + } } .file-content .diff-file { @@ -387,10 +391,6 @@ border: none; } -.diff-file .line_content { - white-space: pre-wrap; -} - .diff-wrap-lines .line_content { white-space: pre-wrap; } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 3b5e411e2c5..b5b0f3d9dfa 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -133,12 +133,11 @@ } .folder-row { - padding: 15px 0; - border-bottom: 1px solid $white-normal; + border-left: none; + border-right: none; - @media (max-width: $screen-sm-max) { - border-top: 1px solid $white-normal; - margin-top: 10px; + @media (min-width: $screen-sm-max) { + border-top: none; } } @@ -256,23 +255,6 @@ width: 100%; padding: 0; padding-bottom: 100%; -} - -.prometheus-svg-container > svg { - position: absolute; - height: 100%; - width: 100%; - left: 0; - top: 0; - - text { - fill: $gl-text-color; - stroke-width: 0; - } - - .text-metric-bold { - font-weight: $gl-font-weight-bold; - } .label-axis-text { fill: $black; @@ -287,42 +269,51 @@ font-size: 12px; } - .legend-axis-text { - fill: $black; - } + > svg { + position: absolute; + height: 100%; + width: 100%; + left: 0; + top: 0; - .tick { - > line { - stroke: $gray-darker; + .label-axis-text, + .text-metric-usage { + fill: $black; + font-weight: $gl-font-weight-normal; + font-size: 12px; } - > text { - font-size: 12px; + .legend-axis-text { + fill: $black; } - } - .text-metric-title { - font-size: 12px; - } + .tick > text { + font-size: 12px; + } - .y-label-text, - .x-label-text { - fill: $gray-darkest; - } + .text-metric-title { + font-size: 12px; + } - .axis-tick { - stroke: $gray-darker; - } + .y-label-text, + .x-label-text { + fill: $gray-darkest; + } - @media (max-width: $screen-sm-max) { - .label-axis-text, - .text-metric-usage, - .legend-axis-text { - font-size: 8px; + .axis-tick { + stroke: $gray-darker; } - .tick > text { - font-size: 8px; + @media (max-width: $screen-sm-max) { + .label-axis-text, + .text-metric-usage, + .legend-axis-text { + font-size: 8px; + } + + .tick > text { + font-size: 8px; + } } } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 88600a0e6d3..7059a4cfe85 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -127,7 +127,16 @@ } .right-sidebar { - a:not(.btn-retry), + position: absolute; + top: $header-height; + bottom: 0; + right: 0; + transition: width .3s; + background: $gray-light; + z-index: 200; + overflow: hidden; + + a, .btn-link { color: inherit; } @@ -228,17 +237,6 @@ .btn-clipboard:hover { color: $gl-text-color; } -} - -.right-sidebar { - position: absolute; - top: $header-height; - bottom: 0; - right: 0; - transition: width $right-sidebar-transition-duration; - background: $gray-light; - z-index: 200; - overflow: hidden; .issuable-sidebar { width: calc(100% + 100px); diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index cf5f933a762..92d49bd864a 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -109,6 +109,30 @@ border-top-right-radius: $border-radius-default; border-top-left-radius: $border-radius-default; + // Ldap configurations may need more tabs & the tab labels are user generated (arbitrarily long). + // These styles prevent this from breaking the layout, and only applied when providers are configured. + &.custom-provider-tabs { + flex-wrap: wrap; + + li { + min-width: 85px; + flex-basis: auto; + + // This styles tab elements that have wrapped to a second line. We cannot easily predict when this will happen. + // We are making somewhat of an assumption about the configuration here: that users do not have more than + // 3 LDAP servers configured (in addition to standard login) and they are not using especially long names for any + // of them. If either condition is false, this will work as expected. If both are true, there may be a missing border + // above one of the bottom row elements. If you know a better way, please implement it! + &:nth-child(n+5) { + border-top: 1px solid $border-color; + } + } + + a { + font-size: 16px; + } + } + li { flex: 1; text-align: center; @@ -154,32 +178,6 @@ } } - // Ldap configurations may need more tabs & the tab labels are user generated (arbitrarily long). - // These styles prevent this from breaking the layout, and only applied when providers are configured. - - .new-session-tabs.custom-provider-tabs { - flex-wrap: wrap; - - li { - min-width: 85px; - flex-basis: auto; - - // This styles tab elements that have wrapped to a second line. We cannot easily predict when this will happen. - // We are making somewhat of an assumption about the configuration here: that users do not have more than - // 3 LDAP servers configured (in addition to standard login) and they are not using especially long names for any - // of them. If either condition is false, this will work as expected. If both are true, there may be a missing border - // above one of the bottom row elements. If you know a better way, please implement it! - &:nth-child(n+5) { - border-top: 1px solid $border-color; - } - } - - a { - font-size: 16px; - } - } - - .form-control { &:active, &:focus { @@ -231,35 +229,35 @@ margin: 0; padding: 0; height: 100%; -} -// Fixes footer container to bottom of viewport -.devise-layout-html body { - // offset height of fixed header + 1 to avoid scroll - height: calc(100% - 51px); - margin: 0; - padding: 0; + // Fixes footer container to bottom of viewport + body { + // offset height of fixed header + 1 to avoid scroll + height: calc(100% - 51px); + margin: 0; + padding: 0; - .page-wrap { - min-height: 100%; - position: relative; - } + .page-wrap { + min-height: 100%; + position: relative; + } - .footer-container, - hr.footer-fixed { - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 40px; - background: $white-light; - } + .footer-container, + hr.footer-fixed { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 40px; + background: $white-light; + } - .navless-container { - padding: 65px 15px; // height of footer + bottom padding of email confirmation link + .navless-container { + padding: 65px 15px; // height of footer + bottom padding of email confirmation link - @media (max-width: $screen-xs-max) { - padding: 0 15px 65px; + @media (max-width: $screen-xs-max) { + padding: 0 15px 65px; + } } } } diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index 692acf74a58..18c48405ecd 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -49,9 +49,17 @@ width: auto; } } + + &.existing-title { + @media (min-width: $screen-sm-min) { + float: left; + } + } } .member-form-control { + @include new-style-dropdown; + @media (max-width: $screen-xs-max) { padding-bottom: 5px; margin-left: 0; @@ -64,12 +72,6 @@ line-height: 43px; } -.member.existing-title { - @media (min-width: $screen-sm-min) { - float: left; - } -} - .member-search-form { @include new-style-dropdown; @@ -281,7 +283,3 @@ } } } - -.member-form-control { - @include new-style-dropdown; -} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index d9fb3b44d29..6e485ebad1b 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -156,6 +156,10 @@ &.media > *:first-child { margin-right: 10px; } + + .approve-btn { + margin-right: 5px; + } } .mr-widget-pipeline-graph { @@ -165,8 +169,9 @@ z-index: 300; } - .ci-action-icon-wrapper { - line-height: 16px; + .ci-action-icon-wrapper svg { + width: 16px; + height: 16px; } } @@ -190,6 +195,10 @@ overflow: hidden; word-break: break-all; + &.media > *:first-child { + margin-right: 10px; + } + &.label-truncated { position: relative; display: inline-block; @@ -207,14 +216,7 @@ background-color: $gray-light; } } - } - .mr-widget-help { - padding: 10px 16px 10px 48px; - font-style: italic; - } - - .mr-widget-body { h4 { float: left; font-weight: $gl-font-weight-bold; @@ -237,6 +239,10 @@ margin-right: 7px; } + .approve-btn { + margin-right: 5px; + } + label { font-weight: $gl-font-weight-normal; } @@ -336,6 +342,22 @@ } } + .mini-pipeline-graph-dropdown-menu .mini-pipeline-graph-dropdown-item { + display: flex; + align-items: center; + + .ci-status-text, + .ci-status-icon { + top: 0; + margin-right: 10px; + } + } + + .mr-widget-help { + padding: 10px 16px 10px 48px; + font-style: italic; + } + .ci-coverage { float: right; } @@ -350,12 +372,6 @@ } } -.mr-state-widget .mr-widget-body { - .approve-btn { - margin-right: 5px; - } -} - .mr-widget-body-controls { flex-wrap: wrap; } @@ -469,16 +485,16 @@ padding-bottom: 0; } } -} -.mr-info-list.mr-memory-usage { - p { - float: left; - } + &.mr-memory-usage { + p { + float: left; + } - .memory-graph-container { - float: left; - margin-left: 5px; + .memory-graph-container { + float: left; + margin-left: 5px; + } } } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 32039936be7..ae8fa45a2d7 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -66,6 +66,15 @@ height: 6px; margin: 0; } + + .sidebar-collapsed-icon { + clear: both; + padding: 15px 5px 5px; + + .progress { + margin: 5px 0; + } + } } .collapsed-milestone-date { @@ -93,17 +102,6 @@ margin-right: 0; } - .milestone-progress { - .sidebar-collapsed-icon { - clear: both; - padding: 15px 5px 5px; - - .progress { - margin: 5px 0; - } - } - } - .right-sidebar-collapsed & { .reference { border-top: 1px solid $border-gray-normal; @@ -156,18 +154,16 @@ .status-box { margin-top: 0; - } - - .milestone-buttons { - margin-left: auto; - } - - .status-box { order: 1; } .milestone-buttons { + margin-left: auto; order: 2; + + .verbose { + display: none; + } } .header-text-content { @@ -175,10 +171,6 @@ width: 100%; } - .milestone-buttons .verbose { - display: none; - } - @media (min-width: $screen-xs-min) { .milestone-buttons .verbose { display: inline; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index f0cad30f4f3..5127307c5e7 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -111,24 +111,9 @@ margin: auto; align-items: center; - .icon { - margin-right: $issuable-warning-icon-margin; - } -} - -.disabled-comment .issuable-note-warning { - border: none; - border-radius: $label-border-radius; - padding-top: $gl-vert-padding; - padding-bottom: $gl-vert-padding; - - .icon svg { - position: relative; - top: 2px; - margin-right: $btn-xs-side-margin; - width: $gl-font-size; - height: $gl-font-size; - fill: $orange-600; + + .md-area { + border-top-left-radius: 0; + border-top-right-radius: 0; } } @@ -155,11 +140,6 @@ } } -.issuable-note-warning + .md-area { - border-top-left-radius: 0; - border-top-right-radius: 0; -} - .discussion-form { background-color: $white-light; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 312917bd13a..ca363c6eac4 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -312,57 +312,72 @@ ul.notes { } } -.diff-file .notes_holder { - font-family: $regular_font; +.diff-file { + .is-over { + .add-diff-note { + display: inline-block; + } + } - td { - border: 1px solid $white-normal; - border-left: none; + // Merge request notes in diffs + // Diff is inline + .notes_content .note-header .note-headline-light { + display: inline-block; + position: relative; + } - &.notes_line { - vertical-align: middle; - text-align: center; - padding: 10px 0; - background: $gray-light; - color: $text-color; - } + .notes_holder { + font-family: $regular_font; - &.notes_line2 { - text-align: center; - padding: 10px 0; - border-left: 1px solid $note-line2-border !important; - } + td { + border: 1px solid $white-normal; + border-left: none; - &.notes_content { - background-color: $gray-light; - border-width: 1px 0; - padding: 0; - vertical-align: top; - white-space: normal; + &.notes_line { + vertical-align: middle; + text-align: center; + padding: 10px 0; + background: $gray-light; + color: $text-color; + } - &.parallel { - border-width: 1px; + &.notes_line2 { + text-align: center; + padding: 10px 0; + border-left: 1px solid $note-line2-border !important; } - .discussion-notes { - &:not(:first-child) { - border-top: 1px solid $white-normal; - margin-top: 20px; + &.notes_content { + background-color: $gray-light; + border-width: 1px 0; + padding: 0; + vertical-align: top; + white-space: normal; + + &.parallel { + border-width: 1px; } - &:not(:last-child) { - border-bottom: 1px solid $white-normal; - margin-bottom: 20px; + .discussion-notes { + &:not(:first-child) { + border-top: 1px solid $white-normal; + margin-top: 20px; + } + + &:not(:last-child) { + border-bottom: 1px solid $white-normal; + margin-bottom: 20px; + } } - } - .notes { - background-color: $white-light; - } + .notes { + background-color: $white-light; + } - a code { - top: 0; - margin-right: 0; + a code { + top: 0; + margin-right: 0; + } } } } @@ -457,8 +472,9 @@ ul.notes { margin-left: 10px; color: $gray-darkest; - .btn-group > .discussion-next-btn { - margin-left: -1px; + @include notes-media('max', $screen-md-max) { + float: none; + margin-left: 0; } } @@ -469,8 +485,6 @@ ul.notes { flex-shrink: 0; display: inline-flex; align-items: center; - // For PhantomJS that does not support flex - float: right; margin-left: 10px; color: $gray-darkest; @@ -481,7 +495,6 @@ ul.notes { } .more-actions { - float: right; // phantomjs fallback display: flex; align-items: flex-end; @@ -502,13 +515,6 @@ ul.notes { min-width: 180px; } -.discussion-actions { - @include notes-media('max', $screen-md-max) { - float: none; - margin-left: 0; - } -} - .note-actions-item { margin-left: 12px; display: flex; @@ -665,14 +671,6 @@ ul.notes { } } -.diff-file { - .is-over { - .add-diff-note { - display: inline-block; - } - } -} - .disabled-comment { background-color: $gray-light; border-radius: $border-radius-base; @@ -714,20 +712,20 @@ ul.notes { svg path { fill: $gray-darkest; } - } - .btn.discussion-create-issue-btn { - margin-left: -4px; - border-radius: 0; - border-right: 0; + &.discussion-create-issue-btn { + margin-left: -4px; + border-radius: 0; + border-right: 0; - a { - padding: 0; - line-height: 0; + a { + padding: 0; + line-height: 0; - &:hover { - text-decoration: none; - border: 0; + &:hover { + text-decoration: none; + border: 0; + } } } } @@ -801,12 +799,3 @@ ul.notes { .line-resolve-text { vertical-align: middle; } - -// Merge request notes in diffs -.diff-file { - // Diff is inline - .notes_content .note-header .note-headline-light { - display: inline-block; - position: relative; - } -} diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 8fc7a5eec9b..2a8cbc61af7 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -31,7 +31,6 @@ } .pipeline-actions { - padding-right: 0; min-width: 170px; //Guarantees buttons don't break in several lines. .btn-default { @@ -176,6 +175,25 @@ } } + /** + * Play button with icon in dropdowns + */ + .no-btn { + border: none; + background: none; + outline: none; + width: 100%; + text-align: left; + + .icon-play { + position: relative; + top: 2px; + margin-right: 5px; + height: 13px; + width: 12px; + } + } + .duration, .finished-at { color: $gl-text-color-secondary; @@ -451,36 +469,46 @@ @extend .build-content:hover; } - // Action Icons in big pipeline-graph nodes - .ci-action-icon-container .ci-action-icon-wrapper { - height: 30px; - width: 30px; - background: $white-light; - border: 1px solid $border-color; - border-radius: 100%; - display: block; - - &:hover { - background-color: $stage-hover-bg; - border: 1px solid $dropdown-toggle-active-border-color; - } - - svg { - fill: $gl-text-color-secondary; - position: relative; - left: -1px; - top: -1px; - } - - &:hover svg { - fill: $gl-text-color; - } - } - .ci-action-icon-container { position: absolute; right: 5px; top: 5px; + + // Action Icons in big pipeline-graph nodes + &.ci-action-icon-wrapper { + height: 30px; + width: 30px; + background: $white-light; + border: 1px solid $border-color; + border-radius: 100%; + display: block; + + &:hover { + background-color: $stage-hover-bg; + border: 1px solid $dropdown-toggle-active-border-color; + + svg { + fill: $gl-text-color; + } + } + + svg { + fill: $gl-text-color-secondary; + position: relative; + left: 5px; + top: 2px; + width: 18px; + height: 18px; + } + + &.play { + svg { + width: #{$ci-action-icon-size - 8}; + height: #{$ci-action-icon-size - 8}; + left: 8px; + } + } + } } .ci-status-icon svg { @@ -721,17 +749,50 @@ button.mini-pipeline-graph-dropdown-toggle { svg { fill: $gl-text-color-secondary; - width: $ci-action-icon-size; - height: $ci-action-icon-size; - left: -6px; + width: #{$ci-action-icon-size - 6}; + height: #{$ci-action-icon-size - 6}; + left: -3px; position: relative; - top: -3px; + top: -2px; + + &.icon-action-stop, + &.icon-action-cancel { + width: 12px; + height: 12px; + top: 1px; + left: -1px; + } + + &.icon-action-play { + width: 11px; + height: 11px; + top: 1px; + left: 1px; + } + + &.icon-action-retry { + width: 16px; + height: 16px; + top: 0; + left: -3px; + } } &:hover svg, &:focus svg { fill: $gl-text-color; } + + &.icon-action-retry, + &.icon-action-play { + svg { + width: #{$ci-action-icon-size - 6}; + height: #{$ci-action-icon-size - 6}; + left: 8px; + } + } + + } // link to the build @@ -799,13 +860,10 @@ button.mini-pipeline-graph-dropdown-toggle { left: 100%; top: -10px; box-shadow: 0 1px 5px $black-transparent; -} - -/** - * Top arrow in the dropdown in the big pipeline graph - */ -.big-pipeline-graph-dropdown-menu { + /** + * Top arrow in the dropdown in the big pipeline graph + */ &::before, &::after { content: ''; @@ -867,22 +925,23 @@ button.mini-pipeline-graph-dropdown-toggle { margin-top: 1px; border-bottom-color: $white-light; } -} -/** - * Center dropdown menu in mini graph - */ -.mini-pipeline-graph-dropdown-menu.dropdown-menu { - transform: translate(-80%, 0); - min-width: 150px; + /** + * Center dropdown menu in mini graph + */ + &.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; + @media(min-width: $screen-md-min) { + transform: translate(-50%, 0); + right: auto; + left: 50%; + min-width: 240px; + } } } + /** * Terminal */ @@ -906,25 +965,6 @@ button.mini-pipeline-graph-dropdown-toggle { } } -/** - * Play button with icon in dropdowns - */ -.ci-table .no-btn { - border: none; - background: none; - outline: none; - width: 100%; - text-align: left; - - .icon-play { - position: relative; - top: 2px; - margin-right: 5px; - height: 13px; - width: 12px; - } -} - .ci-header-container { min-height: 55px; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index bd385db9692..b0c3474e3d5 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -88,7 +88,8 @@ transition: background 2s ease-out; &:disabled { - opacity: 0.75; + opacity: 0.5; + pointer-events: none; } .highlight-changes & { @@ -778,35 +779,35 @@ a.deploy-project-label { .nav { padding-top: 12px; padding-bottom: 12px; - } - .nav > li { - display: inline-block; + > li { + display: inline-block; - &:not(:last-child) { - margin-right: $gl-padding; - } + &:not(:last-child) { + margin-right: $gl-padding; + } - &.right { - vertical-align: top; - margin-top: 0; + &.right { + vertical-align: top; + margin-top: 0; - @media (min-width: $screen-lg-min) { - float: right; + @media (min-width: $screen-lg-min) { + float: right; + } } - } - } - .nav > li > a { - padding: 0; - background-color: transparent; - font-size: 14px; - line-height: 29px; - color: $notes-light-color; + > a { + padding: 0; + background-color: transparent; + font-size: 14px; + line-height: 29px; + color: $notes-light-color; - &:hover, - &:focus { - color: $gl-text-color; + &:hover, + &:focus { + color: $gl-text-color; + } + } } } @@ -1160,13 +1161,6 @@ pre.light-well { } } -.project-repo-select { - &.disabled { - opacity: 0.5; - pointer-events: none; - } -} - .variables-table { table-layout: fixed; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index db0a04a5eb3..eed711b1b66 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -78,6 +78,10 @@ input[type="checkbox"]:hover { } .search-input-wrap { + // Fallback if flexbox is not supported + display: inline-block; + width: 100%; + .search-icon, .clear-icon { position: absolute; diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 968a94c68cf..8b9b47a41bc 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -241,11 +241,11 @@ margin-left: 5px; background: $badge-bg; } - } - /* Ensure we don't add border if there's only single li */ - li + li { - border-top: 1px solid $border-color; + /* Ensure we don't add border if there's only single li */ + + li { + border-top: 1px solid $border-color; + } } } } diff --git a/app/assets/stylesheets/pages/sherlock.scss b/app/assets/stylesheets/pages/sherlock.scss index bfe065dbbaf..2bf0bedb1f5 100644 --- a/app/assets/stylesheets/pages/sherlock.scss +++ b/app/assets/stylesheets/pages/sherlock.scss @@ -5,10 +5,10 @@ table .sherlock-code { .sherlock-code { pre { word-wrap: normal; - } - pre code { - white-space: pre; + code { + white-space: pre; + } } } @@ -21,13 +21,13 @@ table .sherlock-code { text-align: right; padding: 0 10px !important; } + + .slow { + color: $red-500; + font-weight: $gl-font-weight-bold; + } } .sherlock-file-sample pre { padding-top: 28px !important; } - -.sherlock-line-samples-table .slow { - color: $red-500; - font-weight: $gl-font-weight-bold; -} diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss index dfa4d033fb8..cede147d559 100644 --- a/app/assets/stylesheets/pages/stat_graph.scss +++ b/app/assets/stylesheets/pages/stat_graph.scss @@ -40,16 +40,16 @@ @media (max-width: $screen-xs-max) { width: 100%; } - } - .person .spark { - display: block; - background: $stat-graph-common-bg; - width: 100%; - } + .spark { + display: block; + background: $stat-graph-common-bg; + width: 100%; + } - .person .area-contributor { - fill: $stat-graph-orange-fill; + .area-contributor { + fill: $stat-graph-orange-fill; + } } } diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index b7d4e7bf582..e150f96f3fa 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -161,10 +161,10 @@ ul.wiki-pages-list.content-list { list-style: none; margin-left: 0; padding-left: 15px; - } - ul li { - padding: 5px 0; + li { + padding: 5px 0; + } } } diff --git a/app/assets/stylesheets/test.scss b/app/assets/stylesheets/test.scss index 06733b7f1a9..e65b49c36f3 100644 --- a/app/assets/stylesheets/test.scss +++ b/app/assets/stylesheets/test.scss @@ -4,11 +4,6 @@ -ms-transition: none !important; -webkit-transition: none !important; transition: none !important; - -o-transform: none !important; - -moz-transform: none !important; - -ms-transform: none !important; - -webkit-transform: none !important; - transform: none !important; -webkit-animation: none !important; -moz-animation: none !important; -o-animation: none !important; diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb index fb6d8c0bb81..5be23c76a95 100644 --- a/app/controllers/admin/applications_controller.rb +++ b/app/controllers/admin/applications_controller.rb @@ -19,10 +19,12 @@ class Admin::ApplicationsController < Admin::ApplicationController end def create - @application = Doorkeeper::Application.new(application_params) + @application = Applications::CreateService.new(current_user, application_params).execute(request) - if @application.save - redirect_to_admin_page + if @application.persisted? + flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) + + redirect_to admin_application_url(@application) else render :new end @@ -41,13 +43,6 @@ class Admin::ApplicationsController < Admin::ApplicationController redirect_to admin_applications_url, status: 302, notice: 'Application was successfully destroyed.' end - protected - - def redirect_to_admin_page - flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) - redirect_to admin_application_url(@application) - end - private def set_application diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb index 07c8bf714fc..7a2c7234a1e 100644 --- a/app/controllers/admin/impersonation_tokens_controller.rb +++ b/app/controllers/admin/impersonation_tokens_controller.rb @@ -44,7 +44,7 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController end def set_index_vars - @scopes = Gitlab::Auth::API_SCOPES + @scopes = Gitlab::Auth.available_scopes(current_user) @impersonation_token ||= finder.build @inactive_impersonation_tokens = finder(state: 'inactive').execute diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 391a0519195..3be7aee69bc 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -11,7 +11,7 @@ class ApplicationController < ActionController::Base include EnforcesTwoFactorAuthentication include WithPerformanceBar - before_action :authenticate_user_from_private_token! + before_action :authenticate_user_from_personal_access_token! before_action :authenticate_user_from_rss_token! before_action :authenticate_user! before_action :validate_user_service_ticket! @@ -100,13 +100,12 @@ class ApplicationController < ActionController::Base return try(:authenticated_user) end - # This filter handles both private tokens and personal access tokens - def authenticate_user_from_private_token! + def authenticate_user_from_personal_access_token! token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence return unless token.present? - user = User.find_by_authentication_token(token) || User.find_by_personal_access_token(token) + user = User.find_by_personal_access_token(token) sessionless_sign_in(user) end diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 4079072a930..b1ed973d178 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -7,6 +7,54 @@ module IssuableActions before_action :authorize_admin_issuable!, only: :bulk_update end + def show + respond_to do |format| + format.html do + render show_view + end + format.json do + render json: serializer.represent(issuable, serializer: params[:serializer]) + end + end + end + + def update + @issuable = update_service.execute(issuable) + + respond_to do |format| + format.html do + recaptcha_check_with_fallback { render :edit } + end + + format.json do + render_entity_json + end + end + + rescue ActiveRecord::StaleObjectError + render_conflict_response + end + + def realtime_changes + Gitlab::PollingInterval.set_header(response, interval: 3_000) + + response = { + title: view_context.markdown_field(issuable, :title), + title_text: issuable.title, + description: view_context.markdown_field(issuable, :description), + description_text: issuable.description, + task_status: issuable.task_status + } + + if issuable.edited? + response[:updated_at] = issuable.updated_at + response[:updated_by_name] = issuable.last_edited_by.name + response[:updated_by_path] = user_path(issuable.last_edited_by) + end + + render json: response + end + def destroy issuable.destroy destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym @@ -68,6 +116,10 @@ module IssuableActions end end + def authorize_update_issuable! + render_404 unless can?(current_user, :"update_#{resource_name}", issuable) + end + def bulk_update_params permitted_keys = [ :issuable_ids, @@ -92,4 +144,24 @@ module IssuableActions def resource_name @resource_name ||= controller_name.singularize end + + def render_entity_json + if @issuable.valid? + render json: serializer.represent(@issuable) + else + render json: { errors: @issuable.errors.full_messages }, status: :unprocessable_entity + end + end + + def show_view + 'show' + end + + def serializer + raise NotImplementedError + end + + def update_service + raise NotImplementedError + end end diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb index 2b6afaa6233..738afd612f0 100644 --- a/app/controllers/concerns/lfs_request.rb +++ b/app/controllers/concerns/lfs_request.rb @@ -94,10 +94,9 @@ module LfsRequest @storage_project ||= begin result = project - loop do - break unless result.forked? - result = result.forked_from_project - end + # TODO: Make this go to the fork_network root immeadiatly + # dependant on the discussion in: https://gitlab.com/gitlab-org/gitlab-ce/issues/39769 + result = result.fork_source while result.forked? result end diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 1126f706393..57b45f335fa 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -4,6 +4,7 @@ module NotesActions included do before_action :set_polling_interval_header, only: [:index] + before_action :noteable, only: :index before_action :authorize_admin_note!, only: [:update, :destroy] before_action :note_project, only: [:create] end @@ -108,6 +109,8 @@ module NotesActions diff_discussion_html: diff_discussion_html(discussion), discussion_html: discussion_html(discussion) ) + + attrs[:discussion_line_code] = discussion.line_code if discussion.diff_discussion? end end else @@ -188,7 +191,7 @@ module NotesActions end def noteable - @noteable ||= notes_finder.target + @noteable ||= notes_finder.target || render_404 end def last_fetched_at diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 4bceb1d67a3..7d6fe6a0232 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -30,11 +30,11 @@ class JwtController < ApplicationController render_unauthorized end end - rescue Gitlab::Auth::MissingPersonalTokenError - render_missing_personal_token + rescue Gitlab::Auth::MissingPersonalAccessTokenError + render_missing_personal_access_token end - def render_missing_personal_token + def render_missing_personal_access_token render json: { errors: [ { code: 'UNAUTHORIZED', diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index b02e64a132b..2443f529c7b 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -16,25 +16,18 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController end def create - @application = Doorkeeper::Application.new(application_params) + @application = Applications::CreateService.new(current_user, create_application_params).execute(request) - @application.owner = current_user + if @application.persisted? + flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) - if @application.save - redirect_to_oauth_application_page + redirect_to oauth_application_url(@application) else set_index_vars render :index end end - protected - - def redirect_to_oauth_application_page - flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create]) - redirect_to oauth_application_url(@application) - end - private def verify_user_oauth_applications_enabled @@ -61,4 +54,10 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController rescue_from ActiveRecord::RecordNotFound do |exception| render "errors/not_found", layout: "errors", status: 404 end + + def create_application_params + application_params.tap do |params| + params[:owner] = current_user + end + end end diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index 069e6a810f2..f0e5d2aa94e 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -11,10 +11,10 @@ class Profiles::KeysController < Profiles::ApplicationController end def create - @key = Keys::CreateService.new(current_user, key_params).execute + @key = Keys::CreateService.new(current_user, key_params.merge(ip_address: request.remote_ip)).execute if @key.persisted? - redirect_to_profile_key_path + redirect_to profile_key_path(@key) else @keys = current_user.keys.select(&:persisted?) render :index @@ -50,12 +50,6 @@ class Profiles::KeysController < Profiles::ApplicationController end end - protected - - def redirect_to_profile_key_path - redirect_to profile_key_path(@key) - end - private def key_params diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index 4146deefa89..6d9873e38df 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -39,7 +39,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController end def set_index_vars - @scopes = Gitlab::Auth.available_scopes + @scopes = Gitlab::Auth.available_scopes(current_user) @inactive_personal_access_tokens = finder(state: 'inactive').execute @active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at) diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 5d87037f012..dbf61a17724 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -24,16 +24,6 @@ class ProfilesController < Profiles::ApplicationController end end - def reset_private_token - Users::UpdateService.new(current_user, user: @user).execute! do |user| - user.reset_authentication_token! - end - - flash[:notice] = "Private token was successfully reset" - - redirect_to profile_account_path - end - def reset_incoming_email_token Users::UpdateService.new(current_user, user: @user).execute! do |user| user.reset_incoming_email_token! @@ -41,7 +31,7 @@ class ProfilesController < Profiles::ApplicationController flash[:notice] = "Incoming email token was successfully reset" - redirect_to profile_account_path + redirect_to profile_personal_access_tokens_path end def reset_rss_token @@ -51,7 +41,7 @@ class ProfilesController < Profiles::ApplicationController flash[:notice] = "RSS token was successfully reset" - redirect_to profile_account_path + redirect_to profile_personal_access_tokens_path end def audit_log diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 95d7a02e9e9..dd5e66f60e3 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -53,8 +53,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController send_challenges render plain: "HTTP Basic: Access denied\n", status: 401 - rescue Gitlab::Auth::MissingPersonalTokenError - render_missing_personal_token + rescue Gitlab::Auth::MissingPersonalAccessTokenError + render_missing_personal_access_token end def basic_auth_provided? @@ -78,7 +78,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController @project, @wiki, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:project_id]}") end - def render_missing_personal_token + def render_missing_personal_access_token render plain: "HTTP Basic: Access denied\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}", diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 6a5e4538717..d4e763aa5b8 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController before_action :authorize_create_issue!, only: [:new, :create] # Allow modify issue - before_action :authorize_update_issue!, only: [:edit, :update, :move] + before_action :authorize_update_issuable!, only: [:edit, :update, :move] # Allow create a new branch and empty WIP merge request from current issue before_action :authorize_create_merge_request!, only: [:create_merge_request] @@ -67,18 +67,6 @@ class Projects::IssuesController < Projects::ApplicationController respond_with(@issue) end - def show - @noteable = @issue - @note = @project.notes.new(noteable: @issue) - - respond_to do |format| - format.html - format.json do - render json: serializer.represent(@issue, serializer: params[:serializer]) - end - end - end - def discussions notes = @issue.notes .inc_relations_for_view @@ -120,25 +108,6 @@ class Projects::IssuesController < Projects::ApplicationController end end - def update - update_params = issue_params.merge(spammable_params) - - @issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue) - - respond_to do |format| - format.html do - recaptcha_check_with_fallback { render :edit } - end - - format.json do - render_issue_json - end - end - - rescue ActiveRecord::StaleObjectError - render_conflict_response - end - def move params.require(:move_to_project_id) @@ -196,26 +165,6 @@ class Projects::IssuesController < Projects::ApplicationController end end - def realtime_changes - Gitlab::PollingInterval.set_header(response, interval: 3_000) - - response = { - title: view_context.markdown_field(@issue, :title), - title_text: @issue.title, - description: view_context.markdown_field(@issue, :description), - description_text: @issue.description, - task_status: @issue.task_status - } - - if @issue.edited? - response[:updated_at] = @issue.updated_at - response[:updated_by_name] = @issue.last_edited_by.name - response[:updated_by_path] = user_path(@issue.last_edited_by) - end - - render json: response - end - def create_merge_request result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute @@ -231,7 +180,8 @@ class Projects::IssuesController < Projects::ApplicationController 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! + @issuable = @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take! + @note = @project.notes.new(noteable: @issuable) return render_404 unless can?(current_user, :read_issue, @issue) @@ -246,14 +196,6 @@ class Projects::IssuesController < Projects::ApplicationController project_issue_path(@project, @issue) end - def authorize_update_issue! - render_404 unless can?(current_user, :update_issue, @issue) - end - - def authorize_admin_issues! - render_404 unless can?(current_user, :admin_issue, @project) - end - def authorize_create_merge_request! render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user) end @@ -305,4 +247,9 @@ class Projects::IssuesController < Projects::ApplicationController def serializer IssueSerializer.new(current_user: current_user, project: issue.project) end + + def update_service + update_params = issue_params.merge(spammable_params) + Issues::UpdateService.new(project, current_user, update_params) + end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 2b0294c8387..17cac69e588 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -9,7 +9,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo skip_before_action :merge_request, only: [:index, :bulk_update] skip_before_action :ensure_ref_fetched, only: [:index, :bulk_update] - before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort] + before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort] before_action :authenticate_user!, only: [:assign_related_issues] @@ -256,14 +256,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo alias_method :issuable, :merge_request alias_method :awardable, :merge_request - def authorize_update_merge_request! - return render_404 unless can?(current_user, :update_merge_request, @merge_request) - end - - def authorize_admin_merge_request! - return render_404 unless can?(current_user, :admin_merge_request, @merge_request) - end - def validates_merge_request # Show git not found page # if there is no saved commits between source & target branch diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 8022547a6ad..4dd573c61f1 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -63,34 +63,34 @@ module CiStatusHelper def ci_icon_for_status(status) if detailed_status?(status) - return custom_icon(status.icon) + return sprite_icon(status.icon) end icon_name = case status when 'success' - 'icon_status_success' + 'status_success' when 'success_with_warnings' - 'icon_status_warning' + 'status_warning' when 'failed' - 'icon_status_failed' + 'status_failed' when 'pending' - 'icon_status_pending' + 'status_pending' when 'running' - 'icon_status_running' + 'status_running' when 'play' - 'icon_play' + 'play' when 'created' - 'icon_status_created' + 'status_created' when 'skipped' - 'icon_status_skipped' + 'status_skipped' when 'manual' - 'icon_status_manual' + 'status_manual' else - 'icon_status_canceled' + 'status_canceled' end - custom_icon(icon_name) + sprite_icon(icon_name, size: 16) end def pipeline_status_cache_key(pipeline_status) diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index d4a91e533c1..a77aa0ad2cc 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -71,11 +71,13 @@ module GitlabRoutingHelper project_commit_url(entity.project, entity.sha, *args) end - def preview_markdown_path(project, *args) + def preview_markdown_path(parent, *args) + return group_preview_markdown_path(parent) if parent.is_a?(Group) + if @snippet.is_a?(PersonalSnippet) preview_markdown_snippets_path else - preview_markdown_project_path(project, *args) + preview_markdown_project_path(parent, *args) end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index d0069cd48cf..85407e38532 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -211,15 +211,13 @@ module IssuablesHelper def issuable_initial_data(issuable) data = { - endpoint: project_issue_path(@project, issuable), - canUpdate: can?(current_user, :update_issue, issuable), - canDestroy: can?(current_user, :destroy_issue, issuable), + endpoint: issuable_path(issuable), + canUpdate: can?(current_user, :"update_#{issuable.to_ability_name}", issuable), + canDestroy: can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable), issuableRef: issuable.to_reference, - markdownPreviewPath: preview_markdown_path(@project), + markdownPreviewPath: preview_markdown_path(parent), markdownDocsPath: help_page_path('user/markdown'), issuableTemplates: issuable_templates(issuable), - projectPath: ref_project.path, - projectNamespace: ref_project.namespace.full_path, initialTitleHtml: markdown_field(issuable, :title), initialTitleText: issuable.title, initialDescriptionHtml: markdown_field(issuable, :description), @@ -227,6 +225,12 @@ module IssuablesHelper initialTaskStatus: issuable.task_status } + if parent.is_a?(Group) + data[:groupPath] = parent.path + else + data.merge!(projectPath: ref_project.path, projectNamespace: ref_project.namespace.full_path) + end + data.merge!(updated_at_by(issuable)) data.to_json @@ -263,12 +267,7 @@ module IssuablesHelper end def issuable_path(issuable, *options) - case issuable - when Issue - issue_path(issuable, *options) - when MergeRequest - merge_request_path(issuable, *options) - end + polymorphic_path(issuable, *options) end def issuable_url(issuable, *options) @@ -369,4 +368,8 @@ module IssuablesHelper fullPath: @project.full_path } end + + def parent + @project || @group + end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index d085c1a0e57..f48d47953e4 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -110,7 +110,15 @@ module ProjectsHelper def remove_fork_project_message(project) _("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 } + { forked_from_project: fork_source_name(project) } + end + + def fork_source_name(project) + if @project.fork_source + @project.fork_source.full_name + else + @project.fork_network&.deleted_root_project_name + end end def project_nav_tabs @@ -140,8 +148,8 @@ module ProjectsHelper def can_change_visibility_level?(project, current_user) return false unless can?(current_user, :change_visibility_level, project) - if project.forked? - project.forked_from_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE + if project.fork_source + project.fork_source.visibility_level > Gitlab::VisibilityLevel::PRIVATE else true end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 9417033d1f6..98776eab424 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -49,7 +49,8 @@ module CacheMarkdownField # Always include a project key, or Banzai complains project = self.project if self.respond_to?(:project) - context = cached_markdown_fields[field].merge(project: project) + group = self.group if self.respond_to?(:group) + context = cached_markdown_fields[field].merge(project: project, group: group) # Banzai is less strict about authors, so don't always have an author key context[:author] = self.author if self.respond_to?(:author) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 27f4dedffd3..a928b9d6367 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -14,7 +14,6 @@ module Issuable include StripAttribute include Awardable include Taskable - include TimeTrackable include Importable include Editable include AfterCommitQueue @@ -95,8 +94,6 @@ module Issuable strip_attributes :title - acts_as_paranoid - after_save :record_metrics, unless: :imported? # We want to use optimistic lock for cases when only title or description are involved diff --git a/app/models/concerns/repository_mirroring.rb b/app/models/concerns/repository_mirroring.rb deleted file mode 100644 index f6aba91bc4c..00000000000 --- a/app/models/concerns/repository_mirroring.rb +++ /dev/null @@ -1,32 +0,0 @@ -module RepositoryMirroring - IMPORT_HEAD_REFS = '+refs/heads/*:refs/heads/*'.freeze - IMPORT_TAG_REFS = '+refs/tags/*:refs/tags/*'.freeze - - def set_remote_as_mirror(name) - # This is used to define repository as equivalent as "git clone --mirror" - raw_repository.rugged.config["remote.#{name}.fetch"] = 'refs/*:refs/*' - raw_repository.rugged.config["remote.#{name}.mirror"] = true - raw_repository.rugged.config["remote.#{name}.prune"] = true - end - - def set_import_remote_as_mirror(remote_name) - # Add first fetch with Rugged so it does not create its own. - raw_repository.rugged.config["remote.#{remote_name}.fetch"] = IMPORT_HEAD_REFS - - add_remote_fetch_config(remote_name, IMPORT_TAG_REFS) - - raw_repository.rugged.config["remote.#{remote_name}.mirror"] = true - raw_repository.rugged.config["remote.#{remote_name}.prune"] = true - end - - def add_remote_fetch_config(remote_name, refspec) - run_git(%W[config --add remote.#{remote_name}.fetch #{refspec}]) - end - - def fetch_mirror(remote, url) - add_remote(remote, url) - set_remote_as_mirror(remote) - fetch_remote(remote, forced: true) - remove_remote(remote) - end -end diff --git a/app/models/environment.rb b/app/models/environment.rb index e613d21add6..21a028e351c 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -30,7 +30,6 @@ class Environment < ActiveRecord::Base message: Gitlab::Regex.environment_slug_regex_message } validates :external_url, - uniqueness: { scope: :project_id }, length: { maximum: 255 }, allow_nil: true, addressable_url: true @@ -110,7 +109,7 @@ class Environment < ActiveRecord::Base end def ref_path - "refs/#{Repository::REF_ENVIRONMENTS}/#{generate_slug}" + "refs/#{Repository::REF_ENVIRONMENTS}/#{slug}" end def formatted_external_url @@ -164,6 +163,10 @@ class Environment < ActiveRecord::Base end end + def slug + super.presence || generate_slug + end + # An environment name is not necessarily suitable for use in URLs, DNS # or other third-party contexts, so provide a slugified version. A slug has # the following properties: diff --git a/app/models/epic.rb b/app/models/epic.rb new file mode 100644 index 00000000000..62898a02e2d --- /dev/null +++ b/app/models/epic.rb @@ -0,0 +1,7 @@ +# Placeholder class for model that is implemented in EE +# It will reserve (ee#3853) '&' as a reference prefix, but the table does not exists in CE +class Epic < ActiveRecord::Base + # TODO: this will be implemented as part of #3853 + def to_reference + end +end diff --git a/app/models/fork_network.rb b/app/models/fork_network.rb index 218e37a5312..7f1728e8c77 100644 --- a/app/models/fork_network.rb +++ b/app/models/fork_network.rb @@ -12,4 +12,8 @@ class ForkNetwork < ActiveRecord::Base def find_forks_in(other_projects) projects.where(id: other_projects) end + + def merge_requests + MergeRequest.where(target_project: projects) + end end diff --git a/app/models/group.rb b/app/models/group.rb index 07fb62bb249..c660de7fcb6 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -42,6 +42,7 @@ class Group < Namespace after_create :post_create_hook after_destroy :post_destroy_hook after_save :update_two_factor_requirement + after_update :path_changed_hook, if: :path_changed? class << self def supports_nested_groups? @@ -180,6 +181,12 @@ class Group < Namespace add_user(user, :owner, current_user: current_user) end + def member?(user, min_access_level = Gitlab::Access::GUEST) + return false unless user + + max_member_access_for_user(user) >= min_access_level + end + def has_owner?(user) return false unless user @@ -289,6 +296,12 @@ class Group < Namespace list_of_ids.reverse.map { |group| variables[group.id] }.compact.flatten end + def full_path_was + return path_was unless has_parent? + + "#{parent.full_path}/#{path_was}" + end + private def update_two_factor_requirement @@ -297,6 +310,10 @@ class Group < Namespace users.find_each(&:update_two_factor_requirement) end + def path_changed_hook + system_hook_service.execute_hooks_for(self, :rename) + end + def visibility_level_allowed_by_parent return if visibility_level_allowed_by_parent? diff --git a/app/models/issue.rb b/app/models/issue.rb index 36e4108b9d6..fc590f9257e 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -10,6 +10,7 @@ class Issue < ActiveRecord::Base include FasterCacheKeys include RelativePositioning include CreatedAtFilterable + include TimeTrackable DueDateStruct = Struct.new(:title, :name).freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze @@ -74,6 +75,8 @@ class Issue < ActiveRecord::Base end end + acts_as_paranoid + def self.reference_prefix '#' end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index d45b9c805a4..3133dc9e7eb 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -6,6 +6,7 @@ class MergeRequest < ActiveRecord::Base include Sortable include IgnorableColumn include CreatedAtFilterable + include TimeTrackable ignore_column :locked_at @@ -119,6 +120,8 @@ class MergeRequest < ActiveRecord::Base after_save :keep_around_commit + acts_as_paranoid + def self.reference_prefix '!' end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index faf0b95f842..1eda0f9cbbd 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -48,6 +48,10 @@ class MergeRequestDiff < ActiveRecord::Base # Collect information about commits and diff from repository # and save it to the database as serialized data def save_git_content + MergeRequest + .where('id = ? AND COALESCE(latest_merge_request_diff_id, 0) < ?', self.merge_request_id, self.id) + .update_all(latest_merge_request_diff_id: self.id) + ensure_commit_shas save_commits save_diffs diff --git a/app/models/note.rb b/app/models/note.rb index 8939e590ef1..f9676361072 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -69,7 +69,7 @@ class Note < ActiveRecord::Base delegate :title, to: :noteable, allow_nil: true validates :note, presence: true - validates :project, presence: true, unless: :for_personal_snippet? + validates :project, presence: true, if: :for_project_noteable? # Attachments are deprecated and are handled by Markdown uploader validates :attachment, file_size: { maximum: :max_attachment_size } @@ -114,7 +114,7 @@ class Note < ActiveRecord::Base after_initialize :ensure_discussion_id before_validation :nullify_blank_type, :nullify_blank_line_code before_validation :set_discussion_id, on: :create - after_save :keep_around_commit, unless: :for_personal_snippet? + after_save :keep_around_commit, if: :for_project_noteable? after_save :expire_etag_cache after_destroy :expire_etag_cache @@ -208,6 +208,10 @@ class Note < ActiveRecord::Base noteable.is_a?(PersonalSnippet) end + def for_project_noteable? + !for_personal_snippet? + end + def skip_project_check? for_personal_snippet? end diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb index f89e60ad9f4..e8595b13d6d 100644 --- a/app/models/oauth_access_token.rb +++ b/app/models/oauth_access_token.rb @@ -2,5 +2,13 @@ class OauthAccessToken < Doorkeeper::AccessToken belongs_to :resource_owner, class_name: 'User' belongs_to :application, class_name: 'Doorkeeper::Application' - alias_method :user, :resource_owner + alias_attribute :user, :resource_owner + + def scopes=(value) + if value.is_a?(Array) + super(Doorkeeper::OAuth::Scopes.from_array(value).to_s) + else + super + end + end end diff --git a/app/models/project.rb b/app/models/project.rb index 413866b994a..3f810ee977b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1040,6 +1040,10 @@ class Project < ActiveRecord::Base !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?) end + def fork_source + forked_from_project || fork_network&.root_project + end + def personal? !group end @@ -1488,7 +1492,8 @@ class Project < ActiveRecord::Base { key: 'CI_PROJECT_PATH', value: full_path, public: true }, { key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug, public: true }, { key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path, public: true }, - { key: 'CI_PROJECT_URL', value: web_url, public: true } + { key: 'CI_PROJECT_URL', value: web_url, public: true }, + { key: 'CI_PROJECT_VISIBILITY', value: Gitlab::VisibilityLevel.string_level(visibility_level), public: true } ] end @@ -1679,6 +1684,10 @@ class Project < ActiveRecord::Base Gitlab::GlRepository.gl_repository(self, is_wiki) end + def reference_counter(wiki: false) + Gitlab::ReferenceCounter.new(gl_repository(is_wiki: wiki)) + end + private def storage @@ -1697,11 +1706,11 @@ class Project < ActiveRecord::Base end def repo_reference_count - Gitlab::ReferenceCounter.new(gl_repository(is_wiki: false)).value + reference_counter.value end def wiki_reference_count - Gitlab::ReferenceCounter.new(gl_repository(is_wiki: true)).value + reference_counter(wiki: true).value end def check_repository_absence! diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index bb7be29ef66..43de6809178 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -135,7 +135,7 @@ class ProjectWiki end def repository - @repository ||= Repository.new(full_path, @project, disk_path: disk_path) + @repository ||= Repository.new(full_path, @project, disk_path: disk_path, is_wiki: true) end def default_branch diff --git a/app/models/repository.rb b/app/models/repository.rb index 44a1e9ce529..69cddb36b2e 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -15,9 +15,8 @@ class Repository ].freeze include Gitlab::ShellAdapter - include RepositoryMirroring - attr_accessor :full_path, :disk_path, :project + attr_accessor :full_path, :disk_path, :project, :is_wiki delegate :ref_name_for_sha, to: :raw_repository @@ -72,11 +71,12 @@ class Repository end end - def initialize(full_path, project, disk_path: nil) + def initialize(full_path, project, disk_path: nil, is_wiki: false) @full_path = full_path @disk_path = disk_path || full_path @project = project @commit_cache = {} + @is_wiki = is_wiki end def ==(other) @@ -965,21 +965,8 @@ class Repository run_git(args).first.lines.map(&:strip) end - def add_remote(name, url) - raw_repository.remote_add(name, url) - rescue Rugged::ConfigError - raw_repository.remote_update(name, url: url) - end - - def remove_remote(name) - raw_repository.remote_delete(name) - true - rescue Rugged::ConfigError - false - end - - def fetch_remote(remote, forced: false, no_tags: false) - gitlab_shell.fetch_remote(raw_repository, remote, forced: forced, no_tags: no_tags) + def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false) + gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags) end def fetch_source_branch(source_repository, source_branch, local_ref) @@ -1141,7 +1128,7 @@ class Repository end def initialize_raw_repository - Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, false)) + Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, is_wiki)) end def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset) diff --git a/app/models/user.rb b/app/models/user.rb index 9459b6d4fa4..bcda4564595 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -21,8 +21,8 @@ class User < ActiveRecord::Base ignore_column :external_email ignore_column :email_provider + ignore_column :authentication_token - add_authentication_token_field :authentication_token add_authentication_token_field :incoming_email_token add_authentication_token_field :rss_token @@ -163,11 +163,12 @@ class User < ActiveRecord::Base before_validation :sanitize_attrs before_validation :set_notification_email, if: :email_changed? before_validation :set_public_email, if: :public_email_changed? - before_save :ensure_authentication_token, :ensure_incoming_email_token + before_save :ensure_incoming_email_token before_save :ensure_user_rights_and_limits, if: :external_changed? before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) } before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? } after_save :ensure_namespace_correct + after_update :username_changed_hook, if: :username_changed? after_destroy :post_destroy_hook after_commit :update_emails_with_primary_email, on: :update, if: -> { previous_changes.key?('email') } after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') } @@ -185,8 +186,6 @@ class User < ActiveRecord::Base # Note: When adding an option, it MUST go on the end of the array. enum project_view: [:readme, :activity, :files] - alias_attribute :private_token, :authentication_token - delegate :path, to: :namespace, allow_nil: true, prefix: true state_machine :state, initial: :active do @@ -873,6 +872,10 @@ class User < ActiveRecord::Base end end + def username_changed_hook + system_hook_service.execute_hooks_for(self, :rename) + end + def post_destroy_hook log_info("User \"#{name}\" (#{email}) was removed") system_hook_service.execute_hooks_for(self, :destroy) diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb index 61c7a428745..3b5a4fd4f79 100644 --- a/app/serializers/issuable_entity.rb +++ b/app/serializers/issuable_entity.rb @@ -1,20 +1,16 @@ class IssuableEntity < Grape::Entity + include RequestAwareEntity + expose :id expose :iid expose :author_id expose :description expose :lock_version expose :milestone_id - expose :state expose :title expose :updated_by_id expose :created_at expose :updated_at - expose :deleted_at - expose :time_estimate - expose :total_time_spent - expose :human_time_estimate - expose :human_total_time_spent expose :milestone, using: API::Entities::Milestone expose :labels, using: LabelEntity end diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb index 10d3ad0214b..5f47592e4ad 100644 --- a/app/serializers/issue_entity.rb +++ b/app/serializers/issue_entity.rb @@ -1,6 +1,8 @@ class IssueEntity < IssuableEntity - include RequestAwareEntity + include TimeTrackableEntity + expose :state + expose :deleted_at expose :branch_name expose :confidential expose :discussion_locked diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb index 297a459e394..b53a49fe59e 100644 --- a/app/serializers/merge_request_entity.rb +++ b/app/serializers/merge_request_entity.rb @@ -1,6 +1,8 @@ class MergeRequestEntity < IssuableEntity - include RequestAwareEntity + include TimeTrackableEntity + expose :state + expose :deleted_at expose :in_progress_merge_commit_sha expose :merge_commit_sha expose :merge_error diff --git a/app/serializers/time_trackable_entity.rb b/app/serializers/time_trackable_entity.rb new file mode 100644 index 00000000000..e81cd7bec72 --- /dev/null +++ b/app/serializers/time_trackable_entity.rb @@ -0,0 +1,11 @@ +module TimeTrackableEntity + extend ActiveSupport::Concern + extend Grape + + included do + expose :time_estimate + expose :total_time_spent + expose :human_time_estimate + expose :human_total_time_spent + end +end diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb index 9c00ea789ec..46e19230328 100644 --- a/app/services/access_token_validation_service.rb +++ b/app/services/access_token_validation_service.rb @@ -39,11 +39,8 @@ class AccessTokenValidationService token_scopes = token.scopes.map(&:to_sym) required_scopes.any? do |scope| - if scope.respond_to?(:sufficient?) - scope.sufficient?(token_scopes, request) - else - API::Scope.new(scope).sufficient?(token_scopes, request) - end + scope = API::Scope.new(scope) unless scope.is_a?(API::Scope) + scope.sufficient?(token_scopes, request) end end end diff --git a/app/services/applications/create_service.rb b/app/services/applications/create_service.rb new file mode 100644 index 00000000000..35d45f25a71 --- /dev/null +++ b/app/services/applications/create_service.rb @@ -0,0 +1,13 @@ +module Applications + class CreateService + def initialize(current_user, params) + @current_user = current_user + @params = params + @ip_address = @params.delete(:ip_address) + end + + def execute(request = nil) + Doorkeeper::Application.create(@params) + end + end +end diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb new file mode 100644 index 00000000000..92eaa5d5115 --- /dev/null +++ b/app/services/issuable/common_system_notes_service.rb @@ -0,0 +1,81 @@ +module Issuable + class CommonSystemNotesService < ::BaseService + attr_reader :issuable + + def execute(issuable, old_labels) + @issuable = issuable + + if issuable.previous_changes.include?('title') + create_title_change_note(issuable.previous_changes['title'].first) + end + + handle_description_change_note + + handle_time_tracking_note if issuable.is_a?(TimeTrackable) + create_labels_note(old_labels) if issuable.labels != old_labels + create_discussion_lock_note if issuable.previous_changes.include?('discussion_locked') + create_milestone_note if issuable.previous_changes.include?('milestone_id') + end + + private + + def handle_time_tracking_note + if issuable.previous_changes.include?('time_estimate') + create_time_estimate_note + end + + if issuable.time_spent? + create_time_spent_note + end + end + + def handle_description_change_note + if issuable.previous_changes.include?('description') + if issuable.tasks? && issuable.updated_tasks.any? + create_task_status_note + else + # TODO: Show this note if non-task content was modified. + # https://gitlab.com/gitlab-org/gitlab-ce/issues/33577 + create_description_change_note + end + end + end + + def create_labels_note(old_labels) + added_labels = issuable.labels - old_labels + removed_labels = old_labels - issuable.labels + + SystemNoteService.change_label(issuable, issuable.project, current_user, added_labels, removed_labels) + end + + def create_title_change_note(old_title) + SystemNoteService.change_title(issuable, issuable.project, current_user, old_title) + end + + def create_description_change_note + SystemNoteService.change_description(issuable, issuable.project, current_user) + end + + def create_task_status_note + issuable.updated_tasks.each do |task| + SystemNoteService.change_task_status(issuable, issuable.project, current_user, task) + end + end + + def create_time_estimate_note + SystemNoteService.change_time_estimate(issuable, issuable.project, current_user) + end + + def create_time_spent_note + SystemNoteService.change_time_spent(issuable, issuable.project, issuable.time_spent_user) + end + + def create_milestone_note + SystemNoteService.change_milestone(issuable, issuable.project, current_user, issuable.milestone) + end + + def create_discussion_lock_note + SystemNoteService.discussion_lock(issuable, current_user) + end + end +end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index d61a342ebad..68b49d880f7 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -1,56 +1,10 @@ class IssuableBaseService < BaseService private - def create_milestone_note(issuable) - SystemNoteService.change_milestone( - issuable, issuable.project, current_user, issuable.milestone) - end - - def create_labels_note(issuable, old_labels) - added_labels = issuable.labels - old_labels - removed_labels = old_labels - issuable.labels - - SystemNoteService.change_label( - issuable, issuable.project, current_user, added_labels, removed_labels) - end - - def create_title_change_note(issuable, old_title) - SystemNoteService.change_title( - issuable, issuable.project, current_user, old_title) - end - - def create_description_change_note(issuable) - SystemNoteService.change_description(issuable, issuable.project, current_user) - end - - def create_branch_change_note(issuable, branch_type, old_branch, new_branch) - SystemNoteService.change_branch( - issuable, issuable.project, current_user, branch_type, - old_branch, new_branch) - end - - def create_task_status_note(issuable) - issuable.updated_tasks.each do |task| - SystemNoteService.change_task_status(issuable, issuable.project, current_user, task) - end - end - - def create_time_estimate_note(issuable) - SystemNoteService.change_time_estimate(issuable, issuable.project, current_user) - end - - def create_time_spent_note(issuable) - SystemNoteService.change_time_spent(issuable, issuable.project, current_user) - end - - def create_discussion_lock_note(issuable) - SystemNoteService.discussion_lock(issuable, current_user) - end - def filter_params(issuable) ability_name = :"admin_#{issuable.to_ability_name}" - unless can?(current_user, ability_name, project) + unless can?(current_user, ability_name, issuable) params.delete(:milestone_id) params.delete(:labels) params.delete(:add_label_ids) @@ -233,15 +187,14 @@ class IssuableBaseService < BaseService # We have to perform this check before saving the issuable as Rails resets # the changed fields upon calling #save. - update_project_counters = issuable.update_project_counter_caches? + update_project_counters = issuable.project && issuable.update_project_counter_caches? if issuable.with_transaction_returning_status { issuable.save } # We do not touch as it will affect a update on updated_at field ActiveRecord::Base.no_touching do - handle_common_system_notes(issuable, old_labels: old_labels) + Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_labels) end - change_discussion_lock(issuable) handle_changes( issuable, old_labels: old_labels, @@ -300,12 +253,6 @@ class IssuableBaseService < BaseService end end - def change_discussion_lock(issuable) - if issuable.previous_changes.include?('discussion_locked') - create_discussion_lock_note(issuable) - end - end - def toggle_award(issuable) award = params.delete(:emoji_award) if award @@ -328,35 +275,17 @@ class IssuableBaseService < BaseService attrs_changed || labels_changed || assignees_changed end - def handle_common_system_notes(issuable, old_labels: []) - if issuable.previous_changes.include?('title') - create_title_change_note(issuable, issuable.previous_changes['title'].first) - end - - if issuable.previous_changes.include?('description') - 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') - create_time_estimate_note(issuable) - end - - if issuable.time_spent? - create_time_spent_note(issuable) - end - - create_labels_note(issuable, old_labels) if issuable.labels != old_labels - end - def invalidate_cache_counts(issuable, users: []) users.each do |user| user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") # rubocop:disable GitlabSecurity/PublicSend end end + + # override if needed + def handle_changes(issuable, options) + end + + # override if needed + def execute_hooks(issuable, action = 'open', params = {}) + end end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index e0339ddf9bb..1b7b5927c5a 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -27,10 +27,6 @@ module Issues todo_service.update_issue(issue, current_user, old_mentioned_users) end - if issue.previous_changes.include?('milestone_id') - create_milestone_note(issue) - end - if issue.assignees != old_assignees create_assignee_note(issue, old_assignees) notification_service.reassigned_issue(issue, current_user, old_assignees) diff --git a/app/services/keys/base_service.rb b/app/services/keys/base_service.rb index 545832d0bd4..f78791932a7 100644 --- a/app/services/keys/base_service.rb +++ b/app/services/keys/base_service.rb @@ -4,6 +4,7 @@ module Keys def initialize(user, params) @user, @params = user, params + @ip_address = @params.delete(:ip_address) end def notification_service diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 2832d893e95..1f394cacc64 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -40,10 +40,6 @@ module MergeRequests merge_request.target_branch) end - if merge_request.previous_changes.include?('milestone_id') - create_milestone_note(merge_request) - end - if merge_request.previous_changes.include?('assignee_id') create_assignee_note(merge_request) notification_service.reassigned_merge_request(merge_request, current_user) @@ -111,5 +107,11 @@ module MergeRequests end end end + + def create_branch_change_note(issuable, branch_type, old_branch, new_branch) + SystemNoteService.change_branch( + issuable, issuable.project, current_user, branch_type, + old_branch, new_branch) + end end end diff --git a/app/services/metrics_service.rb b/app/services/metrics_service.rb index a02eee4961b..6b3939aeba5 100644 --- a/app/services/metrics_service.rb +++ b/app/services/metrics_service.rb @@ -6,8 +6,7 @@ class MetricsService Gitlab::HealthChecks::Redis::RedisCheck, Gitlab::HealthChecks::Redis::CacheCheck, Gitlab::HealthChecks::Redis::QueuesCheck, - Gitlab::HealthChecks::Redis::SharedStateCheck, - Gitlab::HealthChecks::FsShardsCheck + Gitlab::HealthChecks::Redis::SharedStateCheck ].freeze def prometheus_metrics_text diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index c3bf0031409..455b302d819 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -44,7 +44,7 @@ module Projects else clone_repository end - rescue Gitlab::Shell::Error => e + rescue Gitlab::Shell::Error, Gitlab::Git::RepositoryMirroring::RemoteError => e # Expire cache to prevent scenarios such as: # 1. First import failed, but the repo was imported successfully, so +exists?+ returns true # 2. Retried import, repo is broken or not imported but +exists?+ still returns true diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb index 2b82e5732e4..c499f384426 100644 --- a/app/services/projects/unlink_fork_service.rb +++ b/app/services/projects/unlink_fork_service.rb @@ -3,18 +3,24 @@ module Projects def execute return unless @project.forked? - @project.forked_from_project.lfs_objects.find_each do |lfs_object| - lfs_object.projects << @project + if fork_source = @project.fork_source + fork_source.lfs_objects.find_each do |lfs_object| + lfs_object.projects << @project + end + + refresh_forks_count(fork_source) end - merge_requests = @project.forked_from_project.merge_requests.opened.from_project(@project) + merge_requests = @project.fork_network + .merge_requests + .opened + .where.not(target_project: @project) + .from_project(@project) merge_requests.each do |mr| ::MergeRequests::CloseService.new(@project, @current_user).execute(mr) end - refresh_forks_count(@project.forked_from_project) - @project.fork_network_member.destroy @project.forked_project_link.destroy end diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index 5d275967821..911cc919bb8 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -35,24 +35,22 @@ class SystemHooksService data[:old_path_with_namespace] = model.old_path_with_namespace end when User - data.merge!({ - name: model.name, - email: model.email, - user_id: model.id, - username: model.username - }) + data.merge!(user_data(model)) + + if event == :rename + data[:old_username] = model.username_was + end when ProjectMember data.merge!(project_member_data(model)) when Group - owner = model.owner + data.merge!(group_data(model)) - data.merge!( - name: model.name, - path: model.path, - group_id: model.id, - owner_name: owner.respond_to?(:name) ? owner.name : nil, - owner_email: owner.respond_to?(:email) ? owner.email : nil - ) + if event == :rename + data.merge!( + old_path: model.path_was, + old_full_path: model.full_path_was + ) + end when GroupMember data.merge!(group_member_data(model)) end @@ -104,6 +102,19 @@ class SystemHooksService } end + def group_data(model) + owner = model.owner + + { + name: model.name, + path: model.path, + full_path: model.full_path, + group_id: model.id, + owner_name: owner.try(:name), + owner_email: owner.try(:email) + } + end + def group_member_data(model) { group_name: model.group.name, @@ -116,4 +127,13 @@ class SystemHooksService group_access: model.human_access } end + + def user_data(model) + { + name: model.name, + email: model.email, + user_id: model.id, + username: model.username + } + end end diff --git a/app/views/admin/hook_logs/_index.html.haml b/app/views/admin/hook_logs/_index.html.haml index 7dd9943190f..91a8c0c62fe 100644 --- a/app/views/admin/hook_logs/_index.html.haml +++ b/app/views/admin/hook_logs/_index.html.haml @@ -24,7 +24,7 @@ %td = truncate(hook_log.url, length: 50) %td.light - #{number_with_precision(hook_log.execution_duration, precision: 2)} ms + #{number_with_precision(hook_log.execution_duration, precision: 2)} sec %td.light = time_ago_with_tooltip(hook_log.created_at) %td diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 4d8754afdd2..c37d8ac45b9 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -14,7 +14,7 @@ = hidden_field_tag :namespace_id, params[:namespace_id] - namespace = Namespace.find(params[:namespace_id]) - toggle_text = "#{namespace.kind}: #{namespace.full_path}" - = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' }) + = dropdown_toggle(toggle_text, { toggle: 'dropdown', is_filter: 'true' }, { toggle_class: 'js-namespace-select large' }) .dropdown-menu.dropdown-select.dropdown-menu-align-right = dropdown_title('Namespaces') = dropdown_filter("Search for Namespace") diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index ab4165c0bf2..42f92079d85 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -115,7 +115,7 @@ = f.label :new_namespace_id, "Namespace", class: 'control-label' .col-sm-10 .dropdown - = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id', show_any: 'false' }, { toggle_class: 'js-namespace-select large' }) + = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id' }, { toggle_class: 'js-namespace-select large' }) .dropdown-menu.dropdown-select = dropdown_title('Namespaces') = dropdown_filter("Search for Namespace") diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml index 39c7fb0eba2..35a3563dff1 100644 --- a/app/views/ci/status/_badge.html.haml +++ b/app/views/ci/status/_badge.html.haml @@ -5,9 +5,9 @@ - if link && status.has_details? = link_to status.details_path, class: css_classes, title: title do - = custom_icon(status.icon) + = sprite_icon(status.icon) = status.text - else %span{ class: css_classes, title: title } - = custom_icon(status.icon) + = sprite_icon(status.icon) = status.text diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml index dcfb7f0c32d..c5b4439e273 100644 --- a/app/views/ci/status/_dropdown_graph_badge.html.haml +++ b/app/views/ci/status/_dropdown_graph_badge.html.haml @@ -7,13 +7,13 @@ - if status.has_details? = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do - %span{ class: klass }= custom_icon(status.icon) + %span{ class: klass }= sprite_icon(status.icon) %span.ci-build-text= subject.name - else .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } } - %span{ class: klass }= custom_icon(status.icon) + %span{ class: klass }= sprite_icon(status.icon) %span.ci-build-text= subject.name - if status.has_action? - = link_to status.action_path, class: 'ci-action-icon-wrapper js-ci-action-icon', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do - = custom_icon(status.action_icon) + = link_to status.action_path, class: "ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do + = sprite_icon(status.action_icon, css_class: "icon-action-#{status.action_icon}") diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index f62a0cd681e..a5686002328 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -8,7 +8,7 @@ %li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }> = link_to todos_filter_path(state: 'pending') do %span - To do + Todos %span.badge = number_with_delimiter(todos_pending_count) %li.todos-done{ class: active_when(params[:state] == 'done') }> diff --git a/app/views/profiles/accounts/_reset_token.html.haml b/app/views/profiles/accounts/_reset_token.html.haml deleted file mode 100644 index c31a4a8ecd4..00000000000 --- a/app/views/profiles/accounts/_reset_token.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -- name = label.parameterize -- attribute = name.underscore - -.reset-action - %p.cgray - = label_tag name, label, class: "label-light" - = text_field_tag name, current_user.send(attribute), class: 'form-control', readonly: true, onclick: 'this.select()' - %p.help-block - = help_text - .prepend-top-default - = link_to button_label, [:reset, attribute, :profile], method: :put, data: { confirm: 'Are you sure?' }, class: 'btn btn-default private-token' diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 7f79168dfb3..ced58dffcdc 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -9,22 +9,6 @@ .row.prepend-top-default .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 - Private Tokens - %p - Keep these tokens secret, anyone with access to them can interact with - GitLab as if they were you. - .col-lg-8.private-tokens-reset - = render partial: 'reset_token', locals: { label: 'Private token', button_label: 'Reset private token', help_text: 'Your private token is used to access the API and Atom feeds without username/password authentication.' } - - = render partial: 'reset_token', locals: { label: 'RSS token', button_label: 'Reset RSS token', help_text: 'Your RSS token is used to create urls for personalized RSS feeds.' } - - - if incoming_email_token_enabled? - = render partial: 'reset_token', locals: { label: 'Incoming email token', button_label: 'Reset incoming email token', help_text: 'Your incoming email token is used to create new issues by email, and is included in your project-specific email addresses.' } - -%hr -.row.prepend-top-default - .col-lg-4.profile-settings-sidebar - %h4.prepend-top-0 Two-Factor Authentication %p Increase your account's security by enabling Two-Factor Authentication (2FA). diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index 06bb72b9f0d..26c2e4c5936 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -30,3 +30,40 @@ = render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes = render "shared/personal_access_tokens_table", impersonation: false, active_tokens: @active_personal_access_tokens, inactive_tokens: @inactive_personal_access_tokens + +%hr +.row.prepend-top-default + .col-lg-4.profile-settings-sidebar + %h4.prepend-top-0 + RSS token + %p + Your RSS token is used to authenticate you when your RSS reader loads a personalized RSS feed, and is included in your personal RSS feed URLs. + %p + It cannot be used to access any other data. + .col-lg-8.rss-token-reset + = label_tag :rss_token, 'RSS token', class: "label-light" + = text_field_tag :rss_token, current_user.rss_token, class: 'form-control', readonly: true, onclick: 'this.select()' + %p.help-block + Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds as if they were you. + You should + = link_to 'reset it', [:reset, :rss_token, :profile], method: :put, data: { confirm: 'Are you sure? Any RSS URLs currently in use will stop working.' } + if that ever happens. + +- if incoming_email_token_enabled? + %hr + .row.prepend-top-default + .col-lg-4.profile-settings-sidebar + %h4.prepend-top-0 + Incoming email token + %p + Your incoming email token is used to authenticate you when you create a new issue by email, and is included in your personal project-specific email addresses. + %p + It cannot be used to access any other data. + .col-lg-8.incoming-email-token-reset + = label_tag :incoming_email_token, 'Incoming email token', class: "label-light" + = text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control', readonly: true, onclick: 'this.select()' + %p.help-block + Keep this token secret. Anyone who gets ahold of it can create issues as if they were you. + You should + = link_to 'reset it', [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: 'Are you sure? Any issue email addresses currently in use will stop working.' } + if that ever happens. diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 619b632918e..1d644dda177 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,6 +1,5 @@ - empty_repo = @project.empty_repo? - fork_network = @project.fork_network -- forked_from_project = @project.forked_from_project || fork_network&.root_project .project-home-panel.text-center{ class: ("empty-project" if empty_repo) } .limit-container-width{ class: container_class } .avatar-container.s70.project-avatar @@ -16,13 +15,13 @@ - if @project.forked? %p - - if forked_from_project + - if @project.fork_source #{ s_('ForkedFromProjectPath|Forked from') } - = link_to project_path(forked_from_project) do - = forked_from_project.full_name + = link_to project_path(@project.fork_source) do + = fork_source_name(@project) - else - deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)') - = deleted_message % { project_name: fork_network.deleted_root_project_name } + = deleted_message % { project_name: fork_source_name(@project) } .project-repo-buttons .count-buttons diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index b127e06030e..dbe6f8beb95 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -10,9 +10,9 @@ cluster_status: @cluster.status_name, cluster_status_reason: @cluster.status_reason } } - %section.settings + %section.settings.no-animate.expanded %h4= s_('ClusterIntegration|Enable cluster integration') - .settings-content.expanded + .settings-content .hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' } = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine') @@ -49,14 +49,14 @@ .form-group = field.submit _('Save'), class: 'btn btn-success' - %section.settings#js-cluster-details + %section.settings.no-animate#js-cluster-details{ class: ('expanded' if expanded) } .settings-header %h4= s_('ClusterIntegration|Cluster details') %button.btn.js-settings-toggle = expanded ? 'Collapse' : 'Expand' %p= s_('ClusterIntegration|See and edit the details for your cluster') - .settings-content.no-animate{ class: ('expanded' if expanded) } + .settings-content .form_group.append-bottom-20 %label.append-bottom-10{ for: 'cluter-name' } @@ -66,11 +66,11 @@ %span.input-group-addon.clipboard-addon = clipboard_button(text: @cluster.gcp_cluster_name, title: s_('ClusterIntegration|Copy cluster name')) - %section.settings#js-cluster-advanced-settings + %section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) } .settings-header %h4= _('Advanced settings') %button.btn.js-settings-toggle = expanded ? 'Collapse' : 'Expand' %p= s_('ClusterIntegration|Manage Cluster integration on your GitLab project') - .settings-content.no-animate{ class: ('expanded' if expanded) } + .settings-content = render 'advanced_settings' diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 5703ef1d4bb..5ebeae5c35f 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -173,7 +173,10 @@ %p This will remove the fork relationship to source project = succeed "." do - = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project) + - if @project.fork_source + = link_to(fork_source_name(@project), project_path(@project.fork_source)) + - else + = fork_source_name(@project) = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f| %p %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source. diff --git a/app/views/projects/hook_logs/_index.html.haml b/app/views/projects/hook_logs/_index.html.haml index 05b06cfc8b2..8096d9530c3 100644 --- a/app/views/projects/hook_logs/_index.html.haml +++ b/app/views/projects/hook_logs/_index.html.haml @@ -24,7 +24,7 @@ %td = truncate(hook_log.url, length: 50) %td.light - #{number_with_precision(hook_log.execution_duration, precision: 2)} ms + #{number_with_precision(hook_log.execution_duration, precision: 2)} sec %td.light = time_ago_with_tooltip(hook_log.created_at) %td diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml index 7da4ffd5e43..b5067367802 100644 --- a/app/views/projects/jobs/_sidebar.html.haml +++ b/app/views/projects/jobs/_sidebar.html.haml @@ -91,7 +91,7 @@ - builds.select{|build| build.status == build_status}.each do |build| .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } } = link_to project_job_path(@project, build) do - = icon('arrow-right') + = sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right') %span{ class: "ci-status-icon-#{build.status}" } = ci_icon_for_status(build.status) %span @@ -100,4 +100,5 @@ - else = build.id - if build.retried? - %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' } + %span.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' } + = sprite_icon('retry', size:16, css_class: 'icon-retry') diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index cb723fe6a18..72d5c4961ec 100644 --- a/app/views/projects/merge_requests/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml @@ -34,7 +34,7 @@ %li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] } = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request' %li{ class: merge_request_button_visibility(@merge_request, false) } - = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' + = link_to 'Reopen', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' - if can_update_merge_request = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit" diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml index dff847159d3..901a177323b 100644 --- a/app/views/shared/_mini_pipeline_graph.html.haml +++ b/app/views/shared/_mini_pipeline_graph.html.haml @@ -7,7 +7,7 @@ .stage-container.dropdown{ class: klass } %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } } - = custom_icon(icon_status) + = sprite_icon(icon_status) = icon('caret-down') %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container diff --git a/app/views/shared/hook_logs/_content.html.haml b/app/views/shared/hook_logs/_content.html.haml index af6a499fadb..c80b179d525 100644 --- a/app/views/shared/hook_logs/_content.html.haml +++ b/app/views/shared/hook_logs/_content.html.haml @@ -11,7 +11,7 @@ = hook_log.trigger.singularize.titleize %p %strong Elapsed time: - #{number_with_precision(hook_log.execution_duration, precision: 2)} ms + #{number_with_precision(hook_log.execution_duration, precision: 2)} sec %p %strong Request time: = time_ago_with_tooltip(hook_log.created_at) diff --git a/changelogs/unreleased/3274-geo-route-whitelisting.yml b/changelogs/unreleased/3274-geo-route-whitelisting.yml new file mode 100644 index 00000000000..43a5af80497 --- /dev/null +++ b/changelogs/unreleased/3274-geo-route-whitelisting.yml @@ -0,0 +1,5 @@ +--- +title: Tighten up whitelisting of certain Geo routes +merge_request: 15082 +author: +type: fixed diff --git a/changelogs/unreleased/37473-expose-project-visibility-as-ci-variable.yml b/changelogs/unreleased/37473-expose-project-visibility-as-ci-variable.yml new file mode 100644 index 00000000000..f6906a3b0e0 --- /dev/null +++ b/changelogs/unreleased/37473-expose-project-visibility-as-ci-variable.yml @@ -0,0 +1,5 @@ +--- +title: Expose project visibility as CI variable - CI_PROJECT_VISIBILITY +merge_request: 15193 +author: +type: added diff --git a/changelogs/unreleased/37631-add-a-merge_request_diff_id-column-to-merge_requests.yml b/changelogs/unreleased/37631-add-a-merge_request_diff_id-column-to-merge_requests.yml new file mode 100644 index 00000000000..a7127f49c16 --- /dev/null +++ b/changelogs/unreleased/37631-add-a-merge_request_diff_id-column-to-merge_requests.yml @@ -0,0 +1,5 @@ +--- +title: Add a latest_merge_request_diff_id column to merge_requests +merge_request: 15035 +author: +type: performance diff --git a/changelogs/unreleased/38677-render-new-discussions-on-diff-tab.yml b/changelogs/unreleased/38677-render-new-discussions-on-diff-tab.yml new file mode 100644 index 00000000000..9de6e54e3af --- /dev/null +++ b/changelogs/unreleased/38677-render-new-discussions-on-diff-tab.yml @@ -0,0 +1,5 @@ +--- +title: Add new diff discussions on MR diffs tab in "realtime" +merge_request: 14981 +author: +type: fixed diff --git a/changelogs/unreleased/39054-activerecord-statementinvalid-pg-querycanceled-error-canceling-statement-due-to-statement-timeout.yml b/changelogs/unreleased/39054-activerecord-statementinvalid-pg-querycanceled-error-canceling-statement-due-to-statement-timeout.yml deleted file mode 100644 index 47bf30ecb5a..00000000000 --- a/changelogs/unreleased/39054-activerecord-statementinvalid-pg-querycanceled-error-canceling-statement-due-to-statement-timeout.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Stop merge requests with thousands of commits from timing out -merge_request: 15063 -author: -type: performance diff --git a/changelogs/unreleased/39188-change-default-disabled-merge-message.yml b/changelogs/unreleased/39188-change-default-disabled-merge-message.yml deleted file mode 100644 index 7de65f5c3f6..00000000000 --- a/changelogs/unreleased/39188-change-default-disabled-merge-message.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update default disabled merge request widget message to reflect a general failure -merge_request: 14960 -author: -type: changed diff --git a/changelogs/unreleased/39366-email-confirmation-fails.yml b/changelogs/unreleased/39366-email-confirmation-fails.yml deleted file mode 100644 index a5568670c70..00000000000 --- a/changelogs/unreleased/39366-email-confirmation-fails.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Fix bug preventing secondary emails from being confirmed' -merge_request: 15010 -author: -type: fixed diff --git a/changelogs/unreleased/39417-todos-spelled-correctly-on-todos-list-page.yml b/changelogs/unreleased/39417-todos-spelled-correctly-on-todos-list-page.yml new file mode 100644 index 00000000000..edf142f0311 --- /dev/null +++ b/changelogs/unreleased/39417-todos-spelled-correctly-on-todos-list-page.yml @@ -0,0 +1,5 @@ +--- +title: Todos spelled correctly on Todos list page +merge_request: 15015 +author: +type: changed diff --git a/changelogs/unreleased/39441-bring-edit-form-back.yml b/changelogs/unreleased/39441-bring-edit-form-back.yml deleted file mode 100644 index 025417e4da9..00000000000 --- a/changelogs/unreleased/39441-bring-edit-form-back.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix editing issue description in mobile view -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/39495-fix-bitbucket-login.yml b/changelogs/unreleased/39495-fix-bitbucket-login.yml deleted file mode 100644 index b48d557108b..00000000000 --- a/changelogs/unreleased/39495-fix-bitbucket-login.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix bitbucket login -merge_request: 15051 -author: -type: fixed diff --git a/changelogs/unreleased/39639-clusters-poll.yml b/changelogs/unreleased/39639-clusters-poll.yml deleted file mode 100644 index f0a82f58b19..00000000000 --- a/changelogs/unreleased/39639-clusters-poll.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adds callback functions for initial request in clusters page -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/39704_fix_webhooks_log_time.yml b/changelogs/unreleased/39704_fix_webhooks_log_time.yml new file mode 100644 index 00000000000..1234663e66b --- /dev/null +++ b/changelogs/unreleased/39704_fix_webhooks_log_time.yml @@ -0,0 +1,5 @@ +--- +title: Fix webhooks recent deliveries +merge_request: 15146 +author: Alexander Randa (@randaalex) +type: fixed diff --git a/changelogs/unreleased/39776-remove-responsive-table-bottom-border.yml b/changelogs/unreleased/39776-remove-responsive-table-bottom-border.yml new file mode 100644 index 00000000000..52b6a267ced --- /dev/null +++ b/changelogs/unreleased/39776-remove-responsive-table-bottom-border.yml @@ -0,0 +1,5 @@ +--- +title: Fix double border UI bug on pipelines/environments table and pagination +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/bvl-circuitbreaker-backoff.yml b/changelogs/unreleased/bvl-circuitbreaker-backoff.yml deleted file mode 100644 index 5cb90e7c085..00000000000 --- a/changelogs/unreleased/bvl-circuitbreaker-backoff.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Make the circuitbreaker more robust by adding higher thresholds, and multiple - access attempts. -merge_request: 14933 -author: -type: fixed diff --git a/changelogs/unreleased/bvl-circuitbreaker-improvements.yml b/changelogs/unreleased/bvl-circuitbreaker-improvements.yml deleted file mode 100644 index 15cbd5592e9..00000000000 --- a/changelogs/unreleased/bvl-circuitbreaker-improvements.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Store circuitbreaker settings in the database instead of config -merge_request: 14842 -author: -type: changed diff --git a/changelogs/unreleased/bvl-do-not-use-redis-keys.yml b/changelogs/unreleased/bvl-do-not-use-redis-keys.yml deleted file mode 100644 index f703aad2065..00000000000 --- a/changelogs/unreleased/bvl-do-not-use-redis-keys.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Forbid the usage of `Redis#keys` -merge_request: 14889 -author: -type: fixed diff --git a/changelogs/unreleased/bvl-dont-rename-free-names.yml b/changelogs/unreleased/bvl-dont-rename-free-names.yml deleted file mode 100644 index 60a4ec8afbe..00000000000 --- a/changelogs/unreleased/bvl-dont-rename-free-names.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Don't rename paths that were freed up when upgrading -merge_request: 15029 -author: -type: fixed diff --git a/changelogs/unreleased/bvl-fix-push-event-service-for-forks.yml b/changelogs/unreleased/bvl-fix-push-event-service-for-forks.yml deleted file mode 100644 index 2a7d80270ac..00000000000 --- a/changelogs/unreleased/bvl-fix-push-event-service-for-forks.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Only cache last push event for existing projects when pushing to a fork -merge_request: 14989 -author: -type: fixed diff --git a/changelogs/unreleased/bvl-fix-system-hook-project-visibility.yml b/changelogs/unreleased/bvl-fix-system-hook-project-visibility.yml deleted file mode 100644 index a17ed51c9b8..00000000000 --- a/changelogs/unreleased/bvl-fix-system-hook-project-visibility.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Use the correct visibility attribute for projects in system hooks -merge_request: 15065 -author: -type: fixed diff --git a/changelogs/unreleased/bvl-unlink-fixes.yml b/changelogs/unreleased/bvl-unlink-fixes.yml new file mode 100644 index 00000000000..685d78f479d --- /dev/null +++ b/changelogs/unreleased/bvl-unlink-fixes.yml @@ -0,0 +1,5 @@ +--- +title: Fix issues with forked projects of which the source was deleted +merge_request: 15150 +author: +type: fixed diff --git a/changelogs/unreleased/dm-add-sudo-scope.yml b/changelogs/unreleased/dm-add-sudo-scope.yml new file mode 100644 index 00000000000..a0c173ce781 --- /dev/null +++ b/changelogs/unreleased/dm-add-sudo-scope.yml @@ -0,0 +1,6 @@ +--- +title: Add sudo scope for OAuth and Personal Access Tokens to be used by admins to + impersonate other users on the API +merge_request: +author: +type: added diff --git a/changelogs/unreleased/dm-convert-private-tokens.yml b/changelogs/unreleased/dm-convert-private-tokens.yml new file mode 100644 index 00000000000..8f5145c897b --- /dev/null +++ b/changelogs/unreleased/dm-convert-private-tokens.yml @@ -0,0 +1,5 @@ +--- +title: Convert private tokens to Personal Access Tokens with sudo scope +merge_request: +author: +type: security diff --git a/changelogs/unreleased/dm-ldap-identity-normalize-dn.yml b/changelogs/unreleased/dm-ldap-identity-normalize-dn.yml deleted file mode 100644 index 7ab25f79143..00000000000 --- a/changelogs/unreleased/dm-ldap-identity-normalize-dn.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Normalize LDAP DN when looking up identity -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/dm-remove-private-token-from-interface.yml b/changelogs/unreleased/dm-remove-private-token-from-interface.yml new file mode 100644 index 00000000000..1b8996b08c3 --- /dev/null +++ b/changelogs/unreleased/dm-remove-private-token-from-interface.yml @@ -0,0 +1,5 @@ +--- +title: Remove private tokens from web interface and API +merge_request: +author: +type: security diff --git a/changelogs/unreleased/dm-remove-private-token.yml b/changelogs/unreleased/dm-remove-private-token.yml new file mode 100644 index 00000000000..d721495721a --- /dev/null +++ b/changelogs/unreleased/dm-remove-private-token.yml @@ -0,0 +1,5 @@ +--- +title: Remove Session API now that private tokens are removed from user API endpoints +merge_request: +author: +type: removed diff --git a/changelogs/unreleased/enable-scss-lint-mergeable-selector.yml b/changelogs/unreleased/enable-scss-lint-mergeable-selector.yml new file mode 100644 index 00000000000..5f6e0cafe88 --- /dev/null +++ b/changelogs/unreleased/enable-scss-lint-mergeable-selector.yml @@ -0,0 +1,4 @@ +--- +title: Enable MergeableSelector in scss-lint +merge_request: 12810 +author: Takuya Noguchi diff --git a/changelogs/unreleased/fix-add-path-attr-to-wiki-file.yml b/changelogs/unreleased/fix-add-path-attr-to-wiki-file.yml deleted file mode 100644 index 0847b5f6733..00000000000 --- a/changelogs/unreleased/fix-add-path-attr-to-wiki-file.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix broken wiki pages that link to a wiki file -merge_request: 15019 -author: -type: fixed diff --git a/changelogs/unreleased/fix-import-issue-assignees.yml b/changelogs/unreleased/fix-import-issue-assignees.yml deleted file mode 100644 index 063b6afaf08..00000000000 --- a/changelogs/unreleased/fix-import-issue-assignees.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix missing Import/Export issue assignees -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/fix_global_board_routes_39073.yml b/changelogs/unreleased/fix_global_board_routes_39073.yml deleted file mode 100644 index cc9ae8592db..00000000000 --- a/changelogs/unreleased/fix_global_board_routes_39073.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow boards as top level route -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/fix_migration_that_adds_ff_merge_field.yml b/changelogs/unreleased/fix_migration_that_adds_ff_merge_field.yml new file mode 100644 index 00000000000..a1685497331 --- /dev/null +++ b/changelogs/unreleased/fix_migration_that_adds_ff_merge_field.yml @@ -0,0 +1,5 @@ +--- +title: Fix a migration that adds merge_requests_ff_only_enabled column to MR table +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/issue_39176.yml b/changelogs/unreleased/issue_39176.yml new file mode 100644 index 00000000000..6255b51c094 --- /dev/null +++ b/changelogs/unreleased/issue_39176.yml @@ -0,0 +1,5 @@ +--- +title: Render 404 when polling commit notes without having permissions +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/jivl-fix-cancel-button-file-upload-new-issue.yml b/changelogs/unreleased/jivl-fix-cancel-button-file-upload-new-issue.yml new file mode 100644 index 00000000000..0205d9626b1 --- /dev/null +++ b/changelogs/unreleased/jivl-fix-cancel-button-file-upload-new-issue.yml @@ -0,0 +1,5 @@ +--- +title: Fix cancel button not working while uploading on the new issue page +merge_request: 15137 +author: +type: fixed diff --git a/changelogs/unreleased/make-merge-jid-handling-less-stateful.yml b/changelogs/unreleased/make-merge-jid-handling-less-stateful.yml deleted file mode 100644 index fe945e822fd..00000000000 --- a/changelogs/unreleased/make-merge-jid-handling-less-stateful.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix widget of locked merge requests not being presented -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/mr-14642.yml b/changelogs/unreleased/mr-14642.yml deleted file mode 100644 index 048cc79e323..00000000000 --- a/changelogs/unreleased/mr-14642.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Auto Devops kubernetes default namespace is now correctly built out of gitlab - project group-name -merge_request: 14642 -author: Mircea Danila Dumitrescu -type: fixed diff --git a/changelogs/unreleased/pawel-disable_nfs_metrics_checks_39730.yml b/changelogs/unreleased/pawel-disable_nfs_metrics_checks_39730.yml new file mode 100644 index 00000000000..556d7d069d3 --- /dev/null +++ b/changelogs/unreleased/pawel-disable_nfs_metrics_checks_39730.yml @@ -0,0 +1,5 @@ +--- +title: Remove Filesystem check metrics that use too much CPU to handle requests +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/sh-fix-container-registry-destroy.yml b/changelogs/unreleased/sh-fix-container-registry-destroy.yml deleted file mode 100644 index 21a463da62a..00000000000 --- a/changelogs/unreleased/sh-fix-container-registry-destroy.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix deletion of container registry or images returning an error -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/sh-fix-environment-slug-generation.yml b/changelogs/unreleased/sh-fix-environment-slug-generation.yml new file mode 100644 index 00000000000..8a9c670c52c --- /dev/null +++ b/changelogs/unreleased/sh-fix-environment-slug-generation.yml @@ -0,0 +1,5 @@ +--- +title: Avoid regenerating the ref path for the environment +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-environment-write-ref.yml b/changelogs/unreleased/sh-fix-environment-write-ref.yml deleted file mode 100644 index 8f291843ebe..00000000000 --- a/changelogs/unreleased/sh-fix-environment-write-ref.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix the writing of invalid environment refs -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/winh-admin-projects-namespace-filter.yml b/changelogs/unreleased/winh-admin-projects-namespace-filter.yml new file mode 100644 index 00000000000..7e906f446b0 --- /dev/null +++ b/changelogs/unreleased/winh-admin-projects-namespace-filter.yml @@ -0,0 +1,5 @@ +--- +title: Make NamespaceSelect change URL when filtering +merge_request: 14888 +author: +type: fixed diff --git a/changelogs/unreleased/winh-namespace-rename-hooks.yml b/changelogs/unreleased/winh-namespace-rename-hooks.yml new file mode 100644 index 00000000000..f5090b03b74 --- /dev/null +++ b/changelogs/unreleased/winh-namespace-rename-hooks.yml @@ -0,0 +1,5 @@ +--- +title: Add system hooks user_rename and group_rename +merge_request: 15123 +author: +type: changed diff --git a/config/environments/test.rb b/config/environments/test.rb index 1edb6fd39b8..d09e51e766a 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,6 +1,7 @@ Rails.application.configure do # Make sure the middleware is inserted first in middleware chain config.middleware.insert_before('ActionDispatch::Static', 'Gitlab::Testing::RequestBlockerMiddleware') + config.middleware.insert_before('ActionDispatch::Static', 'Gitlab::Testing::RequestInspectorMiddleware') # Settings specified here will take precedence over those in config/application.rb diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 4bfa5be0136..7547ba4a8fa 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -501,7 +501,7 @@ production: &base # Gitaly settings gitaly: # Path to the directory containing Gitaly client executables. - client_path: /home/git/gitaly + client_path: /home/git/gitaly/bin # Default Gitaly authentication token. Can be overriden per storage. Can # be left blank when Gitaly is running locally on a Unix socket, which # is the normal way to deploy Gitaly. diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index 14d49885fb3..0da6b14c29e 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -58,9 +58,10 @@ en: expired: "The access token expired" unknown: "The access token is invalid" scopes: - api: Access your API - read_user: Read user information + api: Access the authenticated user's API + read_user: Read the authenticated user's personal information openid: Authenticate using OpenID Connect + sudo: Perform API actions as any user in the system (if the authenticated user is an admin) flash: applications: diff --git a/config/routes/profile.rb b/config/routes/profile.rb index ddc852f0132..bcfc17a5f66 100644 --- a/config/routes/profile.rb +++ b/config/routes/profile.rb @@ -6,7 +6,6 @@ resource :profile, only: [:show, :update] do get :audit_log get :applications, to: 'oauth/applications#index' - put :reset_private_token put :reset_incoming_email_token put :reset_rss_token put :update_username diff --git a/db/migrate/20150827121444_add_fast_forward_option_to_project.rb b/db/migrate/20150827121444_add_fast_forward_option_to_project.rb index 6f22641077d..35df121519e 100644 --- a/db/migrate/20150827121444_add_fast_forward_option_to_project.rb +++ b/db/migrate/20150827121444_add_fast_forward_option_to_project.rb @@ -8,7 +8,11 @@ class AddFastForwardOptionToProject < ActiveRecord::Migration disable_ddl_transaction! def up - add_column_with_default(:projects, :merge_requests_ff_only_enabled, :boolean, default: false) + # We put condition here because of a mistake we made a couple of years ago + # see https://gitlab.com/gitlab-org/gitlab-ce/issues/39382#note_45716103 + unless column_exists?(:projects, :merge_requests_ff_only_enabled) + add_column_with_default(:projects, :merge_requests_ff_only_enabled, :boolean, default: false) + end end def down diff --git a/db/migrate/20171012125712_migrate_user_authentication_token_to_personal_access_token.rb b/db/migrate/20171012125712_migrate_user_authentication_token_to_personal_access_token.rb new file mode 100644 index 00000000000..9a909644a44 --- /dev/null +++ b/db/migrate/20171012125712_migrate_user_authentication_token_to_personal_access_token.rb @@ -0,0 +1,78 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MigrateUserAuthenticationTokenToPersonalAccessToken < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # disable_ddl_transaction! + + TOKEN_NAME = 'Private Token'.freeze + + def up + execute <<~SQL + INSERT INTO personal_access_tokens (user_id, token, name, created_at, updated_at, scopes) + SELECT id, authentication_token, '#{TOKEN_NAME}', NOW(), NOW(), '#{%w[api].to_yaml}' + FROM users + WHERE authentication_token IS NOT NULL + AND admin = FALSE + AND NOT EXISTS ( + SELECT true + FROM personal_access_tokens + WHERE user_id = users.id + AND token = users.authentication_token + ) + SQL + + # Admins also need the `sudo` scope + execute <<~SQL + INSERT INTO personal_access_tokens (user_id, token, name, created_at, updated_at, scopes) + SELECT id, authentication_token, '#{TOKEN_NAME}', NOW(), NOW(), '#{%w[api sudo].to_yaml}' + FROM users + WHERE authentication_token IS NOT NULL + AND admin = TRUE + AND NOT EXISTS ( + SELECT true + FROM personal_access_tokens + WHERE user_id = users.id + AND token = users.authentication_token + ) + SQL + end + + def down + if Gitlab::Database.postgresql? + execute <<~SQL + UPDATE users + SET authentication_token = pats.token + FROM ( + SELECT user_id, token + FROM personal_access_tokens + WHERE name = '#{TOKEN_NAME}' + ) AS pats + WHERE id = pats.user_id + SQL + else + execute <<~SQL + UPDATE users + INNER JOIN personal_access_tokens AS pats + ON users.id = pats.user_id + SET authentication_token = pats.token + WHERE pats.name = '#{TOKEN_NAME}' + SQL + end + + execute <<~SQL + DELETE FROM personal_access_tokens + WHERE name = '#{TOKEN_NAME}' + AND EXISTS ( + SELECT true + FROM users + WHERE id = personal_access_tokens.user_id + AND authentication_token = personal_access_tokens.token + ) + SQL + end +end diff --git a/db/migrate/20171025110159_add_latest_merge_request_diff_id_to_merge_requests.rb b/db/migrate/20171025110159_add_latest_merge_request_diff_id_to_merge_requests.rb new file mode 100644 index 00000000000..74a2badc130 --- /dev/null +++ b/db/migrate/20171025110159_add_latest_merge_request_diff_id_to_merge_requests.rb @@ -0,0 +1,26 @@ +class AddLatestMergeRequestDiffIdToMergeRequests < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column :merge_requests, :latest_merge_request_diff_id, :integer + add_concurrent_index :merge_requests, :latest_merge_request_diff_id + + add_concurrent_foreign_key :merge_requests, :merge_request_diffs, + column: :latest_merge_request_diff_id, + on_delete: :nullify + end + + def down + remove_foreign_key :merge_requests, column: :latest_merge_request_diff_id + + if index_exists?(:merge_requests, :latest_merge_request_diff_id) + remove_concurrent_index :merge_requests, :latest_merge_request_diff_id + end + + remove_column :merge_requests, :latest_merge_request_diff_id + end +end diff --git a/db/post_migrate/20171012150314_remove_user_authentication_token.rb b/db/post_migrate/20171012150314_remove_user_authentication_token.rb new file mode 100644 index 00000000000..d0f3aa06e98 --- /dev/null +++ b/db/post_migrate/20171012150314_remove_user_authentication_token.rb @@ -0,0 +1,20 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveUserAuthenticationToken < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + remove_column :users, :authentication_token + end + + def down + add_column :users, :authentication_token, :string + + add_concurrent_index :users, :authentication_token, unique: true + end +end diff --git a/db/post_migrate/20171026082505_populate_merge_requests_latest_merge_request_diff_id.rb b/db/post_migrate/20171026082505_populate_merge_requests_latest_merge_request_diff_id.rb new file mode 100644 index 00000000000..a7ebbbf34c0 --- /dev/null +++ b/db/post_migrate/20171026082505_populate_merge_requests_latest_merge_request_diff_id.rb @@ -0,0 +1,27 @@ +class PopulateMergeRequestsLatestMergeRequestDiffId < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + BATCH_SIZE = 1_000 + + class MergeRequest < ActiveRecord::Base + self.table_name = 'merge_requests' + + include ::EachBatch + end + + disable_ddl_transaction! + + def up + update = ' + latest_merge_request_diff_id = ( + SELECT MAX(id) + FROM merge_request_diffs + WHERE merge_requests.id = merge_request_diffs.merge_request_id + )'.squish + + MergeRequest.where(latest_merge_request_diff_id: nil).each_batch(of: BATCH_SIZE) do |relation| + relation.update_all(update) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 530f08022be..80d8ff92d6e 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: 20171017145932) do +ActiveRecord::Schema.define(version: 20171026082505) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -973,6 +973,7 @@ ActiveRecord::Schema.define(version: 20171017145932) do t.boolean "ref_fetched" t.string "merge_jid" t.boolean "discussion_locked" + t.integer "latest_merge_request_diff_id" end add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree @@ -981,6 +982,7 @@ ActiveRecord::Schema.define(version: 20171017145932) do add_index "merge_requests", ["deleted_at"], name: "index_merge_requests_on_deleted_at", using: :btree add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "merge_requests", ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree + add_index "merge_requests", ["latest_merge_request_diff_id"], name: "index_merge_requests_on_latest_merge_request_diff_id", using: :btree add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree @@ -1670,7 +1672,6 @@ ActiveRecord::Schema.define(version: 20171017145932) do t.string "skype", default: "", null: false t.string "linkedin", default: "", null: false t.string "twitter", default: "", null: false - t.string "authentication_token" t.string "bio" t.integer "failed_attempts", default: 0 t.datetime "locked_at" @@ -1720,7 +1721,6 @@ ActiveRecord::Schema.define(version: 20171017145932) do end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree - add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree add_index "users", ["created_at"], name: "index_users_on_created_at", using: :btree add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree @@ -1846,6 +1846,7 @@ ActiveRecord::Schema.define(version: 20171017145932) do add_foreign_key "merge_request_metrics", "ci_pipelines", column: "pipeline_id", on_delete: :cascade add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade add_foreign_key "merge_requests", "ci_pipelines", column: "head_pipeline_id", name: "fk_fd82eae0b9", on_delete: :nullify + add_foreign_key "merge_requests", "merge_request_diffs", column: "latest_merge_request_diff_id", name: "fk_06067f5644", on_delete: :nullify add_foreign_key "merge_requests", "projects", column: "target_project_id", name: "fk_a6963e8447", on_delete: :cascade add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade diff --git a/doc/administration/operations/sidekiq_memory_killer.md b/doc/administration/operations/sidekiq_memory_killer.md index b5e78348989..cbffd883774 100644 --- a/doc/administration/operations/sidekiq_memory_killer.md +++ b/doc/administration/operations/sidekiq_memory_killer.md @@ -28,7 +28,7 @@ The MemoryKiller is controlled using environment variables. delayed shutdown is triggered. The default value for Omnibus packages is set [in the omnibus-gitlab repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/attributes/default.rb). -- `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`: defaults 900 seconds (15 minutes). When +- `SIDEKIQ_MEMORY_KILLER_GRACE_TIME`: defaults to 900 seconds (15 minutes). When a shutdown is triggered, the Sidekiq process will keep working normally for another 15 minutes. - `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT`: defaults to 30 seconds. When the grace @@ -36,5 +36,3 @@ The MemoryKiller is controlled using environment variables. Existing jobs get 30 seconds to finish. After that, the MemoryKiller tells Sidekiq to shut down, and an external supervision mechanism (e.g. Runit) must restart Sidekiq. -- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL`: defaults to `SIGKILL`. The name of - the final signal sent to the Sidekiq process when we want it to shut down. diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md index 6f1356ddf8f..be538ea250a 100644 --- a/doc/administration/troubleshooting/debug.md +++ b/doc/administration/troubleshooting/debug.md @@ -141,7 +141,7 @@ separate Rails process to debug the issue: 1. Log in to your GitLab account. 1. Copy the URL that is causing problems (e.g. https://gitlab.com/ABC). -1. Obtain the private token for your user (Profile Settings -> Account). +1. Create a Personal Access Token for your user (Profile Settings -> Access Tokens). 1. Bring up the GitLab Rails console. For omnibus users, run: ``` diff --git a/doc/api/README.md b/doc/api/README.md index 89ffe9d7868..f226716c3b5 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -50,7 +50,6 @@ following locations: - [Repository Files](repository_files.md) - [Runners](runners.md) - [Services](services.md) -- [Session](session.md) - [Settings](settings.md) - [Sidekiq metrics](sidekiq_metrics.md) - [System Hooks](system_hooks.md) @@ -86,27 +85,10 @@ API requests should be prefixed with `api` and the API version. The API version is defined in [`lib/api.rb`][lib-api-url]. For example, the root of the v4 API is at `/api/v4`. -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: - -``` -GET /projects?private_token=9koXpg98eAheJpvBs5tK -``` - -Example of a valid API request using cURL and authentication via header: +Example of a valid API request using cURL: ```shell -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" +curl "https://gitlab.example.com/api/v4/projects" ``` The API uses JSON to serialize data. You don't need to specify `.json` at the @@ -114,15 +96,20 @@ end of an API URL. ## Authentication -Most API requests require authentication via a session cookie or token. For +Most API requests require authentication, or will only return public data when +authentication is not provided. 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: +There are three ways to authenticate with the GitLab API: 1. [OAuth2 tokens](#oauth2-tokens) -1. [Private tokens](#private-tokens) 1. [Personal access tokens](#personal-access-tokens) +1. [Session cookie](#session-cookie) + +For admins who want to authenticate with the API as a specific user, or who want to build applications or scripts that do so, two options are available: +1. [Impersonation tokens](#impersonation-tokens) +2. [Sudo](#sudo) If authentication information is invalid or omitted, an error message will be returned with status code `401`: @@ -133,74 +120,84 @@ returned with status code `401`: } ``` -### Session cookie +### OAuth2 tokens -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. +You can use an [OAuth2 token](oauth2.md) to authenticate with the API by passing it in either the +`access_token` parameter or the `Authorization` header. -### OAuth2 tokens +Example of using the OAuth2 token in a parameter: -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. +```shell +curl https://gitlab.example.com/api/v4/projects?access_token=OAUTH-TOKEN +``` -Example of using the OAuth2 token in the header: +Example of using the OAuth2 token in a header: ```shell curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/projects ``` -Read more about [GitLab as an OAuth2 client](oauth2.md). +Read more about [GitLab as an OAuth2 provider](oauth2.md). -### Private tokens +### Personal access tokens -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 use a [personal access token][pat] to authenticate with the API by passing it in either the +`private_token` parameter or the `Private-Token` header. -For examples of usage, [read the basic usage section](#basic-usage). +Example of using the personal access token in a parameter: -### Personal access tokens +```shell +curl https://gitlab.example.com/api/v4/projects?private_token=9koXpg98eAheJpvBs5tK +``` + +Example of using the personal access token in a header: -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. +```shell +curl --header "Private-Token: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects +``` -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). +Read more about [personal access tokens][pat]. + +### Session cookie + +When signing in to the main GitLab application, 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. -[Read more about personal access tokens.][pat] +The primary user of this authentication method is the web frontend of GitLab itself, +which can use the API as the authenticated user to get a list of their projects, +for example, without needing to explicitly pass an access token. ### Impersonation tokens > [Introduced][ce-9099] in GitLab 9.0. Needs admin permissions. Impersonation tokens are a type of [personal access token][pat] -that can only be created by an admin for a specific user. +that can only be created by an admin for a specific user. They are a great fit +if you want to build applications or scripts that authenticate with the API as a specific user. -They are a better alternative to using the user's password/private token -or using the [Sudo](#sudo) feature which also requires the admin's password -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. +They are an alternative to directly using the user's password or one of their +personal access tokens, and to using the [Sudo](#sudo) feature, since the user's (or admin's, in the case of Sudo) +password/token may not be known or may change over time. 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). +Impersonation tokens are used exactly like regular personal access tokens, and can be passed in either the +`private_token` parameter or the `Private-Token` header. ### Sudo > Needs admin permissions. All API requests support performing an API call as if you were another user, -provided your private token is from an administrator account. You need to pass -the `sudo` parameter either via query string or a header with an ID/username of +provided you are authenticated as an administrator with an OAuth or Personal Access Token that has the `sudo` scope. + +You need to pass the `sudo` parameter either via query string or a header with an ID/username of the user you want to perform the operation as. If passed as a header, the -header name must be `SUDO` (uppercase). +header name must be `Sudo`. -If a non administrative `private_token` is provided, then an error message will +If a non administrative access token is provided, an error message will be returned with status code `403`: ```json @@ -209,12 +206,23 @@ be returned with status code `403`: } ``` +If an access token without the `sudo` scope is provided, an error message will +be returned with status code `403`: + +```json +{ + "error": "insufficient_scope", + "error_description": "The request requires higher privileges than provided by the access token.", + "scope": "sudo" +} +``` + If the sudo user ID or username cannot be found, an error message will be returned with status code `404`: ```json { - "message": "404 Not Found: No user id or username for: <id/username>" + "message": "404 User with ID or username '123' Not Found" } ``` @@ -228,7 +236,7 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=username ``` ```shell -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: username" "https://gitlab.example.com/api/v4/projects" +curl --header "Private-Token: 9koXpg98eAheJpvBs5tK" --header "Sudo: username" "https://gitlab.example.com/api/v4/projects" ``` Example of a valid API call and a request using cURL with sudo request, @@ -239,7 +247,7 @@ GET /projects?private_token=9koXpg98eAheJpvBs5tK&sudo=23 ``` ```shell -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "SUDO: 23" "https://gitlab.example.com/api/v4/projects" +curl --header "Private-Token: 9koXpg98eAheJpvBs5tK" --header "Sudo: 23" "https://gitlab.example.com/api/v4/projects" ``` ## Status codes diff --git a/doc/api/session.md b/doc/api/session.md deleted file mode 100644 index b97e26f34a2..00000000000 --- a/doc/api/session.md +++ /dev/null @@ -1,55 +0,0 @@ -# Session API - ->**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. - -``` -POST /session -``` - -| Attribute | Type | Required | Description | -| ---------- | ------- | -------- | -------- | -| `login` | string | yes | The username of the user| -| `email` | string | yes if login is not provided | The email of the user | -| `password` | string | yes | The password of the user | - -```bash -curl --request POST "https://gitlab.example.com/api/v4/session?login=john_smith&password=strongpassw0rd" -``` - -Example response: - -```json -{ - "name": "John Smith", - "username": "john_smith", - "id": 32, - "state": "active", - "avatar_url": null, - "created_at": "2015-01-29T21:07:19.440Z", - "is_admin": true, - "bio": null, - "skype": "", - "linkedin": "", - "twitter": "", - "website_url": "", - "email": "john@example.com", - "theme_id": 1, - "color_scheme_id": 1, - "projects_limit": 10, - "current_sign_in_at": "2015-07-07T07:10:58.392Z", - "identities": [], - "can_create_group": true, - "can_create_project": true, - "two_factor_enabled": false, - "private_token": "9koXpg98eAheJpvBs5tK" -} -``` - -[2fa]: ../user/profile/account/two_factor_authentication.md -[personal access tokens]: ../user/profile/personal_access_tokens.md diff --git a/doc/api/users.md b/doc/api/users.md index 1643c584244..aa711090af1 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -410,8 +410,7 @@ GET /user "can_create_group": true, "can_create_project": true, "two_factor_enabled": true, - "external": false, - "private_token": "dd34asd13as" + "external": false } ``` diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index 4586caa457d..0a2419b7ed2 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -31,12 +31,12 @@ There are three methods to enable the use of `docker build` and `docker run` dur The simplest approach is to install GitLab Runner in `shell` execution mode. GitLab Runner then executes job scripts as the `gitlab-runner` user. -1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation). +1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner/#installation). 1. During GitLab Runner installation select `shell` as method of executing job scripts or use command: ```bash - sudo gitlab-ci-multi-runner register -n \ + sudo gitlab-runner register -n \ --url https://gitlab.com/ \ --registration-token REGISTRATION_TOKEN \ --executor shell \ @@ -93,7 +93,7 @@ In order to do that, follow the steps: mode: ```bash - sudo gitlab-ci-multi-runner register -n \ + sudo gitlab-runner register -n \ --url https://gitlab.com/ \ --registration-token REGISTRATION_TOKEN \ --executor docker \ @@ -178,7 +178,7 @@ In order to do that, follow the steps: 1. Register GitLab Runner from the command line to use `docker` and share `/var/run/docker.sock`: ```bash - sudo gitlab-ci-multi-runner register -n \ + sudo gitlab-runner register -n \ --url https://gitlab.com/ \ --registration-token REGISTRATION_TOKEN \ --executor docker \ diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index f7493794b6a..ecb8f15c851 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -501,8 +501,8 @@ First start with creating a file named `build_script`: ```bash cat <<EOF > build_script -git clone https://gitlab.com/gitlab-org/gitlab-ci-multi-runner.git /builds/gitlab-org/gitlab-ci-multi-runner -cd /builds/gitlab-org/gitlab-ci-multi-runner +git clone https://gitlab.com/gitlab-org/gitlab-runner.git /builds/gitlab-org/gitlab-runner +cd /builds/gitlab-org/gitlab-runner make EOF ``` diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md index f2dd12b67d3..6768a2e012f 100644 --- a/doc/ci/examples/php.md +++ b/doc/ci/examples/php.md @@ -267,10 +267,10 @@ terminal execute: ```bash # Check using docker executor -gitlab-ci-multi-runner exec docker test:app +gitlab-runner exec docker test:app # Check using shell executor -gitlab-ci-multi-runner exec shell test:app +gitlab-runner exec shell test:app ``` ## Example project diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md index 0f7ed055e79..a6ed1c54e16 100644 --- a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md +++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md @@ -64,7 +64,7 @@ To build this project you also need to have [GitLab Runner](https://docs.gitlab. You can use public runners available on `gitlab.com`, but you can register your own: ``` -gitlab-ci-multi-runner register \ +gitlab-runner register \ --non-interactive \ --url "https://gitlab.com/" \ --registration-token "PROJECT_REGISTRATION_TOKEN" \ diff --git a/doc/ci/git_submodules.md b/doc/ci/git_submodules.md index 36c6e153d95..c83d3f6f248 100644 --- a/doc/ci/git_submodules.md +++ b/doc/ci/git_submodules.md @@ -61,7 +61,7 @@ correctly with your CI jobs: 1. First, make sure you have used [relative URLs](#configuring-the-gitmodules-file) for the submodules located in the same GitLab server. -1. Next, if you are using `gitlab-ci-multi-runner` v1.10+, you can set the +1. Next, if you are using `gitlab-runner` v1.10+, you can set the `GIT_SUBMODULE_STRATEGY` variable to either `normal` or `recursive` to tell the runner to fetch your submodules before the job: ```yaml @@ -71,7 +71,7 @@ correctly with your CI jobs: See the [`.gitlab-ci.yml` reference](yaml/README.md#git-submodule-strategy) for more details about `GIT_SUBMODULE_STRATEGY`. -1. If you are using an older version of `gitlab-ci-multi-runner`, then use +1. If you are using an older version of `gitlab-runner`, then use `git submodule sync/update` in `before_script`: ```yaml diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 535ed351366..a9e6bda9916 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -66,6 +66,7 @@ future GitLab releases.** | **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name | | **CI_PROJECT_PATH_SLUG** | 9.3 | all | `$CI_PROJECT_PATH` lowercased and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. | | **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project | +| **CI_PROJECT_VISIBILITY** | 10.3 | all | The project visibility (internal, private, public) | | **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry | | **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project | | **CI_REGISTRY_PASSWORD** | 9.0 | all | The password to use to push containers to the GitLab Container Registry | diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 798f40eef3d..0e4ffbd7910 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -459,11 +459,11 @@ Rendered example: ### cURL commands - Use `https://gitlab.example.com/api/v4/` as an endpoint. -- Wherever needed use this private token: `9koXpg98eAheJpvBs5tK`. +- Wherever needed use this personal access token: `9koXpg98eAheJpvBs5tK`. - Always put the request first. `GET` is the default so you don't have to include it. - Use double quotes to the URL when it includes additional parameters. -- Prefer to use examples using the private token and don't pass data of +- Prefer to use examples using the personal access token and don't pass data of username and password. | Methods | Description | diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index 7ddd02e6c73..8b7b015427f 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -60,6 +60,35 @@ writing one](testing_levels.md#consider-not-writing-a-system-test)! - It's ok to look for DOM elements but don't abuse it since it makes the tests more brittle +#### Debugging Capybara + +Sometimes you may need to debug Capybara tests by observing browser behavior. + +You can pause Capybara and view the website on the browser by using the +`live_debug` method in your spec. The current page will be automatically opened +in your default browser. +You may need to sign in first (the current user's credentials are displayed in +the terminal). + +To resume the test run, press any key. + +For example: + +``` +$ bin/rspec spec/features/auto_deploy_spec.rb:34 +Running via Spring preloader in process 8999 +Run options: include {:locations=>{"./spec/features/auto_deploy_spec.rb"=>[34]}} + +Current example is paused for live debugging +The current user credentials are: user2 / 12345678 +Press any key to resume the execution of the example! +Back to the example! +. + +Finished in 34.51 seconds (files took 0.76702 seconds to load) +1 example, 0 failures +``` + ### `let` variables GitLab's RSpec suite has made extensive use of `let` variables to reduce diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md index 9b9ba0baa71..1cbd4350284 100644 --- a/doc/development/testing_guide/testing_levels.md +++ b/doc/development/testing_guide/testing_levels.md @@ -126,7 +126,7 @@ always in-sync with the codebase. [GitLab Workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse [Gitaly]: https://gitlab.com/gitlab-org/gitaly [GitLab Pages]: https://gitlab.com/gitlab-org/gitlab-pages -[GitLab Runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner +[GitLab Runner]: https://gitlab.com/gitlab-org/gitlab-runner [GitLab Omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab [GitLab QA]: https://gitlab.com/gitlab-org/gitlab-qa [part of GitLab Rails]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 7d9bbca4168..7bf126eec5d 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -184,7 +184,7 @@ Runner. We recommend using a separate machine for each GitLab Runner, if you plan to use the CI features. -[security reasons]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/security/index.md +[security reasons]: https://gitlab.com/gitlab-org/gitlab-runner/blob/master/docs/security/index.md ## Supported web browsers diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md index 3ae46019daf..5554a0c8b78 100644 --- a/doc/raketasks/user_management.md +++ b/doc/raketasks/user_management.md @@ -149,18 +149,3 @@ cp config/secrets.yml.bak config/secrets.yml sudo /etc/init.d/gitlab start ``` - -## Clear authentication tokens for all users. Important! Data loss! - -Clear authentication tokens for all users in the GitLab database. This -task is useful if your users' authentication tokens might have been exposed in -any way. All the existing tokens will become invalid, and new tokens are -automatically generated upon sign-in or user modification. - -``` -# omnibus-gitlab -sudo gitlab-rake gitlab:users:clear_all_authentication_tokens - -# installation from source -bundle exec rake gitlab:users:clear_all_authentication_tokens RAILS_ENV=production -``` diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index a45a4eb9e49..f2a9b1d769b 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -1,6 +1,24 @@ # System hooks -Your GitLab instance can perform HTTP POST requests on the following events: `project_create`, `project_destroy`, `project_rename`, `project_transfer`, `project_update`, `user_add_to_team`, `user_remove_from_team`, `user_create`, `user_destroy`, `key_create`, `key_destroy`, `group_create`, `group_destroy`, `user_add_to_group` and `user_remove_from_group`. +Your GitLab instance can perform HTTP POST requests on the following events: + +- `project_create` +- `project_destroy` +- `project_rename` +- `project_transfer` +- `project_update` +- `user_add_to_team` +- `user_remove_from_team` +- `user_create` +- `user_destroy` +- `user_rename` +- `key_create` +- `key_destroy` +- `group_create` +- `group_destroy` +- `group_rename` +- `user_add_to_group` +- `user_remove_from_group` The triggers for most of these are self-explanatory, but `project_update` and `project_rename` deserve some clarification: `project_update` is fired any time an attribute of a project is changed (name, description, tags, etc.) *unless* the `path` attribute is also changed. In that case, a `project_rename` is triggered instead (so that, for instance, if all you care about is the repo URL, you can just listen for `project_rename`). @@ -72,6 +90,9 @@ X-Gitlab-Event: System Hook } ``` +Note that `project_rename` is not triggered if the namespace changes. +Please refer to `group_rename` and `user_rename` for that case. + **Project transferred:** ```json @@ -175,6 +196,21 @@ X-Gitlab-Event: System Hook } ``` +**User renamed:** + +```json +{ + "event_name": "user_rename", + "created_at": "2017-11-01T11:21:04Z", + "updated_at": "2017-11-01T14:04:47Z", + "name": "new-name", + "email": "best-email@example.tld", + "user_id": 58, + "username": "new-exciting-name", + "old_username": "old-boring-name" +} +``` + **Key added** ```json @@ -209,13 +245,15 @@ X-Gitlab-Event: System Hook "updated_at": "2012-07-21T07:38:22Z", "event_name": "group_create", "name": "StoreCloud", - "owner_email": "johnsmith@gmail.com", - "owner_name": "John Smith", + "owner_email": null, + "owner_name": null, "path": "storecloud", "group_id": 78 } ``` +`owner_name` and `owner_email` are always `null`. Please see https://gitlab.com/gitlab-org/gitlab-ce/issues/39675. + **Group removed:** ```json @@ -224,13 +262,35 @@ X-Gitlab-Event: System Hook "updated_at": "2012-07-21T07:38:22Z", "event_name": "group_destroy", "name": "StoreCloud", - "owner_email": "johnsmith@gmail.com", - "owner_name": "John Smith", + "owner_email": null, + "owner_name": null, "path": "storecloud", "group_id": 78 } ``` +`owner_name` and `owner_email` are always `null`. Please see https://gitlab.com/gitlab-org/gitlab-ce/issues/39675. + +**Group renamed:** + +```json +{ + "event_name": "group_rename", + "created_at": "2017-10-30T15:09:00Z", + "updated_at": "2017-11-01T10:23:52Z", + "name": "Better Name", + "path": "better-name", + "full_path": "parent-group/better-name", + "group_id": 64, + "owner_name": null, + "owner_email": null, + "old_path": "old-name", + "old_full_path": "parent-group/old-name" +} +``` + +`owner_name` and `owner_email` are always `null`. Please see https://gitlab.com/gitlab-org/gitlab-ce/issues/39675. + **New Group Member:** ```json diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 5561784ed0b..1cfdabac248 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -144,6 +144,12 @@ has a `.gitlab-ci.yml` or not: All you need to do is remove your existing `.gitlab-ci.yml`, and you can even do that in a branch to test Auto DevOps before committing to `master`. +NOTE: **Note:** +If you are a GitLab Administrator, you can enable Auto DevOps instance wide +in **Admin Area > Settings > Continuous Integration and Deployment**. Doing that, +all the projects that haven't explicitly set an option will have Auto DevOps +enabled by default. + ## Stages of Auto DevOps The following sections describe the stages of Auto DevOps. Read them carefully @@ -517,7 +523,7 @@ Feature.get(:auto_devops_banner_disabled).enable Or through the HTTP API with an admin access token: ```sh -curl --data "value=true" --header "PRIVATE-TOKEN: private_token" https://gitlab.example.com/api/v4/features/auto_devops_banner_disabled +curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https://gitlab.example.com/api/v4/features/auto_devops_banner_disabled ``` [ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115 diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md index 02c0233d75a..c6a91c8d5c2 100644 --- a/doc/university/glossary/README.md +++ b/doc/university/glossary/README.md @@ -460,7 +460,7 @@ A route table contains rules (called routes) that determine where network traffi ### Runners -Actual build machines/containers that [run and execute tests](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner) you have specified to be run on GitLab CI. +Actual build machines/containers that [run and execute tests](https://gitlab.com/gitlab-org/gitlab-runner) you have specified to be run on GitLab CI. ### Sidekiq diff --git a/doc/university/training/topics/git_log.md b/doc/university/training/topics/git_log.md index 21d81840ea7..f2709ae3890 100644 --- a/doc/university/training/topics/git_log.md +++ b/doc/university/training/topics/git_log.md @@ -53,8 +53,8 @@ git log --since=1.month.ago --until=3.weeks.ago ``` cd ~/workspace -git clone git@gitlab.com:gitlab-org/gitlab-ci-multi-runner.git -cd gitlab-ci-multi-runner +git clone git@gitlab.com:gitlab-org/gitlab-runner.git +cd gitlab-runner git log --author="Travis" git log --since=1.month.ago --until=3.weeks.ago git log --since=1.month.ago --until=1.day.ago --author="Travis" diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index 5ebb88bf324..5fcc0501dc1 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -52,7 +52,7 @@ You can edit your account settings by navigating from the up-right corner menu b From there, you can: - Update your personal information -- Manage [private tokens](../../api/README.md#private-tokens), email tokens, [2FA](account/two_factor_authentication.md) +- Manage [2FA](account/two_factor_authentication.md) - Change your username and [delete your account](account/delete_account.md) - Manage applications that can [use GitLab as an OAuth provider](../../integration/oauth_provider.md#introduction-to-oauth) diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index f28c034e74c..9b4fdd65e2f 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -2,17 +2,15 @@ > [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). +Personal access tokens are the preferred way for third party applications and scripts to +authenticate with the [GitLab API][api], if using [OAuth2](../../api/oauth2.md) is not practical. 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. +`private_token` parameter or the `Private-Token` header. The expiration of personal access tokens happens on the date you define, at midnight UTC. @@ -49,12 +47,14 @@ the following table. |`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). | +| `sudo` | Allows performing API actions as any user in the system (if the authenticated user is an admin) ([introduced][ce-14838] in GitLab 10.2). | [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 +[ce-14838]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14838 [container registry]: ../project/container_registry.md [users]: ../../api/users.md -[usage]: ../../api/README.md#basic-usage +[usage]: ../../api/README.md#personal-access-tokens diff --git a/doc/user/project/integrations/img/webhook_logs.png b/doc/user/project/integrations/img/webhook_logs.png Binary files differindex 917068d9398..803678db6b6 100644 --- a/doc/user/project/integrations/img/webhook_logs.png +++ b/doc/user/project/integrations/img/webhook_logs.png diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md index 9ef6f9185c9..f9a268fb789 100644 --- a/doc/user/project/pipelines/job_artifacts.md +++ b/doc/user/project/pipelines/job_artifacts.md @@ -52,7 +52,8 @@ directly in the job artifacts browser without the need to download them. >**Note:** With [GitLab 10.1][ce-14399], HTML files in a public project can be previewed -directly in a new tab without the need to download them. +directly in a new tab without the need to download them when +[GitLab Pages](../../../administration/pages/index.md) is enabled 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 @@ -69,7 +70,8 @@ browse inside them. Below you can see how browsing looks like. In this case we have browsed inside the archive and at this point there is one directory, a couple files, and -one HTML file that you can view directly online (opens in a new tab). +one HTML file that you can view directly online when +[GitLab Pages](../../../administration/pages/index.md) is enabled (opens in a new tab). ![Job artifacts browser](img/job_artifacts_browser.png) diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb index 7e339443b75..f8eb0f01de8 100644 --- a/features/steps/profile/notifications.rb +++ b/features/steps/profile/notifications.rb @@ -11,7 +11,7 @@ class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps end step 'I select Mention setting from dropdown' do - first(:link, "On mention").trigger('click') + first(:link, "On mention").click end step 'I should see Notification saved message' do diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb index ccaf3237815..c3ae33d2aa9 100644 --- a/features/steps/project/commits/branches.rb +++ b/features/steps/project/commits/branches.rb @@ -40,6 +40,7 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps step 'I submit new branch form with invalid name' do fill_in 'branch_name', with: '1.0 stable' + page.find("body").click # defocus the branch_name input select_branch('master') click_button 'Create branch' end @@ -70,17 +71,16 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps step "I click branch 'improve/awesome' delete link" do page.within '.js-branch-improve\/awesome' do - find('.btn-remove').click - sleep 0.05 + accept_alert { find('.btn-remove').click } end end step "I should not see branch 'improve/awesome'" do - expect(page.all(visible: true)).not_to have_content 'improve/awesome' + expect(page).to have_css('.js-branch-improve\\/awesome', visible: :hidden) end def select_branch(branch_name) - click_button 'master' + find('.git-revision-dropdown-toggle').click page.within '#new-branch-form .dropdown-menu' do click_link branch_name diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb index dac18c537ac..196e0fff63a 100644 --- a/features/steps/project/issues/labels.rb +++ b/features/steps/project/issues/labels.rb @@ -16,7 +16,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps step 'I delete all labels' do page.within '.labels' do page.all('.remove-row').each do - first('.remove-row').click + accept_confirm { first('.remove-row').click } end end end diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb index 16a2d4a6f93..33a24e8913a 100644 --- a/features/steps/project/issues/milestones.rb +++ b/features/steps/project/issues/milestones.rb @@ -3,6 +3,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps include SharedProject include SharedPaths include SharedMarkdown + include CapybaraHelpers step 'I should see milestone "v2.2"' do milestone = @project.milestones.find_by(title: "v2.2") @@ -65,7 +66,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps end step 'I click link to remove milestone' do - click_link 'Delete' + confirm_modal_if_present { click_link 'Delete' } end step 'I should see no milestones' do diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index c872bd6f861..aa32528a7ca 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -215,7 +215,7 @@ module SharedDiffNote end step 'I click side-by-side diff button' do - find('#parallel-diff-btn').trigger('click') + find('#parallel-diff-btn').click end step 'I see side-by-side diff button' do @@ -227,12 +227,11 @@ module SharedDiffNote end def click_diff_line(code) - find(".line_holder[id='#{code}'] td:nth-of-type(1)").trigger 'mouseover' - find(".line_holder[id='#{code}'] button").trigger 'click' + find(".line_holder[id='#{code}'] button").click end def click_parallel_diff_line(code, line_type) - find(".line_holder.parallel td[id='#{code}']").find(:xpath, 'preceding-sibling::*[1][self::td]').trigger 'mouseover' - find(".line_holder.parallel button[data-line-code='#{code}']").trigger 'click' + find(".line_holder.parallel td[id='#{code}']").find(:xpath, 'preceding-sibling::*[1][self::td]').hover + find(".line_holder.parallel button[data-line-code='#{code}']").click end end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index 0cd7b506a95..95f0cd2156e 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -14,7 +14,7 @@ module SharedNote find('.more-actions').click find('.more-actions .dropdown-menu li', match: :first) - find(".js-note-delete").click + accept_confirm { find(".js-note-delete").click } end end diff --git a/features/support/capybara.rb b/features/support/capybara.rb index f4691647d4b..3c4db8b9601 100644 --- a/features/support/capybara.rb +++ b/features/support/capybara.rb @@ -1,22 +1,21 @@ -require 'capybara/poltergeist' require 'capybara-screenshot/spinach' # Give CI some extra time timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30 -Capybara.javascript_driver = :poltergeist -Capybara.register_driver :poltergeist do |app| - Capybara::Poltergeist::Driver.new( - 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=yes' - ] +Capybara.javascript_driver = :chrome +Capybara.register_driver :chrome do |app| + extra_args = [] + extra_args << 'headless' unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i + + capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( + chromeOptions: { + 'args' => %w[no-sandbox disable-gpu --window-size=1240,1400] + extra_args + } ) + + Capybara::Selenium::Driver + .new(app, browser: :chrome, desired_capabilities: capabilities) end Capybara.default_max_wait_time = timeout @@ -24,6 +23,10 @@ Capybara.ignore_hidden_elements = false # Keep only the screenshots generated from the last failing test suite Capybara::Screenshot.prune_strategy = :keep_last_run +# From https://github.com/mattheworiordan/capybara-screenshot/issues/84#issuecomment-41219326 +Capybara::Screenshot.register_driver(:chrome) do |driver, path| + driver.browser.save_screenshot(path) +end Spinach.hooks.before_run do TestEnv.eager_load_driver_server diff --git a/features/support/capybara_helpers.rb b/features/support/capybara_helpers.rb new file mode 100644 index 00000000000..647f8d087c3 --- /dev/null +++ b/features/support/capybara_helpers.rb @@ -0,0 +1,10 @@ +module CapybaraHelpers + def confirm_modal_if_present + if Capybara.current_driver == Capybara.javascript_driver + accept_confirm { yield } + return + end + + yield + end +end diff --git a/lib/api/api.rb b/lib/api/api.rb index 7db18e25a5f..c37e596eb9d 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -142,7 +142,6 @@ module API mount ::API::Runner mount ::API::Runners mount ::API::Services - mount ::API::Session mount ::API::Settings mount ::API::SidekiqMetrics mount ::API::Snippets diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index 87b9db66efd..b9c7d443f6c 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -42,72 +42,42 @@ module API # Helper Methods for Grape Endpoint module HelperMethods - def find_current_user - user = - find_user_from_private_token || - find_user_from_oauth_token || - find_user_from_warden + def find_current_user! + user = find_user_from_access_token || find_user_from_warden + return unless user - return nil unless user - - raise UnauthorizedError unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api) + forbidden!('User is blocked') unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api) user end - def private_token - params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER] - end - - private - - def find_user_from_private_token - token_string = private_token.to_s - return nil unless token_string.present? + def access_token + return @access_token if defined?(@access_token) - user = - find_user_by_authentication_token(token_string) || - find_user_by_personal_access_token(token_string) - - raise UnauthorizedError unless user - - user + @access_token = find_oauth_access_token || find_personal_access_token end - # Invokes the doorkeeper guard. - # - # If token is presented and valid, then it sets @current_user. - # - # If the token does not have sufficient scopes to cover the requred scopes, - # then it raises InsufficientScopeError. - # - # If the token is expired, then it raises ExpiredError. - # - # If the token is revoked, then it raises RevokedError. - # - # If the token is not found (nil), then it returns nil - # - # Arguments: - # - # scopes: (optional) scopes required for this guard. - # Defaults to empty array. - # - def find_user_from_oauth_token - access_token = find_oauth_access_token + def validate_access_token!(scopes: []) return unless access_token - find_user_by_access_token(access_token) + case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes) + when AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) + when AccessTokenValidationService::EXPIRED + raise ExpiredError + when AccessTokenValidationService::REVOKED + raise RevokedError + end end - def find_user_by_authentication_token(token_string) - User.find_by_authentication_token(token_string) - end + private - def find_user_by_personal_access_token(token_string) - access_token = PersonalAccessToken.find_by_token(token_string) + def find_user_from_access_token return unless access_token - find_user_by_access_token(access_token) + validate_access_token! + + access_token.user || raise(UnauthorizedError) end # Check the Rails session for valid authentication details @@ -125,34 +95,26 @@ module API end def find_oauth_access_token - return @oauth_access_token if defined?(@oauth_access_token) - token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods) - return @oauth_access_token = nil unless token + return unless token - @oauth_access_token = OauthAccessToken.by_token(token) - raise UnauthorizedError unless @oauth_access_token + # Expiration, revocation and scopes are verified in `find_user_by_access_token` + access_token = OauthAccessToken.by_token(token) + raise UnauthorizedError unless access_token - @oauth_access_token.revoke_previous_refresh_token! - @oauth_access_token + access_token.revoke_previous_refresh_token! + access_token end - def find_user_by_access_token(access_token) - scopes = scopes_registered_for_endpoint + def find_personal_access_token + token = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s + return unless token.present? - case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes) - when AccessTokenValidationService::INSUFFICIENT_SCOPE - raise InsufficientScopeError.new(scopes) - - when AccessTokenValidationService::EXPIRED - raise ExpiredError + # Expiration, revocation and scopes are verified in `find_user_by_access_token` + access_token = PersonalAccessToken.find_by(token: token) + raise UnauthorizedError unless access_token - when AccessTokenValidationService::REVOKED - raise RevokedError - - when AccessTokenValidationService::VALID - access_token.user - end + access_token end def doorkeeper_request @@ -236,7 +198,7 @@ module API class InsufficientScopeError < StandardError attr_reader :scopes def initialize(scopes) - @scopes = scopes + @scopes = scopes.map { |s| s.try(:name) || s } end end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index efe874b2e6b..67cecb6a7ad 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -57,10 +57,6 @@ module API expose :admin?, as: :is_admin end - class UserWithPrivateDetails < UserWithAdmin - expose :private_token - end - class Email < Grape::Entity expose :id, :email end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 7a2ec865860..1c12166e434 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -41,6 +41,8 @@ module API sudo! + validate_access_token!(scopes: scopes_registered_for_endpoint) unless sudo? + @current_user end @@ -385,7 +387,7 @@ module API return @initial_current_user if defined?(@initial_current_user) begin - @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user } + @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user! } rescue APIGuard::UnauthorizedError unauthorized! end @@ -393,24 +395,23 @@ module API def sudo! return unless sudo_identifier - return unless initial_current_user + + unauthorized! unless initial_current_user unless initial_current_user.admin? forbidden!('Must be admin to use sudo') end - # Only private tokens should be used for the SUDO feature - unless private_token == initial_current_user.private_token - forbidden!('Private token must be specified in order to use sudo') + unless access_token + forbidden!('Must be authenticated using an OAuth or Personal Access Token to use sudo') end + validate_access_token!(scopes: [:sudo]) + sudoed_user = find_user(sudo_identifier) + not_found!("User with ID or username '#{sudo_identifier}'") unless sudoed_user - if sudoed_user - @current_user = sudoed_user - else - not_found!("No user id or username for: #{sudo_identifier}") - end + @current_user = sudoed_user end def sudo_identifier diff --git a/lib/api/session.rb b/lib/api/session.rb deleted file mode 100644 index 016415c3023..00000000000 --- a/lib/api/session.rb +++ /dev/null @@ -1,20 +0,0 @@ -module API - class Session < Grape::API - desc 'Login to get token' do - success Entities::UserWithPrivateDetails - end - params do - optional :login, type: String, desc: 'The username' - optional :email, type: String, desc: 'The email of the user' - requires :password, type: String, desc: 'The password of the user' - at_least_one_of :login, :email - end - post "/session" do - user = Gitlab::Auth.find_with_user_password(params[:email] || params[:login], params[:password]) - - return unauthorized! unless user - return render_api_error!('401 Unauthorized. You have 2FA enabled. Please use a personal access token to access the API', 401) if user.two_factor_enabled? - present user, with: Entities::UserWithPrivateDetails - end - end -end diff --git a/lib/api/users.rb b/lib/api/users.rb index b6f97a1eac2..d80b364bd09 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -507,9 +507,7 @@ module API end get do entity = - if sudo? - Entities::UserWithPrivateDetails - elsif current_user.admin? + if current_user.admin? Entities::UserWithAdmin else Entities::UserPublic diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index ef4578aabd6..a0f7e4e5ad5 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -95,7 +95,7 @@ module Banzai end def call - return doc if project.nil? + return doc unless project || group ref_pattern = object_class.reference_pattern link_pattern = object_class.link_reference_pattern @@ -288,10 +288,14 @@ module Banzai end def current_project_path + return unless project + @current_project_path ||= project.full_path end def current_project_namespace_path + return unless project + @current_project_namespace_path ||= project.namespace.full_path end diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index a6f8650ed3d..c6ae28adf87 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -55,6 +55,10 @@ module Banzai context[:project] end + def group + context[:group] + end + def skip_project_check? context[:skip_project_check] end diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb index f3356d6c51e..afb6e25963c 100644 --- a/lib/banzai/filter/user_reference_filter.rb +++ b/lib/banzai/filter/user_reference_filter.rb @@ -24,7 +24,7 @@ module Banzai end def call - return doc if project.nil? && !skip_project_check? + return doc if project.nil? && group.nil? && !skip_project_check? ref_pattern = User.reference_pattern ref_pattern_start = /\A#{ref_pattern}\z/ @@ -101,19 +101,12 @@ module Banzai end def link_to_all(link_content: nil) - project = context[:project] author = context[:author] - if author && !project.team.member?(author) + if author && !team_member?(author) link_content else - url = urls.project_url(project, - only_path: context[:only_path]) - - data = data_attribute(project: project.id, author: author.try(:id)) - content = link_content || User.reference_prefix + 'all' - - link_tag(url, data, content, 'All Project and Group Members') + parent_url(link_content, author) end end @@ -144,6 +137,35 @@ module Banzai def link_tag(url, data, link_content, title) %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>) end + + def parent + context[:project] || context[:group] + end + + def parent_group? + parent.is_a?(Group) + end + + def team_member?(user) + if parent_group? + parent.member?(user) + else + parent.team.member?(user) + end + end + + def parent_url(link_content, author) + if parent_group? + url = urls.group_url(parent, only_path: context[:only_path]) + data = data_attribute(group: group.id, author: author.try(:id)) + else + url = urls.project_url(parent, only_path: context[:only_path]) + data = data_attribute(project: project.id, author: author.try(:id)) + end + + content = link_content || User.reference_prefix + 'all' + link_tag(url, data, content, 'All Project and Group Members') + end end end end diff --git a/lib/github/import.rb b/lib/github/import.rb index 76612799412..8cabbdec940 100644 --- a/lib/github/import.rb +++ b/lib/github/import.rb @@ -60,7 +60,9 @@ module Github project.repository.set_import_remote_as_mirror('github') project.repository.add_remote_fetch_config('github', '+refs/pull/*/head:refs/merge-requests/*/head') fetch_remote(forced: true) - rescue Gitlab::Git::Repository::NoRepository, Gitlab::Shell::Error => e + rescue Gitlab::Git::Repository::NoRepository, + Gitlab::Git::RepositoryMirroring::RemoteError, + Gitlab::Shell::Error => e error(:project, repo_url, e.message) raise Github::RepositoryFetchError end diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 87aeb76b66a..0ad9285c0ea 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -1,11 +1,11 @@ module Gitlab module Auth - MissingPersonalTokenError = Class.new(StandardError) + MissingPersonalAccessTokenError = Class.new(StandardError) REGISTRY_SCOPES = [:read_registry].freeze # Scopes used for GitLab API access - API_SCOPES = [:api, :read_user].freeze + API_SCOPES = [:api, :read_user, :sudo].freeze # Scopes used for OpenID Connect OPENID_SCOPES = [:openid].freeze @@ -38,7 +38,7 @@ module Gitlab # If sign-in is disabled and LDAP is not configured, recommend a # personal access token on failed auth attempts - raise Gitlab::Auth::MissingPersonalTokenError + raise Gitlab::Auth::MissingPersonalAccessTokenError end def find_with_user_password(login, password) @@ -106,7 +106,7 @@ module Gitlab user = find_with_user_password(login, password) return unless user - raise Gitlab::Auth::MissingPersonalTokenError if user.two_factor_enabled? + raise Gitlab::Auth::MissingPersonalAccessTokenError if user.two_factor_enabled? Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities) end @@ -128,7 +128,7 @@ module Gitlab token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password) if token && valid_scoped_token?(token, available_scopes) - Gitlab::Auth::Result.new(token.user, nil, :personal_token, abilities_for_scope(token.scopes)) + Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scope(token.scopes)) end end @@ -226,8 +226,10 @@ module Gitlab [] end - def available_scopes - API_SCOPES + registry_scopes + def available_scopes(current_user = nil) + scopes = API_SCOPES + registry_scopes + scopes.delete(:sudo) if current_user && !current_user.admin? + scopes end # Other available scopes diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb index 8ad3e57e59d..2d9166d6bdd 100644 --- a/lib/gitlab/ci/status/build/cancelable.rb +++ b/lib/gitlab/ci/status/build/cancelable.rb @@ -8,7 +8,7 @@ module Gitlab end def action_icon - 'icon_action_cancel' + 'cancel' end def action_path diff --git a/lib/gitlab/ci/status/build/failed_allowed.rb b/lib/gitlab/ci/status/build/failed_allowed.rb index e42d3574357..d71e63e73eb 100644 --- a/lib/gitlab/ci/status/build/failed_allowed.rb +++ b/lib/gitlab/ci/status/build/failed_allowed.rb @@ -8,7 +8,7 @@ module Gitlab end def icon - 'icon_status_warning' + 'warning' end def group diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb index c7726543599..b7b45466d3b 100644 --- a/lib/gitlab/ci/status/build/play.rb +++ b/lib/gitlab/ci/status/build/play.rb @@ -12,7 +12,7 @@ module Gitlab end def action_icon - 'icon_action_play' + 'play' end def action_title diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb index 8c8fdc56d75..44ffe783e50 100644 --- a/lib/gitlab/ci/status/build/retryable.rb +++ b/lib/gitlab/ci/status/build/retryable.rb @@ -8,7 +8,7 @@ module Gitlab end def action_icon - 'icon_action_retry' + 'retry' end def action_title diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb index d464738deaf..46e730797e4 100644 --- a/lib/gitlab/ci/status/build/stop.rb +++ b/lib/gitlab/ci/status/build/stop.rb @@ -12,7 +12,7 @@ module Gitlab end def action_icon - 'icon_action_stop' + 'stop' end def action_title diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb index e5fdc1f8136..e6195a60d4f 100644 --- a/lib/gitlab/ci/status/canceled.rb +++ b/lib/gitlab/ci/status/canceled.rb @@ -11,7 +11,7 @@ module Gitlab end def icon - 'icon_status_canceled' + 'status_canceled' end def favicon diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb index d188bd286a6..846f00b83dd 100644 --- a/lib/gitlab/ci/status/created.rb +++ b/lib/gitlab/ci/status/created.rb @@ -11,7 +11,7 @@ module Gitlab end def icon - 'icon_status_created' + 'status_created' end def favicon diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb index 38e45714c22..27ce85bd3ed 100644 --- a/lib/gitlab/ci/status/failed.rb +++ b/lib/gitlab/ci/status/failed.rb @@ -11,7 +11,7 @@ module Gitlab end def icon - 'icon_status_failed' + 'status_failed' end def favicon diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb index a4a7edadac9..fc387e2fd25 100644 --- a/lib/gitlab/ci/status/manual.rb +++ b/lib/gitlab/ci/status/manual.rb @@ -11,7 +11,7 @@ module Gitlab end def icon - 'icon_status_manual' + 'status_manual' end def favicon diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb index 5164260b861..6780780db32 100644 --- a/lib/gitlab/ci/status/pending.rb +++ b/lib/gitlab/ci/status/pending.rb @@ -11,7 +11,7 @@ module Gitlab end def icon - 'icon_status_pending' + 'status_pending' end def favicon diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb index 993937e98ca..ee13905e46d 100644 --- a/lib/gitlab/ci/status/running.rb +++ b/lib/gitlab/ci/status/running.rb @@ -11,7 +11,7 @@ module Gitlab end def icon - 'icon_status_running' + 'status_running' end def favicon diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb index 0c942920b02..0dbdc4de426 100644 --- a/lib/gitlab/ci/status/skipped.rb +++ b/lib/gitlab/ci/status/skipped.rb @@ -11,7 +11,7 @@ module Gitlab end def icon - 'icon_status_skipped' + 'status_skipped' end def favicon diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb index d7af98857b0..731013ec017 100644 --- a/lib/gitlab/ci/status/success.rb +++ b/lib/gitlab/ci/status/success.rb @@ -11,7 +11,7 @@ module Gitlab end def icon - 'icon_status_success' + 'status_success' end def favicon diff --git a/lib/gitlab/ci/status/success_warning.rb b/lib/gitlab/ci/status/success_warning.rb index 4d7d82e04cf..32b4cf43e48 100644 --- a/lib/gitlab/ci/status/success_warning.rb +++ b/lib/gitlab/ci/status/success_warning.rb @@ -15,7 +15,7 @@ module Gitlab end def icon - 'icon_status_warning' + 'status_warning' end def group diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index 23ae37ff71e..d5518814483 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -73,7 +73,7 @@ module Gitlab decorate(repo, commit) if commit rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError, Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository, - Rugged::OdbError, Rugged::TreeError + Rugged::OdbError, Rugged::TreeError, ArgumentError nil end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index a9e4e1130c3..182ffc96ef9 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -6,6 +6,7 @@ require "rubygems/package" module Gitlab module Git class Repository + include Gitlab::Git::RepositoryMirroring include Gitlab::Git::Popen ALLOWED_OBJECT_DIRECTORIES_VARIABLES = %w[ @@ -758,13 +759,13 @@ module Gitlab end def ff_merge(user, source_sha, target_branch) - OperationService.new(user, self).with_branch(target_branch) do |our_commit| - raise ArgumentError, 'Invalid merge target' unless our_commit - - source_sha + gitaly_migrate(:operation_user_ff_branch) do |is_enabled| + if is_enabled + gitaly_ff_merge(user, source_sha, target_branch) + else + rugged_ff_merge(user, source_sha, target_branch) + end end - rescue Rugged::ReferenceError - raise ArgumentError, 'Invalid merge source' end def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:) @@ -898,16 +899,25 @@ module Gitlab end end - # Delete the specified remote from this repository. - def remote_delete(remote_name) - rugged.remotes.delete(remote_name) - nil + def add_remote(remote_name, url) + rugged.remotes.create(remote_name, url) + rescue Rugged::ConfigError + remote_update(remote_name, url: url) end - # Add a new remote to this repository. - def remote_add(remote_name, url) - rugged.remotes.create(remote_name, url) - nil + def remove_remote(remote_name) + # When a remote is deleted all its remote refs are deleted too, but in + # the case of mirrors we map its refs (that would usualy go under + # [remote_name]/) to the top level namespace. We clean the mapping so + # those don't get deleted. + if rugged.config["remote.#{remote_name}.mirror"] + rugged.config.delete("remote.#{remote_name}.fetch") + end + + rugged.remotes.delete(remote_name) + true + rescue Rugged::ConfigError + false end # Update the specified remote using the values in the +options+ hash @@ -1177,10 +1187,10 @@ module Gitlab Gitlab::GitalyClient.migrate(method, status: status, &block) rescue GRPC::NotFound => e raise NoRepository.new(e) - rescue GRPC::BadStatus => e - raise CommandError.new(e) rescue GRPC::InvalidArgument => e raise ArgumentError.new(e) + rescue GRPC::BadStatus => e + raise CommandError.new(e) end private @@ -1622,6 +1632,22 @@ module Gitlab run_git(args, env: env) end + + def gitaly_ff_merge(user, source_sha, target_branch) + gitaly_operations_client.user_ff_branch(user, source_sha, target_branch) + rescue GRPC::FailedPrecondition => e + raise CommitError, e + end + + def rugged_ff_merge(user, source_sha, target_branch) + OperationService.new(user, self).with_branch(target_branch) do |our_commit| + raise ArgumentError, 'Invalid merge target' unless our_commit + + source_sha + end + rescue Rugged::ReferenceError + raise ArgumentError, 'Invalid merge source' + end end end end diff --git a/lib/gitlab/git/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb new file mode 100644 index 00000000000..637e7a0659c --- /dev/null +++ b/lib/gitlab/git/repository_mirroring.rb @@ -0,0 +1,95 @@ +module Gitlab + module Git + module RepositoryMirroring + IMPORT_HEAD_REFS = '+refs/heads/*:refs/heads/*'.freeze + IMPORT_TAG_REFS = '+refs/tags/*:refs/tags/*'.freeze + MIRROR_REMOTE = 'mirror'.freeze + + RemoteError = Class.new(StandardError) + + def set_remote_as_mirror(remote_name) + # This is used to define repository as equivalent as "git clone --mirror" + rugged.config["remote.#{remote_name}.fetch"] = 'refs/*:refs/*' + rugged.config["remote.#{remote_name}.mirror"] = true + rugged.config["remote.#{remote_name}.prune"] = true + end + + def set_import_remote_as_mirror(remote_name) + # Add first fetch with Rugged so it does not create its own. + rugged.config["remote.#{remote_name}.fetch"] = IMPORT_HEAD_REFS + + add_remote_fetch_config(remote_name, IMPORT_TAG_REFS) + + rugged.config["remote.#{remote_name}.mirror"] = true + rugged.config["remote.#{remote_name}.prune"] = true + end + + def add_remote_fetch_config(remote_name, refspec) + run_git(%W[config --add remote.#{remote_name}.fetch #{refspec}]) + end + + def fetch_mirror(url) + add_remote(MIRROR_REMOTE, url) + set_remote_as_mirror(MIRROR_REMOTE) + fetch(MIRROR_REMOTE) + remove_remote(MIRROR_REMOTE) + end + + def remote_tags(remote) + # Each line has this format: "dc872e9fa6963f8f03da6c8f6f264d0845d6b092\trefs/tags/v1.10.0\n" + # We want to convert it to: [{ 'v1.10.0' => 'dc872e9fa6963f8f03da6c8f6f264d0845d6b092' }, ...] + list_remote_tags(remote).map do |line| + target, path = line.strip.split("\t") + + # When the remote repo does not have tags. + if target.nil? || path.nil? + Rails.logger.info "Empty or invalid list of tags for remote: #{remote}. Output: #{output}" + return [] + end + + name = path.split('/', 3).last + # We're only interested in tag references + # See: http://stackoverflow.com/questions/15472107/when-listing-git-ls-remote-why-theres-after-the-tag-name + next if name =~ /\^\{\}\Z/ + + target_commit = Gitlab::Git::Commit.find(self, target) + Gitlab::Git::Tag.new(self, name, target, target_commit) + end.compact + end + + def remote_branches(remote_name) + branches = [] + + rugged.references.each("refs/remotes/#{remote_name}/*").map do |ref| + name = ref.name.sub(/\Arefs\/remotes\/#{remote_name}\//, '') + + begin + target_commit = Gitlab::Git::Commit.find(self, ref.target) + branches << Gitlab::Git::Branch.new(self, name, ref.target, target_commit) + rescue Rugged::ReferenceError + # Omit invalid branch + end + end + + branches + end + + private + + def list_remote_tags(remote) + tag_list, exit_code, error = nil + cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{full_path} ls-remote --tags #{remote}) + + Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr| + tag_list = stdout.read + error = stderr.read + exit_code = wait_thr.value.exitstatus + end + + raise RemoteError, error unless exit_code.zero? + + tag_list.split('\n') + end + end + end +end diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb index 45362ac438b..fe901d049d4 100644 --- a/lib/gitlab/git/wiki.rb +++ b/lib/gitlab/git/wiki.rb @@ -10,6 +10,8 @@ module Gitlab end PageBlob = Struct.new(:name) + attr_reader :repository + def self.default_ref 'master' end diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 6868be26758..0b35a787e07 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -34,10 +34,11 @@ module Gitlab private_constant :MUTEX class << self - attr_accessor :query_time + attr_accessor :query_time, :migrate_histogram end self.query_time = 0 + self.migrate_histogram = Gitlab::Metrics.histogram(:gitaly_migrate_call_duration, "Gitaly migration call execution timings") def self.stub(name, storage) MUTEX.synchronize do @@ -171,8 +172,11 @@ module Gitlab feature_stack = Thread.current[:gitaly_feature_stack] ||= [] feature_stack.unshift(feature) begin + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) yield is_enabled ensure + total_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start + migrate_histogram.observe({ gitaly_enabled: is_enabled, feature: feature }, total_time) feature_stack.shift Thread.current[:gitaly_feature_stack] = nil if feature_stack.empty? end diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index adaf255f24b..526d44a8b77 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -105,6 +105,23 @@ module Gitlab ensure request_enum.close end + + def user_ff_branch(user, source_sha, target_branch) + request = Gitaly::UserFFBranchRequest.new( + repository: @gitaly_repo, + user: Gitlab::Git::User.from_gitlab(user).to_gitaly, + commit_id: source_sha, + branch: GitalyClient.encode(target_branch) + ) + + branch_update = GitalyClient.call( + @repository.storage, + :operation_service, + :user_ff_branch, + request + ).branch_update + Gitlab::Git::OperationService::BranchUpdate.from_gitaly(branch_update) + end end end end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index e68761066d8..561779182bc 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -114,6 +114,7 @@ excluded_attributes: - :milestone_id - :ref_fetched - :merge_jid + - :latest_merge_request_diff_id award_emoji: - :awardable_id statuses: diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb index aba3e0df382..c2cbd3c16a1 100644 --- a/lib/gitlab/metrics/system.rb +++ b/lib/gitlab/metrics/system.rb @@ -46,14 +46,14 @@ module Gitlab # Returns the current real time in a given precision. # - # Returns the time as a Float. + # Returns the time as a Fixnum. def self.real_time(precision = :millisecond) Process.clock_gettime(Process::CLOCK_REALTIME, precision) end # Returns the current monotonic clock time in a given precision. # - # Returns the time as a Float. + # Returns the time as a Fixnum. def self.monotonic_time(precision = :millisecond) Process.clock_gettime(Process::CLOCK_MONOTONIC, precision) end diff --git a/lib/gitlab/middleware/read_only.rb b/lib/gitlab/middleware/read_only.rb index 0de0cddcce4..8853dfa3d2d 100644 --- a/lib/gitlab/middleware/read_only.rb +++ b/lib/gitlab/middleware/read_only.rb @@ -12,6 +12,7 @@ module Gitlab def call(env) @env = env + @route_hash = nil if disallowed_request? && Gitlab::Database.read_only? Rails.logger.debug('GitLab ReadOnly: preventing possible non read-only operation') @@ -77,11 +78,11 @@ module Gitlab end def grack_route - request.path.end_with?('.git/git-upload-pack') + route_hash[:controller] == 'projects/git_http' && route_hash[:action] == 'git_upload_pack' end def lfs_route - request.path.end_with?('/info/lfs/objects/batch') + route_hash[:controller] == 'projects/lfs_api' && route_hash[:action] == 'batch' end end end diff --git a/lib/gitlab/performance_bar/peek_query_tracker.rb b/lib/gitlab/performance_bar/peek_query_tracker.rb index 69e117f1da9..f2825db59ae 100644 --- a/lib/gitlab/performance_bar/peek_query_tracker.rb +++ b/lib/gitlab/performance_bar/peek_query_tracker.rb @@ -36,7 +36,7 @@ module Gitlab end def track_query(raw_query, bindings, start, finish) - duration = finish - start + duration = (finish - start) * 1000.0 query_info = { duration: duration.round(3), sql: raw_query } PEEK_DB_CLIENT.query_details << query_info diff --git a/lib/gitlab/sherlock/transaction.rb b/lib/gitlab/sherlock/transaction.rb index 3489fb251b6..400a552bf99 100644 --- a/lib/gitlab/sherlock/transaction.rb +++ b/lib/gitlab/sherlock/transaction.rb @@ -89,7 +89,9 @@ module Gitlab ActiveSupport::Notifications.subscribe('sql.active_record') do |_, start, finish, _, data| next unless same_thread? - track_query(data[:sql].strip, data[:binds], start, finish) + unless data.fetch(:cached, data[:name] == 'CACHE') + track_query(data[:sql].strip, data[:binds], start, finish) + end end end diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb index d7d24eeb37b..2bfb7caefd9 100644 --- a/lib/gitlab/sidekiq_middleware/memory_killer.rb +++ b/lib/gitlab/sidekiq_middleware/memory_killer.rb @@ -7,7 +7,6 @@ module Gitlab GRACE_TIME = (ENV['SIDEKIQ_MEMORY_KILLER_GRACE_TIME'] || 15 * 60).to_s.to_i # Wait 30 seconds for running jobs to finish during graceful shutdown SHUTDOWN_WAIT = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT'] || 30).to_s.to_i - SHUTDOWN_SIGNAL = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL'] || 'SIGKILL').to_s # Create a mutex used to ensure there will be only one thread waiting to # shut Sidekiq down @@ -15,6 +14,7 @@ module Gitlab def call(worker, job, queue) yield + current_rss = get_rss return unless MAX_RSS > 0 && current_rss > MAX_RSS @@ -23,32 +23,45 @@ module Gitlab # Return if another thread is already waiting to shut Sidekiq down return unless MUTEX.try_lock - Sidekiq.logger.warn "current RSS #{current_rss} exceeds maximum RSS "\ - "#{MAX_RSS}" - Sidekiq.logger.warn "this thread will shut down PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']} "\ - "in #{GRACE_TIME} seconds" - sleep(GRACE_TIME) + Sidekiq.logger.warn "Sidekiq worker PID-#{pid} current RSS #{current_rss}"\ + " exceeds maximum RSS #{MAX_RSS} after finishing job #{worker.class} JID-#{job['jid']}" + Sidekiq.logger.warn "Sidekiq worker PID-#{pid} will stop fetching new jobs in #{GRACE_TIME} seconds, and will be shut down #{SHUTDOWN_WAIT} seconds later" - Sidekiq.logger.warn "sending SIGTERM to PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']}" - Process.kill('SIGTERM', Process.pid) + # Wait `GRACE_TIME` to give the memory intensive job time to finish. + # Then, tell Sidekiq to stop fetching new jobs. + wait_and_signal(GRACE_TIME, 'SIGSTP', 'stop fetching new jobs') - Sidekiq.logger.warn "waiting #{SHUTDOWN_WAIT} seconds before sending "\ - "#{SHUTDOWN_SIGNAL} to PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']}" - sleep(SHUTDOWN_WAIT) + # Wait `SHUTDOWN_WAIT` to give already fetched jobs time to finish. + # Then, tell Sidekiq to gracefully shut down by giving jobs a few more + # moments to finish, killing and requeuing them if they didn't, and + # then terminating itself. + wait_and_signal(SHUTDOWN_WAIT, 'SIGTERM', 'gracefully shut down') - Sidekiq.logger.warn "sending #{SHUTDOWN_SIGNAL} to PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']}" - Process.kill(SHUTDOWN_SIGNAL, Process.pid) + # Wait for Sidekiq to shutdown gracefully, and kill it if it didn't. + wait_and_signal(Sidekiq.options[:timeout] + 2, 'SIGKILL', 'die') end end private def get_rss - output, status = Gitlab::Popen.popen(%W(ps -o rss= -p #{Process.pid})) + output, status = Gitlab::Popen.popen(%W(ps -o rss= -p #{pid})) return 0 unless status.zero? output.to_i end + + def wait_and_signal(time, signal, explanation) + Sidekiq.logger.warn "waiting #{time} seconds before sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})" + sleep(time) + + Sidekiq.logger.warn "sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})" + Process.kill(signal, pid) + end + + def pid + Process.pid + end end end end diff --git a/lib/gitlab/testing/request_blocker_middleware.rb b/lib/gitlab/testing/request_blocker_middleware.rb index aa67fa08577..4a8e3c2eee0 100644 --- a/lib/gitlab/testing/request_blocker_middleware.rb +++ b/lib/gitlab/testing/request_blocker_middleware.rb @@ -7,6 +7,7 @@ module Gitlab class RequestBlockerMiddleware @@num_active_requests = Concurrent::AtomicFixnum.new(0) @@block_requests = Concurrent::AtomicBoolean.new(false) + @@slow_requests = Concurrent::AtomicBoolean.new(false) # Returns the number of requests the server is currently processing. def self.num_active_requests @@ -19,9 +20,15 @@ module Gitlab @@block_requests.value = true end + # Slows down incoming requests (useful for race conditions). + def self.slow_requests! + @@slow_requests.value = true + end + # Allows the server to accept requests again. def self.allow_requests! @@block_requests.value = false + @@slow_requests.value = false end def initialize(app) @@ -33,6 +40,7 @@ module Gitlab if block_requests? block_request(env) else + sleep 0.2 if slow_requests? @app.call(env) end ensure @@ -45,6 +53,10 @@ module Gitlab @@block_requests.true? end + def slow_requests? + @@slow_requests.true? + end + def block_request(env) [503, {}, []] end diff --git a/lib/gitlab/testing/request_inspector_middleware.rb b/lib/gitlab/testing/request_inspector_middleware.rb new file mode 100644 index 00000000000..e387667480d --- /dev/null +++ b/lib/gitlab/testing/request_inspector_middleware.rb @@ -0,0 +1,71 @@ +# rubocop:disable Style/ClassVars + +module Gitlab + module Testing + class RequestInspectorMiddleware + @@log_requests = Concurrent::AtomicBoolean.new(false) + @@logged_requests = Concurrent::Array.new + @@inject_headers = Concurrent::Hash.new + + # Resets the current request log and starts logging requests + def self.log_requests!(headers = {}) + @@inject_headers.replace(headers) + @@logged_requests.replace([]) + @@log_requests.value = true + end + + # Stops logging requests + def self.stop_logging! + @@log_requests.value = false + end + + def self.requests + @@logged_requests + end + + def initialize(app) + @app = app + end + + def call(env) + return @app.call(env) unless @@log_requests.true? + + url = env['REQUEST_URI'] + env.merge! http_headers_env(@@inject_headers) if @@inject_headers.any? + request_headers = env_http_headers(env) + status, headers, body = @app.call(env) + + request = OpenStruct.new( + url: url, + status_code: status, + request_headers: request_headers, + response_headers: headers + ) + log_request request + + [status, headers, body] + end + + private + + def env_http_headers(env) + Hash[*env.select { |k, v| k.start_with? 'HTTP_' } + .collect { |k, v| [k.sub(/^HTTP_/, ''), v] } + .collect { |k, v| [k.split('_').collect(&:capitalize).join('-'), v] } + .sort + .flatten] + end + + def http_headers_env(headers) + Hash[*headers + .collect { |k, v| [k.split('-').collect(&:upcase).join('_'), v] } + .collect { |k, v| [k.prepend('HTTP_'), v] } + .flatten] + end + + def log_request(response) + @@logged_requests.push(response) + end + end + end +end diff --git a/lib/system_check/app/git_user_default_ssh_config_check.rb b/lib/system_check/app/git_user_default_ssh_config_check.rb index 9af21078403..ad41760dff2 100644 --- a/lib/system_check/app/git_user_default_ssh_config_check.rb +++ b/lib/system_check/app/git_user_default_ssh_config_check.rb @@ -11,10 +11,10 @@ module SystemCheck ].freeze set_name 'Git user has default SSH configuration?' - set_skip_reason 'skipped (GitLab read-only, or git user is not present / configured)' + set_skip_reason 'skipped (git user is not present / configured)' def skip? - Gitlab::Database.read_only? || !home_dir || !File.directory?(home_dir) + !home_dir || !File.directory?(home_dir) end def check? diff --git a/lib/tasks/gitlab/users.rake b/lib/tasks/gitlab/users.rake deleted file mode 100644 index 3a16ace60bd..00000000000 --- a/lib/tasks/gitlab/users.rake +++ /dev/null @@ -1,11 +0,0 @@ -namespace :gitlab do - namespace :users do - desc "GitLab | Clear the authentication token for all users" - task clear_all_authentication_tokens: :environment do |t, args| - # Do small batched updates because these updates will be slow and locking - User.select(:id).find_in_batches(batch_size: 100) do |batch| - User.where(id: batch.map(&:id)).update_all(authentication_token: nil) - end - end - end -end diff --git a/lib/tasks/tokens.rake b/lib/tasks/tokens.rake index ad1818ff1fa..693597afdf8 100644 --- a/lib/tasks/tokens.rake +++ b/lib/tasks/tokens.rake @@ -1,12 +1,7 @@ require_relative '../../app/models/concerns/token_authenticatable.rb' namespace :tokens do - desc "Reset all GitLab user auth tokens" - task reset_all_auth: :environment do - reset_all_users_token(:reset_authentication_token!) - end - - desc "Reset all GitLab email tokens" + desc "Reset all GitLab incoming email tokens" task reset_all_email: :environment do reset_all_users_token(:reset_incoming_email_token!) end @@ -31,11 +26,6 @@ class TmpUser < ActiveRecord::Base self.table_name = 'users' - def reset_authentication_token! - write_new_token(:authentication_token) - save!(validate: false) - end - def reset_incoming_email_token! write_new_token(:incoming_email_token) save!(validate: false) diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 6802b839eaa..b73ca0c2346 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -50,70 +50,36 @@ describe ApplicationController do end end - describe "#authenticate_user_from_token!" do - describe "authenticating a user from a private token" do - controller(described_class) do - def index - render text: "authenticated" - end - end - - context "when the 'private_token' param is populated with the private token" do - it "logs the user in" do - get :index, private_token: user.private_token - expect(response).to have_gitlab_http_status(200) - expect(response.body).to eq("authenticated") - end - end - - context "when the 'PRIVATE-TOKEN' header is populated with the private token" do - it "logs the user in" do - @request.headers['PRIVATE-TOKEN'] = user.private_token - get :index - expect(response).to have_gitlab_http_status(200) - expect(response.body).to eq("authenticated") - end - end - - it "doesn't log the user in otherwise" do - @request.headers['PRIVATE-TOKEN'] = "token" - get :index, private_token: "token", authenticity_token: "token" - expect(response.status).not_to eq(200) - expect(response.body).not_to eq("authenticated") + describe "#authenticate_user_from_personal_access_token!" do + controller(described_class) do + def index + render text: 'authenticated' end end - describe "authenticating a user from a personal access token" do - controller(described_class) do - def index - render text: 'authenticated' - end - end - - let(:personal_access_token) { create(:personal_access_token, user: user) } + let(:personal_access_token) { create(:personal_access_token, user: user) } - context "when the 'personal_access_token' param is populated with the personal access token" do - it "logs the user in" do - get :index, private_token: personal_access_token.token - expect(response).to have_gitlab_http_status(200) - expect(response.body).to eq('authenticated') - end + context "when the 'personal_access_token' param is populated with the personal access token" do + it "logs the user in" do + get :index, private_token: personal_access_token.token + expect(response).to have_gitlab_http_status(200) + expect(response.body).to eq('authenticated') end + end - context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do - it "logs the user in" do - @request.headers["PRIVATE-TOKEN"] = personal_access_token.token - get :index - expect(response).to have_gitlab_http_status(200) - expect(response.body).to eq('authenticated') - end + context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do + it "logs the user in" do + @request.headers["PRIVATE-TOKEN"] = personal_access_token.token + get :index + expect(response).to have_gitlab_http_status(200) + expect(response.body).to eq('authenticated') end + end - it "doesn't log the user in otherwise" do - get :index, private_token: "token" - expect(response.status).not_to eq(200) - expect(response.body).not_to eq('authenticated') - end + it "doesn't log the user in otherwise" do + get :index, private_token: "token" + expect(response.status).not_to eq(200) + expect(response.body).not_to eq('authenticated') end end @@ -152,11 +118,15 @@ describe ApplicationController do end end + before do + sign_in user + end + context 'when format is handled' do let(:requested_format) { :json } it 'returns 200 response' do - get :index, private_token: user.private_token, format: requested_format + get :index, format: requested_format expect(response).to have_gitlab_http_status 200 end @@ -164,7 +134,7 @@ describe ApplicationController do context 'when format is not handled' do it 'returns 404 response' do - get :index, private_token: user.private_token + get :index expect(response).to have_gitlab_http_status 404 end diff --git a/spec/controllers/concerns/lfs_request_spec.rb b/spec/controllers/concerns/lfs_request_spec.rb new file mode 100644 index 00000000000..33b23db302a --- /dev/null +++ b/spec/controllers/concerns/lfs_request_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe LfsRequest do + include ProjectForksHelper + + controller(Projects::GitHttpClientController) do + # `described_class` is not available in this context + include LfsRequest # rubocop:disable RSpec/DescribedClass + + def show + storage_project + + render nothing: true + end + + def project + @project ||= Project.find(params[:id]) + end + + def download_request? + true + end + + def ci? + false + end + end + + let(:project) { create(:project, :public) } + + before do + stub_lfs_setting(enabled: true) + end + + describe '#storage_project' do + it 'assigns the project as storage project' do + get :show, id: project.id + + expect(assigns(:storage_project)).to eq(project) + end + + it 'assigns the source of a forked project' do + forked_project = fork_project(project) + + get :show, id: forked_project.id + + expect(assigns(:storage_project)).to eq(project) + end + end +end diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb index 7b0976e3e67..4aed2a25baa 100644 --- a/spec/controllers/metrics_controller_spec.rb +++ b/spec/controllers/metrics_controller_spec.rb @@ -59,17 +59,6 @@ describe MetricsController do expect(response.body).to match(/^redis_shared_state_ping_latency_seconds [0-9\.]+$/) end - it 'returns file system check metrics' do - get :index - - expect(response.body).to match(/^filesystem_access_latency_seconds{shard="default"} [0-9\.]+$/) - expect(response.body).to match(/^filesystem_accessible{shard="default"} 1$/) - expect(response.body).to match(/^filesystem_write_latency_seconds{shard="default"} [0-9\.]+$/) - expect(response.body).to match(/^filesystem_writable{shard="default"} 1$/) - expect(response.body).to match(/^filesystem_read_latency_seconds{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) diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index aecdfb50759..8016176110e 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -248,6 +248,45 @@ describe Projects::IssuesController do end end + describe 'PUT #update' do + subject do + put :update, + namespace_id: project.namespace, + project_id: project, + id: issue.to_param, + issue: { title: 'New title' }, format: :json + end + + before do + sign_in(user) + end + + context 'when user has access to update issue' do + before do + project.add_developer(user) + end + + it 'updates the issue' do + subject + + expect(response).to have_http_status(:ok) + expect(issue.reload.title).to eq('New title') + end + end + + context 'when user does not have access to update issue' do + before do + project.add_guest(user) + end + + it 'responds with 404' do + subject + + expect(response).to have_http_status(:not_found) + end + end + end + describe 'Confidential Issues' do let(:project) { create(:project_empty_repo, :public) } let(:assignee) { create(:assignee) } diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 52ef8c6a589..14021b8ca50 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -186,17 +186,23 @@ describe Projects::MergeRequestsController do end describe 'PUT update' do + def update_merge_request(mr_params, additional_params = {}) + params = { + namespace_id: project.namespace, + project_id: project, + id: merge_request.iid, + merge_request: mr_params + }.merge(additional_params) + + put :update, params + end + context 'changing the assignee' do it 'limits the attributes exposed on the assignee' do assignee = create(:user) project.add_developer(assignee) - put :update, - namespace_id: project.namespace.to_param, - project_id: project, - id: merge_request.iid, - merge_request: { assignee_id: assignee.id }, - format: :json + update_merge_request({ assignee_id: assignee.id }, format: :json) body = JSON.parse(response.body) expect(body['assignee'].keys) @@ -204,6 +210,20 @@ describe Projects::MergeRequestsController do end end + context 'when user does not have access to update issue' do + before do + reporter = create(:user) + project.add_reporter(reporter) + sign_in(reporter) + end + + it 'responds with 404' do + update_merge_request(title: 'New title') + + expect(response).to have_http_status(:not_found) + end + end + context 'there is no source project' do let(:project) { create(:project, :repository) } let(:forked_project) { fork_project_with_submodules(project) } @@ -214,13 +234,7 @@ describe Projects::MergeRequestsController do end it 'closes MR without errors' do - post :update, - namespace_id: project.namespace, - project_id: project, - id: merge_request.iid, - merge_request: { - state_event: 'close' - } + update_merge_request(state_event: 'close') expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request]) expect(merge_request.reload.closed?).to be_truthy @@ -229,13 +243,7 @@ describe Projects::MergeRequestsController do it 'allows editing of a closed merge request' do merge_request.close! - put :update, - namespace_id: project.namespace, - project_id: project, - id: merge_request.iid, - merge_request: { - title: 'New title' - } + update_merge_request(title: 'New title') expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request]) expect(merge_request.reload.title).to eq 'New title' @@ -244,13 +252,7 @@ describe Projects::MergeRequestsController do it 'does not allow to update target branch closed merge request' do merge_request.close! - put :update, - namespace_id: project.namespace, - project_id: project, - id: merge_request.iid, - merge_request: { - target_branch: 'new_branch' - } + update_merge_request(target_branch: 'new_branch') expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch } end diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 1184c55e540..5f5a789d5cc 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -59,6 +59,7 @@ describe Projects::NotesController do expect(note_json[:id]).to eq(note.id) expect(note_json[:discussion_html]).not_to be_nil expect(note_json[:diff_discussion_html]).to be_nil + expect(note_json[:discussion_line_code]).to be_nil end end @@ -74,6 +75,7 @@ describe Projects::NotesController do expect(note_json[:id]).to eq(note.id) expect(note_json[:discussion_html]).not_to be_nil expect(note_json[:diff_discussion_html]).not_to be_nil + expect(note_json[:discussion_line_code]).not_to be_nil end end @@ -92,6 +94,7 @@ describe Projects::NotesController do expect(note_json[:id]).to eq(note.id) expect(note_json[:discussion_html]).not_to be_nil expect(note_json[:diff_discussion_html]).to be_nil + expect(note_json[:discussion_line_code]).to be_nil end end @@ -104,6 +107,20 @@ describe Projects::NotesController do expect(note_json[:id]).to eq(note.id) expect(note_json[:discussion_html]).to be_nil expect(note_json[:diff_discussion_html]).to be_nil + expect(note_json[:discussion_line_code]).to be_nil + end + + context 'when user cannot read commit' do + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :download_code, project).and_return(false) + end + + it 'renders 404' do + get :index, params + + expect(response).to have_gitlab_http_status(404) + end end end end @@ -120,6 +137,7 @@ describe Projects::NotesController do expect(note_json[:html]).not_to be_nil expect(note_json[:discussion_html]).to be_nil expect(note_json[:diff_discussion_html]).to be_nil + expect(note_json[:discussion_line_code]).to be_nil end end diff --git a/spec/features/admin/admin_disables_two_factor_spec.rb b/spec/features/admin/admin_disables_two_factor_spec.rb index 6a97378391b..2abdd3c9ef2 100644 --- a/spec/features/admin/admin_disables_two_factor_spec.rb +++ b/spec/features/admin/admin_disables_two_factor_spec.rb @@ -7,7 +7,7 @@ feature 'Admin disables 2FA for a user' do edit_user(user) page.within('.two-factor-status') do - click_link 'Disable' + accept_confirm { click_link 'Disable' } end page.within('.two-factor-status') do diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index 771fb5253da..a5f22848031 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -152,7 +152,7 @@ feature 'Admin Groups' do expect(page).to have_content('Developer') end - find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click + accept_confirm { find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click } visit group_group_members_path(group) diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb index 2e65fcc5231..eec44549a03 100644 --- a/spec/features/admin/admin_hooks_spec.rb +++ b/spec/features/admin/admin_hooks_spec.rb @@ -62,14 +62,14 @@ describe 'Admin::Hooks', :js do it 'from hooks list page' do visit admin_hooks_path - expect { click_link 'Remove' }.to change(SystemHook, :count).by(-1) + expect { accept_confirm { find(:link, 'Remove').send_keys(:return) } }.to change(SystemHook, :count).by(-1) end it 'from hook edit page' do visit admin_hooks_path click_link 'Edit' - expect { click_link 'Remove' }.to change(SystemHook, :count).by(-1) + expect { accept_confirm { find(:link, 'Remove').send_keys(:return) } }.to change(SystemHook, :count).by(-1) end end end diff --git a/spec/features/admin/admin_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb index a5834056a1d..de406d7d966 100644 --- a/spec/features/admin/admin_labels_spec.rb +++ b/spec/features/admin/admin_labels_spec.rb @@ -33,7 +33,7 @@ RSpec.describe 'admin issues labels' do it 'deletes all labels', :js do page.within '.labels' do page.all('.btn-remove').each do |remove| - remove.click + accept_confirm { remove.click } wait_for_requests end end diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 85561511101..1218ea52227 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -73,7 +73,7 @@ feature 'Admin updates settings' do context 'sign-in restrictions', :js do it 'de-activates oauth sign-in source' do - find('.btn', text: 'GitLab.com').click + find('input#application_setting_enabled_oauth_sign_in_sources_[value=gitlab]').send_keys(:return) expect(find('.btn', text: 'GitLab.com')).not_to have_css('.active') end diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb index 388d30828a7..e16eae219a4 100644 --- a/spec/features/admin/admin_users_impersonation_tokens_spec.rb +++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb @@ -24,7 +24,7 @@ describe 'Admin > Users > Impersonation Tokens', :js do fill_in "Name", with: name # Set date to 1st of next month - find_field("Expires at").trigger('focus') + find_field("Expires at").click find(".pika-next").click click_on "1" @@ -60,7 +60,7 @@ describe 'Admin > Users > Impersonation Tokens', :js do it "allows revocation of an active impersonation token" do visit admin_user_impersonation_tokens_path(user_id: user.username) - click_on "Revoke" + accept_confirm { click_on "Revoke" } expect(page).to have_selector(".settings-message") expect(no_personal_access_tokens_message).to have_text("This user has no active Impersonation Tokens.") diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index f9f4bd6f5b9..b47f9055d29 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -290,7 +290,7 @@ describe "Admin::Users" do it 'allows group membership to be revoked', :js do page.within(first('.group_member')) do - find('.btn-remove').click + accept_confirm { find('.btn-remove').click } end wait_for_requests @@ -319,7 +319,7 @@ describe "Admin::Users" do expect(page).to have_content("Secondary email: #{secondary_email.email}") - find("#remove_email_#{secondary_email.id}").click + accept_confirm { find("#remove_email_#{secondary_email.id}").click } expect(page).not_to have_content(secondary_email.email) end diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb index 42f5b5eb8dc..f1ac73ff819 100644 --- a/spec/features/admin/admin_uses_repository_checks_spec.rb +++ b/spec/features/admin/admin_uses_repository_checks_spec.rb @@ -37,7 +37,7 @@ feature 'Admin uses repository checks' do expect(RepositoryCheck::ClearWorker).to receive(:perform_async) - click_link 'Clear all repository checks' + accept_confirm { find(:link, 'Clear all repository checks').send_keys(:return) } expect(page).to have_content('Started asynchronous removal of all repository check states.') end diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb index 5aae2dbaf91..89c9d377003 100644 --- a/spec/features/atom/dashboard_issues_spec.rb +++ b/spec/features/atom/dashboard_issues_spec.rb @@ -13,8 +13,10 @@ describe "Dashboard Issues Feed" do end describe "atom feed" do - it "renders atom feed via private token" do - visit issues_dashboard_path(:atom, private_token: user.private_token) + it "renders atom feed via personal access token" do + personal_access_token = create(:personal_access_token, user: user) + + visit issues_dashboard_path(:atom, private_token: personal_access_token.token) expect(response_headers['Content-Type']).to have_content('application/atom+xml') expect(body).to have_selector('title', text: "#{user.name} issues") diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb index 321c8a2a670..2c0c331b6db 100644 --- a/spec/features/atom/dashboard_spec.rb +++ b/spec/features/atom/dashboard_spec.rb @@ -4,9 +4,11 @@ describe "Dashboard Feed" do describe "GET /" do let!(:user) { create(:user, name: "Jonh") } - context "projects atom feed via private token" do + context "projects atom feed via personal access token" do it "renders projects atom feed" do - visit dashboard_projects_path(:atom, private_token: user.private_token) + personal_access_token = create(:personal_access_token, user: user) + + visit dashboard_projects_path(:atom, private_token: personal_access_token.token) expect(body).to have_selector('feed title') end end diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb index 3eeb4d35131..4102ac0588a 100644 --- a/spec/features/atom/issues_spec.rb +++ b/spec/features/atom/issues_spec.rb @@ -28,10 +28,12 @@ describe 'Issues Feed' do end end - context 'when authenticated via private token' do + context 'when authenticated via personal access token' do it 'renders atom feed' do + personal_access_token = create(:personal_access_token, user: user) + visit project_issues_path(project, :atom, - private_token: user.private_token) + private_token: personal_access_token.token) expect(response_headers['Content-Type']) .to have_content('application/atom+xml') diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index 9ce687afb31..2b934d81674 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -4,9 +4,11 @@ describe "User Feed" do describe "GET /" do let!(:user) { create(:user) } - context 'user atom feed via private token' do + context 'user atom feed via personal access token' do it "renders user atom feed" do - visit user_path(user, :atom, private_token: user.private_token) + personal_access_token = create(:personal_access_token, user: user) + + visit user_path(user, :atom, private_token: personal_access_token.token) expect(body).to have_selector('feed title') end end diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index c480b5b7e34..e4cfcea45a5 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -101,7 +101,7 @@ describe 'Issue Boards add issue modal', :js do click_button 'Cancel' end - first('.board-delete').click + accept_confirm { first('.board-delete').click } click_button('Add issues') diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index ebe6939df4c..e8d779f5772 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -2,6 +2,7 @@ require 'rails_helper' describe 'Issue Boards', :js do include DragTo + include MobileHelpers let(:group) { create(:group, :nested) } let(:project) { create(:project, :public, namespace: group) } @@ -13,7 +14,7 @@ describe 'Issue Boards', :js do project.team << [user, :master] project.team << [user2, :master] - page.driver.set_cookie('sidebar_collapsed', 'true') + set_cookie('sidebar_collapsed', 'true') sign_in(user) end @@ -135,7 +136,7 @@ describe 'Issue Boards', :js do it 'allows user to delete board' do page.within(find('.board:nth-child(2)')) do - find('.board-delete').click + accept_confirm { find('.board-delete').click } end wait_for_requests @@ -150,7 +151,7 @@ describe 'Issue Boards', :js do find('.dropdown-menu-close').click page.within(find('.board:nth-child(2)')) do - find('.board-delete').click + accept_confirm { find('.board-delete').click } end wait_for_requests @@ -379,7 +380,7 @@ describe 'Issue Boards', :js do end it 'filters by milestone' do - set_filter("milestone", "\"#{milestone.title}\"") + set_filter("milestone", "\"#{milestone.title}") click_filter_link(milestone.title) submit_filter @@ -400,7 +401,7 @@ describe 'Issue Boards', :js do end it 'filters by label with space after reload' do - set_filter("label", "\"#{accepting.title}\"") + set_filter("label", "\"#{accepting.title}") click_filter_link(accepting.title) submit_filter @@ -521,7 +522,7 @@ describe 'Issue Boards', :js do end it 'allows user to use keyboard shortcuts' do - find('.boards-list').native.send_keys('i') + find('body').native.send_keys('i') expect(page).to have_content('New Issue') end end @@ -563,6 +564,9 @@ describe 'Issue Boards', :js do end def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0) + # ensure there is enough horizontal space for four boards + resize_window(2000, 800) + drag_to(selector: selector, scrollable: '#board-app', list_from_index: list_from_index, diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 4965f803883..9137ab82ff4 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -51,7 +51,7 @@ describe 'Issue Boards', :js do expect(page).to have_selector('.issue-boards-sidebar') - find('.gutter-toggle').trigger('click') + find('.gutter-toggle').click expect(page).not_to have_selector('.issue-boards-sidebar') end @@ -171,7 +171,7 @@ describe 'Issue Boards', :js do end page.within(find('.board:nth-child(2)')) do - find('.card:nth-child(2)').trigger('click') + find('.card:nth-child(2)').click end page.within('.assignee') do diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb index 4fc6956d111..a9530becb65 100644 --- a/spec/features/calendar_spec.rb +++ b/spec/features/calendar_spec.rb @@ -63,8 +63,8 @@ feature 'Contributions Calendar', :js do Event.create(note_comment_params) end - def selected_day_activities - find('.user-calendar-activities').text + def selected_day_activities(visible: true) + find('.user-calendar-activities', visible: visible).text end before do @@ -112,7 +112,7 @@ feature 'Contributions Calendar', :js do end it 'hides calendar day activities' do - expect(selected_day_activities).to be_empty + expect(selected_day_activities(visible: false)).to be_empty end end end diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb index d5e9de20e59..bef2aa9e0e5 100644 --- a/spec/features/container_registry_spec.rb +++ b/spec/features/container_registry_spec.rb @@ -47,7 +47,7 @@ describe "Container Registry", :js do scenario 'user removes a specific tag from container repository' do visit_container_registry - find('.js-toggle-repo').trigger('click') + find('.js-toggle-repo').click wait_for_requests expect_any_instance_of(ContainerRegistry::Tag) diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb index 1213f8c32eb..1c7932e7964 100644 --- a/spec/features/dashboard/group_spec.rb +++ b/spec/features/dashboard/group_spec.rb @@ -13,7 +13,7 @@ RSpec.describe 'Dashboard Group' do it 'creates new group', :js do visit dashboard_groups_path - find('.btn-new').trigger('click') + find('.btn-new').click new_path = 'Samurai' new_description = 'Tokugawa Shogunate' diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb index c6873d1923c..d92c002b4e7 100644 --- a/spec/features/dashboard/groups_list_spec.rb +++ b/spec/features/dashboard/groups_list_spec.rb @@ -138,7 +138,7 @@ feature 'Dashboard Groups page', :js do expect(page).not_to have_selector("#group-#{group.id}") # Go to next page - find(".gl-pagination .page:not(.active) a").trigger('click') + find(".gl-pagination .page:not(.active) a").click wait_for_requests diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb index a8919976c31..5b4c00b3c7e 100644 --- a/spec/features/dashboard/issues_spec.rb +++ b/spec/features/dashboard/issues_spec.rb @@ -33,7 +33,7 @@ RSpec.describe 'Dashboard Issues' do end it 'shows issues when current user is author', :js do - find('#assignee_id', visible: false).set('') + execute_script("document.querySelector('#assignee_id').value=''") find('.js-author-search', match: :first).click expect(find('li[data-user-id="null"] a.is-active')).to be_visible @@ -71,7 +71,7 @@ RSpec.describe 'Dashboard Issues' do describe 'new issue dropdown' do it 'shows projects only with issues feature enabled', :js do - find('.new-project-item-select-button').trigger('click') + find('.new-project-item-select-button').click page.within('.select2-results') do expect(page).to have_content(project.name_with_namespace) @@ -80,7 +80,7 @@ RSpec.describe 'Dashboard Issues' do end it 'shows the new issue page', :js do - find('.new-project-item-select-button').trigger('click') + find('.new-project-item-select-button').click wait_for_requests @@ -93,7 +93,7 @@ RSpec.describe 'Dashboard Issues' do find('#select2-drop-mask', visible: false) execute_script("$('#select2-drop-mask').remove();") - find('.new-project-item-link').trigger('click') + find('.new-project-item-link').click expect(page).to have_current_path("#{project_path}/issues/new") diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb index f01ba442e58..991d360ccaf 100644 --- a/spec/features/dashboard/merge_requests_spec.rb +++ b/spec/features/dashboard/merge_requests_spec.rb @@ -25,7 +25,7 @@ feature 'Dashboard Merge Requests' do end it 'shows projects only with merge requests feature enabled', :js do - find('.new-project-item-select-button').trigger('click') + find('.new-project-item-select-button').click page.within('.select2-results') do expect(page).to have_content(project.name_with_namespace) diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb index 01aca443f4a..6f916078b1a 100644 --- a/spec/features/dashboard/todos/todos_spec.rb +++ b/spec/features/dashboard/todos/todos_spec.rb @@ -52,7 +52,7 @@ feature 'Dashboard Todos' do end it 'updates todo count' do - expect(page).to have_content 'To do 0' + expect(page).to have_content 'Todos 0' expect(page).to have_content 'Done 1' end @@ -81,7 +81,7 @@ feature 'Dashboard Todos' do end it 'updates todo count' do - expect(page).to have_content 'To do 1' + expect(page).to have_content 'Todos 1' expect(page).to have_content 'Done 0' end end @@ -200,7 +200,7 @@ feature 'Dashboard Todos' do end it 'updates todo count' do - expect(page).to have_content 'To do 1' + expect(page).to have_content 'Todos 1' expect(page).to have_content 'Done 0' end end @@ -252,11 +252,11 @@ feature 'Dashboard Todos' do describe 'mark all as done', :js do before do visit dashboard_todos_path - find('.js-todos-mark-all').trigger('click') + find('.js-todos-mark-all').click end it 'shows "All done" message!' do - expect(page).to have_content 'To do 0' + expect(page).to have_content 'Todos 0' expect(page).to have_content "You're all done!" expect(page).not_to have_selector('.gl-pagination') end @@ -283,7 +283,7 @@ feature 'Dashboard Todos' do it 'updates todo count' do mark_all_and_undo - expect(page).to have_content 'To do 2' + expect(page).to have_content 'Todos 2' expect(page).to have_content 'Done 0' end @@ -309,9 +309,9 @@ feature 'Dashboard Todos' do end def mark_all_and_undo - find('.js-todos-mark-all').trigger('click') + find('.js-todos-mark-all').click wait_for_requests - find('.js-todos-undo-all').trigger('click') + find('.js-todos-undo-all').click wait_for_requests end end diff --git a/spec/features/discussion_comments/commit_spec.rb b/spec/features/discussion_comments/commit_spec.rb index 0375d0bf8ff..69d35cdbc72 100644 --- a/spec/features/discussion_comments/commit_spec.rb +++ b/spec/features/discussion_comments/commit_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Discussion Comments Merge Request', :js do +describe 'Discussion Comments Commit', :js do include RepoHelpers let(:user) { create(:user) } diff --git a/spec/features/discussion_comments/snippets_spec.rb b/spec/features/discussion_comments/snippets_spec.rb index 1e6389d9a13..4a236c4639b 100644 --- a/spec/features/discussion_comments/snippets_spec.rb +++ b/spec/features/discussion_comments/snippets_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Discussion Comments Issue', :js do +describe 'Discussion Comments Snippet', :js do let(:user) { create(:user) } let(:project) { create(:project) } let(:snippet) { create(:project_snippet, :private, project: project, author: user) } diff --git a/spec/features/explore/new_menu_spec.rb b/spec/features/explore/new_menu_spec.rb index c5ec495a418..8d5233d0c0f 100644 --- a/spec/features/explore/new_menu_spec.rb +++ b/spec/features/explore/new_menu_spec.rb @@ -65,9 +65,9 @@ feature 'Top Plus Menu', :js do visit project_path(project) page.within '.header-content' do - find('.header-new-dropdown-toggle').trigger('click') + find('.header-new-dropdown-toggle').click expect(page).to have_selector('.header-new.dropdown.open', count: 1) - find('.header-new-project-snippet a').trigger('click') + find('.header-new-project-snippet a').click end expect(page).to have_content('New Snippet') @@ -87,9 +87,9 @@ feature 'Top Plus Menu', :js do visit group_path(group) page.within '.header-content' do - find('.header-new-dropdown-toggle').trigger('click') + find('.header-new-dropdown-toggle').click expect(page).to have_selector('.header-new.dropdown.open', count: 1) - find('.header-new-group-project a').trigger('click') + find('.header-new-group-project a').click end expect(page).to have_content('Project path') @@ -155,7 +155,7 @@ feature 'Top Plus Menu', :js do def click_topmenuitem(item_name) page.within '.header-content' do - find('.header-new-dropdown-toggle').trigger('click') + find('.header-new-dropdown-toggle').click expect(page).to have_selector('.header-new.dropdown.open', count: 1) click_link item_name end diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index cc8906fa969..c1f3d94bc20 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -65,7 +65,7 @@ feature 'Group' do end it 'updates the team URL on graph path update', :js do - out_span = find('span[data-bind-out="create_chat_team"]') + out_span = find('span[data-bind-out="create_chat_team"]', visible: false) expect(out_span.text).to be_empty diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index 3223eb20b55..fa4d3a55c62 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -405,7 +405,7 @@ feature 'Issues > Labels bulk assignment' do end def update_issues - find('.update-selected-issues').trigger('click') + find('.update-selected-issues').click wait_for_requests end diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index 1c4649d0ba9..2e4a25ee15d 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -43,15 +43,16 @@ describe 'Dropdown assignee', :js do end it 'should show loading indicator when opened' do - filtered_search.set('assignee:') + slow_requests do + filtered_search.set('assignee:') - expect(page).to have_css('#js-dropdown-assignee .filter-dropdown-loading', visible: true) + expect(page).to have_css('#js-dropdown-assignee .filter-dropdown-loading', visible: true) + end end it 'should hide loading indicator when loaded' do filtered_search.set('assignee:') - expect(find(js_dropdown_assignee)).to have_css('.filter-dropdown-loading') expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading') end diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index 5e20fb48768..2fb5e7cdba4 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -51,9 +51,11 @@ describe 'Dropdown author', :js do end it 'should show loading indicator when opened' do - filtered_search.set('author:') + slow_requests do + filtered_search.set('author:') - expect(page).to have_css('#js-dropdown-author .filter-dropdown-loading', visible: true) + expect(page).to have_css('#js-dropdown-author .filter-dropdown-loading', visible: true) + end end it 'should hide loading indicator when loaded' do diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb index 3012c77f2b9..8db435634fd 100644 --- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb @@ -70,9 +70,11 @@ describe 'Dropdown emoji', :js do end it 'should show loading indicator when opened' do - filtered_search.set('my-reaction:') + slow_requests do + filtered_search.set('my-reaction:') - expect(page).to have_css('#js-dropdown-my-reaction .filter-dropdown-loading', visible: true) + expect(page).to have_css('#js-dropdown-my-reaction .filter-dropdown-loading', visible: true) + end end it 'should hide loading indicator when loaded' do diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb index cbc4f8d4c50..18cdb199c70 100644 --- a/spec/features/issues/filtered_search/dropdown_label_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -66,9 +66,11 @@ describe 'Dropdown label', :js do end it 'shows loading indicator when opened and hides it when loaded' do - filtered_search.set('label:') + slow_requests do + filtered_search.set('label:') - expect(find(js_dropdown_label)).to have_css('.filter-dropdown-loading') + expect(page).to have_css("#{js_dropdown_label} .filter-dropdown-loading", visible: true) + end expect(find(js_dropdown_label)).not_to have_css('.filter-dropdown-loading') end diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb index f6c2e952bea..031eb06723a 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -50,15 +50,16 @@ describe 'Dropdown milestone', :js do end it 'should show loading indicator when opened' do - filtered_search.set('milestone:') + slow_requests do + filtered_search.set('milestone:') - expect(page).to have_css('#js-dropdown-milestone .filter-dropdown-loading', visible: true) + expect(page).to have_css('#js-dropdown-milestone .filter-dropdown-loading', visible: true) + end end it 'should hide loading indicator when loaded' do filtered_search.set('milestone:') - expect(find(js_dropdown_milestone)).to have_css('.filter-dropdown-loading') expect(find(js_dropdown_milestone)).not_to have_css('.filter-dropdown-loading') end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 2974016c6a7..b3c50964810 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -139,7 +139,7 @@ describe 'Filter issues', :js do input_filtered_search('label:none') expect_tokens([label_token('none', false)]) - expect_issues_list_count(8) + expect_issues_list_count(4) expect_filtered_search_input_empty end diff --git a/spec/features/issues/filtered_search/recent_searches_spec.rb b/spec/features/issues/filtered_search/recent_searches_spec.rb index eef7988e2bd..f355cec3ba9 100644 --- a/spec/features/issues/filtered_search/recent_searches_spec.rb +++ b/spec/features/issues/filtered_search/recent_searches_spec.rb @@ -27,9 +27,8 @@ describe 'Recent searches', :js do input_filtered_search('foo', submit: true) input_filtered_search('bar', submit: true) - items = all('.filtered-search-history-dropdown-item', visible: false) + items = all('.filtered-search-history-dropdown-item', visible: false, count: 2) - expect(items.count).to eq(2) expect(items[0].text).to eq('bar') expect(items[1].text).to eq('foo') end @@ -38,9 +37,8 @@ describe 'Recent searches', :js do visit project_issues_path(project_1, label_name: 'foo', search: 'bar') visit project_issues_path(project_1, label_name: 'qux', search: 'garply') - items = all('.filtered-search-history-dropdown-item', visible: false) + items = all('.filtered-search-history-dropdown-item', visible: false, count: 2) - expect(items.count).to eq(2) expect(items[0].text).to eq('label:~qux garply') expect(items[1].text).to eq('label:~foo bar') end @@ -50,9 +48,8 @@ describe 'Recent searches', :js do visit project_issues_path(project_1, search: 'foo') - items = all('.filtered-search-history-dropdown-item', visible: false) + items = all('.filtered-search-history-dropdown-item', visible: false, count: 3) - expect(items.count).to eq(3) expect(items[0].text).to eq('foo') expect(items[1].text).to eq('saved1') expect(items[2].text).to eq('saved2') @@ -69,9 +66,8 @@ describe 'Recent searches', :js do input_filtered_search('more', submit: true) input_filtered_search('things', submit: true) - items = all('.filtered-search-history-dropdown-item', visible: false) + items = all('.filtered-search-history-dropdown-item', visible: false, count: 2) - expect(items.count).to eq(2) expect(items[0].text).to eq('things') expect(items[1].text).to eq('more') end @@ -80,7 +76,8 @@ describe 'Recent searches', :js do set_recent_searches(project_1_local_storage_key, '["foo", "bar"]') visit project_issues_path(project_1) - all('.filtered-search-history-dropdown-item', visible: false)[0].trigger('click') + find('.filtered-search-history-dropdown-toggle-button').click + all('.filtered-search-history-dropdown-item', count: 2)[0].click wait_for_filtered_search('foo') expect(find('.filtered-search').value.strip).to eq('foo') @@ -90,12 +87,11 @@ describe 'Recent searches', :js do set_recent_searches(project_1_local_storage_key, '["foo"]') visit project_issues_path(project_1) - items_before = all('.filtered-search-history-dropdown-item', visible: false) + find('.filtered-search-history-dropdown-toggle-button').click + all('.filtered-search-history-dropdown-item', count: 1) - expect(items_before.count).to eq(1) - - find('.filtered-search-history-clear-button', visible: false).trigger('click') - items_after = all('.filtered-search-history-dropdown-item', visible: false) + find('.filtered-search-history-clear-button').click + items_after = all('.filtered-search-history-dropdown-item', count: 0) expect(items_after.count).to eq(0) end diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb index 920f5546eef..0ae70c855db 100644 --- a/spec/features/issues/filtered_search/visual_tokens_spec.rb +++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb @@ -2,7 +2,6 @@ require 'rails_helper' describe 'Visual tokens', :js do include FilteredSearchHelpers - include WaitForRequests let!(:project) { create(:project) } let!(:user) { create(:user, name: 'administrator', username: 'root') } @@ -28,7 +27,7 @@ describe 'Visual tokens', :js do sign_in(user) create(:issue, project: project) - page.driver.set_cookie('sidebar_collapsed', 'true') + set_cookie('sidebar_collapsed', 'true') visit project_issues_path(project) end diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 15041ff04ea..b8a66245153 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -17,9 +17,9 @@ feature 'GFM autocomplete', :js do it 'updates issue descripton with GFM reference' do find('.issuable-edit').click - find('#issue-description').native.send_keys("@#{user.name[0...3]}") + simulate_input('#issue-description', "@#{user.name[0...3]}") - find('.atwho-view .cur').trigger('click') + find('.atwho-view .cur').click click_button 'Save changes' @@ -28,7 +28,6 @@ feature 'GFM autocomplete', :js do it 'opens autocomplete menu when field starts with text' do page.within '.timeline-content-form' do - find('#note-body').native.send_keys('') find('#note-body').native.send_keys('@') end @@ -46,7 +45,6 @@ feature 'GFM autocomplete', :js do it 'doesnt select the first item for non-assignee dropdowns' do page.within '.timeline-content-form' do - find('#note-body').native.send_keys('') find('#note-body').native.send_keys(':') end @@ -86,7 +84,6 @@ feature 'GFM autocomplete', :js do it 'selects the first item for assignee dropdowns' do page.within '.timeline-content-form' do - find('#note-body').native.send_keys('') find('#note-body').native.send_keys('@') end @@ -100,7 +97,7 @@ feature 'GFM autocomplete', :js do it 'includes items for assignee dropdowns with non-ASCII characters in name' do page.within '.timeline-content-form' do find('#note-body').native.send_keys('') - find('#note-body').native.send_keys("@#{user.name[0...8]}") + simulate_input('#note-body', "@#{user.name[0...8]}") end expect(page).to have_selector('.atwho-container') @@ -112,7 +109,6 @@ feature 'GFM autocomplete', :js do it 'selects the first item for non-assignee dropdowns if a query is entered' do page.within '.timeline-content-form' do - find('#note-body').native.send_keys('') find('#note-body').native.send_keys(':1') end @@ -127,9 +123,8 @@ feature 'GFM autocomplete', :js do it 'wraps the result in double quotes' do note = find('#note-body') page.within '.timeline-content-form' do - note.native.send_keys('') - note.native.send_keys("~#{label.title[0]}") - note.click + find('#note-body').native.send_keys('') + simulate_input('#note-body', "~#{label.title[0]}") end label_item = find('.atwho-view li', text: label.title) @@ -152,16 +147,13 @@ feature 'GFM autocomplete', :js do it "does not show dropdown when preceded with a special character" do note = find('#note-body') page.within '.timeline-content-form' do - note.native.send_keys('') note.native.send_keys("@") - note.click end expect(page).to have_selector('.atwho-container') page.within '.timeline-content-form' do note.native.send_keys("@") - note.click end expect(page).to have_selector('.atwho-container', visible: false) @@ -170,9 +162,7 @@ feature 'GFM autocomplete', :js do it "does not throw an error if no labels exist" do note = find('#note-body') page.within '.timeline-content-form' do - note.native.send_keys('') note.native.send_keys('~') - note.click end expect(page).to have_selector('.atwho-container', visible: false) @@ -181,9 +171,7 @@ feature 'GFM autocomplete', :js do it 'doesn\'t wrap for assignee values' do note = find('#note-body') page.within '.timeline-content-form' do - note.native.send_keys('') note.native.send_keys("@#{user.username[0]}") - note.click end user_item = find('.atwho-view li', text: user.username) @@ -194,9 +182,7 @@ feature 'GFM autocomplete', :js do it 'doesn\'t wrap for emoji values' do note = find('#note-body') page.within '.timeline-content-form' do - note.native.send_keys('') - note.native.send_keys(":cartwheel") - note.click + note.native.send_keys(":cartwheel_") end emoji_item = find('.atwho-view li', text: 'cartwheel_tone1') @@ -223,12 +209,11 @@ feature 'GFM autocomplete', :js do it 'triggers autocomplete after selecting a quick action' do note = find('#note-body') page.within '.timeline-content-form' do - note.native.send_keys('') note.native.send_keys('/as') - note.click end - find('.atwho-view li', text: '/assign').native.send_keys(:tab) + find('.atwho-view li', text: '/assign') + note.native.send_keys(:tab) user_item = find('.atwho-view li', text: user.username) expect(user_item).to have_content(user.username) diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index bc9c3d825c1..a9de52bd8d5 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -130,8 +130,8 @@ feature 'Issue Sidebar' do it 'adds new label' do page.within('.block.labels') do fill_in 'new_label_name', with: 'wontfix' - page.find('.suggest-colors a', match: :first).trigger('click') - page.find('button', text: 'Create').trigger('click') + page.find('.suggest-colors a', match: :first).click + page.find('button', text: 'Create').click page.within('.dropdown-page-one') do expect(page).to have_content 'wontfix' @@ -142,8 +142,8 @@ feature 'Issue Sidebar' do it 'shows error message if label title is taken' do page.within('.block.labels') do fill_in 'new_label_name', with: label.title - page.find('.suggest-colors a', match: :first).trigger('click') - page.find('button', text: 'Create').trigger('click') + page.find('.suggest-colors a', match: :first).click + page.find('button', text: 'Create').click page.within('.dropdown-page-two') do expect(page).to have_content 'Title has already been taken' @@ -170,7 +170,7 @@ feature 'Issue Sidebar' do end def open_issue_sidebar - find('aside.right-sidebar.right-sidebar-collapsed .js-sidebar-toggle').trigger('click') + find('aside.right-sidebar.right-sidebar-collapsed .js-sidebar-toggle').click find('aside.right-sidebar.right-sidebar-expanded') end end diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb index 6d7b1b1cd8f..17035b5501c 100644 --- a/spec/features/issues/move_spec.rb +++ b/spec/features/issues/move_spec.rb @@ -38,7 +38,7 @@ feature 'issue move to another project' do end scenario 'moving issue to another project', :js do - find('.js-move-issue').trigger('click') + find('.js-move-issue').click wait_for_requests all('.js-move-issue-dropdown-item')[0].click find('.js-move-issue-confirmation-button').click @@ -52,7 +52,7 @@ feature 'issue move to another project' do scenario 'searching project dropdown', :js do new_project_search.team << [user, :reporter] - find('.js-move-issue').trigger('click') + find('.js-move-issue').click wait_for_requests page.within '.js-sidebar-move-issue-block' do @@ -69,7 +69,7 @@ feature 'issue move to another project' do background { another_project.team << [user, :guest] } scenario 'browsing projects in projects select' do - find('.js-move-issue').trigger('click') + find('.js-move-issue').click wait_for_requests page.within '.js-sidebar-move-issue-block' do diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb index 1f57c110c11..bcc6e9bab0f 100644 --- a/spec/features/issues/update_issues_spec.rb +++ b/spec/features/issues/update_issues_spec.rb @@ -118,7 +118,7 @@ feature 'Multiple issue updating from issues#index', :js do end def click_update_issues_button - find('.update-selected-issues').trigger('click') + find('.update-selected-issues').click wait_for_requests end end diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb index 9f5e25ff2cb..c4c06ed514b 100644 --- a/spec/features/issues/user_uses_slash_commands_spec.rb +++ b/spec/features/issues/user_uses_slash_commands_spec.rb @@ -226,7 +226,7 @@ feature 'Issues > User uses quick actions', :js do end it 'applies the commands to both issues and moves the issue' do - write_note("/label ~#{bug.title} ~#{wontfix.title}\n/milestone %\"#{milestone.title}\"\n/move #{target_project.full_path}") + write_note("/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"\n\n/move #{target_project.full_path}") expect(page).to have_content 'Commands applied' expect(issue.reload).to be_closed @@ -245,7 +245,7 @@ feature 'Issues > User uses quick actions', :js do end it 'moves the issue and applies the commands to both issues' do - write_note("/move #{target_project.full_path}\n/label ~#{bug.title} ~#{wontfix.title}\n/milestone %\"#{milestone.title}\"") + write_note("/move #{target_project.full_path}\n\n/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"") expect(page).to have_content 'Commands applied' expect(issue.reload).to be_closed diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index d4fd3a50008..b9af77f918a 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -367,7 +367,7 @@ describe 'Issues' do it 'changes incoming email address token', :js do find('.issue-email-modal-btn').click previous_token = find('input#issue_email').value - find('.incoming-email-token-reset').trigger('click') + find('.incoming-email-token-reset').click wait_for_requests @@ -583,6 +583,18 @@ describe 'Issues' do expect(page.find_field("issue_description").value).not_to match /\n\n$/ end + + it "cancels a file upload correctly" do + slow_requests do + dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false) + + click_button 'Cancel' + end + + expect(page).to have_button('Attach a file') + expect(page).not_to have_button('Cancel') + expect(page).not_to have_selector('.uploading-progress-container', visible: true) + end end context 'form filled by URL parameters' do diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index ba976bc7216..4e2963c116d 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -23,11 +23,11 @@ feature 'Merge request conflict resolution', :js do within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do all('button', text: 'Use ours').each do |button| - button.trigger('click') + button.send_keys(:return) end end - click_button 'Commit conflict resolution' + find_button('Commit conflict resolution').send_keys(:return) expect(page).to have_content('All merge conflicts were resolved') merge_request.reload_diff @@ -71,7 +71,7 @@ feature 'Merge request conflict resolution', :js do execute_script('ace.edit($(".files-wrapper .diff-file pre")[1]).setValue("Gregor Samsa woke from troubled dreams");') end - click_button 'Commit conflict resolution' + find_button('Commit conflict resolution').send_keys(:return) expect(page).to have_content('All merge conflicts were resolved') merge_request.reload_diff diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb index 9aa0672feae..9e816cf041b 100644 --- a/spec/features/merge_requests/diff_notes_avatars_spec.rb +++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb @@ -22,7 +22,7 @@ feature 'Diff note avatars', :js do project.team << [user, :master] sign_in user - page.driver.set_cookie('sidebar_collapsed', 'true') + set_cookie('sidebar_collapsed', 'true') end context 'discussion tab' do @@ -56,7 +56,7 @@ feature 'Diff note avatars', :js do end it 'does not render avatar after commenting' do - first('.diff-line-num').trigger('mouseover') + first('.diff-line-num').click find('.js-add-diff-note-button').click page.within('.js-discussion-note-form') do @@ -85,7 +85,7 @@ feature 'Diff note avatars', :js do it 'shows note avatar' do page.within find_line(position.line_code(project.repository)) do - find('.diff-notes-collapse').click + find('.diff-notes-collapse').send_keys(:return) expect(page).to have_selector('img.js-diff-comment-avatar', count: 1) end @@ -93,7 +93,7 @@ feature 'Diff note avatars', :js do it 'shows comment on note avatar' do page.within find_line(position.line_code(project.repository)) do - find('.diff-notes-collapse').click + find('.diff-notes-collapse').send_keys(:return) expect(first('img.js-diff-comment-avatar')["data-original-title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}") end @@ -101,7 +101,7 @@ feature 'Diff note avatars', :js do it 'toggles comments when clicking avatar' do page.within find_line(position.line_code(project.repository)) do - find('.diff-notes-collapse').click + find('.diff-notes-collapse').send_keys(:return) end expect(page).to have_selector('.notes_holder', visible: false) @@ -117,7 +117,7 @@ feature 'Diff note avatars', :js do open_more_actions_dropdown(note) page.within find(".note-row-#{note.id}") do - find('.js-note-delete').click + accept_confirm { find('.js-note-delete').click } end wait_for_requests @@ -139,7 +139,7 @@ feature 'Diff note avatars', :js do end page.within find_line(position.line_code(project.repository)) do - find('.diff-notes-collapse').trigger('click') + find('.diff-notes-collapse').send_keys(:return) expect(page).to have_selector('img.js-diff-comment-avatar', count: 2) end @@ -152,14 +152,14 @@ feature 'Diff note avatars', :js do page.within '.js-discussion-note-form' do find('.js-note-text').native.send_keys('Test') - find('.js-comment-button').trigger('click') + find('.js-comment-button').click wait_for_requests end end page.within find_line(position.line_code(project.repository)) do - find('.diff-notes-collapse').trigger('click') + find('.diff-notes-collapse').send_keys(:return) expect(page).to have_selector('img.js-diff-comment-avatar', count: 3) expect(find('.diff-comments-more-count')).to have_content '+1' @@ -177,7 +177,7 @@ feature 'Diff note avatars', :js do it 'shows extra comment count' do page.within find_line(position.line_code(project.repository)) do - find('.diff-notes-collapse').click + find('.diff-notes-collapse').send_keys(:return) expect(find('.diff-comments-more-count')).to have_content '+1' end diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb index 3db0729cafb..15d380b1bf4 100644 --- a/spec/features/merge_requests/diff_notes_resolve_spec.rb +++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb @@ -192,7 +192,7 @@ feature 'Diff notes resolve', :js do page.find('.discussion-next-btn').click end - expect(page.evaluate_script("$('body').scrollTop()")).to be > 0 + expect(page.evaluate_script("window.pageYOffset")).to be > 0 end it 'hides jump to next button when all resolved' do @@ -241,10 +241,8 @@ feature 'Diff notes resolve', :js do end it 'resolves discussion' do - page.all('.note').each do |note| - note.all('.line-resolve-btn').each do |button| - button.click - end + page.all('.note .line-resolve-btn').each do |button| + button.click end expect(page).to have_content('Resolved by') @@ -305,10 +303,10 @@ feature 'Diff notes resolve', :js do end page.within '.line-resolve-all-container' do - page.find('.discussion-next-btn').trigger('click') + page.find('.discussion-next-btn').click end - expect(page.evaluate_script("$('body').scrollTop()")).to be > 0 + expect(page.evaluate_script("window.pageYOffset")).to be > 0 end it 'updates updated text after resolving note' do diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb index 2adca58620f..1bf77296ae6 100644 --- a/spec/features/merge_requests/diffs_spec.rb +++ b/spec/features/merge_requests/diffs_spec.rb @@ -7,14 +7,12 @@ feature 'Diffs URL', :js do let(:merge_request) { create(:merge_request, source_project: project) } context 'when visit with */* as accept header' do - before do - page.driver.add_header('Accept', '*/*') - end - it 'renders the notes' do create :note_on_merge_request, project: project, noteable: merge_request, note: 'Rebasing with master' - visit diffs_project_merge_request_path(project, merge_request) + inspect_requests(inject_headers: { 'Accept' => '*/*' }) do + visit diffs_project_merge_request_path(project, merge_request) + end # Load notes and diff through AJAX expect(page).to have_css('.note-text', visible: false, text: 'Rebasing with master') @@ -90,7 +88,7 @@ feature 'Diffs URL', :js do visit diffs_project_merge_request_path(project, merge_request) # Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax - find("[id=\"#{changelog_id}\"] .js-edit-blob").trigger('click') + find("[id=\"#{changelog_id}\"] .js-edit-blob").click expect(page).to have_selector('.js-fork-suggestion-button', count: 1) expect(page).to have_selector('.js-cancel-fork-suggestion-button', count: 1) diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb index 758fc9b139d..1dcc1e139a0 100644 --- a/spec/features/merge_requests/form_spec.rb +++ b/spec/features/merge_requests/form_spec.rb @@ -43,7 +43,7 @@ describe 'New/edit merge request', :js do expect(page).to have_content user2.name end - find('a', text: 'Assign to me').trigger('click') + find('a', text: 'Assign to me').click expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s) page.within '.js-assignee-search' do expect(page).to have_content user.name diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb index bf21a719901..bac56270362 100644 --- a/spec/features/merge_requests/mini_pipeline_graph_spec.rb +++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb @@ -92,7 +92,7 @@ feature 'Mini Pipeline Graph', :js do end it 'should close when toggle is clicked again' do - toggle.trigger('click') + toggle.click expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu') end diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb index 1a41fd36a4f..c5498563b39 100644 --- a/spec/features/merge_requests/update_merge_requests_spec.rb +++ b/spec/features/merge_requests/update_merge_requests_spec.rb @@ -127,7 +127,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do end def click_update_merge_requests_button - find('.update-selected-issues').trigger('click') + find('.update-selected-issues').click wait_for_requests end end diff --git a/spec/features/merge_requests/user_posts_diff_notes_spec.rb b/spec/features/merge_requests/user_posts_diff_notes_spec.rb index 7a773fb2baa..d44eb23d7f4 100644 --- a/spec/features/merge_requests/user_posts_diff_notes_spec.rb +++ b/spec/features/merge_requests/user_posts_diff_notes_spec.rb @@ -8,7 +8,7 @@ feature 'Merge requests > User posts diff notes', :js do let(:project) { merge_request.source_project } before do - page.driver.set_cookie('sidebar_collapsed', 'true') + set_cookie('sidebar_collapsed', 'true') project.add_developer(user) sign_in(user) @@ -103,7 +103,10 @@ feature 'Merge requests > User posts diff notes', :js do it 'allows commenting' do should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]')) - first('.js-note-delete', visible: false).trigger('click') + accept_confirm do + first('button.more-actions-toggle').click + first('.js-note-delete').click + end should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]')) end @@ -236,7 +239,7 @@ feature 'Merge requests > User posts diff notes', :js do def should_allow_dismissing_a_comment(line_holder, diff_side = nil) write_comment_on_line(line_holder, diff_side) - find('.js-close-discussion-note-form').trigger('click') + find('.js-close-discussion-note-form').click assert_comment_dismissal(line_holder) end diff --git a/spec/features/merge_requests/user_posts_notes_spec.rb b/spec/features/merge_requests/user_posts_notes_spec.rb index d7cda73ab40..f4c75a2f265 100644 --- a/spec/features/merge_requests/user_posts_notes_spec.rb +++ b/spec/features/merge_requests/user_posts_notes_spec.rb @@ -141,7 +141,7 @@ describe 'Merge requests > User posts notes', :js do end it 'removes the attachment div and resets the edit form' do - find('.js-note-attachment-delete').click + accept_confirm { find('.js-note-attachment-delete').click } is_expected.not_to have_css('.note-attachment') is_expected.not_to have_css('.current-note-edit-form') wait_for_requests diff --git a/spec/features/merge_requests/versions_spec.rb b/spec/features/merge_requests/versions_spec.rb index 50f7d721ff3..29f95039af8 100644 --- a/spec/features/merge_requests/versions_spec.rb +++ b/spec/features/merge_requests/versions_spec.rb @@ -67,8 +67,8 @@ feature 'Merge Request versions', :js do line_code = '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_2_2' page.within(diff_file_selector) do - find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").trigger 'mouseover' - find(".line_holder[id='#{line_code}'] button").trigger 'click' + find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").hover + find(".line_holder[id='#{line_code}'] button").click page.within("form[data-line-code='#{line_code}']") do fill_in "note[note]", with: "Typo, please fix" @@ -137,8 +137,8 @@ feature 'Merge Request versions', :js do line_code = '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_4_4' page.within(diff_file_selector) do - find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").trigger 'mouseover' - find(".line_holder[id='#{line_code}'] button").trigger 'click' + find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").hover + find(".line_holder[id='#{line_code}'] button").click page.within("form[data-line-code='#{line_code}']") do fill_in "note[note]", with: "Typo, please fix" diff --git a/spec/features/merge_requests/widget_deployments_spec.rb b/spec/features/merge_requests/widget_deployments_spec.rb index 5658c2c5122..72a52c979b3 100644 --- a/spec/features/merge_requests/widget_deployments_spec.rb +++ b/spec/features/merge_requests/widget_deployments_spec.rb @@ -42,7 +42,7 @@ feature 'Widget Deployments Header', :js do end scenario 'does start build when stop button clicked' do - click_button('Stop environment') + accept_confirm { click_button('Stop environment') } expect(page).to have_content('close_app') end diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index 1cddd35fd8a..c60883911f7 100644 --- a/spec/features/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Profile account page' do +describe 'Profile account page', :js do let(:user) { create(:user) } before do @@ -56,47 +56,38 @@ describe 'Profile account page' do end end - describe 'when I reset private token' do - before do - visit profile_account_path - end - - it 'resets private token' do - previous_token = find("#private-token").value - - click_link('Reset private token') - - expect(find('#private-token').value).not_to eq(previous_token) - end - end - describe 'when I reset RSS token' do before do - visit profile_account_path + visit profile_personal_access_tokens_path end it 'resets RSS token' do - previous_token = find("#rss-token").value + within('.rss-token-reset') do + previous_token = find("#rss_token").value - click_link('Reset RSS token') + accept_confirm { click_link('reset it') } + + expect(find('#rss_token').value).not_to eq(previous_token) + end expect(page).to have_content 'RSS token was successfully reset' - expect(find('#rss-token').value).not_to eq(previous_token) end end describe 'when I reset incoming email token' do before do allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true) - visit profile_account_path + visit profile_personal_access_tokens_path end it 'resets incoming email token' do - previous_token = find('#incoming-email-token').value + within('.incoming-email-token-reset') do + previous_token = find('#incoming_email_token').value - click_link('Reset incoming email token') + accept_confirm { click_link('reset it') } - expect(find('#incoming-email-token').value).not_to eq(previous_token) + expect(find('#incoming_email_token').value).not_to eq(previous_token) + end end end diff --git a/spec/features/profiles/oauth_applications_spec.rb b/spec/features/profiles/oauth_applications_spec.rb index 8cb240077eb..d1edeef8da4 100644 --- a/spec/features/profiles/oauth_applications_spec.rb +++ b/spec/features/profiles/oauth_applications_spec.rb @@ -14,7 +14,7 @@ describe 'Profile > Applications' do page.within('.oauth-applications') do expect(page).to have_content('Your applications (1)') - click_button 'Destroy' + accept_confirm { click_button 'Destroy' } end expect(page).to have_content('The application was deleted successfully') @@ -28,7 +28,7 @@ describe 'Profile > Applications' do page.within('.oauth-authorized-applications') do expect(page).to have_content('Authorized applications (1)') - click_button 'Revoke' + accept_confirm { click_button 'Revoke' } end expect(page).to have_content('The application was revoked access.') diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb index a572160dae9..8461cd0027c 100644 --- a/spec/features/profiles/personal_access_tokens_spec.rb +++ b/spec/features/profiles/personal_access_tokens_spec.rb @@ -34,7 +34,7 @@ describe 'Profile > Personal Access Tokens', :js do fill_in "Name", with: name # Set date to 1st of next month - find_field("Expires at").trigger('focus') + find_field("Expires at").click find(".pika-next").click click_on "1" @@ -78,7 +78,7 @@ describe 'Profile > Personal Access Tokens', :js do it "allows revocation of an active token" do visit profile_personal_access_tokens_path - click_on "Revoke" + accept_confirm { click_on "Revoke" } expect(page).to have_selector(".settings-message") expect(no_personal_access_tokens_message).to have_text("This user has no active Personal Access Tokens.") @@ -100,7 +100,7 @@ describe 'Profile > Personal Access Tokens', :js do 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" + accept_confirm { click_on "Revoke" } expect(active_personal_access_tokens).to have_text(personal_access_token.name) expect(page).to have_content("Could not revoke") end diff --git a/spec/features/profiles/user_visits_notifications_tab_spec.rb b/spec/features/profiles/user_visits_notifications_tab_spec.rb index 923ca8b1c80..df89918f17a 100644 --- a/spec/features/profiles/user_visits_notifications_tab_spec.rb +++ b/spec/features/profiles/user_visits_notifications_tab_spec.rb @@ -13,7 +13,7 @@ feature 'User visits the notifications tab', :js do it 'changes the project notifications setting' do expect(page).to have_content('Notifications') - first('#notifications-button').trigger('click') + first('#notifications-button').click click_link('On mention') expect(page).to have_content('On mention') diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb index 924ee0e4174..90d6841af0e 100644 --- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb +++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb @@ -53,7 +53,7 @@ describe 'User visits the profile preferences page' do expect(page).to have_content("You don't have starred projects yet") expect(page.current_path).to eq starred_dashboard_projects_path - find('.shortcuts-activity').trigger('click') + find('.shortcuts-activity').click expect(page).not_to have_content("You don't have starred projects yet") expect(page.current_path).to eq dashboard_projects_path diff --git a/spec/features/projects/artifacts/download_spec.rb b/spec/features/projects/artifacts/download_spec.rb index f1bdb2812c6..6f76c14910b 100644 --- a/spec/features/projects/artifacts/download_spec.rb +++ b/spec/features/projects/artifacts/download_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Download artifact', :js do +feature 'Download artifact' do let(:project) { create(:project, :public) } let(:pipeline) { create(:ci_empty_pipeline, status: :success, project: project) } let(:job) { create(:ci_build, :artifacts, :success, pipeline: pipeline) } diff --git a/spec/features/projects/artifacts/file_spec.rb b/spec/features/projects/artifacts/file_spec.rb index b2be10a7e0c..df1d17bdcb7 100644 --- a/spec/features/projects/artifacts/file_spec.rb +++ b/spec/features/projects/artifacts/file_spec.rb @@ -39,7 +39,6 @@ feature 'Artifact file', :js 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/branches_spec.rb b/spec/features/projects/branches_spec.rb index 941d34dd660..7a77df83034 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -67,7 +67,7 @@ describe 'Branches' do expect(page).to have_content('fix') expect(find('.all-branches')).to have_selector('li', count: 1) - find('.js-branch-fix .btn-remove').trigger(:click) + accept_confirm { find('.js-branch-fix .btn-remove').click } expect(page).not_to have_content('fix') expect(find('.all-branches')).to have_selector('li', count: 0) diff --git a/spec/features/projects/commit/diff_notes_spec.rb b/spec/features/projects/commit/diff_notes_spec.rb index f0fe4e00acc..4dbfc6f6edf 100644 --- a/spec/features/projects/commit/diff_notes_spec.rb +++ b/spec/features/projects/commit/diff_notes_spec.rb @@ -20,8 +20,8 @@ feature 'Commit diff', :js do it "adds comment to diff" do diff_line_num = first('.diff-line-num.new') - diff_line_num.trigger('mouseover') - diff_line_num.find('.js-add-diff-note-button').trigger('click') + diff_line_num.hover + diff_line_num.find('.js-add-diff-note-button').click page.within(first('.diff-viewer')) do find('.js-note-text').set 'test comment' diff --git a/spec/features/projects/deploy_keys_spec.rb b/spec/features/projects/deploy_keys_spec.rb index 2d1a9b931b5..e445758cb5e 100644 --- a/spec/features/projects/deploy_keys_spec.rb +++ b/spec/features/projects/deploy_keys_spec.rb @@ -20,7 +20,7 @@ describe 'Project deploy keys', :js do page.within(find('.deploy-keys')) do expect(page).to have_selector('.deploy-keys li', count: 1) - click_on 'Remove' + accept_confirm { find(:button, text: 'Remove').send_keys(:return) } expect(page).not_to have_selector('.fa-spinner', count: 0) expect(page).to have_selector('.deploy-keys li', count: 0) diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index 0fe1eb4c293..5fc3ba54f65 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -193,12 +193,14 @@ feature 'Environment' do create(:environment, project: project, name: 'staging-1.0/review', state: :available) - - visit folder_project_environments_path(project, id: 'staging-1.0') end it 'renders a correct environment folder' do - expect(page).to have_gitlab_http_status(:ok) + reqs = inspect_requests do + visit folder_project_environments_path(project, id: 'staging-1.0') + end + + expect(reqs.first.status_code).to eq(200) expect(page).to have_content('Environments / staging-1.0') end end diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 610f566c0cf..b4eb5795470 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -151,7 +151,7 @@ feature 'Environments page', :js do find('.js-dropdown-play-icon-container').click expect(page).to have_content(action.name.humanize) - expect { find('.js-manual-action-link').trigger('click') } + expect { find('.js-manual-action-link').click } .not_to change { Ci::Pipeline.count } end diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb index e5282b42a4f..951456763dc 100644 --- a/spec/features/projects/features_visibility_spec.rb +++ b/spec/features/projects/features_visibility_spec.rb @@ -22,7 +22,7 @@ describe 'Edit Project Settings' do # disable by clicking toggle toggle_feature_off("project[project_feature_attributes][#{tool_name}_access_level]") page.within('.sharing-permissions') do - click_button 'Save changes' + find('input[value="Save changes"]').click end wait_for_requests expect(page).not_to have_selector(".shortcuts-#{shortcut_name}") @@ -30,7 +30,7 @@ describe 'Edit Project Settings' do # re-enable by clicking toggle again toggle_feature_on("project[project_feature_attributes][#{tool_name}_access_level]") page.within('.sharing-permissions') do - click_button 'Save changes' + find('input[value="Save changes"]').click end wait_for_requests expect(page).to have_selector(".shortcuts-#{shortcut_name}") diff --git a/spec/features/projects/files/edit_file_soft_wrap_spec.rb b/spec/features/projects/files/edit_file_soft_wrap_spec.rb index 25f7e18ac5c..3ab43b3c656 100644 --- a/spec/features/projects/files/edit_file_soft_wrap_spec.rb +++ b/spec/features/projects/files/edit_file_soft_wrap_spec.rb @@ -7,18 +7,18 @@ feature 'User uses soft wrap whilst editing file', :js do project.team << [user, :master] sign_in user visit project_new_blob_path(project, 'master', file_name: 'test_file-name') - editor = find('.file-editor.code') - editor.click - editor.send_keys 'Touch water with paw then recoil in horror chase dog then - run away chase the pig around the house eat owner\'s food, and knock - dish off table head butt cant eat out of my own dish. Cat is love, cat - is life rub face on everything poop on grasses so meow. Playing with - balls of wool flee in terror at cucumber discovered on floor run in - circles tuxedo cats always looking dapper, but attack dog, run away - and pretend to be victim so all of a sudden cat goes crazy, yet chase - laser. Make muffins sit in window and stare ooo, a bird! yum lick yarn - hanging out of own butt jump off balcony, onto stranger\'s head yet - chase laser. Purr for no reason stare at ceiling hola te quiero.'.squish + page.within('.file-editor.code') do + find('.ace_text-input', visible: false).send_keys 'Touch water with paw then recoil in horror chase dog then + run away chase the pig around the house eat owner\'s food, and knock + dish off table head butt cant eat out of my own dish. Cat is love, cat + is life rub face on everything poop on grasses so meow. Playing with + balls of wool flee in terror at cucumber discovered on floor run in + circles tuxedo cats always looking dapper, but attack dog, run away + and pretend to be victim so all of a sudden cat goes crazy, yet chase + laser. Make muffins sit in window and stare ooo, a bird! yum lick yarn + hanging out of own butt jump off balcony, onto stranger\'s head yet + chase laser. Purr for no reason stare at ceiling hola te quiero.'.squish + end end let(:toggle_button) { find('.soft-wrap-toggle') } @@ -36,6 +36,6 @@ feature 'User uses soft wrap whilst editing file', :js do end def get_content_width - find('.ace_content')[:style].slice!(/width: \d+/).slice!(/\d+/) + find('.ace_content')[:style].slice!(/width: \d+/).slice!(/\d+/).to_i end end diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb index 05776c50f9d..461aa39d0ad 100644 --- a/spec/features/projects/import_export/export_file_spec.rb +++ b/spec/features/projects/import_export/export_file_spec.rb @@ -41,7 +41,7 @@ feature 'Import/Export - project export integration test', :js do expect(page).to have_content('Export project') - click_link 'Export project' + find(:link, 'Export project').send_keys(:return) visit edit_project_path(project) diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index 026aa03f7cf..af125e1b9d3 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -94,6 +94,6 @@ feature 'Import/Export - project import integration test', :js do end def click_import_project_tab - find('#import-project-tab').trigger('click') + find('#import-project-tab').click end end diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb index b6a7c3cdcdb..e76bc6f1220 100644 --- a/spec/features/projects/import_export/namespace_export_file_spec.rb +++ b/spec/features/projects/import_export/namespace_export_file_spec.rb @@ -52,7 +52,7 @@ feature 'Import/Export - Namespace export file cleanup', :js do expect(page).to have_content('Export project') - click_link 'Export project' + find(:link, 'Export project').send_keys(:return) visit edit_project_path(project) diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb index 21c9acc7ac0..5d9208ebadd 100644 --- a/spec/features/projects/jobs/user_browses_job_spec.rb +++ b/spec/features/projects/jobs/user_browses_job_spec.rb @@ -21,12 +21,12 @@ describe 'User browses a job', :js do expect(page).to have_content("Job ##{build.id}") expect(page).to have_css('#build-trace') - click_link('Erase') + accept_confirm { click_link('Erase') } + expect(page).to have_no_css('.artifacts') expect(build).not_to have_trace expect(build.artifacts_file.exists?).to be_falsy expect(build.artifacts_metadata.exists?).to be_falsy - expect(page).to have_no_css('.artifacts') page.within('.erased') do expect(page).to have_content('Job has been erased') diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index b095c3e6f7b..c2a0d2395a9 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -380,7 +380,6 @@ feature 'Jobs' do end it 'loads the page and shows all needed controls' do - expect(page.status_code).to eq(200) expect(page).to have_content 'Retry' end end @@ -392,11 +391,10 @@ feature 'Jobs' do job.run! visit project_job_path(project, job) find('.js-cancel-job').click() - find('.js-retry-button').trigger('click') + find('.js-retry-button').click end it 'shows the right status and buttons', :js do - expect(page).to have_gitlab_http_status(200) page.within('aside.right-sidebar') do expect(page).to have_content 'Cancel' end @@ -443,28 +441,30 @@ feature 'Jobs' do context 'access source' do context 'job from project' do before do - Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' } job.run! - visit project_job_path(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(job.trace.send(:current_path)) + requests = inspect_requests(inject_headers: { 'X-Sendfile-Type' => 'X-Sendfile' }) do + visit raw_project_job_path(project, job) + end + + expect(requests.first.status_code).to eq(200) + expect(requests.first.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') + expect(requests.first.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' } job2.run! - visit raw_project_job_path(project, job2) end it 'sends the right headers' do - expect(page.status_code).to eq(404) + requests = inspect_requests(inject_headers: { 'X-Sendfile-Type' => 'X-Sendfile' }) do + visit raw_project_job_path(project, job2) + end + expect(requests.first.status_code).to eq(404) end end end @@ -473,8 +473,6 @@ feature 'Jobs' do let(:existing_file) { Tempfile.new('existing-trace-file').path } before do - Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' } - job.run! end @@ -483,16 +481,14 @@ feature 'Jobs' do allow_any_instance_of(Gitlab::Ci::Trace) .to receive(:paths) .and_return([existing_file]) - - visit project_job_path(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(existing_file) + requests = inspect_requests(inject_headers: { 'X-Sendfile-Type' => 'X-Sendfile' }) do + visit raw_project_job_path(project, job) + end + expect(requests.first.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') + expect(requests.first.response_headers['X-Sendfile']).to eq(existing_file) end end diff --git a/spec/features/projects/members/groups_with_access_list_spec.rb b/spec/features/projects/members/groups_with_access_list_spec.rb index b1053982eee..7f067aadec6 100644 --- a/spec/features/projects/members/groups_with_access_list_spec.rb +++ b/spec/features/projects/members/groups_with_access_list_spec.rb @@ -31,6 +31,7 @@ feature 'Projects > Members > Groups with access list', :js do tomorrow = Date.today + 3 fill_in "member_expires_at_#{group.id}", with: tomorrow.strftime("%F") + find('body').click wait_for_requests page.within(find('li.group_member')) do @@ -40,7 +41,7 @@ feature 'Projects > Members > Groups with access list', :js do scenario 'deletes group link' do page.within(first('.group_member')) do - find('.btn-remove').click + accept_confirm { find('.btn-remove').click } end wait_for_requests diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb index 5f7b4ee0e77..0f88f4cb1e8 100644 --- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb +++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb @@ -20,7 +20,7 @@ feature 'Projects > Members > Master adds member with expiration date', :js do page.within '.users-project-form' do select2(new_member.id, from: '#user_ids', multiple: true) - fill_in 'expires_at', with: date.to_s(:medium) + fill_in 'expires_at', with: date.to_s(:medium) + "\n" click_on 'Add to project' end @@ -37,7 +37,7 @@ feature 'Projects > Members > Master adds member with expiration date', :js do visit project_project_members_path(project) page.within "#project_member_#{new_member.project_members.first.id}" do - find('.js-access-expiration-date').set date.to_s(:medium) + find('.js-access-expiration-date').set date.to_s(:medium) + "\n" wait_for_requests expect(page).to have_content('Expires in 3 days') end diff --git a/spec/features/projects/members/share_with_group_spec.rb b/spec/features/projects/members/share_with_group_spec.rb index 63b5df5a8f5..3198798306c 100644 --- a/spec/features/projects/members/share_with_group_spec.rb +++ b/spec/features/projects/members/share_with_group_spec.rb @@ -41,7 +41,7 @@ feature 'Project > Members > Share with Group', :js do select2 group_to_share_with.id, from: '#link_group_id' page.find('body').click - find('.btn-create').trigger('click') + find('.btn-create').click page.within('.project-members-groups') do expect(page).to have_content(group_to_share_with.name) @@ -123,7 +123,7 @@ feature 'Project > Members > Share with Group', :js do fill_in 'expires_at_groups', with: (Time.now + 4.5.days).strftime('%Y-%m-%d') page.find('body').click - find('.btn-create').trigger('click') + find('.btn-create').click end scenario 'the group link shows the expiration time with a warning class' do diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb index 0fbe1ddb2a5..4eb36156812 100644 --- a/spec/features/projects/members/user_requests_access_spec.rb +++ b/spec/features/projects/members/user_requests_access_spec.rb @@ -60,7 +60,7 @@ feature 'Projects > Members > User requests access', :js do expect(project.requesters.exists?(user_id: user)).to be_truthy - click_link 'Withdraw Access Request' + accept_confirm { click_link 'Withdraw Access Request' } expect(project.requesters.exists?(user_id: user)).to be_falsey expect(page).to have_content 'Your access request to the project has been withdrawn.' diff --git a/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb b/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb index f34302f25f8..e3f90a78cb5 100644 --- a/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb +++ b/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb @@ -31,7 +31,7 @@ describe 'User comments on a diff', :js do page.within('.files > div:nth-child(3)') do expect(page).to have_content('Line is wrong') - find('.js-toggle-diff-comments').trigger('click') + find('.js-toggle-diff-comments').click expect(page).not_to have_content('Line is wrong') end @@ -64,7 +64,7 @@ describe 'User comments on a diff', :js do # Hide the comment. page.within('.files > div:nth-child(3)') do - find('.js-toggle-diff-comments').trigger('click') + find('.js-toggle-diff-comments').click expect(page).not_to have_content('Line is wrong') end @@ -77,7 +77,7 @@ describe 'User comments on a diff', :js do # Show the comment. page.within('.files > div:nth-child(3)') do - find('.js-toggle-diff-comments').trigger('click') + find('.js-toggle-diff-comments').click end # Now both the comments should be shown. @@ -90,6 +90,7 @@ describe 'User comments on a diff', :js do end # Check the same comments in the side-by-side view. + execute_script("window.scrollTo(0,0);") click_link('Side-by-side') wait_for_requests @@ -153,11 +154,11 @@ describe 'User comments on a diff', :js do find('.more-actions').click find('.more-actions .dropdown-menu li', match: :first) - find('.js-note-delete').click + accept_confirm { find('.js-note-delete').click } end page.within('.merge-request-tabs') do - find('.notes-tab').trigger('click') + find('.notes-tab').click end wait_for_requests diff --git a/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb b/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb index f6e3997383f..3d19a2923b9 100644 --- a/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb +++ b/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe 'User edits a merge request', :js do + include Select2Helper + let(:project) { create(:project, :repository) } let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:user) { create(:user) } @@ -15,8 +17,7 @@ describe 'User edits a merge request', :js do it 'changes the target branch' do expect(page).to have_content('Target branch') - first('.target_branch').click - select('merge-test', from: 'merge_request_target_branch', visible: false) + select2('merge-test', from: '#merge_request_target_branch') click_button('Save changes') expect(page).to have_content("Request to merge #{merge_request.source_branch} into merge-test") diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index 8e11cb94350..6f097ad16c7 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -15,7 +15,7 @@ feature 'New project' do expect(page).to have_content('Project path') expect(page).to have_content('Project name') - find('#import-project-tab').trigger('click') + find('#import-project-tab').click expect(page).to have_link('GitHub') expect(page).to have_link('Bitbucket') @@ -137,7 +137,7 @@ feature 'New project' do context 'Import project options', :js do before do visit new_project_path - find('#import-project-tab').trigger('click') + find('#import-project-tab').click end context 'from git repository url' do diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb index 24b335a7068..fa2f7a1fd78 100644 --- a/spec/features/projects/pipeline_schedules_spec.rb +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -54,7 +54,7 @@ feature 'Pipeline Schedules', :js do end it 'deletes the pipeline' do - click_link 'Delete' + accept_confirm { click_link 'Delete' } expect(page).not_to have_css(".pipeline-schedule-table-row") end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index acbc5b046e6..b8fa1a54c24 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -67,13 +67,13 @@ describe 'Pipeline', :js do it 'shows a running icon and a cancel action for the running build' do page.within('#ci-badge-deploy') do expect(page).to have_selector('.js-ci-status-icon-running') - expect(page).to have_selector('.js-icon-action-cancel') + expect(page).to have_selector('.js-icon-cancel') expect(page).to have_content('deploy') end end it 'should be possible to cancel the running build' do - find('#ci-badge-deploy .ci-action-icon-container').trigger('click') + find('#ci-badge-deploy .ci-action-icon-container').click expect(page).not_to have_content('Cancel running') end @@ -86,13 +86,13 @@ describe 'Pipeline', :js do expect(page).to have_content('build') end - page.within('#ci-badge-build .ci-action-icon-container') do - expect(page).to have_selector('.js-icon-action-retry') + page.within('#ci-badge-build .ci-action-icon-container.js-icon-retry') do + expect(page).to have_selector('svg') end end it 'should be possible to retry the success job' do - find('#ci-badge-build .ci-action-icon-container').trigger('click') + find('#ci-badge-build .ci-action-icon-container').click expect(page).not_to have_content('Retry job') end @@ -105,13 +105,13 @@ describe 'Pipeline', :js do expect(page).to have_content('test') end - page.within('#ci-badge-test .ci-action-icon-container') do - expect(page).to have_selector('.js-icon-action-retry') + page.within('#ci-badge-test .ci-action-icon-container.js-icon-retry') do + expect(page).to have_selector('svg') end end it 'should be possible to retry the failed build' do - find('#ci-badge-test .ci-action-icon-container').trigger('click') + find('#ci-badge-test .ci-action-icon-container').click expect(page).not_to have_content('Retry job') end @@ -124,13 +124,13 @@ describe 'Pipeline', :js do expect(page).to have_content('manual') end - page.within('#ci-badge-manual-build .ci-action-icon-container') do - expect(page).to have_selector('.js-icon-action-play') + page.within('#ci-badge-manual-build .ci-action-icon-container.js-icon-play') do + expect(page).to have_selector('svg') end end it 'should be possible to play the manual job' do - find('#ci-badge-manual-build .ci-action-icon-container').trigger('click') + find('#ci-badge-manual-build .ci-action-icon-container').click expect(page).not_to have_content('Play job') end @@ -165,7 +165,7 @@ describe 'Pipeline', :js do context 'when retrying' do before do - find('.js-retry-button').trigger('click') + find('.js-retry-button').click end it { expect(page).not_to have_content('Retry') } @@ -231,7 +231,7 @@ describe 'Pipeline', :js do context 'when retrying' do before do - find('.js-retry-button').trigger('click') + find('.js-retry-button').click end it { expect(page).not_to have_content('Retry') } diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index ae888fd4343..fc689bbb486 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -103,7 +103,7 @@ describe 'Pipelines', :js do context 'when canceling' do before do - find('.js-pipelines-cancel-button').click + accept_confirm { find('.js-pipelines-cancel-button').click } wait_for_requests end @@ -232,7 +232,7 @@ describe 'Pipelines', :js do context 'when canceling' do before do - find('.js-pipelines-cancel-button').trigger('click') + accept_alert { find('.js-pipelines-cancel-button').click } end it 'indicates that pipeline was canceled' do @@ -345,14 +345,14 @@ describe 'Pipelines', :js do context 'when clicking a stage badge' do it 'should open a dropdown' do - find('.js-builds-dropdown-button').trigger('click') + find('.js-builds-dropdown-button').click expect(page).to have_link build.name end it 'should be possible to cancel pending build' do - find('.js-builds-dropdown-button').trigger('click') - find('a.js-ci-action-icon').trigger('click') + find('.js-builds-dropdown-button').click + find('a.js-ci-action-icon').click expect(page).to have_content('canceled') expect(build.reload).to be_canceled @@ -361,11 +361,16 @@ describe 'Pipelines', :js do context 'dropdown jobs list' do it 'should keep the dropdown open when the user ctr/cmd + clicks in the job name' do - find('.js-builds-dropdown-button').trigger('click') - - execute_script('var e = $.Event("keydown", { keyCode: 64 }); $("body").trigger(e);') - - find('.mini-pipeline-graph-dropdown-item').trigger('click') + find('.js-builds-dropdown-button').click + dropdown_item = find('.mini-pipeline-graph-dropdown-item').native + + %i(alt control).each do |meta_key| + page.driver.browser.action + .key_down(meta_key) + .click(dropdown_item) + .key_up(meta_key) + .perform + end expect(page).to have_selector('.js-ci-action-icon') end @@ -525,7 +530,6 @@ describe 'Pipelines', :js do let(:project) { create(:project, :public, :repository) } it { expect(page).to have_content 'Build with confidence' } - it { expect(page).to have_gitlab_http_status(:success) } end context 'when project is private' do diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb index 50c0bfd580d..33ccbc1a29f 100644 --- a/spec/features/projects/ref_switcher_spec.rb +++ b/spec/features/projects/ref_switcher_spec.rb @@ -6,7 +6,7 @@ feature 'Ref switcher', :js do before do project.team << [user, :master] - page.driver.set_cookie('new_repo', 'true') + set_cookie('new_repo', 'true') sign_in(user) visit project_tree_path(project, 'master') end diff --git a/spec/features/projects/services/user_activates_jira_spec.rb b/spec/features/projects/services/user_activates_jira_spec.rb index 0a86292ae6c..ac78b1dfb1c 100644 --- a/spec/features/projects/services/user_activates_jira_spec.rb +++ b/spec/features/projects/services/user_activates_jira_spec.rb @@ -65,7 +65,7 @@ describe 'User activates Jira', :js do expect(find('.flash-container-page')).to have_content 'Test failed. message' expect(find('.flash-container-page')).to have_content 'Save anyway' - find('.flash-alert .flash-action').trigger('click') + find('.flash-alert .flash-action').click wait_for_requests expect(page).to have_content('JIRA activated.') diff --git a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb index 95d5e8b14b9..6f057137867 100644 --- a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb @@ -76,7 +76,7 @@ feature 'Setup Mattermost slash commands', :js do select_element = find('#mattermost_team_id') selected_option = select_element.find('option[selected]') - expect(select_element['disabled']).to be(true) + expect(select_element['disabled']).to eq("true") expect(selected_option).to have_content(team_name.to_s) end @@ -104,7 +104,7 @@ feature 'Setup Mattermost slash commands', :js do select_element = find('#mattermost_team_id') - expect(select_element['disabled']).to be(false) + expect(select_element['disabled']).to be_falsey expect(select_element.all('option').count).to eq(3) end @@ -122,7 +122,7 @@ feature 'Setup Mattermost slash commands', :js do click_link 'Add to Mattermost' - expect(find('input[type="submit"]')['disabled']).not_to be(true) + expect(find('input[type="submit"]')['disabled']).not_to eq("true") end it 'disables the submit button if the required fields are not provided', :js do @@ -132,7 +132,7 @@ feature 'Setup Mattermost slash commands', :js do fill_in('mattermost_trigger', with: '') - expect(find('input[type="submit"]')['disabled']).to be(true) + expect(find('input[type="submit"]')['disabled']).to eq("true") end def stub_teams(count: 0) diff --git a/spec/features/projects/settings/forked_project_settings_spec.rb b/spec/features/projects/settings/forked_project_settings_spec.rb new file mode 100644 index 00000000000..28954a4fb40 --- /dev/null +++ b/spec/features/projects/settings/forked_project_settings_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +feature 'Settings for a forked project', :js do + include ProjectForksHelper + let(:user) { create(:user) } + let(:original_project) { create(:project) } + let(:forked_project) { fork_project(original_project, user) } + + before do + original_project.add_master(user) + forked_project.add_master(user) + sign_in(user) + end + + shared_examples 'project settings for a forked projects' do + it 'allows deleting the link to the forked project' do + visit edit_project_path(forked_project) + + click_button 'Remove fork relationship' + + wait_for_requests + + fill_in('confirm_name_input', with: forked_project.name) + click_button('Confirm') + + expect(page).to have_content('The fork relationship has been removed.') + expect(forked_project.reload.forked?).to be_falsy + end + end + + it_behaves_like 'project settings for a forked projects' + + context 'when the original project is deleted' do + before do + original_project.destroy! + end + + it_behaves_like 'project settings for a forked projects' + end +end diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb index b1ec556bf16..ac76c30cc7c 100644 --- a/spec/features/projects/settings/merge_requests_settings_spec.rb +++ b/spec/features/projects/settings/merge_requests_settings_spec.rb @@ -21,7 +21,7 @@ feature 'Project settings > Merge Requests', :js do within('.sharing-permissions-form') do find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click - click_on('Save changes') + find('input[value="Save changes"]').send_keys(:return) end expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds') @@ -41,7 +41,7 @@ feature 'Project settings > Merge Requests', :js do within('.sharing-permissions-form') do find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .project-feature-toggle').click - click_on('Save changes') + find('input[value="Save changes"]').send_keys(:return) end expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds') @@ -62,7 +62,7 @@ feature 'Project settings > Merge Requests', :js do within('.sharing-permissions-form') do find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click - click_on('Save changes') + find('input[value="Save changes"]').send_keys(:return) end expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds') diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb index de8fbb15b9c..ea8f997409d 100644 --- a/spec/features/projects/settings/pipelines_settings_spec.rb +++ b/spec/features/projects/settings/pipelines_settings_spec.rb @@ -22,7 +22,7 @@ feature "Pipelines settings" do context 'for master' do given(:role) { :master } - scenario 'be allowed to change', :js do + scenario 'be allowed to change' do fill_in('Test coverage parsing', with: 'coverage_regex') click_on 'Save changes' diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index a4fefb0d0e7..e2a5619c22b 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -34,7 +34,6 @@ feature 'Repository settings' do visit project_settings_repository_path(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 @@ -86,7 +85,7 @@ feature 'Repository settings' do project.deploy_keys << private_deploy_key visit project_settings_repository_path(project) - find('li', text: private_deploy_key.title).click_button('Remove') + accept_confirm { find('li', text: private_deploy_key.title).click_button('Remove') } expect(page).not_to have_content(private_deploy_key.title) end diff --git a/spec/features/projects/snippets/create_snippet_spec.rb b/spec/features/projects/snippets/create_snippet_spec.rb index 3e79dba3f19..e4215291f99 100644 --- a/spec/features/projects/snippets/create_snippet_spec.rb +++ b/spec/features/projects/snippets/create_snippet_spec.rb @@ -10,7 +10,7 @@ feature 'Create Snippet', :js do 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!') + find('.ace_text-input', visible: false).send_keys('Hello World!') end end @@ -59,7 +59,7 @@ feature 'Create Snippet', :js do fill_form dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') - click_button('Create snippet') + find("input[value='Create snippet']").send_keys(:return) wait_for_requests expect(page).to have_content('My Snippet Title') diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb index 4c1fa5a666e..8ee7b9cf015 100644 --- a/spec/features/projects/tree/create_directory_spec.rb +++ b/spec/features/projects/tree/create_directory_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' feature 'Multi-file editor new directory', :js do - include WaitForRequests - let(:user) { create(:user) } let(:project) { create(:project, :repository) } @@ -10,7 +8,7 @@ feature 'Multi-file editor new directory', :js do project.add_master(user) sign_in(user) - page.driver.set_cookie('new_repo', 'true') + set_cookie('new_repo', 'true') visit project_tree_path(project, :master) @@ -32,7 +30,6 @@ feature 'Multi-file editor new directory', :js do click_button('Commit 1 file') - expect(page).to have_content('Your changes have been committed') expect(page).to have_selector('td', text: 'commit message') click_link('foldername') diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb index ed3b52a5790..1e2de0711b8 100644 --- a/spec/features/projects/tree/create_file_spec.rb +++ b/spec/features/projects/tree/create_file_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' feature 'Multi-file editor new file', :js do - include WaitForRequests - let(:user) { create(:user) } let(:project) { create(:project, :repository) } @@ -10,7 +8,7 @@ feature 'Multi-file editor new file', :js do project.add_master(user) sign_in(user) - page.driver.set_cookie('new_repo', 'true') + set_cookie('new_repo', 'true') visit project_tree_path(project, :master) @@ -32,7 +30,6 @@ feature 'Multi-file editor new file', :js do click_button('Commit 1 file') - expect(page).to have_content('Your changes have been committed') expect(page).to have_selector('td', text: 'commit message') end end diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb index 7dbe4fd0aa5..8439bb5a69e 100644 --- a/spec/features/projects/tree/upload_file_spec.rb +++ b/spec/features/projects/tree/upload_file_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' feature 'Multi-file editor upload file', :js do - include WaitForRequests - let(:user) { create(:user) } let(:project) { create(:project, :repository) } let(:txt_file) { File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt') } @@ -12,7 +10,7 @@ feature 'Multi-file editor upload file', :js do project.add_master(user) sign_in(user) - page.driver.set_cookie('new_repo', 'true') + set_cookie('new_repo', 'true') visit project_tree_path(project, :master) @@ -29,7 +27,7 @@ feature 'Multi-file editor upload file', :js do find('.add-to-tree').click expect(page).to have_selector('.repo-tab', text: 'doc_sample.txt') - expect(page).to have_content(File.open(txt_file, &:readline)) + expect(find('.blob-editor-container .lines-content')['innerText']).to have_content(File.open(txt_file, &:readline)) end it 'uploads image file' do diff --git a/spec/features/projects/user_browses_files_spec.rb b/spec/features/projects/user_browses_files_spec.rb index f43b11c9485..f5e4d7f5130 100644 --- a/spec/features/projects/user_browses_files_spec.rb +++ b/spec/features/projects/user_browses_files_spec.rb @@ -175,10 +175,11 @@ describe 'User browses files' do page.within('#modal-upload-blob') do fill_in(:commit_message, with: 'New commit message') + fill_in(:branch_name, with: 'new_branch_name', visible: true) + click_button('Upload file') end - fill_in(:branch_name, with: 'new_branch_name', visible: true) - click_button('Upload file') + wait_for_all_requests visit(project_blob_path(project, 'new_branch_name/logo_sample.svg')) diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb index d63cbe578d8..337baaf4dcd 100644 --- a/spec/features/projects/wiki/markdown_preview_spec.rb +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -18,13 +18,13 @@ feature 'Projects > Wiki > User previews markdown changes', :js do sign_in(user) visit project_path(project) - find('.shortcuts-wiki').trigger('click') + find('.shortcuts-wiki').click end context "while creating a new wiki page" do context "when there are no spaces or hyphens in the page name" do it "rewrites relative links as expected" do - find('.add-new-wiki').trigger('click') + find('.add-new-wiki').click page.within '#modal-new-wiki' do fill_in :new_wiki_path, with: 'a/b/c/d' click_button 'Create page' @@ -91,7 +91,7 @@ feature 'Projects > Wiki > User previews markdown changes', :js do context "while editing a wiki page" do def create_wiki_page(path) - find('.add-new-wiki').trigger('click') + find('.add-new-wiki').click page.within '#modal-new-wiki' do fill_in :new_wiki_path, with: path diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index e72b7dc0dd5..4a9d1cb87e1 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -77,14 +77,14 @@ describe 'User creates wiki page' do [stem] ++++ - \sqrt{4} = 2 + \\sqrt{4} = 2 ++++ another part [latexmath] ++++ - \beta_x \gamma + \\beta_x \\gamma ++++ stem:[2+2] is 4 diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb index 89f6901eb01..ff325aeadd3 100644 --- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb @@ -138,7 +138,7 @@ describe 'User views a wiki page' do it 'opens a default wiki page', :js do visit(project_path(project)) - find('.shortcuts-wiki').trigger('click') + find('.shortcuts-wiki').click expect(page).to have_content('Home · Create Page') end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 3b01ed442bf..63e6051b571 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -13,8 +13,8 @@ feature 'Project' do end it "allows creation from templates", :js do - find('#create-from-template-tab').trigger('click') - find("##{template.name}").trigger('click') + find('#create-from-template-tab').click + find("label[for=#{template.name}]").click fill_in("project_path", with: template.name) page.within '#content-body' do diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index 2ab1eda90f1..a4084818284 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -48,7 +48,7 @@ feature 'Protected Branches', :js do expect(page).to have_content('fix') expect(find('.all-branches')).to have_selector('li', count: 1) - page.find('[data-target="#modal-delete-branch"]').trigger(:click) + page.find('[data-target="#modal-delete-branch"]').click expect(page).to have_css('.js-delete-branch[disabled]') fill_in 'delete_branch_input', with: 'fix' @@ -67,9 +67,9 @@ feature 'Protected Branches', :js do form = '.js-new-protected-branch' within form do - find(".js-allowed-to-merge").trigger('click') + find(".js-allowed-to-merge").click click_link 'No one' - find(".js-allowed-to-push").trigger('click') + find(".js-allowed-to-push").click click_link 'Developers + Masters' end @@ -171,7 +171,7 @@ feature 'Protected Branches', :js do end def set_protected_branch_name(branch_name) - find(".js-protected-branch-select").trigger('click') + find(".js-protected-branch-select").click find(".dropdown-input-field").set(branch_name) click_on("Create wildcard #{branch_name}") end diff --git a/spec/features/raven_js_spec.rb b/spec/features/raven_js_spec.rb index b1f51959d54..74890c86047 100644 --- a/spec/features/raven_js_spec.rb +++ b/spec/features/raven_js_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'RavenJS', :js do +feature 'RavenJS' do let(:raven_path) { '/raven.bundle.js' } it 'should not load raven if sentry is disabled' do @@ -18,6 +18,8 @@ feature 'RavenJS', :js do end def has_requested_raven - page.driver.network_traffic.one? {|request| request.url.end_with?(raven_path)} + page.all('script', visible: false).one? do |elm| + elm[:src] =~ /#{raven_path}$/ + end end end diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb index 0ed797a62ea..77212fb105b 100644 --- a/spec/features/search/user_searches_for_code_spec.rb +++ b/spec/features/search/user_searches_for_code_spec.rb @@ -32,14 +32,14 @@ describe 'User searches for code' do include_examples 'top right search form' it 'finds code' do - find('.js-search-project-dropdown').trigger('click') + find('.js-search-project-dropdown').click page.within('.project-filter') do click_link(project.name_with_namespace) end fill_in('dashboard_search', with: 'rspec') - find('.btn-search').trigger('click') + find('.btn-search').click page.within('.results') do expect(find(:css, '.search-results')).to have_content('Update capybara, rspec-rails, poltergeist to recent versions') diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb index 630a81b1c5e..ef9553f2a91 100644 --- a/spec/features/search/user_searches_for_issues_spec.rb +++ b/spec/features/search/user_searches_for_issues_spec.rb @@ -18,7 +18,7 @@ describe 'User searches for issues', :js do it 'finds an issue' do fill_in('dashboard_search', with: issue1.title) - find('.btn-search').trigger('click') + find('.btn-search').click page.within('.search-filter') do click_link('Issues') @@ -31,14 +31,14 @@ describe 'User searches for issues', :js do context 'when on a project page' do it 'finds an issue' do - find('.js-search-project-dropdown').trigger('click') + find('.js-search-project-dropdown').click page.within('.project-filter') do click_link(project.name_with_namespace) end fill_in('dashboard_search', with: issue1.title) - find('.btn-search').trigger('click') + find('.btn-search').click page.within('.search-filter') do click_link('Issues') @@ -62,7 +62,7 @@ describe 'User searches for issues', :js do it 'finds an issue' do fill_in('dashboard_search', with: issue1.title) - find('.btn-search').trigger('click') + find('.btn-search').click page.within('.search-filter') do click_link('Issues') diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb index 116256682f4..3b6739aecbd 100644 --- a/spec/features/search/user_searches_for_merge_requests_spec.rb +++ b/spec/features/search/user_searches_for_merge_requests_spec.rb @@ -17,7 +17,7 @@ describe 'User searches for merge requests', :js do it 'finds a merge request' do fill_in('dashboard_search', with: merge_request1.title) - find('.btn-search').trigger('click') + find('.btn-search').click page.within('.search-filter') do click_link('Merge requests') @@ -30,14 +30,14 @@ describe 'User searches for merge requests', :js do context 'when on a project page' do it 'finds a merge request' do - find('.js-search-project-dropdown').trigger('click') + find('.js-search-project-dropdown').click page.within('.project-filter') do click_link(project.name_with_namespace) end fill_in('dashboard_search', with: merge_request1.title) - find('.btn-search').trigger('click') + find('.btn-search').click page.within('.search-filter') do click_link('Merge requests') diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb index 4fa9fe9ce8c..6e197aee498 100644 --- a/spec/features/search/user_searches_for_milestones_spec.rb +++ b/spec/features/search/user_searches_for_milestones_spec.rb @@ -17,7 +17,7 @@ describe 'User searches for milestones', :js do it 'finds a milestone' do fill_in('dashboard_search', with: milestone1.title) - find('.btn-search').trigger('click') + find('.btn-search').click page.within('.search-filter') do click_link('Milestones') @@ -30,14 +30,14 @@ describe 'User searches for milestones', :js do context 'when on a project page' do it 'finds a milestone' do - find('.js-search-project-dropdown').trigger('click') + find('.js-search-project-dropdown').click page.within('.project-filter') do click_link(project.name_with_namespace) end fill_in('dashboard_search', with: milestone1.title) - find('.btn-search').trigger('click') + find('.btn-search').click page.within('.search-filter') do click_link('Milestones') diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb index 1ea56479ecc..00af625dc86 100644 --- a/spec/features/search/user_searches_for_wiki_pages_spec.rb +++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb @@ -15,14 +15,14 @@ describe 'User searches for wiki pages', :js do include_examples 'top right search form' it 'finds a page' do - find('.js-search-project-dropdown').trigger('click') + find('.js-search-project-dropdown').click page.within('.project-filter') do click_link(project.name_with_namespace) end fill_in('dashboard_search', with: 'content') - find('.btn-search').trigger('click') + find('.btn-search').click page.within('.search-filter') do click_link('Wiki') diff --git a/spec/features/search/user_uses_search_filters_spec.rb b/spec/features/search/user_uses_search_filters_spec.rb index 95f3eb5e805..aa883c964d2 100644 --- a/spec/features/search/user_uses_search_filters_spec.rb +++ b/spec/features/search/user_uses_search_filters_spec.rb @@ -16,7 +16,7 @@ describe 'User uses search filters', :js do context' when filtering by group' do it 'shows group projects' do - find('.js-search-group-dropdown').trigger('click') + find('.js-search-group-dropdown').click wait_for_requests @@ -27,7 +27,7 @@ describe 'User uses search filters', :js do expect(find('.js-search-group-dropdown')).to have_content(group.name) page.within('.project-filter') do - find('.js-search-project-dropdown').trigger('click') + find('.js-search-project-dropdown').click wait_for_requests @@ -39,7 +39,7 @@ describe 'User uses search filters', :js do context' when filtering by project' do it 'shows a project' do page.within('.project-filter') do - find('.js-search-project-dropdown').trigger('click') + find('.js-search-project-dropdown').click wait_for_requests diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb index bf79974b8c6..269351e55c9 100644 --- a/spec/features/snippets/notes_on_personal_snippets_spec.rb +++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb @@ -74,24 +74,21 @@ describe 'Comments on personal snippets', :js do it 'should not have autocomplete' do wait_for_requests - request_count_before = page.driver.network_traffic.count find('#note_note').native.send_keys('') fill_in 'note[note]', with: '@' wait_for_requests - request_count_after = page.driver.network_traffic.count # This selector probably won't be in place even if autocomplete was enabled # but we want to make sure expect(page).not_to have_selector('.atwho-view') - expect(request_count_before).to eq(request_count_after) end end context 'when editing a note' do it 'changes the text' do - find('.js-note-edit').trigger('click') + find('.js-note-edit').click page.within('.current-note-edit-form') do fill_in 'note[note]', with: 'new content' @@ -113,7 +110,7 @@ describe 'Comments on personal snippets', :js do open_more_actions_dropdown(snippet_notes[0]) page.within("#notes-list li#note_#{snippet_notes[0].id}") do - click_on 'Delete comment' + accept_confirm { click_on 'Delete comment' } end wait_for_requests diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb index d732383a1e1..941765b7578 100644 --- a/spec/features/snippets/user_creates_snippet_spec.rb +++ b/spec/features/snippets/user_creates_snippet_spec.rb @@ -14,7 +14,7 @@ feature 'User creates snippet', :js do 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!' + find('.ace_text-input', visible: false).send_keys 'Hello World!' end end @@ -43,8 +43,8 @@ feature 'User creates snippet', :js do link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] expect(link).to match(%r{/uploads/-/system/temp/\h{32}/banana_sample\.gif\z}) - visit(link) - expect(page.status_code).to eq(200) + reqs = inspect_requests { visit(link) } + expect(reqs.first.status_code).to eq(200) end end @@ -61,8 +61,8 @@ feature 'User creates snippet', :js do link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z}) - visit(link) - expect(page.status_code).to eq(200) + reqs = inspect_requests { visit(link) } + expect(reqs.first.status_code).to eq(200) end scenario 'validation fails for the first time' do @@ -86,15 +86,15 @@ feature 'User creates snippet', :js do link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z}) - visit(link) - expect(page.status_code).to eq(200) + reqs = inspect_requests { visit(link) } + expect(reqs.first.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 find(:xpath, "//input[@id='personal_snippet_file_name']").set 'snippet+file+name' - find('.ace_editor').native.send_keys 'Hello World!' + find('.ace_text-input', visible: false).send_keys 'Hello World!' end click_button 'Create snippet' diff --git a/spec/features/tags/master_creates_tag_spec.rb b/spec/features/tags/master_creates_tag_spec.rb index 1455345bd56..1f8bd8d681e 100644 --- a/spec/features/tags/master_creates_tag_spec.rb +++ b/spec/features/tags/master_creates_tag_spec.rb @@ -63,7 +63,7 @@ feature 'Master creates tag' do expect(ref_input.value).to eq 'master' expect(find('.dropdown-toggle-text')).to have_content 'master' - find('.js-branch-select').trigger('click') + find('.js-branch-select').click expect(find('.dropdown-menu')).to have_content 'empty-branch' end diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb index f5b3774122b..dfda664d673 100644 --- a/spec/features/tags/master_deletes_tag_spec.rb +++ b/spec/features/tags/master_deletes_tag_spec.rb @@ -64,7 +64,7 @@ feature 'Master deletes tag' do def delete_first_tag page.within('.content') do - first('.btn-remove').click + accept_confirm { first('.btn-remove').click } end end end diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb index 548d8372a07..bc472e74997 100644 --- a/spec/features/triggers_spec.rb +++ b/spec/features/triggers_spec.rb @@ -45,7 +45,7 @@ feature 'Triggers', :js do visit project_settings_ci_cd_path(@project) # See if edit page has correct descrption - find('a[title="Edit"]').click + find('a[title="Edit"]').send_keys(:return) expect(page.find('#trigger_description').value).to have_content 'trigger desc' end @@ -54,7 +54,7 @@ feature 'Triggers', :js do visit project_settings_ci_cd_path(@project) # See if edit page opens, then fill in new description and save - find('a[title="Edit"]').click + find('a[title="Edit"]').send_keys(:return) fill_in 'trigger_description', with: new_trigger_title click_button 'Save trigger' @@ -70,7 +70,7 @@ feature 'Triggers', :js do visit project_settings_ci_cd_path(@project) # See if the trigger can be edited and description is blank - find('a[title="Edit"]').click + find('a[title="Edit"]').send_keys(:return) expect(page.find('#trigger_description').value).to have_content '' # See if trigger can be updated with description and saved successfully @@ -94,12 +94,13 @@ feature 'Triggers', :js do scenario 'take trigger ownership' do # See if "Take ownership" on trigger works post trigger creation - find('a.btn-trigger-take-ownership').click page.accept_confirm do - expect(page.find('.flash-notice')).to have_content 'Trigger was re-assigned.' - expect(page.find('.triggers-list')).to have_content trigger_title - expect(page.find('.triggers-list .trigger-owner')).to have_content user.name + first(:link, "Take ownership").send_keys(:return) end + + expect(page.find('.flash-notice')).to have_content 'Trigger was re-assigned.' + expect(page.find('.triggers-list')).to have_content trigger_title + expect(page.find('.triggers-list .trigger-owner')).to have_content user.name end end @@ -116,11 +117,12 @@ feature 'Triggers', :js do scenario 'revoke trigger' do # See if "Revoke" on trigger works post trigger creation - find('a.btn-trigger-revoke').click page.accept_confirm do - expect(page.find('.flash-notice')).to have_content 'Trigger removed' - expect(page).to have_selector('p.settings-message.text-center.append-bottom-default') + find('a.btn-trigger-revoke').send_keys(:return) end + + expect(page.find('.flash-notice')).to have_content 'Trigger removed' + expect(page).to have_selector('p.settings-message.text-center.append-bottom-default') end end diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb index f3662cb184f..c9afef2a8de 100644 --- a/spec/features/u2f_spec.rb +++ b/spec/features/u2f_spec.rb @@ -79,7 +79,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do first_u2f_device = register_u2f_device second_u2f_device = register_u2f_device(name: 'My other device') - click_on "Delete", match: :first + accept_confirm { click_on "Delete", match: :first } expect(page).to have_content('Successfully deleted') expect(page.body).not_to match(first_u2f_device.name) @@ -162,7 +162,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do @u2f_device.respond_to_u2f_authentication - expect(page).to have_content('We heard back from your U2F device') expect(page).to have_css('.sign-out-link', visible: false) end end @@ -174,23 +173,10 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do @u2f_device.respond_to_u2f_authentication - expect(page).to have_content('We heard back from your U2F device') expect(page).to have_css('.sign-out-link', visible: false) end end - it 'persists remember_me value via hidden field' do - gitlab_sign_in(user, remember: true) - - @u2f_device.respond_to_u2f_authentication - expect(page).to have_content('We heard back from your U2F device') - - within 'div#js-authenticate-u2f' do - field = first('input#user_remember_me', visible: false) - expect(field.value).to eq '1' - end - end - describe "when a given U2F device has already been registered by another user" do describe "but not the current user" do it "does not allow logging in with that particular device" do @@ -205,7 +191,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do # Try authenticating user with the old U2F device gitlab_sign_in(current_user) @u2f_device.respond_to_u2f_authentication - expect(page).to have_content('We heard back from your U2F device') expect(page).to have_content('Authentication via U2F device failed') end end @@ -223,7 +208,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do # Try authenticating user with the same U2F device gitlab_sign_in(current_user) @u2f_device.respond_to_u2f_authentication - expect(page).to have_content('We heard back from your U2F device') expect(page).to have_css('.sign-out-link', visible: false) end @@ -235,7 +219,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do unregistered_device = FakeU2fDevice.new(page, 'My device') gitlab_sign_in(user) unregistered_device.respond_to_u2f_authentication - expect(page).to have_content('We heard back from your U2F device') expect(page).to have_content('Authentication via U2F device failed') end @@ -260,7 +243,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do [first_device, second_device].each do |device| gitlab_sign_in(user) device.respond_to_u2f_authentication - expect(page).to have_content('We heard back from your U2F device') expect(page).to have_css('.sign-out-link', visible: false) @@ -283,7 +265,9 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do it "deletes u2f registrations" do visit profile_account_path - expect { click_on "Disable" }.to change { U2fRegistration.count }.by(-1) + expect do + accept_confirm { click_on "Disable" } + end.to change { U2fRegistration.count }.by(-1) end end end diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb index 1261ffdc2ee..972c10aaf23 100644 --- a/spec/features/uploads/user_uploads_file_to_note_spec.rb +++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb @@ -21,16 +21,12 @@ feature 'User uploads file to note' do end context 'uploading is in progress' do - it 'shows "Cancel" button on uploading', :js do - dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false) - - expect(page).to have_button('Cancel') - end - it 'cancels uploading on clicking to "Cancel" button', :js do - dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false) + slow_requests do + dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false) - click_button 'Cancel' + click_button 'Cancel' + end expect(page).to have_button('Attach a file') expect(page).not_to have_button('Cancel') @@ -38,16 +34,20 @@ feature 'User uploads file to note' do end it 'shows "Attaching a file" message on uploading 1 file', :js do - dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false) + slow_requests do + dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false) - expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -') + expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -') + end end it 'shows "Attaching 2 files" message on uploading 2 file', :js do - dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4'), - Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false) + slow_requests do + dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4'), + Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false) - expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching 2 files -') + expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching 2 files -') + end end it 'shows error message, "retry" and "attach a new file" link a if file is too big', :js do diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb index 0252c957c95..a9973cdf214 100644 --- a/spec/features/users_spec.rb +++ b/spec/features/users_spec.rb @@ -24,6 +24,7 @@ feature 'Users', :js do user.reload expect(user.reset_password_token).not_to be_nil + find('a[href="#login-pane"]').click gitlab_sign_in(user) expect(current_path).to eq root_path diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb index 5d8e818f7bf..c78f7d0d9be 100644 --- a/spec/features/variables_spec.rb +++ b/spec/features/variables_spec.rb @@ -82,7 +82,7 @@ describe 'Project variables', :js do it 'deletes variable' do page.within('.variables-table') do - click_on 'Remove' + accept_confirm { click_on 'Remove' } end expect(page).not_to have_selector('variables-table') diff --git a/spec/fixtures/api/schemas/public_api/v4/user/login.json b/spec/fixtures/api/schemas/public_api/v4/user/login.json index e6c1d9c9d84..aa066883c47 100644 --- a/spec/fixtures/api/schemas/public_api/v4/user/login.json +++ b/spec/fixtures/api/schemas/public_api/v4/user/login.json @@ -27,11 +27,9 @@ "can_create_group", "can_create_project", "two_factor_enabled", - "external", - "private_token" + "external" ], "properties": { - "$ref": "full.json", - "private_token": { "type": "string" } + "$ref": "full.json" } } diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb index 6a3945c0ebc..bc2422aba90 100644 --- a/spec/helpers/ci_status_helper_spec.rb +++ b/spec/helpers/ci_status_helper_spec.rb @@ -8,17 +8,13 @@ describe CiStatusHelper do describe '#ci_icon_for_status' do it 'renders to correct svg on success' do - expect(helper).to receive(:render) - .with('shared/icons/icon_status_success.svg', anything) - - helper.ci_icon_for_status(success_commit.status) + expect(helper.ci_icon_for_status('success').to_s) + .to include 'status_success' end it 'renders the correct svg on failure' do - expect(helper).to receive(:render) - .with('shared/icons/icon_status_failed.svg', anything) - - helper.ci_icon_for_status(failed_commit.status) + expect(helper.ci_icon_for_status('failed').to_s) + .to include 'status_failed' end end diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb index a44b200c5da..6c4f7050ee0 100644 --- a/spec/helpers/gitlab_routing_helper_spec.rb +++ b/spec/helpers/gitlab_routing_helper_spec.rb @@ -63,4 +63,30 @@ describe GitlabRoutingHelper do it { expect(resend_invite_group_member_path(group_member)).to eq resend_invite_group_group_member_path(group_member.source, group_member) } end end + + describe '#preview_markdown_path' do + let(:project) { create(:project) } + + it 'returns group preview markdown path for a group parent' do + group = create(:group) + + expect(preview_markdown_path(group)).to eq("/groups/#{group.path}/preview_markdown") + end + + it 'returns project preview markdown path for a project parent' do + expect(preview_markdown_path(project)).to eq("/#{project.full_path}/preview_markdown") + end + + it 'returns snippet preview markdown path for a personal snippet' do + @snippet = create(:personal_snippet) + + expect(preview_markdown_path(nil)).to eq("/snippets/preview_markdown") + end + + it 'returns project preview markdown path for a project snippet' do + @snippet = create(:project_snippet, project: project) + + expect(preview_markdown_path(project)).to eq("/#{project.full_path}/preview_markdown") + end + end end diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index ead3e28438e..cb851d828f2 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -159,4 +159,36 @@ describe IssuablesHelper do end end end + + describe '#issuable_initial_data' do + let(:user) { create(:user) } + + before do + allow(helper).to receive(:current_user).and_return(user) + allow(helper).to receive(:can?).and_return(true) + end + + it 'returns the correct json for an issue' do + issue = create(:issue, author: user, description: 'issue text') + @project = issue.project + + expected_data = { + 'endpoint' => "/#{@project.full_path}/issues/#{issue.iid}", + 'canUpdate' => true, + 'canDestroy' => true, + 'issuableRef' => "##{issue.iid}", + 'markdownPreviewPath' => "/#{@project.full_path}/preview_markdown", + 'markdownDocsPath' => '/help/user/markdown', + 'issuableTemplates' => [], + 'projectPath' => @project.path, + 'projectNamespace' => @project.namespace.path, + 'initialTitleHtml' => issue.title, + 'initialTitleText' => issue.title, + 'initialDescriptionHtml' => '<p dir="auto">issue text</p>', + 'initialDescriptionText' => 'issue text', + 'initialTaskStatus' => '0 of 0 tasks completed' + } + expect(JSON.parse(helper.issuable_initial_data(issue))).to eq(expected_data) + end + end end diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 60a452f2223..3636aac79a0 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,6 +1,5 @@ /* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */ import Issue from '~/issue'; -import CloseReopenReportToggle from '~/close_reopen_report_toggle'; import '~/lib/utils/text_utility'; describe('Issue', function() { @@ -189,37 +188,4 @@ describe('Issue', function() { }); }); }); - - describe('units', () => { - describe('class constructor', () => { - it('calls .initCloseReopenReport', () => { - spyOn(Issue.prototype, 'initCloseReopenReport'); - - new Issue(); // eslint-disable-line no-new - - expect(Issue.prototype.initCloseReopenReport).toHaveBeenCalled(); - }); - }); - - describe('initCloseReopenReport', () => { - it('calls .initDroplab', () => { - const container = jasmine.createSpyObj('container', ['querySelector']); - const dropdownTrigger = {}; - const dropdownList = {}; - const button = {}; - - spyOn(document, 'querySelector').and.returnValue(container); - spyOn(CloseReopenReportToggle.prototype, 'initDroplab'); - container.querySelector.and.returnValues(dropdownTrigger, dropdownList, button); - - Issue.prototype.initCloseReopenReport(); - - expect(document.querySelector).toHaveBeenCalledWith('.js-issuable-close-dropdown'); - expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-toggle'); - expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-menu'); - expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-button'); - expect(CloseReopenReportToggle.prototype.initDroplab).toHaveBeenCalled(); - }); - }); - }); }); diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js index 17e4ef26b2c..43532275121 100644 --- a/spec/javascripts/jobs/mock_data.js +++ b/spec/javascripts/jobs/mock_data.js @@ -22,7 +22,7 @@ export default { details_path: '/root/ci-mock/-/jobs/4757', favicon: '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', action: { - icon: 'icon_action_retry', + icon: 'retry', title: 'Retry', path: '/root/ci-mock/-/jobs/4757/retry', method: 'post', diff --git a/spec/javascripts/namespace_select_spec.js b/spec/javascripts/namespace_select_spec.js new file mode 100644 index 00000000000..9d7625ca269 --- /dev/null +++ b/spec/javascripts/namespace_select_spec.js @@ -0,0 +1,65 @@ +import NamespaceSelect from '~/namespace_select'; + +describe('NamespaceSelect', () => { + beforeEach(() => { + spyOn($.fn, 'glDropdown'); + }); + + it('initializes glDropdown', () => { + const dropdown = document.createElement('div'); + + // eslint-disable-next-line no-new + new NamespaceSelect({ dropdown }); + + expect($.fn.glDropdown).toHaveBeenCalled(); + }); + + describe('as input', () => { + let glDropdownOptions; + + beforeEach(() => { + const dropdown = document.createElement('div'); + // eslint-disable-next-line no-new + new NamespaceSelect({ dropdown }); + glDropdownOptions = $.fn.glDropdown.calls.argsFor(0)[0]; + }); + + it('prevents click events', () => { + const dummyEvent = new Event('dummy'); + spyOn(dummyEvent, 'preventDefault'); + + glDropdownOptions.clicked({ e: dummyEvent }); + + expect(dummyEvent.preventDefault).toHaveBeenCalled(); + }); + }); + + describe('as filter', () => { + let glDropdownOptions; + + beforeEach(() => { + const dropdown = document.createElement('div'); + dropdown.dataset.isFilter = 'true'; + // eslint-disable-next-line no-new + new NamespaceSelect({ dropdown }); + glDropdownOptions = $.fn.glDropdown.calls.argsFor(0)[0]; + }); + + it('does not prevent click events', () => { + const dummyEvent = new Event('dummy'); + spyOn(dummyEvent, 'preventDefault'); + + glDropdownOptions.clicked({ e: dummyEvent }); + + expect(dummyEvent.preventDefault).not.toHaveBeenCalled(); + }); + + it('sets URL of dropdown items', () => { + const dummyNamespace = { id: 'eal' }; + + const itemUrl = glDropdownOptions.url(dummyNamespace); + + expect(itemUrl).toContain(`namespace_id=${dummyNamespace.id}`); + }); + }); +}); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 53d8faae911..928a4b461cc 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -343,6 +343,7 @@ import '~/notes'; diff_discussion_html: false, }; $form = jasmine.createSpyObj('$form', ['closest', 'find']); + $form.length = 1; row = jasmine.createSpyObj('row', ['prevAll', 'first', 'find']); notes = jasmine.createSpyObj('notes', [ @@ -371,13 +372,29 @@ import '~/notes'; $form.closest.and.returnValues(row, $form); $form.find.and.returnValues(discussionContainer); body.attr.and.returnValue(''); - - Notes.prototype.renderDiscussionNote.call(notes, note, $form); }); it('should call Notes.animateAppendNote', () => { + Notes.prototype.renderDiscussionNote.call(notes, note, $form); + expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.discussion_html, $('.main-notes-list')); }); + + it('should append to row selected with line_code', () => { + $form.length = 0; + note.discussion_line_code = 'line_code'; + note.diff_discussion_html = '<tr></tr>'; + + const line = document.createElement('div'); + line.id = note.discussion_line_code; + document.body.appendChild(line); + + $form.closest.and.returnValues($form); + + Notes.prototype.renderDiscussionNote.call(notes, note, $form); + + expect(line.nextSibling.outerHTML).toEqual(note.diff_discussion_html); + }); }); describe('Discussion sub note', () => { diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js index 85bd87318db..e8fcd4b1a36 100644 --- a/spec/javascripts/pipelines/graph/action_component_spec.js +++ b/spec/javascripts/pipelines/graph/action_component_spec.js @@ -11,7 +11,7 @@ describe('pipeline graph action component', () => { tooltipText: 'bar', link: 'foo', actionMethod: 'post', - actionIcon: 'icon_action_cancel', + actionIcon: 'cancel', }, }).$mount(); diff --git a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js index 25fd18b197e..ba721bc53c6 100644 --- a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js +++ b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js @@ -11,7 +11,7 @@ describe('action component', () => { tooltipText: 'bar', link: 'foo', actionMethod: 'post', - actionIcon: 'icon_action_cancel', + actionIcon: 'cancel', }, }).$mount(); diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js index e90593e0f40..342ee6c1242 100644 --- a/spec/javascripts/pipelines/graph/job_component_spec.js +++ b/spec/javascripts/pipelines/graph/job_component_spec.js @@ -14,7 +14,7 @@ describe('pipeline graph job component', () => { group: 'success', details_path: '/root/ci-mock/builds/4256', action: { - icon: 'icon_action_retry', + icon: 'retry', title: 'Retry', path: '/root/ci-mock/builds/4256/retry', method: 'post', diff --git a/spec/javascripts/pipelines/graph/mock_data.js b/spec/javascripts/pipelines/graph/mock_data.js index 56c522b7f77..b9494f86d74 100644 --- a/spec/javascripts/pipelines/graph/mock_data.js +++ b/spec/javascripts/pipelines/graph/mock_data.js @@ -39,7 +39,7 @@ export default { "details_path": "/root/ci-mock/builds/4153", "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico", "action": { - "icon": "icon_action_retry", + "icon": "retry", "title": "Retry", "path": "/root/ci-mock/builds/4153/retry", "method": "post" @@ -62,7 +62,7 @@ export default { "details_path": "/root/ci-mock/builds/4153", "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico", "action": { - "icon": "icon_action_retry", + "icon": "retry", "title": "Retry", "path": "/root/ci-mock/builds/4153/retry", "method": "post" @@ -96,7 +96,7 @@ export default { "details_path": "/root/ci-mock/builds/4166", "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico", "action": { - "icon": "icon_action_retry", + "icon": "retry", "title": "Retry", "path": "/root/ci-mock/builds/4166/retry", "method": "post" @@ -119,7 +119,7 @@ export default { "details_path": "/root/ci-mock/builds/4166", "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico", "action": { - "icon": "icon_action_retry", + "icon": "retry", "title": "Retry", "path": "/root/ci-mock/builds/4166/retry", "method": "post" @@ -138,7 +138,7 @@ export default { "details_path": "/root/ci-mock/builds/4159", "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico", "action": { - "icon": "icon_action_retry", + "icon": "retry", "title": "Retry", "path": "/root/ci-mock/builds/4159/retry", "method": "post" @@ -161,7 +161,7 @@ export default { "details_path": "/root/ci-mock/builds/4159", "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico", "action": { - "icon": "icon_action_retry", + "icon": "retry", "title": "Retry", "path": "/root/ci-mock/builds/4159/retry", "method": "post" diff --git a/spec/javascripts/pipelines/graph/stage_column_component_spec.js b/spec/javascripts/pipelines/graph/stage_column_component_spec.js index aa4d6eedaf4..063ab53681b 100644 --- a/spec/javascripts/pipelines/graph/stage_column_component_spec.js +++ b/spec/javascripts/pipelines/graph/stage_column_component_spec.js @@ -13,7 +13,7 @@ describe('stage column component', () => { group: 'success', details_path: '/root/ci-mock/builds/4256', action: { - icon: 'icon_action_retry', + icon: 'retry', title: 'Retry', path: '/root/ci-mock/builds/4256/retry', method: 'post', diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js index 690665ae12c..33ed0cb4342 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js @@ -1,5 +1,4 @@ import Vue from 'vue'; -import { statusIconEntityMap } from '~/vue_shared/ci_status_icons'; import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline'; import mockData from '../mock_data'; @@ -29,14 +28,6 @@ describe('MRWidgetPipeline', () => { }); describe('computed', () => { - describe('svg', () => { - it('should have the proper SVG icon', () => { - const vm = createComponent({ pipeline: mockData.pipeline }); - - expect(vm.svg).toEqual(statusIconEntityMap.icon_status_failed); - }); - }); - describe('hasPipeline', () => { it('should return true when there is a pipeline', () => { expect(Object.keys(mockData.pipeline).length).toBeGreaterThan(0); @@ -142,6 +133,7 @@ describe('MRWidgetPipeline', () => { Vue.nextTick(() => { expect(el.querySelectorAll('.js-ci-error').length).toEqual(1); expect(el.innerText).toContain('Could not connect to the CI server'); + expect(el.querySelector('.ci-status-icon svg use').getAttribute('xlink:href')).toContain('status_failed'); done(); }); }); diff --git a/spec/javascripts/vue_shared/ci_action_icons_spec.js b/spec/javascripts/vue_shared/ci_action_icons_spec.js deleted file mode 100644 index 3d53a5ab24d..00000000000 --- a/spec/javascripts/vue_shared/ci_action_icons_spec.js +++ /dev/null @@ -1,27 +0,0 @@ -import getActionIcon from '~/vue_shared/ci_action_icons'; -import cancelSVG from 'icons/_icon_action_cancel.svg'; -import retrySVG from 'icons/_icon_action_retry.svg'; -import playSVG from 'icons/_icon_action_play.svg'; -import stopSVG from 'icons/_icon_action_stop.svg'; - -describe('getActionIcon', () => { - it('should return an empty string', () => { - expect(getActionIcon()).toEqual(''); - }); - - it('should return cancel svg', () => { - expect(getActionIcon('icon_action_cancel')).toEqual(cancelSVG); - }); - - it('should return retry svg', () => { - expect(getActionIcon('icon_action_retry')).toEqual(retrySVG); - }); - - it('should return play svg', () => { - expect(getActionIcon('icon_action_play')).toEqual(playSVG); - }); - - it('should render stop svg', () => { - expect(getActionIcon('icon_action_stop')).toEqual(stopSVG); - }); -}); diff --git a/spec/javascripts/vue_shared/ci_status_icon_spec.js b/spec/javascripts/vue_shared/ci_status_icon_spec.js deleted file mode 100644 index b6621d6054d..00000000000 --- a/spec/javascripts/vue_shared/ci_status_icon_spec.js +++ /dev/null @@ -1,27 +0,0 @@ -import { borderlessStatusIconEntityMap, statusIconEntityMap } from '~/vue_shared/ci_status_icons'; - -describe('CI status icons', () => { - const statuses = [ - 'icon_status_canceled', - 'icon_status_created', - 'icon_status_failed', - 'icon_status_manual', - 'icon_status_pending', - 'icon_status_running', - 'icon_status_skipped', - 'icon_status_success', - 'icon_status_warning', - ]; - - it('should have a dictionary for borderless icons', () => { - statuses.forEach((status) => { - expect(borderlessStatusIconEntityMap[status]).toBeDefined(); - }); - }); - - it('should have a dictionary for icons', () => { - statuses.forEach((status) => { - expect(statusIconEntityMap[status]).toBeDefined(); - }); - }); -}); diff --git a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js index ba303738f71..8762ce9903b 100644 --- a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js +++ b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js @@ -11,63 +11,63 @@ describe('CI Badge Link Component', () => { text: 'canceled', label: 'canceled', group: 'canceled', - icon: 'icon_status_canceled', + icon: 'status_canceled', details_path: 'status/canceled', }, created: { text: 'created', label: 'created', group: 'created', - icon: 'icon_status_created', + icon: 'status_created', details_path: 'status/created', }, failed: { text: 'failed', label: 'failed', group: 'failed', - icon: 'icon_status_failed', + icon: 'status_failed', details_path: 'status/failed', }, manual: { text: 'manual', label: 'manual action', group: 'manual', - icon: 'icon_status_manual', + icon: 'status_manual', details_path: 'status/manual', }, pending: { text: 'pending', label: 'pending', group: 'pending', - icon: 'icon_status_pending', + icon: 'status_pending', details_path: 'status/pending', }, running: { text: 'running', label: 'running', group: 'running', - icon: 'icon_status_running', + icon: 'status_running', details_path: 'status/running', }, skipped: { text: 'skipped', label: 'skipped', group: 'skipped', - icon: 'icon_status_skipped', + icon: 'status_skipped', details_path: 'status/skipped', }, success_warining: { text: 'passed', label: 'passed', group: 'success_with_warnings', - icon: 'icon_status_warning', + icon: 'status_warning', details_path: 'status/warning', }, success: { text: 'passed', label: 'passed', group: 'passed', - icon: 'icon_status_success', + icon: 'status_success', details_path: 'status/passed', }, }; diff --git a/spec/javascripts/vue_shared/components/icon_spec.js b/spec/javascripts/vue_shared/components/icon_spec.js new file mode 100644 index 00000000000..104da4473ce --- /dev/null +++ b/spec/javascripts/vue_shared/components/icon_spec.js @@ -0,0 +1,48 @@ +import Vue from 'vue'; +import Icon from '~/vue_shared/components/icon.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Sprite Icon Component', function () { + describe('Initialization', function () { + let icon; + + beforeEach(function () { + const IconComponent = Vue.extend(Icon); + + icon = mountComponent(IconComponent, { + name: 'test', + size: 99, + cssClasses: 'extraclasses', + }); + }); + + afterEach(() => { + icon.$destroy(); + }); + + it('should return a defined Vue component', function () { + expect(icon).toBeDefined(); + }); + + it('should have <svg> as a child element', function () { + expect(icon.$el.tagName).toBe('svg'); + }); + + it('should have <use> as a child element with the correct href', function () { + expect(icon.$el.firstChild.tagName).toBe('use'); + expect(icon.$el.firstChild.getAttribute('xlink:href')).toBe(`${gon.sprite_icons}#test`); + }); + + it('should properly compute iconSizeClass', function () { + expect(icon.iconSizeClass).toBe('s99'); + }); + + it('should properly render img css', function () { + const classList = icon.$el.classList; + const containsSizeClass = classList.contains('s99'); + const containsCustomClass = classList.contains('extraclasses'); + expect(containsSizeClass).toBe(true); + expect(containsCustomClass).toBe(true); + }); + }); +}); diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index 9c74c9b8c99..3c98b18f99b 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -317,6 +317,68 @@ describe Banzai::Filter::IssueReferenceFilter do end end + context 'group context' do + let(:group) { create(:group) } + let(:context) { { project: nil, group: group } } + + it 'ignores shorthanded issue reference' do + reference = "##{issue.iid}" + text = "Fixed #{reference}" + + expect(reference_filter(text, context).to_html).to eq(text) + end + + it 'ignores valid references when cross-reference project uses external tracker' do + expect_any_instance_of(described_class).to receive(:find_object) + .with(project, issue.iid) + .and_return(nil) + + reference = "#{project.full_path}##{issue.iid}" + text = "Issue #{reference}" + + expect(reference_filter(text, context).to_html).to eq(text) + end + + it 'links to a valid reference for complete cross-reference' do + reference = "#{project.full_path}##{issue.iid}" + doc = reference_filter("See #{reference}", context) + + expect(doc.css('a').first.attr('href')).to eq helper.url_for_issue(issue.iid, project) + end + + it 'ignores reference for shorthand cross-reference' do + reference = "#{project.path}##{issue.iid}" + text = "See #{reference}" + + expect(reference_filter(text, context).to_html).to eq(text) + end + + it 'links to a valid reference for url cross-reference' do + reference = helper.url_for_issue(issue.iid, project) + "#note_123" + + doc = reference_filter("See #{reference}", context) + + expect(doc.css('a').first.attr('href')).to eq(helper.url_for_issue(issue.iid, project) + "#note_123") + end + + it 'links to a valid reference for cross-reference in link href' do + reference = "#{helper.url_for_issue(issue.iid, project) + "#note_123"}" + reference_link = %{<a href="#{reference}">Reference</a>} + + doc = reference_filter("See #{reference_link}", context) + + expect(doc.css('a').first.attr('href')).to eq helper.url_for_issue(issue.iid, project) + "#note_123" + end + + it 'links to a valid reference for issue reference in the link href' do + reference = issue.to_reference(group) + reference_link = %{<a href="#{reference}">Reference</a>} + doc = reference_filter("See #{reference_link}", context) + + expect(doc.css('a').first.attr('href')).to eq helper.url_for_issue(issue.iid, project) + end + end + describe '#issues_per_project' do context 'using an internal issue tracker' do it 'returns a Hash containing the issues per project' do diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index 2cd30a5e302..862b1fe3fd3 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -594,4 +594,16 @@ describe Banzai::Filter::LabelReferenceFilter do expect(reference_filter(act).to_html).to eq exp end end + + describe 'group context' do + it 'points to referenced project issues page' do + project = create(:project) + label = create(:label, project: project) + reference = "#{project.full_path}~#{label.name}" + + result = reference_filter("See #{reference}", { project: nil, group: create(:group) } ) + + expect(result.css('a').first.attr('href')).to eq(urls.project_issues_url(project, label_name: label.name)) + end + end end diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb index ed2788f8a33..158844e25ae 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb @@ -214,4 +214,14 @@ describe Banzai::Filter::MergeRequestReferenceFilter do expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/) end end + + context 'group context' do + it 'links to a valid reference' do + reference = "#{project.full_path}!#{merge.iid}" + + result = reference_filter("See #{reference}", { project: nil, group: create(:group) } ) + + expect(result.css('a').first.attr('href')).to eq(urls.project_merge_request_url(project, merge)) + end + end end diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index fe7a8c84c9e..84578668133 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -343,4 +343,15 @@ describe Banzai::Filter::MilestoneReferenceFilter do expect(doc.css('a')).to be_empty end end + + context 'group context' do + it 'links to a valid reference' do + milestone = create(:milestone, project: project) + reference = "#{project.full_path}%#{milestone.iid}" + + result = reference_filter("See #{reference}", { project: nil, group: create(:group) } ) + + expect(result.css('a').first.attr('href')).to eq(urls.milestone_url(milestone)) + end + end end diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb index 90ac4c7b238..3a07a6dc179 100644 --- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb @@ -201,4 +201,14 @@ describe Banzai::Filter::SnippetReferenceFilter do expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/) end end + + context 'group context' do + it 'links to a valid reference' do + reference = "#{project.full_path}$#{snippet.id}" + + result = reference_filter("See #{reference}", { project: nil, group: create(:group) } ) + + expect(result.css('a').first.attr('href')).to eq(urls.project_snippet_url(project, snippet)) + end + end end diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index 34dac1db69a..fc03741976e 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -208,6 +208,39 @@ describe Banzai::Filter::UserReferenceFilter do end end + context 'in group context' do + let(:group) { create(:group) } + let(:group_member) { create(:user) } + + before do + group.add_developer(group_member) + end + + let(:context) { { author: group_member, project: nil, group: group } } + + it 'supports a special @all mention' do + reference = User.reference_prefix + 'all' + doc = reference_filter("Hey #{reference}", context) + + expect(doc.css('a').length).to eq(1) + expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) + end + + it 'supports mentioning a single user' do + reference = group_member.to_reference + doc = reference_filter("Hey #{reference}", context) + + expect(doc.css('a').first.attr('href')).to eq urls.user_url(group_member) + end + + it 'supports mentioning a group' do + reference = group.to_reference + doc = reference_filter("Hey #{reference}", context) + + expect(doc.css('a').first.attr('href')).to eq urls.user_url(group) + end + end + describe '#namespaces' do it 'returns a Hash containing all Namespaces' do document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>") diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index af1db2c3455..54a853c9ce3 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Auth do describe 'constants' do it 'API_SCOPES contains all scopes for API access' do - expect(subject::API_SCOPES).to eq [:api, :read_user] + expect(subject::API_SCOPES).to eq %i[api read_user sudo] end it 'OPENID_SCOPES contains all scopes for OpenID Connect' do @@ -19,7 +19,7 @@ describe Gitlab::Auth do it 'optional_scopes contains all non-default scopes' do stub_container_registry_config(enabled: true) - expect(subject.optional_scopes).to eq %i[read_user read_registry openid] + expect(subject.optional_scopes).to eq %i[read_user sudo read_registry openid] end context 'registry_scopes' do @@ -164,7 +164,7 @@ describe Gitlab::Auth do personal_access_token = create(:personal_access_token, scopes: ['api']) 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, full_authentication_abilities)) + 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_access_token, full_authentication_abilities)) end context 'when registry is enabled' do @@ -176,7 +176,7 @@ describe Gitlab::Auth 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])) + 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_access_token, [:read_container_image])) end end @@ -184,14 +184,14 @@ describe Gitlab::Auth do impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api']) expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') - 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)) + 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_access_token, full_authentication_abilities)) end 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: 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, [])) + 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_access_token, [])) end it 'fails if password is nil' do @@ -234,7 +234,7 @@ describe Gitlab::Auth do it 'throws an error suggesting user create a PAT when internal auth is disabled' do allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled?) { false } - expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalTokenError) + expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalAccessTokenError) end end diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb index 809fda11879..2a3f7807fdb 100644 --- a/spec/lib/gitlab/ci/cron_parser_spec.rb +++ b/spec/lib/gitlab/ci/cron_parser_spec.rb @@ -77,8 +77,20 @@ describe Gitlab::Ci::CronParser do it_behaves_like "returns time in the future" - it 'converts time in server time zone' do - expect(subject.hour).to eq(hour_in_utc) + context 'when PST (Pacific Standard Time)' do + it 'converts time in server time zone' do + Timecop.freeze(Time.utc(2017, 1, 1)) do + expect(subject.hour).to eq(hour_in_utc) + end + end + end + + context 'when PDT (Pacific Daylight Time)' do + it 'converts time in server time zone' do + Timecop.freeze(Time.utc(2017, 6, 1)) do + expect(subject.hour).to eq(hour_in_utc) + end + end end end end @@ -100,8 +112,20 @@ describe Gitlab::Ci::CronParser do it_behaves_like "returns time in the future" - it 'converts time in server time zone' do - expect(subject.hour).to eq(hour_in_utc) + context 'when CET (Central European Time)' do + it 'converts time in server time zone' do + Timecop.freeze(Time.utc(2017, 1, 1)) do + expect(subject.hour).to eq(hour_in_utc) + end + end + end + + context 'when CEST (Central European Summer Time)' do + it 'converts time in server time zone' do + Timecop.freeze(Time.utc(2017, 6, 1)) do + expect(subject.hour).to eq(hour_in_utc) + end + end end end @@ -111,8 +135,20 @@ describe Gitlab::Ci::CronParser do it_behaves_like "returns time in the future" - it 'converts time in server time zone' do - expect(subject.hour).to eq(hour_in_utc) + context 'when EST (Eastern Standard Time)' do + it 'converts time in server time zone' do + Timecop.freeze(Time.utc(2017, 1, 1)) do + expect(subject.hour).to eq(hour_in_utc) + end + end + end + + context 'when EDT (Eastern Daylight Time)' do + it 'converts time in server time zone' do + Timecop.freeze(Time.utc(2017, 6, 1)) do + expect(subject.hour).to eq(hour_in_utc) + end + end 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 5a7a42d84c0..9cdebaa5cf2 100644 --- a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb +++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb @@ -66,7 +66,7 @@ describe Gitlab::Ci::Status::Build::Cancelable do end describe '#action_icon' do - it { expect(subject.action_icon).to eq 'icon_action_cancel' } + it { expect(subject.action_icon).to eq 'cancel' } end describe '#action_title' do diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index 8768302eda1..2b32e47e9ba 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -30,7 +30,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'passed' - expect(status.icon).to eq 'icon_status_success' + expect(status.icon).to eq 'status_success' expect(status.favicon).to eq 'favicon_status_success' expect(status.label).to eq 'passed' expect(status).to have_details @@ -57,7 +57,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'failed' - expect(status.icon).to eq 'icon_status_failed' + expect(status.icon).to eq 'status_failed' expect(status.favicon).to eq 'favicon_status_failed' expect(status.label).to eq 'failed' expect(status).to have_details @@ -84,7 +84,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'failed' - expect(status.icon).to eq 'icon_status_warning' + expect(status.icon).to eq 'warning' expect(status.favicon).to eq 'favicon_status_failed' expect(status.label).to eq 'failed (allowed to fail)' expect(status).to have_details @@ -113,7 +113,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'canceled' - expect(status.icon).to eq 'icon_status_canceled' + expect(status.icon).to eq 'status_canceled' expect(status.favicon).to eq 'favicon_status_canceled' expect(status.label).to eq 'canceled' expect(status).to have_details @@ -139,7 +139,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'running' - expect(status.icon).to eq 'icon_status_running' + expect(status.icon).to eq 'status_running' expect(status.favicon).to eq 'favicon_status_running' expect(status.label).to eq 'running' expect(status).to have_details @@ -165,7 +165,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'pending' - expect(status.icon).to eq 'icon_status_pending' + expect(status.icon).to eq 'status_pending' expect(status.favicon).to eq 'favicon_status_pending' expect(status.label).to eq 'pending' expect(status).to have_details @@ -190,7 +190,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'skipped' - expect(status.icon).to eq 'icon_status_skipped' + expect(status.icon).to eq 'status_skipped' expect(status.favicon).to eq 'favicon_status_skipped' expect(status.label).to eq 'skipped' expect(status).to have_details @@ -219,7 +219,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'manual' expect(status.group).to eq 'manual' - expect(status.icon).to eq 'icon_status_manual' + expect(status.icon).to eq 'status_manual' expect(status.favicon).to eq 'favicon_status_manual' expect(status.label).to include 'manual play action' expect(status).to have_details @@ -274,7 +274,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'fabricates status with correct details' do expect(status.text).to eq 'manual' expect(status.group).to eq 'manual' - expect(status.icon).to eq 'icon_status_manual' + expect(status.icon).to eq 'status_manual' expect(status.favicon).to eq 'favicon_status_manual' expect(status.label).to eq 'manual stop action (not allowed)' expect(status).to have_details diff --git a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb index 20f71459738..79a65fc67e8 100644 --- a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb +++ b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb @@ -18,7 +18,7 @@ describe Gitlab::Ci::Status::Build::FailedAllowed do describe '#icon' do it 'returns a warning icon' do - expect(subject.icon).to eq 'icon_status_warning' + expect(subject.icon).to eq 'warning' end end diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb index 32b2e62e4e0..81d5f553fd1 100644 --- a/spec/lib/gitlab/ci/status/build/play_spec.rb +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -46,7 +46,7 @@ describe Gitlab::Ci::Status::Build::Play do end describe '#action_icon' do - it { expect(subject.action_icon).to eq 'icon_action_play' } + it { expect(subject.action_icon).to eq 'play' } end describe '#action_title' do diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb index 21026f2c968..14d42e0d70f 100644 --- a/spec/lib/gitlab/ci/status/build/retryable_spec.rb +++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb @@ -66,7 +66,7 @@ describe Gitlab::Ci::Status::Build::Retryable do end describe '#action_icon' do - it { expect(subject.action_icon).to eq 'icon_action_retry' } + it { expect(subject.action_icon).to eq 'retry' } end describe '#action_title' do diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb index e0425103f41..18e250772f0 100644 --- a/spec/lib/gitlab/ci/status/build/stop_spec.rb +++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb @@ -38,7 +38,7 @@ describe Gitlab::Ci::Status::Build::Stop do end describe '#action_icon' do - it { expect(subject.action_icon).to eq 'icon_action_stop' } + it { expect(subject.action_icon).to eq 'stop' } end describe '#action_title' do diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb index 530639a5897..dc74d7e28c5 100644 --- a/spec/lib/gitlab/ci/status/canceled_spec.rb +++ b/spec/lib/gitlab/ci/status/canceled_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Canceled do end describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_canceled' } + it { expect(subject.icon).to eq 'status_canceled' } end describe '#favicon' do diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb index aef982e17f1..ce4333f2aca 100644 --- a/spec/lib/gitlab/ci/status/created_spec.rb +++ b/spec/lib/gitlab/ci/status/created_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Created do end describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_created' } + it { expect(subject.icon).to eq 'status_created' } end describe '#favicon' do diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb index 9a25743885c..a4a92117c7f 100644 --- a/spec/lib/gitlab/ci/status/failed_spec.rb +++ b/spec/lib/gitlab/ci/status/failed_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Failed do end describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_failed' } + it { expect(subject.icon).to eq 'status_failed' } end describe '#favicon' do diff --git a/spec/lib/gitlab/ci/status/manual_spec.rb b/spec/lib/gitlab/ci/status/manual_spec.rb index 6fdc3801d71..0463f2e1aff 100644 --- a/spec/lib/gitlab/ci/status/manual_spec.rb +++ b/spec/lib/gitlab/ci/status/manual_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Manual do end describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_manual' } + it { expect(subject.icon).to eq 'status_manual' } end describe '#favicon' do diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb index ffc53f0506b..0e25358dd8a 100644 --- a/spec/lib/gitlab/ci/status/pending_spec.rb +++ b/spec/lib/gitlab/ci/status/pending_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Pending do end describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_pending' } + it { expect(subject.icon).to eq 'status_pending' } end describe '#favicon' do diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb index 0babf1fb54e..9c9d431bb5d 100644 --- a/spec/lib/gitlab/ci/status/running_spec.rb +++ b/spec/lib/gitlab/ci/status/running_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Running do end describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_running' } + it { expect(subject.icon).to eq 'status_running' } end describe '#favicon' do diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb index 670747c9f0b..63694ca0ea6 100644 --- a/spec/lib/gitlab/ci/status/skipped_spec.rb +++ b/spec/lib/gitlab/ci/status/skipped_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Skipped do end describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_skipped' } + it { expect(subject.icon).to eq 'status_skipped' } end describe '#favicon' do diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb index ff65b074808..2f67df71c4f 100644 --- a/spec/lib/gitlab/ci/status/success_spec.rb +++ b/spec/lib/gitlab/ci/status/success_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::Success do end describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_success' } + it { expect(subject.icon).to eq 'status_success' } end describe '#favicon' do diff --git a/spec/lib/gitlab/ci/status/success_warning_spec.rb b/spec/lib/gitlab/ci/status/success_warning_spec.rb index 7e2269397c6..4582354e739 100644 --- a/spec/lib/gitlab/ci/status/success_warning_spec.rb +++ b/spec/lib/gitlab/ci/status/success_warning_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Ci::Status::SuccessWarning do end describe '#icon' do - it { expect(subject.icon).to eq 'icon_status_warning' } + it { expect(subject.icon).to eq 'status_warning' } end describe '#group' do diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 75ff45ec379..1d4d0c300eb 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -559,10 +559,10 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe "#remote_delete" do + describe "#remove_remote" do before(:all) do @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') - @repo.remote_delete("expendable") + @repo.remove_remote("expendable") end it "should remove the remote" do @@ -575,14 +575,16 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe "#remote_add" do + describe "#remote_update" do before(:all) do @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') - @repo.remote_add("new_remote", SeedHelper::GITLAB_GIT_TEST_REPO_URL) + @repo.remote_update("expendable", url: TEST_NORMAL_REPO_PATH) end it "should add the remote" do - expect(@repo.rugged.remotes.each_name.to_a).to include("new_remote") + expect(@repo.rugged.remotes["expendable"].url).to( + eq(TEST_NORMAL_REPO_PATH) + ) end after(:all) do @@ -591,21 +593,58 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe "#remote_update" do - before(:all) do - @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') - @repo.remote_update("expendable", url: TEST_NORMAL_REPO_PATH) + describe '#fetch_mirror' do + let(:new_repository) do + Gitlab::Git::Repository.new('default', 'my_project.git', '') end - it "should add the remote" do - expect(@repo.rugged.remotes["expendable"].url).to( - eq(TEST_NORMAL_REPO_PATH) - ) + subject { new_repository.fetch_mirror(repository.path) } + + before do + Gitlab::Shell.new.add_repository('default', 'my_project') end - after(:all) do - FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) - ensure_seeds + after do + Gitlab::Shell.new.remove_repository(TestEnv.repos_path, 'my_project') + end + + it 'fetches a url as a mirror remote' do + subject + + expect(refs(new_repository.path)).to eq(refs(repository.path)) + end + + context 'with keep-around refs' do + let(:sha) { SeedRepo::Commit::ID } + let(:keep_around_ref) { "refs/keep-around/#{sha}" } + let(:tmp_ref) { "refs/tmp/#{SecureRandom.hex}" } + + before do + repository.rugged.references.create(keep_around_ref, sha, force: true) + repository.rugged.references.create(tmp_ref, sha, force: true) + end + + it 'includes the temporary and keep-around refs' do + subject + + expect(refs(new_repository.path)).to include(keep_around_ref) + expect(refs(new_repository.path)).to include(tmp_ref) + end + end + end + + describe '#remote_tags' do + let(:target_commit_id) { SeedRepo::Commit::ID } + + subject { repository.remote_tags('upstream') } + + it 'gets the remote tags' do + expect(repository).to receive(:list_remote_tags).with('upstream') + .and_return(["#{target_commit_id}\trefs/tags/v0.0.1\n"]) + + expect(subject.first).to be_an_instance_of(Gitlab::Git::Tag) + expect(subject.first.name).to eq('v0.0.1') + expect(subject.first.dereferenced_target.id).to eq(target_commit_id) end end @@ -1632,38 +1671,71 @@ describe Gitlab::Git::Repository, seed_helper: true do subject { repository.ff_merge(user, source_sha, target_branch) } - it 'performs a ff_merge' do - expect(subject.newrev).to eq(source_sha) - expect(subject.repo_created).to be(false) - expect(subject.branch_created).to be(false) + shared_examples '#ff_merge' do + it 'performs a ff_merge' do + expect(subject.newrev).to eq(source_sha) + expect(subject.repo_created).to be(false) + expect(subject.branch_created).to be(false) - expect(repository.commit(target_branch).id).to eq(source_sha) - end + expect(repository.commit(target_branch).id).to eq(source_sha) + end - context 'with a non-existing target branch' do - subject { repository.ff_merge(user, source_sha, 'this-isnt-real') } + context 'with a non-existing target branch' do + subject { repository.ff_merge(user, source_sha, 'this-isnt-real') } - it 'throws an ArgumentError' do - expect { subject }.to raise_error(ArgumentError) + it 'throws an ArgumentError' do + expect { subject }.to raise_error(ArgumentError) + end + end + + context 'with a non-existing source commit' do + let(:source_sha) { 'f001' } + + it 'throws an ArgumentError' do + expect { subject }.to raise_error(ArgumentError) + end + end + + context 'when the source sha is not a descendant of the branch head' do + let(:source_sha) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' } + + it "doesn't perform the ff_merge" do + expect { subject }.to raise_error(Gitlab::Git::CommitError) + + expect(repository.commit(target_branch).id).to eq(branch_head) + end end end - context 'with a non-existing source commit' do - let(:source_sha) { 'f001' } + context 'with gitaly' do + it "calls Gitaly's OperationService" do + expect_any_instance_of(Gitlab::GitalyClient::OperationService) + .to receive(:user_ff_branch).with(user, source_sha, target_branch) + .and_return(nil) - it 'throws an ArgumentError' do - expect { subject }.to raise_error(ArgumentError) + subject end + + it_behaves_like '#ff_merge' + end + + context 'without gitaly', :skip_gitaly_mock do + it_behaves_like '#ff_merge' end + end - context 'when the source sha is not a descendant of the branch head' do - let(:source_sha) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' } + describe '#fetch' do + let(:git_path) { Gitlab.config.git.bin_path } + let(:remote_name) { 'my_remote' } - it "doesn't perform the ff_merge" do - expect { subject }.to raise_error(Gitlab::Git::CommitError) + subject { repository.fetch(remote_name) } - expect(repository.commit(target_branch).id).to eq(branch_head) - end + it 'fetches the remote and returns true if the command was successful' do + expect(repository).to receive(:popen) + .with(%W(#{git_path} fetch #{remote_name}), repository.path) + .and_return(['', 0]) + + expect(subject).to be(true) end end @@ -1742,4 +1814,10 @@ describe Gitlab::Git::Repository, seed_helper: true do sha = Rugged::Commit.create(repo, options) repo.lookup(sha) end + + def refs(dir) + IO.popen(%W[git -C #{dir} for-each-ref], &:read).split("\n").map do |line| + line.split("\t").last + end + end end diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index e144e28b5d8..d9ec28ab02e 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -89,4 +89,38 @@ describe Gitlab::GitalyClient::OperationService do end end end + + describe '#user_ff_branch' do + let(:target_branch) { 'my-branch' } + let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } + let(:request) do + Gitaly::UserFFBranchRequest.new( + repository: repository.gitaly_repository, + branch: target_branch, + commit_id: source_sha, + user: gitaly_user + ) + end + let(:branch_update) do + Gitaly::OperationBranchUpdate.new( + commit_id: source_sha, + repo_created: false, + branch_created: false + ) + end + let(:response) { Gitaly::UserFFBranchResponse.new(branch_update: branch_update) } + + subject { client.user_ff_branch(user, source_sha, target_branch) } + + it 'sends a user_ff_branch message and returns a BranchUpdate object' do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_ff_branch).with(request, kind_of(Hash)) + .and_return(response) + + expect(subject).to be_a(Gitlab::Git::OperationService::BranchUpdate) + expect(subject.newrev).to eq(source_sha) + expect(subject.repo_created).to be(false) + expect(subject.branch_created).to be(false) + end + end end diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb index 742a792a1af..86be06ff595 100644 --- a/spec/lib/gitlab/middleware/read_only_spec.rb +++ b/spec/lib/gitlab/middleware/read_only_spec.rb @@ -83,6 +83,13 @@ describe Gitlab::Middleware::ReadOnly do expect(subject).to disallow_request end + it 'expects POST of new file that looks like an LFS batch url to be disallowed' do + response = request.post('/root/gitlab-ce/new/master/app/info/lfs/objects/batch') + + expect(response).to be_a_redirect + expect(subject).to disallow_request + end + context 'whitelisted requests' do it 'expects DELETE request to logout to be allowed' do response = request.delete('/users/sign_out') @@ -104,6 +111,25 @@ describe Gitlab::Middleware::ReadOnly do expect(response).not_to be_a_redirect expect(subject).not_to disallow_request end + + it 'expects a POST request to git-upload-pack URL to be allowed' do + response = request.post('/root/rouge.git/git-upload-pack') + + expect(response).not_to be_a_redirect + expect(subject).not_to disallow_request + end + + it 'expects requests to sidekiq admin to be allowed' do + response = request.post('/admin/sidekiq') + + expect(response).not_to be_a_redirect + expect(subject).not_to disallow_request + + response = request.get('/admin/sidekiq') + + expect(response).not_to be_a_redirect + expect(subject).not_to disallow_request + end end end diff --git a/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb new file mode 100644 index 00000000000..8fdbbacd04d --- /dev/null +++ b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe Gitlab::SidekiqMiddleware::MemoryKiller do + subject { described_class.new } + let(:pid) { 999 } + + let(:worker) { double(:worker, class: 'TestWorker') } + let(:job) { { 'jid' => 123 } } + let(:queue) { 'test_queue' } + + def run + thread = subject.call(worker, job, queue) { nil } + thread&.join + end + + before do + allow(subject).to receive(:get_rss).and_return(10.kilobytes) + allow(subject).to receive(:pid).and_return(pid) + end + + context 'when MAX_RSS is set to 0' do + before do + stub_const("#{described_class}::MAX_RSS", 0) + end + + it 'does nothing' do + expect(subject).not_to receive(:sleep) + + run + end + end + + context 'when MAX_RSS is exceeded' do + before do + stub_const("#{described_class}::MAX_RSS", 5.kilobytes) + end + + it 'sends the STP, TERM and KILL signals at expected times' do + expect(subject).to receive(:sleep).with(15 * 60).ordered + expect(Process).to receive(:kill).with('SIGSTP', pid).ordered + + expect(subject).to receive(:sleep).with(30).ordered + expect(Process).to receive(:kill).with('SIGTERM', pid).ordered + + expect(subject).to receive(:sleep).with(10).ordered + expect(Process).to receive(:kill).with('SIGKILL', pid).ordered + + run + end + end + + context 'when MAX_RSS is not exceeded' do + before do + stub_const("#{described_class}::MAX_RSS", 15.kilobytes) + end + + it 'does nothing' do + expect(subject).not_to receive(:sleep) + + run + end + end +end diff --git a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb index b4b83b70d1c..a0fb86345f3 100644 --- a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb +++ b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb @@ -39,14 +39,6 @@ describe SystemCheck::App::GitUserDefaultSSHConfigCheck do it { is_expected.to eq(expected_result) } end - - it 'skips GitLab read-only instances' do - stub_user - stub_home_dir - allow(Gitlab::Database).to receive(:read_only?).and_return(true) - - is_expected.to be_truthy - end end describe '#check?' do diff --git a/spec/migrations/migrate_user_authentication_token_to_personal_access_token_spec.rb b/spec/migrations/migrate_user_authentication_token_to_personal_access_token_spec.rb new file mode 100644 index 00000000000..b4834705011 --- /dev/null +++ b/spec/migrations/migrate_user_authentication_token_to_personal_access_token_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20171012125712_migrate_user_authentication_token_to_personal_access_token.rb') + +describe MigrateUserAuthenticationTokenToPersonalAccessToken, :migration do + let(:users) { table(:users) } + let(:personal_access_tokens) { table(:personal_access_tokens) } + + let!(:user) { users.create!(id: 1, email: 'user@example.com', authentication_token: 'user-token', admin: false) } + let!(:admin) { users.create!(id: 2, email: 'admin@example.com', authentication_token: 'admin-token', admin: true) } + + it 'migrates private tokens to Personal Access Tokens' do + migrate! + + expect(personal_access_tokens.count).to eq(2) + + user_token = personal_access_tokens.find_by(user_id: user.id) + admin_token = personal_access_tokens.find_by(user_id: admin.id) + + expect(user_token.token).to eq('user-token') + expect(admin_token.token).to eq('admin-token') + + expect(user_token.scopes).to eq(%w[api].to_yaml) + expect(admin_token.scopes).to eq(%w[api sudo].to_yaml) + end +end diff --git a/spec/migrations/populate_merge_requests_latest_merge_request_diff_id_spec.rb b/spec/migrations/populate_merge_requests_latest_merge_request_diff_id_spec.rb new file mode 100644 index 00000000000..4ea7f441f7c --- /dev/null +++ b/spec/migrations/populate_merge_requests_latest_merge_request_diff_id_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20171026082505_populate_merge_requests_latest_merge_request_diff_id') + +describe PopulateMergeRequestsLatestMergeRequestDiffId, :migration do + let(:projects_table) { table(:projects) } + let(:merge_requests_table) { table(:merge_requests) } + let(:merge_request_diffs_table) { table(:merge_request_diffs) } + + let(:project) { projects_table.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce') } + + def create_mr!(name, diffs: 0) + merge_request = + merge_requests_table.create!(target_project_id: project.id, + target_branch: 'master', + source_project_id: project.id, + source_branch: name, + title: name) + + diffs.times do + merge_request_diffs_table.create!(merge_request_id: merge_request.id) + end + + merge_request + end + + def diffs_for(merge_request) + merge_request_diffs_table.where(merge_request_id: merge_request.id) + end + + describe '#up' do + it 'ignores MRs without diffs' do + merge_request_without_diff = create_mr!('without_diff') + + expect(merge_request_without_diff.latest_merge_request_diff_id).to be_nil + + expect { migrate! } + .not_to change { merge_request_without_diff.reload.latest_merge_request_diff_id } + end + + it 'ignores MRs that have a diff ID already set' do + merge_request_with_multiple_diffs = create_mr!('with_multiple_diffs', diffs: 3) + diff_id = diffs_for(merge_request_with_multiple_diffs).minimum(:id) + + merge_request_with_multiple_diffs.update!(latest_merge_request_diff_id: diff_id) + + expect { migrate! } + .not_to change { merge_request_with_multiple_diffs.reload.latest_merge_request_diff_id } + end + + it 'migrates multiple MR diffs to the correct values' do + merge_requests = Array.new(3).map.with_index { |_, i| create_mr!(i, diffs: 3) } + + migrate! + + merge_requests.each do |merge_request| + expect(merge_request.reload.latest_merge_request_diff_id) + .to eq(diffs_for(merge_request).maximum(:id)) + end + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 41ecdb604f1..5ed2e1ca99a 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1271,6 +1271,7 @@ describe Ci::Build do { key: 'CI_PROJECT_PATH_SLUG', value: project.full_path_slug, public: true }, { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true }, { key: 'CI_PROJECT_URL', value: project.web_url, public: true }, + { key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true }, { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }, { key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, public: true }, { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true }, diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index ab8773b7ede..3106207811a 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -134,6 +134,7 @@ describe Group, 'Routable' do context 'with RequestStore active', :request_store do it 'does not load the route table more than once' do + group.expires_full_path_cache expect(group).to receive(:uncached_full_path).once.and_call_original 3.times { group.full_path } diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index 882afeccfc6..dfb83578fce 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -12,7 +12,7 @@ shared_examples 'TokenAuthenticatable' do end describe User, 'TokenAuthenticatable' do - let(:token_field) { :authentication_token } + let(:token_field) { :rss_token } it_behaves_like 'TokenAuthenticatable' describe 'ensures authentication token' do diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index e1be23541e8..1ce1d595c60 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -18,7 +18,6 @@ describe Environment do it { is_expected.to validate_length_of(:slug).is_at_most(24) } it { is_expected.to validate_length_of(:external_url).is_at_most(255) } - it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) } describe '.order_by_last_deployed_at' do let(:project) { create(:project, :repository) } @@ -547,6 +546,15 @@ describe Environment do expect(environment.slug).to eq(original_slug) end + + it "regenerates the slug if nil" do + environment = build(:environment, slug: nil) + + new_slug = environment.slug + + expect(new_slug).not_to be_nil + expect(environment.slug).to eq(new_slug) + end end describe '#generate_slug' do @@ -583,6 +591,12 @@ describe Environment do it 'returns a path that uses the slug and does not have spaces' do expect(environment.ref_path).to start_with('refs/environments/staging-review-1-') end + + it "doesn't change when the slug is nil initially" do + environment.slug = nil + + expect(environment.ref_path).to eq(environment.ref_path) + end end describe '#external_url_for' do diff --git a/spec/models/fork_network_spec.rb b/spec/models/fork_network_spec.rb index 605ccd6db06..a43baf1820a 100644 --- a/spec/models/fork_network_spec.rb +++ b/spec/models/fork_network_spec.rb @@ -24,6 +24,16 @@ describe ForkNetwork do end end + describe '#merge_requests' do + it 'finds merge requests within the fork network' do + project = create(:project) + forked_project = fork_project(project) + merge_request = create(:merge_request, source_project: forked_project, target_project: project) + + expect(project.fork_network.merge_requests).to include(merge_request) + end + end + context 'for a deleted project' do it 'keeps the fork network' do project = create(:project, :public) diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index f36d6eeb327..0e1a7fdce0b 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -488,6 +488,47 @@ describe Group do end end + describe '#path_changed_hook' do + let(:system_hook_service) { SystemHooksService.new } + + context 'for a new group' do + let(:group) { build(:group) } + + before do + expect(group).to receive(:system_hook_service).and_return(system_hook_service) + end + + it 'does not trigger system hook' do + expect(system_hook_service).to receive(:execute_hooks_for).with(group, :create) + + group.save! + end + end + + context 'for an existing group' do + let(:group) { create(:group, path: 'old-path') } + + context 'when the path is changed' do + let(:new_path) { 'very-new-path' } + + it 'triggers the rename system hook' do + expect(group).to receive(:system_hook_service).and_return(system_hook_service) + expect(system_hook_service).to receive(:execute_hooks_for).with(group, :rename) + + group.update_attributes!(path: new_path) + end + end + + context 'when the path is not changed' do + it 'does not trigger system hook' do + expect(group).not_to receive(:system_hook_service) + + group.update_attributes!(name: 'new name') + end + end + end + end + describe '#secret_variables_for' do let(:project) { create(:project, group: group) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index ed6e42d476e..e8588975118 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1923,6 +1923,20 @@ describe Project do expect(forked_project.in_fork_network_of?(other_project)).to be_falsy end end + + describe '#fork_source' do + let!(:second_fork) { fork_project(forked_project) } + + it 'returns the direct source if it exists' do + expect(second_fork.fork_source).to eq(forked_project) + end + + it 'returns the root of the fork network when the directs source was deleted' do + forked_project.destroy + + expect(second_fork.fork_source).to eq(project) + end + end end describe '#pushes_since_gc' do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index d7c07676911..8a6aa767ce6 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -2298,4 +2298,24 @@ describe Repository do project.commit_by(oid: '1' * 40) end end + + describe '#raw_repository' do + subject { repository.raw_repository } + + it 'returns a Gitlab::Git::Repository representation of the repository' do + expect(subject).to be_a(Gitlab::Git::Repository) + expect(subject.relative_path).to eq(project.disk_path + '.git') + expect(subject.gl_repository).to eq("project-#{project.id}") + end + + context 'with a wiki repository' do + let(:repository) { project.wiki.repository } + + it 'creates a Gitlab::Git::Repository with the proper attributes' do + expect(subject).to be_a(Gitlab::Git::Repository) + expect(subject.relative_path).to eq(project.disk_path + '.wiki.git') + expect(subject.gl_repository).to eq("wiki-#{project.id}") + end + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 1c3c9068f12..e0896d64c8f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -346,7 +346,6 @@ describe User do describe "Respond to" do it { is_expected.to respond_to(:admin?) } it { is_expected.to respond_to(:name) } - it { is_expected.to respond_to(:private_token) } it { is_expected.to respond_to(:external?) } end @@ -526,14 +525,6 @@ describe User do end end - describe 'authentication token' do - it "has authentication token" do - user = create(:user) - - expect(user.authentication_token).not_to be_blank - end - end - describe 'ensure incoming email token' do it 'has incoming email token' do user = create(:user) @@ -2226,6 +2217,42 @@ describe User do end end + describe '#username_changed_hook' do + context 'for a new user' do + let(:user) { build(:user) } + + it 'does not trigger system hook' do + expect(user).not_to receive(:system_hook_service) + + user.save! + end + end + + context 'for an existing user' do + let(:user) { create(:user, username: 'old-username') } + + context 'when the username is changed' do + let(:new_username) { 'very-new-name' } + + it 'triggers the rename system hook' do + system_hook_service = SystemHooksService.new + expect(system_hook_service).to receive(:execute_hooks_for).with(user, :rename) + expect(user).to receive(:system_hook_service).and_return(system_hook_service) + + user.update_attributes!(username: new_username) + end + end + + context 'when the username is not changed' do + it 'does not trigger system hook' do + expect(user).not_to receive(:system_hook_service) + + user.update_attributes!(email: 'asdf@asdf.com') + end + end + end + end + describe '#sync_attribute?' do let(:user) { described_class.new } diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb index de7ce848a31..308134eba72 100644 --- a/spec/requests/api/doorkeeper_access_spec.rb +++ b/spec/requests/api/doorkeeper_access_spec.rb @@ -25,7 +25,7 @@ describe 'doorkeeper access' do end end - describe "authorization by private token" do + describe "authorization by OAuth token" do it "returns authentication success" do get api("/user", user) expect(response).to have_gitlab_http_status(200) @@ -39,20 +39,20 @@ describe 'doorkeeper access' do end describe "when user is blocked" do - it "returns authentication error" do + it "returns authorization error" do user.block get api("/user"), access_token: token.token - expect(response).to have_gitlab_http_status(401) + expect(response).to have_gitlab_http_status(403) end end describe "when user is ldap_blocked" do - it "returns authentication error" do + it "returns authorization error" do user.ldap_block get api("/user"), access_token: token.token - expect(response).to have_gitlab_http_status(401) + expect(response).to have_gitlab_http_status(403) end end end diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 9f3b5a809d7..6c0996c543d 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -28,39 +28,11 @@ describe API::Helpers do allow_any_instance_of(self.class).to receive(:options).and_return({}) end - def set_env(user_or_token, identifier) - clear_env - clear_param - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token - env[API::Helpers::SUDO_HEADER] = identifier.to_s - end - - def set_param(user_or_token, identifier) - clear_env - clear_param - params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token - params[API::Helpers::SUDO_PARAM] = identifier.to_s - end - - def clear_env - env.delete(API::APIGuard::PRIVATE_TOKEN_HEADER) - env.delete(API::Helpers::SUDO_HEADER) - end - - def clear_param - params.delete(API::APIGuard::PRIVATE_TOKEN_PARAM) - params.delete(API::Helpers::SUDO_PARAM) - end - def warden_authenticate_returns(value) warden = double("warden", authenticate: value) env['warden'] = warden end - def doorkeeper_guard_returns(value) - allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { value } - end - def error!(message, status, header) raise Exception.new("#{status} - #{message}") end @@ -69,10 +41,6 @@ describe API::Helpers do subject { current_user } describe "Warden authentication", :allow_forgery_protection do - before do - doorkeeper_guard_returns false - end - context "with invalid credentials" do context "GET request" do before do @@ -160,75 +128,32 @@ describe API::Helpers do end end - describe "when authenticating using a user's private token" do - it "returns a 401 response for an invalid token" do - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token' - allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false } - - expect { current_user }.to raise_error /401/ - end - - it "returns a 401 response for a user without access" do - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token - allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) - - expect { current_user }.to raise_error /401/ - end - - it 'returns a 401 response for a user who is blocked' do - user.block! - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token - - expect { current_user }.to raise_error /401/ - end - - it "leaves user as is when sudo not specified" do - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token - - expect(current_user).to eq(user) - - clear_env - - params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user.private_token - - expect(current_user).to eq(user) - end - end - describe "when authenticating using a user's personal access tokens" do let(:personal_access_token) { create(:personal_access_token, user: user) } - before do - allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false } - end - it "returns a 401 response for an invalid token" do env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token' expect { current_user }.to raise_error /401/ end - it "returns a 401 response for a user without access" do + it "returns a 403 response for a user without access" do env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) - expect { current_user }.to raise_error /401/ + expect { current_user }.to raise_error /403/ end - it 'returns a 401 response for a user who is blocked' do + it 'returns a 403 response for a user who is blocked' do user.block! env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token - expect { current_user }.to raise_error /401/ + expect { current_user }.to raise_error /403/ end - it "leaves user as is when sudo not specified" do + it "sets current_user" do env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token expect(current_user).to eq(user) - clear_env - params[API::APIGuard::PRIVATE_TOKEN_PARAM] = personal_access_token.token - - expect(current_user).to eq(user) end it "does not allow tokens without the appropriate scope" do @@ -252,210 +177,6 @@ describe API::Helpers do expect { current_user }.to raise_error API::APIGuard::ExpiredError end end - - context 'sudo usage' do - context 'with admin' do - context 'with header' do - context 'with id' do - it 'changes current_user to sudo' do - set_env(admin, user.id) - - expect(current_user).to eq(user) - end - - it 'memoize the current_user: sudo permissions are not run against the sudoed user' do - set_env(admin, user.id) - - expect(current_user).to eq(user) - expect(current_user).to eq(user) - end - - it 'handles sudo to oneself' do - set_env(admin, admin.id) - - expect(current_user).to eq(admin) - end - - it 'throws an error when user cannot be found' do - id = user.id + admin.id - expect(user.id).not_to eq(id) - expect(admin.id).not_to eq(id) - - set_env(admin, id) - - expect { current_user }.to raise_error(Exception) - end - end - - context 'with username' do - it 'changes current_user to sudo' do - set_env(admin, user.username) - - expect(current_user).to eq(user) - end - - it 'handles sudo to oneself' do - set_env(admin, admin.username) - - expect(current_user).to eq(admin) - end - - it "throws an error when the user cannot be found for a given username" do - username = "#{user.username}#{admin.username}" - expect(user.username).not_to eq(username) - expect(admin.username).not_to eq(username) - - set_env(admin, username) - - expect { current_user }.to raise_error(Exception) - end - end - end - - context 'with param' do - context 'with id' do - it 'changes current_user to sudo' do - set_param(admin, user.id) - - expect(current_user).to eq(user) - end - - it 'handles sudo to oneself' do - set_param(admin, admin.id) - - expect(current_user).to eq(admin) - end - - it 'handles sudo to oneself using string' do - set_env(admin, user.id.to_s) - - expect(current_user).to eq(user) - end - - it 'throws an error when user cannot be found' do - id = user.id + admin.id - expect(user.id).not_to eq(id) - expect(admin.id).not_to eq(id) - - set_param(admin, id) - - expect { current_user }.to raise_error(Exception) - end - end - - context 'with username' do - it 'changes current_user to sudo' do - set_param(admin, user.username) - - expect(current_user).to eq(user) - end - - it 'handles sudo to oneself' do - set_param(admin, admin.username) - - expect(current_user).to eq(admin) - end - - it "throws an error when the user cannot be found for a given username" do - username = "#{user.username}#{admin.username}" - expect(user.username).not_to eq(username) - expect(admin.username).not_to eq(username) - - set_param(admin, username) - - expect { current_user }.to raise_error(Exception) - end - end - end - - context 'when user is blocked' do - before do - user.block! - end - - it 'changes current_user to sudo' do - set_env(admin, user.id) - - expect(current_user).to eq(user) - end - end - end - - context 'with regular user' do - context 'with env' do - it 'changes current_user to sudo when admin and user id' do - set_env(user, admin.id) - - expect { current_user }.to raise_error(Exception) - end - - it 'changes current_user to sudo when admin and user username' do - set_env(user, admin.username) - - expect { current_user }.to raise_error(Exception) - end - end - - context 'with params' do - it 'changes current_user to sudo when admin and user id' do - set_param(user, admin.id) - - expect { current_user }.to raise_error(Exception) - end - - it 'changes current_user to sudo when admin and user username' do - set_param(user, admin.username) - - expect { current_user }.to raise_error(Exception) - end - end - end - end - end - - describe '.sudo?' do - context 'when no sudo env or param is passed' do - before do - doorkeeper_guard_returns(nil) - end - - it 'returns false' do - expect(sudo?).to be_falsy - end - end - - context 'when sudo env or param is passed', 'user is not an admin' do - before do - set_env(user, '123') - end - - it 'returns an 403 Forbidden' do - expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Must be admin to use sudo"}' - end - end - - context 'when sudo env or param is passed', 'user is admin' do - context 'personal access token is used' do - before do - personal_access_token = create(:personal_access_token, user: admin) - set_env(personal_access_token.token, user.id) - end - - it 'returns an 403 Forbidden' do - expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Private token must be specified in order to use sudo"}' - end - end - - context 'private access token is used' do - before do - set_env(admin.private_token, user.id) - end - - it 'returns true' do - expect(sudo?).to be_truthy - end - end - end end describe '.handle_api_exception' do @@ -582,4 +303,147 @@ describe API::Helpers do end end end + + context 'sudo' do + shared_examples 'successful sudo' do + it 'sets current_user' do + expect(current_user).to eq(user) + end + + it 'sets sudo?' do + expect(sudo?).to be_truthy + end + end + + shared_examples 'sudo' do + context 'when admin' do + before do + token.user = admin + token.save! + end + + context 'when token has sudo scope' do + before do + token.scopes = %w[sudo] + token.save! + end + + context 'when user exists' do + context 'when using header' do + context 'when providing username' do + before do + env[API::Helpers::SUDO_HEADER] = user.username + end + + it_behaves_like 'successful sudo' + end + + context 'when providing user ID' do + before do + env[API::Helpers::SUDO_HEADER] = user.id.to_s + end + + it_behaves_like 'successful sudo' + end + end + + context 'when using param' do + context 'when providing username' do + before do + params[API::Helpers::SUDO_PARAM] = user.username + end + + it_behaves_like 'successful sudo' + end + + context 'when providing user ID' do + before do + params[API::Helpers::SUDO_PARAM] = user.id.to_s + end + + it_behaves_like 'successful sudo' + end + end + end + + context 'when user does not exist' do + before do + params[API::Helpers::SUDO_PARAM] = 'nonexistent' + end + + it 'raises an error' do + expect { current_user }.to raise_error /User with ID or username 'nonexistent' Not Found/ + end + end + end + + context 'when token does not have sudo scope' do + before do + token.scopes = %w[api] + token.save! + + params[API::Helpers::SUDO_PARAM] = user.id.to_s + end + + it 'raises an error' do + expect { current_user }.to raise_error API::APIGuard::InsufficientScopeError + end + end + end + + context 'when not admin' do + before do + token.user = user + token.save! + + params[API::Helpers::SUDO_PARAM] = user.id.to_s + end + + it 'raises an error' do + expect { current_user }.to raise_error /Must be admin to use sudo/ + end + end + end + + context 'using an OAuth token' do + let(:token) { create(:oauth_access_token) } + + before do + env['HTTP_AUTHORIZATION'] = "Bearer #{token.token}" + end + + it_behaves_like 'sudo' + end + + context 'using a personal access token' do + let(:token) { create(:personal_access_token) } + + context 'passed as param' do + before do + params[API::APIGuard::PRIVATE_TOKEN_PARAM] = token.token + end + + it_behaves_like 'sudo' + end + + context 'passed as header' do + before do + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = token.token + end + + it_behaves_like 'sudo' + end + end + + context 'using warden authentication' do + before do + warden_authenticate_returns admin + env[API::Helpers::SUDO_HEADER] = user.username + end + + it 'raises an error' do + expect { current_user }.to raise_error /Must be authenticated using an OAuth or Personal Access Token to use sudo/ + end + end + end end diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb deleted file mode 100644 index 83d09878813..00000000000 --- a/spec/requests/api/session_spec.rb +++ /dev/null @@ -1,107 +0,0 @@ -require 'spec_helper' - -describe API::Session do - let(:user) { create(:user) } - - describe "POST /session" do - context "when valid password" do - it "returns private token" do - post api("/session"), email: user.email, password: '12345678' - expect(response).to have_gitlab_http_status(201) - - expect(json_response['email']).to eq(user.email) - expect(json_response['private_token']).to eq(user.private_token) - expect(json_response['is_admin']).to eq(user.admin?) - expect(json_response['can_create_project']).to eq(user.can_create_project?) - expect(json_response['can_create_group']).to eq(user.can_create_group?) - end - - context 'with 2FA enabled' do - it 'rejects sign in attempts' do - user = create(:user, :two_factor) - - post api('/session'), email: user.email, password: user.password - - expect(response).to have_gitlab_http_status(401) - expect(response.body).to include('You have 2FA enabled.') - end - end - end - - context 'when email has case-typo and password is valid' do - it 'returns private token' do - post api('/session'), email: user.email.upcase, password: '12345678' - expect(response.status).to eq 201 - - expect(json_response['email']).to eq user.email - expect(json_response['private_token']).to eq user.private_token - expect(json_response['is_admin']).to eq user.admin? - expect(json_response['can_create_project']).to eq user.can_create_project? - expect(json_response['can_create_group']).to eq user.can_create_group? - end - end - - context 'when login has case-typo and password is valid' do - it 'returns private token' do - post api('/session'), login: user.username.upcase, password: '12345678' - expect(response.status).to eq 201 - - expect(json_response['email']).to eq user.email - expect(json_response['private_token']).to eq user.private_token - expect(json_response['is_admin']).to eq user.admin? - expect(json_response['can_create_project']).to eq user.can_create_project? - expect(json_response['can_create_group']).to eq user.can_create_group? - end - end - - context "when invalid password" do - it "returns authentication error" do - post api("/session"), email: user.email, password: '123' - expect(response).to have_gitlab_http_status(401) - - expect(json_response['email']).to be_nil - expect(json_response['private_token']).to be_nil - end - end - - context "when empty password" do - it "returns authentication error with email" do - post api("/session"), email: user.email - - expect(response).to have_gitlab_http_status(400) - end - - it "returns authentication error with username" do - post api("/session"), email: user.username - - expect(response).to have_gitlab_http_status(400) - end - end - - context "when empty name" do - it "returns authentication error" do - post api("/session"), password: user.password - - expect(response).to have_gitlab_http_status(400) - end - end - - context "when user is blocked" do - it "returns authentication error" do - user.block - post api("/session"), email: user.username, password: user.password - - expect(response).to have_gitlab_http_status(401) - end - end - - context "when user is ldap_blocked" do - it "returns authentication error" do - user.ldap_block - post api("/session"), email: user.username, password: user.password - - expect(response).to have_gitlab_http_status(401) - end - end - end -end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 4737f034f21..634c8dae0ba 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -127,8 +127,8 @@ describe API::Users do context "when admin" do context 'when sudo is defined' do it 'does not return 500' do - admin_personal_access_token = create(:personal_access_token, user: admin).token - get api("/users?private_token=#{admin_personal_access_token}&sudo=#{user.id}", admin) + admin_personal_access_token = create(:personal_access_token, user: admin, scopes: [:sudo]) + get api("/users?sudo=#{user.id}", admin, personal_access_token: admin_personal_access_token) expect(response).to have_gitlab_http_status(:success) end @@ -1097,14 +1097,6 @@ describe API::Users do end end - context 'with private token' do - it 'returns 403 without private token when sudo defined' do - get api("/user?private_token=#{user.private_token}&sudo=123") - - expect(response).to have_gitlab_http_status(403) - end - end - it 'returns current user without private token when sudo not defined' do get api("/user", user) @@ -1139,24 +1131,6 @@ describe API::Users do expect(json_response['id']).to eq(admin.id) end end - - context 'with private token' do - it 'returns sudoed user with private token when sudo defined' do - get api("/user?private_token=#{admin.private_token}&sudo=#{user.id}") - - expect(response).to have_gitlab_http_status(200) - expect(response).to match_response_schema('public_api/v4/user/login') - expect(json_response['id']).to eq(user.id) - end - - it 'returns initial current user without private token but with is_admin when sudo not defined' do - get api("/user?private_token=#{admin.private_token}") - - expect(response).to have_gitlab_http_status(200) - expect(response).to match_response_schema('public_api/v4/user/admin') - expect(json_response['id']).to eq(admin.id) - end - end end context 'with unauthenticated user' do diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 407d19c3b2a..609481603af 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -135,7 +135,6 @@ end # profile_history GET /profile/history(.:format) profile#history # profile_password PUT /profile/password(.:format) profile#password_update # profile_token GET /profile/token(.:format) profile#token -# profile_reset_private_token PUT /profile/reset_private_token(.:format) profile#reset_private_token # profile GET /profile(.:format) profile#show # profile_update PUT /profile/update(.:format) profile#update describe ProfilesController, "routing" do @@ -147,10 +146,6 @@ describe ProfilesController, "routing" do expect(get("/profile/audit_log")).to route_to('profiles#audit_log') end - it "to #reset_private_token" do - expect(put("/profile/reset_private_token")).to route_to('profiles#reset_private_token') - end - it "to #reset_rss_token" do expect(put("/profile/reset_rss_token")).to route_to('profiles#reset_rss_token') end diff --git a/spec/serializers/issue_entity_spec.rb b/spec/serializers/issue_entity_spec.rb new file mode 100644 index 00000000000..caa3e41402b --- /dev/null +++ b/spec/serializers/issue_entity_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe IssueEntity do + let(:project) { create(:project) } + let(:resource) { create(:issue, project: project) } + let(:user) { create(:user) } + + let(:request) { double('request', current_user: user) } + + subject { described_class.new(resource, request: request).as_json } + + it 'has Issuable attributes' do + expect(subject).to include(:id, :iid, :author_id, :description, :lock_version, :milestone_id, + :title, :updated_by_id, :created_at, :updated_at, :milestone, :labels) + end + + it 'has time estimation attributes' do + expect(subject).to include(:time_estimate, :total_time_spent, :human_time_estimate, :human_total_time_spent) + end +end diff --git a/spec/serializers/merge_request_entity_spec.rb b/spec/serializers/merge_request_entity_spec.rb index 87832b3dca1..f9285049c0d 100644 --- a/spec/serializers/merge_request_entity_spec.rb +++ b/spec/serializers/merge_request_entity_spec.rb @@ -30,8 +30,17 @@ describe MergeRequestEntity do :assign_to_closing) end + it 'has Issuable attributes' do + expect(subject).to include(:id, :iid, :author_id, :description, :lock_version, :milestone_id, + :title, :updated_by_id, :created_at, :updated_at, :milestone, :labels) + end + + it 'has time estimation attributes' do + expect(subject).to include(:time_estimate, :total_time_spent, :human_time_estimate, :human_total_time_spent) + end + it 'has important MergeRequest attributes' do - expect(subject).to include(:diff_head_sha, :merge_commit_message, + expect(subject).to include(:state, :deleted_at, :diff_head_sha, :merge_commit_message, :has_conflicts, :has_ci, :merge_path, :conflict_resolution_path, :cancel_merge_when_pipeline_succeeds_path, diff --git a/spec/services/applications/create_service_spec.rb b/spec/services/applications/create_service_spec.rb new file mode 100644 index 00000000000..47a2a9d6403 --- /dev/null +++ b/spec/services/applications/create_service_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe ::Applications::CreateService do + let(:user) { create(:user) } + let(:params) { attributes_for(:application) } + let(:request) { ActionController::TestRequest.new(remote_ip: '127.0.0.1') } + + subject { described_class.new(user, params) } + + it 'creates an application' do + expect { subject.execute(request) }.to change { Doorkeeper::Application.count }.by(1) + end +end diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb new file mode 100644 index 00000000000..9f92b662be1 --- /dev/null +++ b/spec/services/issuable/common_system_notes_service_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe Issuable::CommonSystemNotesService do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:issuable) { create(:issue) } + + shared_examples 'system note creation' do |update_params, note_text| + subject { described_class.new(project, user).execute(issuable, [])} + + before do + issuable.assign_attributes(update_params) + issuable.save + end + + it 'creates 1 system note with the correct content' do + expect { subject }.to change { Note.count }.from(0).to(1) + + note = Note.last + expect(note.note).to match(note_text) + expect(note.noteable_type).to eq('Issue') + end + end + + describe '#execute' do + it_behaves_like 'system note creation', { title: 'New title' }, 'changed title' + it_behaves_like 'system note creation', { description: 'New description' }, 'changed the description' + it_behaves_like 'system note creation', { discussion_locked: true }, 'locked this issue' + it_behaves_like 'system note creation', { time_estimate: 5 }, 'changed time estimate' + + context 'when new label is added' do + before do + label = create(:label, project: project) + issuable.labels << label + end + + it_behaves_like 'system note creation', {}, /added ~\w+ label/ + end + + context 'when new milestone is assigned' do + before do + milestone = create(:milestone, project: project) + issuable.milestone_id = milestone.id + end + + it_behaves_like 'system note creation', {}, 'changed milestone' + end + end +end diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb index 50d3a4ec982..2bba71fef4f 100644 --- a/spec/services/projects/unlink_fork_service_spec.rb +++ b/spec/services/projects/unlink_fork_service_spec.rb @@ -12,6 +12,9 @@ describe Projects::UnlinkForkService do context 'with opened merge request on the source project' do let(:merge_request) { create(:merge_request, source_project: forked_project, target_project: fork_link.forked_from_project) } + let(:merge_request2) { create(:merge_request, source_project: forked_project, target_project: fork_project(project)) } + let(:merge_request_in_fork) { create(:merge_request, source_project: forked_project, target_project: forked_project) } + let(:mr_close_service) { MergeRequests::CloseService.new(forked_project, user) } before do @@ -22,9 +25,14 @@ describe Projects::UnlinkForkService do it 'close all pending merge requests' do expect(mr_close_service).to receive(:execute).with(merge_request) + expect(mr_close_service).to receive(:execute).with(merge_request2) subject.execute end + + it 'does not close merge requests for the project being unlinked' do + expect(mr_close_service).not_to receive(:execute).with(merge_request_in_fork) + end end it 'remove fork relation' do @@ -53,4 +61,14 @@ describe Projects::UnlinkForkService do expect(source.forks_count).to be_zero end + + context 'when the original project was deleted' do + it 'does not fail when the original project is deleted' do + source = forked_project.forked_from_project + source.destroy + forked_project.reload + + expect { subject.execute }.not_to raise_error + end + end end diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index 8f7aea533dc..46cd10cdc12 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -69,11 +69,48 @@ describe SystemHooksService do expect(data[:project_visibility]).to eq('private') end + + context 'group_rename' do + it 'contains old and new path' do + allow(group).to receive(:path_was).and_return('old-path') + + data = event_data(group, :rename) + + expect(data).to include(:event_name, :name, :created_at, :updated_at, :full_path, :path, :group_id, :old_path, :old_full_path) + expect(data[:path]).to eq(group.path) + expect(data[:full_path]).to eq(group.path) + expect(data[:old_path]).to eq(group.path_was) + expect(data[:old_full_path]).to eq(group.path_was) + end + + it 'contains old and new full_path for subgroup' do + subgroup = create(:group, parent: group) + allow(subgroup).to receive(:path_was).and_return('old-path') + + data = event_data(subgroup, :rename) + + expect(data[:full_path]).to eq(subgroup.full_path) + expect(data[:old_path]).to eq('old-path') + end + end + + context 'user_rename' do + it 'contains old and new username' do + allow(user).to receive(:username_was).and_return('old-username') + + data = event_data(user, :rename) + + expect(data).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username, :old_username) + expect(data[:username]).to eq(user.username) + expect(data[:old_username]).to eq(user.username_was) + end + end end context 'event names' do it { expect(event_name(user, :create)).to eq "user_create" } it { expect(event_name(user, :destroy)).to eq "user_destroy" } + it { expect(event_name(user, :rename)).to eq 'user_rename' } it { expect(event_name(project, :create)).to eq "project_create" } it { expect(event_name(project, :destroy)).to eq "project_destroy" } it { expect(event_name(project, :rename)).to eq "project_rename" } @@ -85,6 +122,7 @@ describe SystemHooksService do it { expect(event_name(key, :destroy)).to eq 'key_destroy' } it { expect(event_name(group, :create)).to eq 'group_create' } it { expect(event_name(group, :destroy)).to eq 'group_destroy' } + it { expect(event_name(group, :rename)).to eq 'group_rename' } it { expect(event_name(group_member, :create)).to eq 'user_add_to_group' } it { expect(event_name(group_member, :destroy)).to eq 'user_remove_from_group' } end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 48cacba6a8a..7c8331f6c60 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -48,7 +48,11 @@ RSpec.configure do |config| config.include Warden::Test::Helpers, type: :request config.include LoginHelpers, type: :feature config.include SearchHelpers, type: :feature + config.include CookieHelper, :js + config.include InputHelper, :js + config.include InspectRequests, :js config.include WaitForRequests, :js + config.include LiveDebugger, :js config.include StubConfiguration config.include EmailHelpers, :mailer, type: :mailer config.include TestEnv diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb index 01aca74274c..ac0c7a9b493 100644 --- a/spec/support/api_helpers.rb +++ b/spec/support/api_helpers.rb @@ -18,21 +18,23 @@ module ApiHelpers # # Returns the relative path to the requested API resource def api(path, user = nil, version: API::API.version, personal_access_token: nil, oauth_access_token: nil) - "/api/#{version}#{path}" + + full_path = "/api/#{version}#{path}" - # Normalize query string - (path.index('?') ? '' : '?') + + if oauth_access_token + query_string = "access_token=#{oauth_access_token.token}" + elsif personal_access_token + query_string = "private_token=#{personal_access_token.token}" + elsif user + personal_access_token = create(:personal_access_token, user: user) + query_string = "private_token=#{personal_access_token.token}" + end - if personal_access_token.present? - "&private_token=#{personal_access_token.token}" - elsif oauth_access_token.present? - "&access_token=#{oauth_access_token.token}" - # Append private_token if given a User object - elsif user.respond_to?(:private_token) - "&private_token=#{user.private_token}" - else - '' - end + if query_string + full_path << (path.index('?') ? '&' : '?') + full_path << query_string + end + + full_path end # Temporary helper method for simplifying V3 exclusive API specs diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index c45c4a4310d..9f672bc92fc 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -1,25 +1,25 @@ # rubocop:disable Style/GlobalVars require 'capybara/rails' require 'capybara/rspec' -require 'capybara/poltergeist' require 'capybara-screenshot/rspec' +require 'selenium-webdriver' # Give CI some extra time timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30 -Capybara.javascript_driver = :poltergeist -Capybara.register_driver :poltergeist do |app| - Capybara::Poltergeist::Driver.new( - 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=yes' - ] +Capybara.javascript_driver = :chrome +Capybara.register_driver :chrome do |app| + extra_args = [] + extra_args << 'headless' unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i + + capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( + chromeOptions: { + 'args' => %w[no-sandbox disable-gpu --window-size=1240,1400] + extra_args + } ) + + Capybara::Selenium::Driver + .new(app, browser: :chrome, desired_capabilities: capabilities) end Capybara.default_max_wait_time = timeout @@ -27,6 +27,10 @@ Capybara.ignore_hidden_elements = true # Keep only the screenshots generated from the last failing test suite Capybara::Screenshot.prune_strategy = :keep_last_run +# From https://github.com/mattheworiordan/capybara-screenshot/issues/84#issuecomment-41219326 +Capybara::Screenshot.register_driver(:chrome) do |driver, path| + driver.browser.save_screenshot(path) +end RSpec.configure do |config| config.before(:context, :js) do @@ -37,13 +41,23 @@ RSpec.configure do |config| end config.before(:example, :js) do + session = Capybara.current_session + allow(Gitlab::Application.routes).to receive(:default_url_options).and_return( - host: Capybara.current_session.server.host, - port: Capybara.current_session.server.port, + host: session.server.host, + port: session.server.port, protocol: 'http') + + # reset window size between tests + unless session.current_window.size == [1240, 1400] + session.current_window.resize_to(1240, 1400) rescue nil + end end config.after(:example, :js) do |example| + # prevent localstorage from introducing side effects based on test order + execute_script("localStorage.clear();") + # capybara/rspec already calls Capybara.reset_sessions! in an `after` hook, # but `block_and_wait_for_requests_complete` is called before it so by # calling it explicitely here, we prevent any new requests from being fired diff --git a/spec/support/capybara_helpers.rb b/spec/support/capybara_helpers.rb index 3eb7bea3227..868233416bf 100644 --- a/spec/support/capybara_helpers.rb +++ b/spec/support/capybara_helpers.rb @@ -38,7 +38,7 @@ module CapybaraHelpers # Simulate a browser restart by clearing the session cookie. def clear_browser_session - page.driver.remove_cookie('_gitlab_session') + page.driver.browser.manage.delete_cookie('_gitlab_session') end end diff --git a/spec/support/cookie_helper.rb b/spec/support/cookie_helper.rb new file mode 100644 index 00000000000..224619c899c --- /dev/null +++ b/spec/support/cookie_helper.rb @@ -0,0 +1,17 @@ +# Helper for setting cookies in Selenium/WebDriver +# +module CookieHelper + def set_cookie(name, value, options = {}) + # Selenium driver will not set cookies for a given domain when the browser is at `about:blank`. + # It also doesn't appear to allow overriding the cookie path. loading `/` is the most inclusive. + visit options.fetch(:path, '/') unless on_a_page? + page.driver.browser.manage.add_cookie(name: name, value: value, **options) + end + + private + + def on_a_page? + current_url = Capybara.current_session.driver.browser.current_url + current_url && current_url != '' && current_url != 'about:blank' && current_url != 'data:,' + end +end diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb index 7132b9cd221..aabc64d972b 100644 --- a/spec/support/features/discussion_comments_shared_example.rb +++ b/spec/support/features/discussion_comments_shared_example.rb @@ -71,26 +71,28 @@ shared_examples 'discussion comments' do |resource_name| expect(page).not_to have_selector menu_selector find(toggle_selector).click - find('body').trigger 'click' + find('body').click expect(page).not_to have_selector menu_selector end it 'clicking the ul padding or divider should not change the text' do - find(menu_selector).trigger 'click' + execute_script("document.querySelector('#{menu_selector}').click()") + # on issues page, the menu closes when clicking anywhere, on other pages it will + # remain open if clicking divider or menu padding, but should not change button action if resource_name == 'issue' expect(find(dropdown_selector)).to have_content 'Comment' find(toggle_selector).click - find("#{menu_selector} .divider").trigger 'click' + execute_script("document.querySelector('#{menu_selector} .divider').click()") else - find(menu_selector).trigger 'click' + execute_script("document.querySelector('#{menu_selector}').click()") expect(page).to have_selector menu_selector expect(find(dropdown_selector)).to have_content 'Comment' - find("#{menu_selector} .divider").trigger 'click' + execute_script("document.querySelector('#{menu_selector} .divider').click()") expect(page).to have_selector menu_selector end @@ -105,7 +107,12 @@ shared_examples 'discussion comments' do |resource_name| end it 'updates the submit button text and closes the dropdown' do - expect(find(dropdown_selector)).to have_content 'Start discussion' + # on issues page, the submit input is a <button>, on other pages it is <input> + if resource_name == 'issue' + expect(find(submit_selector)).to have_content 'Start discussion' + else + expect(find(submit_selector).value).to eq 'Start discussion' + end expect(page).not_to have_selector menu_selector end @@ -187,7 +194,12 @@ shared_examples 'discussion comments' do |resource_name| end it 'updates the submit button text and closes the dropdown' do - expect(find(dropdown_selector)).to have_content 'Comment' + # on issues page, the submit input is a <button>, on other pages it is <input> + if resource_name == 'issue' + expect(find(submit_selector)).to have_content 'Comment' + else + expect(find(submit_selector).value).to eq 'Comment' + end expect(page).not_to have_selector menu_selector end @@ -226,6 +238,7 @@ shared_examples 'discussion comments' do |resource_name| describe "on a closed #{resource_name}" do before do find("#{form_selector} .js-note-target-close").click + wait_for_requests find("#{form_selector} .note-textarea").send_keys('a') end diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb index 061e0d35590..08e21ee2537 100644 --- a/spec/support/features/issuable_slash_commands_shared_examples.rb +++ b/spec/support/features/issuable_slash_commands_shared_examples.rb @@ -61,7 +61,7 @@ shared_examples 'issuable record that supports quick actions in its description context 'with a note containing commands' do it 'creates a note without the commands and interpret the commands accordingly' do assignee = create(:user, username: 'bob') - write_note("Awesome!\n/assign @bob\n/label ~bug\n/milestone %\"ASAP\"") + write_note("Awesome!\n\n/assign @bob\n\n/label ~bug\n\n/milestone %\"ASAP\"") expect(page).to have_content 'Awesome!' expect(page).not_to have_content '/assign @bob' @@ -82,7 +82,7 @@ shared_examples 'issuable record that supports quick actions in its description context 'with a note containing only commands' do it 'does not create a note but interpret the commands accordingly' do assignee = create(:user, username: 'bob') - write_note("/assign @bob\n/label ~bug\n/milestone %\"ASAP\"") + write_note("/assign @bob\n\n/label ~bug\n\n/milestone %\"ASAP\"") expect(page).not_to have_content '/assign @bob' expect(page).not_to have_content '/label ~bug' diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb index 192a2fed0a8..836e5e7be23 100644 --- a/spec/support/features/reportable_note_shared_examples.rb +++ b/spec/support/features/reportable_note_shared_examples.rb @@ -39,7 +39,7 @@ shared_examples 'reportable note' do |type| end def open_dropdown(dropdown) - dropdown.find('.more-actions-toggle').trigger('click') + dropdown.find('.more-actions-toggle').click dropdown.find('.dropdown-menu li', match: :first) end end diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json index 688175369ae..658ff5871b0 100644 --- a/spec/support/gitlab_stubs/session.json +++ b/spec/support/gitlab_stubs/session.json @@ -14,7 +14,5 @@ "provider":null, "is_admin":false, "can_create_group":false, - "can_create_project":false, - "private_token":"Wvjy2Krpb7y8xi93owUz", - "access_token":"Wvjy2Krpb7y8xi93owUz" + "can_create_project":false } diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json index ce8dfe5ae75..658ff5871b0 100644 --- a/spec/support/gitlab_stubs/user.json +++ b/spec/support/gitlab_stubs/user.json @@ -14,7 +14,5 @@ "provider":null, "is_admin":false, "can_create_group":false, - "can_create_project":false, - "private_token":"Wvjy2Krpb7y8xi93owUz", - "access_token":"Wvjy2Krpb7y8xi93owUz" -}
\ No newline at end of file + "can_create_project":false +} diff --git a/spec/support/helpers/merge_request_diff_helpers.rb b/spec/support/helpers/merge_request_diff_helpers.rb index fd22e384b1b..c98aa503ed1 100644 --- a/spec/support/helpers/merge_request_diff_helpers.rb +++ b/spec/support/helpers/merge_request_diff_helpers.rb @@ -2,7 +2,7 @@ module MergeRequestDiffHelpers def click_diff_line(line_holder, diff_side = nil) line = get_line_components(line_holder, diff_side) line[:content].hover - line[:num].find('.add-diff-note').trigger('click') + line[:num].find('.add-diff-note', visible: false).send_keys(:return) end def get_line_components(line_holder, diff_side = nil) diff --git a/spec/support/helpers/note_interaction_helpers.rb b/spec/support/helpers/note_interaction_helpers.rb index 86008698692..79a0aa174b1 100644 --- a/spec/support/helpers/note_interaction_helpers.rb +++ b/spec/support/helpers/note_interaction_helpers.rb @@ -2,7 +2,7 @@ module NoteInteractionHelpers def open_more_actions_dropdown(note) note_element = find("#note_#{note.id}") - note_element.find('.more-actions-toggle').trigger('click') + note_element.find('.more-actions-toggle').click note_element.find('.more-actions .dropdown-menu li', match: :first) end end diff --git a/spec/support/input_helper.rb b/spec/support/input_helper.rb new file mode 100644 index 00000000000..acbb42274ec --- /dev/null +++ b/spec/support/input_helper.rb @@ -0,0 +1,7 @@ +# see app/assets/javascripts/test_utils/simulate_input.js + +module InputHelper + def simulate_input(selector, input = '') + evaluate_script("window.simulateInput(#{selector.to_json}, #{input.to_json});") + end +end diff --git a/spec/support/inspect_requests.rb b/spec/support/inspect_requests.rb new file mode 100644 index 00000000000..88ddc5c7f6c --- /dev/null +++ b/spec/support/inspect_requests.rb @@ -0,0 +1,17 @@ +require_relative './wait_for_requests' + +module InspectRequests + extend self + include WaitForRequests + + def inspect_requests(inject_headers: {}) + Gitlab::Testing::RequestInspectorMiddleware.log_requests!(inject_headers) + + yield + + wait_for_all_requests + Gitlab::Testing::RequestInspectorMiddleware.requests + ensure + Gitlab::Testing::RequestInspectorMiddleware.stop_logging! + end +end diff --git a/spec/support/live_debugger.rb b/spec/support/live_debugger.rb new file mode 100644 index 00000000000..911eb48a8ca --- /dev/null +++ b/spec/support/live_debugger.rb @@ -0,0 +1,17 @@ +require 'io/console' + +module LiveDebugger + def live_debug + puts + puts "Current example is paused for live debugging." + puts "Opening #{current_url} in your default browser..." + puts "The current user credentials are: #{@current_user.username} / #{@current_user.password}" if @current_user + puts "Press any key to resume the execution of the example!!" + + `open #{current_url}` + + loop until $stdin.getch + + puts "Back to the example!" + end +end diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index 4aed40bf22d..50702a0ac88 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -3,6 +3,21 @@ require_relative 'devise_helpers' module LoginHelpers include DeviseHelpers + # Overriding Devise::Test::IntegrationHelpers#sign_in to store @current_user + # since we may need it in LiveDebugger#live_debug. + def sign_in(resource, scope: nil) + super + + @current_user = resource + end + + # Overriding Devise::Test::IntegrationHelpers#sign_out to clear @current_user. + def sign_out(resource_or_scope) + super + + @current_user = nil + end + # Internal: Log in as a specific user or a new user of a specific role # # user_or_role - User object, or a role to create (e.g., :admin, :user) @@ -28,7 +43,7 @@ module LoginHelpers gitlab_sign_in_with(user, **kwargs) - user + @current_user = user end def gitlab_sign_in_via(provider, user, uid) @@ -41,6 +56,7 @@ module LoginHelpers def gitlab_sign_out find(".header-user-dropdown-toggle").click click_link "Sign out" + @current_user = nil expect(page).to have_button('Sign in') end diff --git a/spec/support/mobile_helpers.rb b/spec/support/mobile_helpers.rb index 431f20a2a5c..3b9eb84e824 100644 --- a/spec/support/mobile_helpers.rb +++ b/spec/support/mobile_helpers.rb @@ -12,6 +12,6 @@ module MobileHelpers end def resize_window(width, height) - page.driver.resize_window width, height + Capybara.current_session.current_window.resize_to(width, height) end end diff --git a/spec/support/protected_tags/access_control_ce_shared_examples.rb b/spec/support/protected_tags/access_control_ce_shared_examples.rb index 421a51fc336..2770cdcbefc 100644 --- a/spec/support/protected_tags/access_control_ce_shared_examples.rb +++ b/spec/support/protected_tags/access_control_ce_shared_examples.rb @@ -9,7 +9,7 @@ RSpec.shared_examples "protected tags > access control > CE" do allowed_to_create_button = find(".js-allowed-to-create") unless allowed_to_create_button.text == access_type_name - allowed_to_create_button.trigger('click') + allowed_to_create_button.click find('.create_access_levels-container .dropdown-menu li', match: :first) within('.create_access_levels-container .dropdown-menu') { click_on access_type_name } end diff --git a/spec/support/quick_actions_helpers.rb b/spec/support/quick_actions_helpers.rb index d2aaae7518f..361190aa352 100644 --- a/spec/support/quick_actions_helpers.rb +++ b/spec/support/quick_actions_helpers.rb @@ -3,7 +3,7 @@ module QuickActionsHelpers Sidekiq::Testing.fake! do page.within('.js-main-target-form') do fill_in 'note[note]', with: text - find('.js-comment-submit-button').trigger('click') + find('.js-comment-submit-button').click end end end diff --git a/spec/support/shared_examples/features/protected_branches_access_control_ce.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce.rb index d5bc12f3bc5..5fde91512da 100644 --- a/spec/support/shared_examples/features/protected_branches_access_control_ce.rb +++ b/spec/support/shared_examples/features/protected_branches_access_control_ce.rb @@ -9,7 +9,7 @@ shared_examples "protected branches > access control > CE" do allowed_to_push_button = find(".js-allowed-to-push") unless allowed_to_push_button.text == access_type_name - allowed_to_push_button.trigger('click') + allowed_to_push_button.click within(".dropdown.open .dropdown-menu") { click_on access_type_name } end end @@ -34,7 +34,7 @@ shared_examples "protected branches > access control > CE" do within('.js-allowed-to-push-container') do expect(first("li")).to have_content("Roles") - click_on access_type_name + find(:link, access_type_name).click end end @@ -79,7 +79,7 @@ shared_examples "protected branches > access control > CE" do within('.js-allowed-to-merge-container') do expect(first("li")).to have_content("Roles") - click_on access_type_name + find(:link, access_type_name).click end end diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb index 4d448a55978..4ead78529c3 100644 --- a/spec/support/stub_configuration.rb +++ b/spec/support/stub_configuration.rb @@ -38,6 +38,10 @@ module StubConfiguration allow(Gitlab.config.backup).to receive_messages(to_settings(messages)) end + def stub_lfs_setting(messages) + allow(Gitlab.config.lfs).to receive_messages(to_settings(messages)) + end + def stub_storage_settings(messages) # Default storage is always required messages['default'] ||= Gitlab.config.repositories.storages.default diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index a27bfdee3d2..fff120fcb88 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -182,6 +182,8 @@ module TestEnv return unless @gitaly_pid Process.kill('KILL', @gitaly_pid) + rescue Errno::ESRCH + # The process can already be gone if the test run was INTerrupted. end def setup_factory_repo diff --git a/spec/support/time_tracking_shared_examples.rb b/spec/support/time_tracking_shared_examples.rb index 0fa74f911f6..909d4e2ee8d 100644 --- a/spec/support/time_tracking_shared_examples.rb +++ b/spec/support/time_tracking_shared_examples.rb @@ -80,6 +80,6 @@ end def submit_time(quick_action) fill_in 'note[note]', with: quick_action - find('.js-comment-submit-button').trigger('click') + find('.js-comment-submit-button').click wait_for_requests end diff --git a/spec/support/wait_for_requests.rb b/spec/support/wait_for_requests.rb index b5c3c0f55b8..f4130d68271 100644 --- a/spec/support/wait_for_requests.rb +++ b/spec/support/wait_for_requests.rb @@ -1,25 +1,47 @@ -require_relative './wait_for_requests' - module WaitForRequests extend self # This is inspired by http://www.salsify.com/blog/engineering/tearing-capybara-ajax-tests def block_and_wait_for_requests_complete + block_requests { wait_for_all_requests } + end + + # Block all requests inside block with 503 response + def block_requests Gitlab::Testing::RequestBlockerMiddleware.block_requests! - wait_for('pending requests complete') do - Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero? && finished_all_requests? - end + yield ensure Gitlab::Testing::RequestBlockerMiddleware.allow_requests! end + # Slow down requests inside block by injecting `sleep 0.2` before each response + def slow_requests + Gitlab::Testing::RequestBlockerMiddleware.slow_requests! + yield + ensure + Gitlab::Testing::RequestBlockerMiddleware.allow_requests! + end + + # Wait for client-side AJAX requests def wait_for_requests - wait_for('JS requests') { finished_all_requests? } + wait_for('JS requests complete') { finished_all_js_requests? } + end + + # Wait for active Rack requests and client-side AJAX requests + def wait_for_all_requests + wait_for('pending requests complete') do + finished_all_rack_reqiests? && + finished_all_js_requests? + end end private - def finished_all_requests? + def finished_all_rack_reqiests? + Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero? + end + + def finished_all_js_requests? return true unless javascript_test? finished_all_ajax_requests? && diff --git a/spec/tasks/gitlab/users_rake_spec.rb b/spec/tasks/gitlab/users_rake_spec.rb deleted file mode 100644 index 972670e7f91..00000000000 --- a/spec/tasks/gitlab/users_rake_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'spec_helper' -require 'rake' - -describe 'gitlab:users namespace rake task' do - let(:enable_registry) { true } - - before :all do - Rake.application.rake_require 'tasks/gitlab/helpers' - Rake.application.rake_require 'tasks/gitlab/users' - - # empty task as env is already loaded - Rake::Task.define_task :environment - end - - def run_rake_task(task_name) - Rake::Task[task_name].reenable - Rake.application.invoke_task task_name - end - - describe 'clear_all_authentication_tokens' do - before do - # avoid writing task output to spec progress - allow($stdout).to receive :write - end - - context 'gitlab version' do - it 'clears the authentication token for all users' do - create_list(:user, 2) - - expect(User.pluck(:authentication_token)).to all(be_present) - - run_rake_task('gitlab:users:clear_all_authentication_tokens') - - expect(User.pluck(:authentication_token)).to all(be_nil) - end - end - end -end diff --git a/spec/tasks/tokens_spec.rb b/spec/tasks/tokens_spec.rb index b84137eb365..51f7a536cbb 100644 --- a/spec/tasks/tokens_spec.rb +++ b/spec/tasks/tokens_spec.rb @@ -7,12 +7,6 @@ describe 'tokens rake tasks' do Rake.application.rake_require 'tasks/tokens' end - describe 'reset_all task' do - it 'invokes create_hooks task' do - expect { run_rake_task('tokens:reset_all_auth') }.to change { user.reload.authentication_token } - end - end - describe 'reset_all_email task' do it 'invokes create_hooks task' do expect { run_rake_task('tokens:reset_all_email') }.to change { user.reload.incoming_email_token } diff --git a/vendor/assets/javascripts/peek.js b/vendor/assets/javascripts/peek.js index f7e77de34ff..6a341a3f0fe 100644 --- a/vendor/assets/javascripts/peek.js +++ b/vendor/assets/javascripts/peek.js @@ -1,5 +1,14 @@ +/* + * This is a modified version of https://github.com/peek/peek/blob/master/app/assets/javascripts/peek.js + * + * - Removed the dependency on jquery.tipsy + * - Removed the initializeTipsy and toggleBar functions + * - Customized updatePerformanceBar to handle SQL queries report specificities + * - Changed /peek/results to /-/peek/results + * - Removed the keypress, pjax:end, page:change, and turbolinks:load handlers + */ (function($) { - var fetchRequestResults, getRequestId, peekEnabled, toggleBar, updatePerformanceBar; + var fetchRequestResults, getRequestId, peekEnabled, updatePerformanceBar; getRequestId = function() { return $('#peek').data('request-id'); }; @@ -41,22 +50,6 @@ }); 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: { @@ -68,7 +61,6 @@ error: function(xhr, textStatus, error) {} }); }; - $(document).on('keypress', toggleBar); $(document).on('peek:update', fetchRequestResults); return $(function() { if (peekEnabled()) { |