diff options
287 files changed, 2886 insertions, 2041 deletions
@@ -1 +1 @@ -7.5
\ No newline at end of file +9.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9b2ee157193..c4e5fd842df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,13 @@ -## Contributor license agreement +## Developer Certificate of Origin + License -By submitting code as an individual you agree to the -[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md). -By submitting code as an entity you agree to the -[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md). +By contributing to GitLab B.V., You accept and agree to the following terms and +conditions for Your present and future Contributions submitted to GitLab B.V. +Except for the license granted herein to GitLab B.V. and recipients of software +distributed by GitLab B.V., You reserve all right, title, and interest in and to +Your Contributions. All Contributions are subject to the following DCO + License +terms. + +[DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md) _This notice should stay as the first item in the CONTRIBUTING.md file._ @@ -100,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. diff --git a/Gemfile.lock b/Gemfile.lock index 4142977285e..8ccf18818a6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -291,7 +291,7 @@ GEM diff-lcs (~> 1.1) mime-types (>= 1.16) posix-spawn (~> 0.3) - gitlab-markup (1.6.2) + gitlab-markup (1.6.3) gitlab_omniauth-ldap (2.0.4) net-ldap (~> 0.16) omniauth (~> 1.3) 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/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/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index cdc4fcf6573..e7232ca3712 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -4,6 +4,7 @@ import _ from 'underscore'; import d3 from 'd3'; import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph'; import ContributorsStatGraphUtil from './stat_graph_contributors_util'; +import { n__ } from '../locale'; export default (function() { function ContributorsStatGraph() {} @@ -44,7 +45,7 @@ export default (function() { commits = $('<span/>', { "class": 'graph-author-commits-count' }); - commits.text(author.commits + " commits"); + commits.text(n__('%d commit', '%d commits', author.commits)); return $('<span/>').append(commits); }; 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/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 0ddbd672bed..40318f9a600 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -122,7 +122,9 @@ // we need to do this to prevent noteForm inconsistent content warning // this is something we intentionally do so we need to recover the content this.note.note = noteText; - this.$refs.noteBody.$refs.noteForm.note = noteText; // TODO: This could be better + if (this.$refs.noteBody) { + this.$refs.noteBody.$refs.noteForm.note = noteText; // TODO: This could be better + } }, }, created() { 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/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue index b2b34cb83e1..6348a2e331d 100644 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -98,7 +98,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), @toggle="toggleOpen" @submit="onSubmit"> - <template slot="body" scope="props"> + <template slot="body" slot-scope="props"> <p v-html="props.text"></p> <form diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue index 0d6729bb99b..1c864b176b1 100644 --- a/app/assets/javascripts/repo/components/repo_editor.vue +++ b/app/assets/javascripts/repo/components/repo_editor.vue @@ -27,6 +27,8 @@ export default { 'changeFileContent', ]), initMonaco() { + if (this.shouldHideEditor) return; + if (this.monacoInstance) { this.monacoInstance.setModel(null); } @@ -94,8 +96,12 @@ export default { <template> <div id="ide" - v-if='!shouldHideEditor' class="blob-viewer-container blob-editor-container" > + <div + v-if="shouldHideEditor" + v-html="activeFile.html" + > + </div> </div> </template> diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js index 8635ccece6e..d34a21b37e1 100644 --- a/app/assets/javascripts/settings_panels.js +++ b/app/assets/javascripts/settings_panels.js @@ -1,34 +1,26 @@ -function expandSectionParent($section, $content) { - $section.addClass('expanded'); - $content.off('animationend.expandSectionParent'); -} - function expandSection($section) { $section.find('.js-settings-toggle').text('Collapse'); - - const $content = $section.find('.settings-content'); - $content.addClass('expanded').off('scroll.expandSection').scrollTop(0); - - if ($content.hasClass('no-animate')) { - expandSectionParent($section, $content); - } else { - $content.on('animationend.expandSectionParent', () => expandSectionParent($section, $content)); + $section.find('.settings-content').off('scroll.expandSection').scrollTop(0); + $section.addClass('expanded'); + if (!$section.hasClass('no-animate')) { + $section.addClass('animating') + .one('animationend.animateSection', () => $section.removeClass('animating')); } } function closeSection($section) { $section.find('.js-settings-toggle').text('Expand'); - - const $content = $section.find('.settings-content'); - $content.removeClass('expanded').on('scroll.expandSection', () => expandSection($section)); - + $section.find('.settings-content').on('scroll.expandSection', () => expandSection($section)); $section.removeClass('expanded'); + if (!$section.hasClass('no-animate')) { + $section.addClass('animating') + .one('animationend.animateSection', () => $section.removeClass('animating')); + } } function toggleSection($section) { - const $content = $section.find('.settings-content'); - $content.removeClass('no-animate'); - if ($content.hasClass('expanded')) { + $section.removeClass('no-animate'); + if ($section.hasClass('expanded')) { closeSection($section); } else { expandSection($section); @@ -39,10 +31,19 @@ export default function initSettingsPanels() { $('.settings').each((i, elm) => { const $section = $(elm); $section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section)); - $section.find('.settings-content:not(.expanded)').on('scroll.expandSection', () => expandSection($section)); + + if (!$section.hasClass('expanded')) { + $section.find('.settings-content').on('scroll.expandSection', () => { + $section.removeClass('no-animate'); + expandSection($section); + }); + } }); if (location.hash) { - expandSection($(location.hash)); + const $target = $(location.hash); + if ($target.length && $target.hasClass('.settings')) { + expandSection($target); + } } } 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/animations.scss b/app/assets/stylesheets/framework/animations.scss index 81439c0d2fe..1b944831082 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -23,11 +23,16 @@ @include webkit-prefix(animation-duration, 2s); } - &.spin { + &.spin-cw { transform-origin: center; animation: spin 4s linear infinite; } + &.spin-ccw { + transform-origin: center; + animation: spin 4s linear infinite reverse; + } + &.flipOutX, &.flipOutY, &.bounceIn, diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index dbd990f84c1..8819a0c20f4 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -209,7 +209,6 @@ padding: 24px 0 0; .nav-links { - justify-content: center; width: 100%; float: none; @@ -217,6 +216,14 @@ float: none; } } + + li:first-child { + margin-left: auto; + } + + li:last-child { + margin-right: auto; + } } .group-info { diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 96f9dda26c4..ed84b17af6a 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; } @@ -448,3 +422,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/responsive-tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss index 8e653c443cf..7adb2f113bb 100644 --- a/app/assets/stylesheets/framework/responsive-tables.scss +++ b/app/assets/stylesheets/framework/responsive_tables.scss @@ -3,57 +3,74 @@ 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; } +} + +.gl-responsive-table-row-col-span { + flex-wrap: wrap; +} - .table-section { - white-space: nowrap; +.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 +78,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 +95,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/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 91296b354a7..278ec16bcd9 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,10 +81,14 @@ overflow-x: scroll; white-space: nowrap; - @media (min-width: $screen-sm-min) { + @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + height: calc(100vh - 90px); + } + + @media (min-width: $screen-md-min) { height: 475px; // Needed for PhantomJS // scss-lint:disable DuplicateProperty - height: calc(100vh - 222px); + height: calc(100vh - 160px); // scss-lint:enable DuplicateProperty min-height: 475px; } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 50ec5110bf1..e87ffe4f374 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -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/environments.scss b/app/assets/stylesheets/pages/environments.scss index 3b5e411e2c5..6c1d32bed2f 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; } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index d9fb3b44d29..645fc1f3ebb 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -165,8 +165,9 @@ z-index: 300; } - .ci-action-icon-wrapper { - line-height: 16px; + .ci-action-icon-wrapper svg { + width: 16px; + height: 16px; } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 8fc7a5eec9b..6604b471560 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 { @@ -452,7 +451,7 @@ } // Action Icons in big pipeline-graph nodes - .ci-action-icon-container .ci-action-icon-wrapper { + .ci-action-icon-container.ci-action-icon-wrapper { height: 30px; width: 30px; background: $white-light; @@ -468,8 +467,18 @@ svg { fill: $gl-text-color-secondary; position: relative; - left: -1px; - top: -1px; + left: 5px; + top: 2px; + width: 18px; + height: 18px; + } + + &.play { + svg { + width: #{$ci-action-icon-size - 8}; + height: #{$ci-action-icon-size - 8}; + left: 8px; + } } &:hover svg { @@ -721,17 +730,49 @@ 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; } &: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; + } + } + + svg.icon-action-stop, + svg.icon-action-cancel { + width: 12px; + height: 12px; + top: 1px; + left: -1px; + } + + svg.icon-action-play { + width: 11px; + height: 11px; + top: 1px; + left: 1px; + } + + svg.icon-action-retry { + width: 16px; + height: 16px; + top: 0; + left: -3px; + } + } // link to the build diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss index 6cac37a4e28..5fb97b13470 100644 --- a/app/assets/stylesheets/pages/runners.scss +++ b/app/assets/stylesheets/pages/runners.scss @@ -50,3 +50,10 @@ font-size: 11px; } } + +@media (max-width: $screen-md-max) { + .runners-content { + width: 100%; + overflow: auto; + } +} diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 41a6ba2023a..968a94c68cf 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -23,15 +23,14 @@ } .settings { - overflow: hidden; border-bottom: 1px solid $gray-darker; &:first-of-type { margin-top: 10px; } - &.expanded { - overflow: visible; + &.animating { + overflow: hidden; } } @@ -56,14 +55,18 @@ overflow-y: scroll; padding-right: 110px; animation: collapseMaxHeight 300ms ease-out; + // Keep the section from expanding when we scroll over it + pointer-events: none; - &.expanded { + .settings.expanded & { max-height: none; overflow-y: visible; animation: expandMaxHeight 300ms ease-in; + // Reset and allow clicks again when expanded + pointer-events: auto; } - &.no-animate { + .settings.no-animate & { animation: none; } 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/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/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/environment.rb b/app/models/environment.rb index e613d21add6..8d6b0a32c13 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -110,7 +110,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 +164,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/group.rb b/app/models/group.rb index 07fb62bb249..4e8023cdb7f 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -180,6 +180,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 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/user.rb b/app/models/user.rb index 9459b6d4fa4..6c9349ed9dd 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,7 +163,7 @@ 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? } @@ -185,8 +185,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 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/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/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 76f4a817744..4965dffab9d 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -52,22 +52,23 @@ %br - if @runners.any? - .table-holder - %table.table - %thead - %tr - %th Type - %th Runner token - %th Description - %th Version - %th Projects - %th Jobs - %th Tags - %th= link_to 'Last contact', admin_runners_path(params.slice(:search).merge(sort: 'contacted_asc')) - %th + .runners-content + .table-holder + %table.table + %thead + %tr + %th Type + %th Runner token + %th Description + %th Version + %th Projects + %th Jobs + %th Tags + %th Last contact + %th - - @runners.each do |runner| - = render "admin/runners/runner", runner: runner - = paginate @runners, theme: "gitlab" + - @runners.each do |runner| + = render "admin/runners/runner", runner: runner + = paginate @runners, theme: "gitlab" - else .nothing-here-block No runners found 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/_export.html.haml b/app/views/projects/_export.html.haml index 623d3bc91c6..c5b1897c492 100644 --- a/app/views/projects/_export.html.haml +++ b/app/views/projects/_export.html.haml @@ -3,7 +3,7 @@ - project = local_assigns.fetch(:project) - expanded = Rails.env.test? -%section.settings +%section.settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 Export project @@ -11,7 +11,7 @@ = expanded ? 'Collapse' : 'Expand' %p Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page. - .settings-content.no-animate{ class: ('expanded' if expanded) } + .settings-content .bs-callout.bs-callout-info %p.append-bottom-0 %p diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml index 45985a5ecef..e75ae87e771 100644 --- a/app/views/projects/deploy_keys/_index.html.haml +++ b/app/views/projects/deploy_keys/_index.html.haml @@ -1,5 +1,5 @@ - expanded = Rails.env.test? -%section.settings +%section.settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 Deploy Keys @@ -7,7 +7,7 @@ = expanded ? 'Collapse' : 'Expand' %p Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one. - .settings-content.no-animate{ class: ('expanded' if expanded) } + .settings-content %h5.prepend-top-0 Create a new deploy key for this project = render @deploy_keys.form_partial_path diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 893e536e289..5703ef1d4bb 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -4,7 +4,7 @@ - expanded = Rails.env.test? .project-edit-container - %section.settings.general-settings + %section.settings.general-settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 General project settings @@ -12,7 +12,7 @@ = expanded ? 'Collapse' : 'Expand' %p Update your project name, description, avatar, and other general settings. - .settings-content.no-animate{ class: ('expanded' if expanded) } + .settings-content .project-edit-errors = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f| %fieldset @@ -61,7 +61,7 @@ = link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" = f.submit 'Save changes', class: "btn btn-save" - %section.settings.sharing-permissions + %section.settings.sharing-permissions.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 Permissions @@ -69,13 +69,13 @@ = expanded ? 'Collapse' : 'Expand' %p Enable or disable certain project features and choose access levels. - .settings-content.no-animate{ class: ('expanded' if expanded) } + .settings-content = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| %script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project) .js-project-permissions-form = f.submit 'Save changes', class: "btn btn-save" - %section.settings.merge-requests-feature{ class: ("hidden" if @project.project_feature.send(:merge_requests_access_level) == 0) } + %section.settings.merge-requests-feature.no-animate{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] } .settings-header %h4 Merge request settings @@ -83,14 +83,14 @@ = expanded ? 'Collapse' : 'Expand' %p Customize your merge request restrictions. - .settings-content.no-animate{ class: ('expanded' if expanded) } + .settings-content = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f| = render 'merge_request_settings', form: f = f.submit 'Save changes', class: "btn btn-save" = render 'export', project: @project - %section.settings.advanced-settings + %section.settings.advanced-settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 Advanced settings @@ -98,7 +98,7 @@ = expanded ? 'Collapse' : 'Expand' %p Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project. - .settings-content.no-animate{ class: ('expanded' if expanded) } + .settings-content .sub-section %h4 Housekeeping %p diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index 70156c03e3c..cce16bc58b3 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -1,5 +1,5 @@ - @no_container = true -- page_title "Contributors" +- page_title _('Contributors') - content_for :page_specific_javascripts do = webpack_bundle_tag('common_d3') = webpack_bundle_tag('graphs') @@ -7,23 +7,23 @@ .js-graphs-show{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) } .sub-header-block - .tree-ref-holder + .tree-ref-holder.inline.vertical-align-middle = render 'shared/ref_switcher', destination: 'graphs' - %ul.breadcrumb.repo-breadcrumb - = commits_breadcrumbs + = link_to s_('Commits|History'), project_commits_path(@project, current_ref), class: 'btn' .loading-graph .center %h3.page-title %i.fa.fa-spinner.fa-spin - Building repository graph. - %p.slead Please wait a moment, this page will automatically refresh when ready. + = s_('ContributorsPage|Building repository graph.') + %p.slead + = s_('ContributorsPage|Please wait a moment, this page will automatically refresh when ready.') .stat-graph.hide .header.clearfix %h3#date_header.page-title %p.light - Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits. + = s_('ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits.') % { branch_name: @ref } %input#brush_change{ :type => "hidden" } .graphs.row #contributors-master 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/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml index 6a47cbdf724..ba7d98228c3 100644 --- a/app/views/projects/protected_branches/shared/_index.html.haml +++ b/app/views/projects/protected_branches/shared/_index.html.haml @@ -1,6 +1,6 @@ - expanded = Rails.env.test? -%section.settings +%section.settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 Protected Branches @@ -8,7 +8,7 @@ = expanded ? 'Collapse' : 'Expand' %p Keep stable branches secure and force developers to use merge requests. - .settings-content.no-animate{ class: ('expanded' if expanded) } + .settings-content %p By default, protected branches are designed to: %ul diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml index c07bd454ff6..e764a37bbd7 100644 --- a/app/views/projects/protected_tags/shared/_index.html.haml +++ b/app/views/projects/protected_tags/shared/_index.html.haml @@ -1,6 +1,6 @@ - expanded = Rails.env.test? -%section.settings +%section.settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 Protected Tags @@ -8,7 +8,7 @@ = expanded ? 'Collapse' : 'Expand' %p Limit access to creating and updating tags. - .settings-content.no-animate{ class: ('expanded' if expanded) } + .settings-content %p By default, protected tags are designed to: %ul diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index 62455d0d40d..664a4554692 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -4,7 +4,7 @@ - expanded = Rails.env.test? -%section.settings#js-general-pipeline-settings +%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 General pipelines settings @@ -12,10 +12,10 @@ = expanded ? 'Collapse' : 'Expand' %p Update your CI/CD configuration, like job timeout or Auto DevOps. - .settings-content.no-animate{ class: ('expanded' if expanded) } + .settings-content = render 'projects/pipelines_settings/show' -%section.settings +%section.settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 Runners settings @@ -23,10 +23,10 @@ = expanded ? 'Collapse' : 'Expand' %p Register and see your runners for this project. - .settings-content.no-animate{ class: ('expanded' if expanded) } + .settings-content = render 'projects/runners/index' -%section.settings +%section.settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 Secret variables @@ -35,10 +35,10 @@ = expanded ? 'Collapse' : 'Expand' %p = render "ci/variables/content" - .settings-content.no-animate{ class: ('expanded' if expanded) } + .settings-content = render 'ci/variables/index' -%section.settings +%section.settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 Pipeline triggers @@ -48,5 +48,5 @@ Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will impersonate their associated user including their access to projects and their project permissions. - .settings-content.no-animate{ class: ('expanded' if expanded) } + .settings-content = render 'projects/triggers/index' 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/app/views/shared/icons/_icon_autodevops.svg b/app/views/shared/icons/_icon_autodevops.svg index dde84e14048..423ca6d760d 100644 --- a/app/views/shared/icons/_icon_autodevops.svg +++ b/app/views/shared/icons/_icon_autodevops.svg @@ -29,7 +29,7 @@ </g> <g fill-rule="nonzero" transform="rotate(15 -315.035 277.714)"> <path fill="#FFFFFF" d="M12.275,10.57 C13.986216,9.15630755 15.921048,8.03765363 18,7.26 L18,5.5 C18,2.463 20.47,0 23.493,0 L26.507,0 C27.9648848,0.000530018716 29.3628038,0.580386367 30.3930274,1.61192286 C31.4232511,2.64345935 32.0013267,4.04211574 32,5.5 L32,7.26 C34.098,8.043 36.03,9.17 37.725,10.57 L39.253,9.688 C41.8816141,8.17268496 45.2407537,9.07039379 46.763,11.695 L48.27,14.305 C48.9984289,15.5678669 49.1951495,17.0684426 48.8168566,18.4763972 C48.4385638,19.8843518 47.5162683,21.0842673 46.253,21.812 L44.728,22.693 C44.907,23.769 45,24.873 45,26 C45,27.127 44.907,28.231 44.728,29.307 L46.253,30.187 C48.8800379,31.705769 49.7822744,35.0642181 48.27,37.695 L46.763,40.305 C46.0335844,41.5673849 44.8323832,42.4881439 43.4238487,42.8645658 C42.0153143,43.2409877 40.5149245,43.0422119 39.253,42.312 L37.725,41.43 C36.013784,42.8436924 34.078952,43.9623464 32,44.74 L32,46.5 C32,49.537 29.53,52 26.507,52 L23.493,52 C22.0351152,51.99947 20.6371962,51.4196136 19.6069726,50.3880771 C18.5767489,49.3565406 17.9986733,47.9578843 18,46.5 L18,44.74 C15.921048,43.9623464 13.986216,42.8436924 12.275,41.43 L10.747,42.312 C8.11838594,43.827315 4.75924629,42.9296062 3.237,40.305 L1.73,37.695 C1.00157113,36.4321331 0.804850523,34.9315574 1.18314337,33.5236028 C1.56143621,32.1156482 2.48373172,30.9157327 3.747,30.188 L5.272,29.307 C5.09051204,28.2140265 4.9995366,27.107939 5,26 C5,24.873 5.093,23.769 5.272,22.693 L3.747,21.813 C1.11996213,20.294231 0.217725591,16.9357819 1.73,14.305 L3.237,11.695 C3.96641559,10.4326151 5.16761682,9.51185609 6.57615125,9.13543417 C7.98468568,8.75901226 9.48507553,8.95778814 10.747,9.688 L12.275,10.57 Z"/> - <path class="animated spin infinite" fill="#E1DBF1" d="M17.9996486,7.25963195 L18.0000013,5.49772675 C18.0034459,2.46713881 20.4561478,0.00952173148 23.493,0 L26.507,0 C29.542757,0 32,2.46161709 32,5.5 L32,7.25850184 C34.0799663,8.03664754 36.0149544,9.15559094 37.7260175,10.5694605 L39.2547869,9.68691874 C41.8812087,8.17416302 45.2363972,9.06948854 46.7630175,11.6949424 L48.270687,14.3061027 C48.9989901,15.569417 49.1952874,17.0704122 48.816349,18.4785295 C48.4374106,19.8866468 47.5143145,21.0864021 46.2530682,21.8120114 L44.7278655,22.6926677 C44.9091017,23.7802451 45,24.8850821 45,26 C45,27.1144218 44.9091826,28.218078 44.7278653,29.3073326 L46.2547984,30.1889888 C48.8778516,31.7070439 49.7801588,35.0599752 48.2700175,37.6950576 L46.7625317,40.3058986 C46.0327098,41.5684739 44.8309328,42.4891542 43.4219037,42.8651509 C42.0128746,43.2411475 40.512172,43.0416186 39.2533538,42.312255 L37.7244858,41.4299789 C36.013753,42.8435912 34.0794396,43.9622923 32.0003514,44.7403681 L31.9999987,46.5022733 C31.9965541,49.5328612 29.5438522,51.9904783 26.507,52 L23.493,52 C20.457243,52 18,49.5383829 18,46.5 L18,44.7414988 C15.9200337,43.9633525 13.9850456,42.8444091 12.2739825,41.4305395 L10.7452131,42.3130813 C8.11879127,43.825837 4.76360277,42.9305115 3.23698247,40.3050576 L1.72931303,37.6938973 C1.0010099,36.430583 0.804712603,34.9295878 1.18365098,33.5214705 C1.56258936,32.1133532 2.48568546,30.9135979 3.74693178,30.1879886 L5.27213454,29.3073323 C5.09089825,28.2197549 5,27.114918 5.00000019,26.0008761 C4.99951488,24.8930059 5.0904571,23.7869854 5.27213502,22.6926675 L3.74520157,21.8110112 C1.12214836,20.2929561 0.219841192,16.9400248 1.72998247,14.3049424 L3.23746831,11.6941014 C3.96729024,10.4315261 5.16906725,9.51084579 6.5780963,9.13484913 C7.98712536,8.75885247 9.48782803,8.95838137 10.7466462,9.687745 L12.2748018,10.56961 C14.0209791,9.13635584 15.9392199,8.03072455 17.9996486,7.25963195 Z M13.7518374,14.537862 C13.108069,15.069723 12.2016163,15.1456339 11.4783538,14.728255 L8.74433999,13.1505123 C8.40103903,12.9516035 7.99274958,12.8973186 7.60940137,12.9996143 C7.22605315,13.10191 6.89909107,13.3523954 6.70101753,13.6950576 L5.19724591,16.2994454 C4.78547321,17.0179634 5.03203388,17.9341714 5.74706822,18.3479886 L8.47306822,19.9219886 C9.19530115,20.3390079 9.58295216,21.1604138 9.44574883,21.983032 L9.21798321,23.3486236 C9.07251948,24.2246212 8.99961081,25.111131 9,26 C9,26.8953847 9.0728258,27.7804297 9.21774883,28.649968 L9.44574883,30.016968 C9.58295216,30.8395862 9.19530115,31.6609921 8.47306822,32.0780114 L5.74435077,33.6535776 C5.40046982,33.851417 5.14932721,34.1778291 5.04623114,34.5609292 C4.94313508,34.9440294 4.9965408,35.3523984 5.19401753,35.6949424 L6.69795587,38.2996585 C7.11427713,39.0156351 8.03110189,39.260288 8.7470791,38.8479035 L11.4770791,37.2719035 C12.200376,36.8543519 13.1069795,36.9302031 13.7508374,37.462138 L14.8210499,38.3463136 C16.1898549,39.4774943 17.737648,40.3725891 19.3990866,40.9941596 L20.6990866,41.4791596 C21.4813437,41.7710017 22,42.5180761 22,43.353 L22,46.5 C22,47.3308348 22.6679761,48 23.493,48 L26.5007228,48.0000099 C27.328845,47.9974107 27.99906,47.3258525 28,46.5 L28,43.353 C28,42.5185702 28.5180515,41.771829 29.2996486,41.4796319 L30.599003,40.9938734 C32.261836,40.3715765 33.8093225,39.4764853 35.1790197,38.3444304 L36.2490197,37.4614304 C36.8927697,36.9301861 37.798736,36.8545694 38.5216462,37.271745 L41.25566,38.8494877 C41.598961,39.0483965 42.0072504,39.1026814 42.3905986,39.0003857 C42.7739468,38.89809 43.1009089,38.6476046 43.2989825,38.3049424 L44.8027541,35.7005546 C45.2145268,34.9820366 44.9679661,34.0658286 44.2529318,33.6520114 L41.5269318,32.0780114 C40.8046988,31.6609921 40.4170478,30.8395862 40.5542512,30.016968 L40.7821577,28.6505288 C40.9272286,27.7792134 41,26.8950523 41,26 C41,25.1046153 40.9271742,24.2195703 40.7822512,23.350032 L40.5542512,21.983032 C40.4170478,21.1604138 40.8046988,20.3390079 41.5269318,19.9219886 L44.2556492,18.3464224 C44.5995302,18.148583 44.8506728,17.8221709 44.9537689,17.4390708 C45.0568649,17.0559706 45.0034592,16.6476016 44.8059825,16.3050576 L43.3020441,13.7003415 C42.8857229,12.9843649 41.9688981,12.739712 41.2529209,13.1520965 L38.5229209,14.7280965 C37.799624,15.1456481 36.8930205,15.0697969 36.2491626,14.537862 L35.1789501,13.6536864 C33.8101451,12.5225057 32.262352,11.6274109 30.6021792,11.0063122 L29.3021792,10.5223122 C28.5192618,10.230826 28,9.48341836 28,8.648 L28,5.5 C28,4.66916515 27.3320239,4 26.507,4 L23.4992772,3.99999015 C22.671155,4.00258933 22.00094,4.67414748 22,5.5 L22,8.647 C22,9.48142977 21.4819485,10.228171 20.7003514,10.5203681 L19.400997,11.0061266 C17.738164,11.6284235 16.1906775,12.5235147 14.822142,13.6546103 C14.8121128,13.6628994 14.4553446,13.9573166 13.7518374,14.537862 Z"/> + <path class="animated spin-cw infinite" fill="#E1DBF1" d="M17.9996486,7.25963195 L18.0000013,5.49772675 C18.0034459,2.46713881 20.4561478,0.00952173148 23.493,0 L26.507,0 C29.542757,0 32,2.46161709 32,5.5 L32,7.25850184 C34.0799663,8.03664754 36.0149544,9.15559094 37.7260175,10.5694605 L39.2547869,9.68691874 C41.8812087,8.17416302 45.2363972,9.06948854 46.7630175,11.6949424 L48.270687,14.3061027 C48.9989901,15.569417 49.1952874,17.0704122 48.816349,18.4785295 C48.4374106,19.8866468 47.5143145,21.0864021 46.2530682,21.8120114 L44.7278655,22.6926677 C44.9091017,23.7802451 45,24.8850821 45,26 C45,27.1144218 44.9091826,28.218078 44.7278653,29.3073326 L46.2547984,30.1889888 C48.8778516,31.7070439 49.7801588,35.0599752 48.2700175,37.6950576 L46.7625317,40.3058986 C46.0327098,41.5684739 44.8309328,42.4891542 43.4219037,42.8651509 C42.0128746,43.2411475 40.512172,43.0416186 39.2533538,42.312255 L37.7244858,41.4299789 C36.013753,42.8435912 34.0794396,43.9622923 32.0003514,44.7403681 L31.9999987,46.5022733 C31.9965541,49.5328612 29.5438522,51.9904783 26.507,52 L23.493,52 C20.457243,52 18,49.5383829 18,46.5 L18,44.7414988 C15.9200337,43.9633525 13.9850456,42.8444091 12.2739825,41.4305395 L10.7452131,42.3130813 C8.11879127,43.825837 4.76360277,42.9305115 3.23698247,40.3050576 L1.72931303,37.6938973 C1.0010099,36.430583 0.804712603,34.9295878 1.18365098,33.5214705 C1.56258936,32.1133532 2.48568546,30.9135979 3.74693178,30.1879886 L5.27213454,29.3073323 C5.09089825,28.2197549 5,27.114918 5.00000019,26.0008761 C4.99951488,24.8930059 5.0904571,23.7869854 5.27213502,22.6926675 L3.74520157,21.8110112 C1.12214836,20.2929561 0.219841192,16.9400248 1.72998247,14.3049424 L3.23746831,11.6941014 C3.96729024,10.4315261 5.16906725,9.51084579 6.5780963,9.13484913 C7.98712536,8.75885247 9.48782803,8.95838137 10.7466462,9.687745 L12.2748018,10.56961 C14.0209791,9.13635584 15.9392199,8.03072455 17.9996486,7.25963195 Z M13.7518374,14.537862 C13.108069,15.069723 12.2016163,15.1456339 11.4783538,14.728255 L8.74433999,13.1505123 C8.40103903,12.9516035 7.99274958,12.8973186 7.60940137,12.9996143 C7.22605315,13.10191 6.89909107,13.3523954 6.70101753,13.6950576 L5.19724591,16.2994454 C4.78547321,17.0179634 5.03203388,17.9341714 5.74706822,18.3479886 L8.47306822,19.9219886 C9.19530115,20.3390079 9.58295216,21.1604138 9.44574883,21.983032 L9.21798321,23.3486236 C9.07251948,24.2246212 8.99961081,25.111131 9,26 C9,26.8953847 9.0728258,27.7804297 9.21774883,28.649968 L9.44574883,30.016968 C9.58295216,30.8395862 9.19530115,31.6609921 8.47306822,32.0780114 L5.74435077,33.6535776 C5.40046982,33.851417 5.14932721,34.1778291 5.04623114,34.5609292 C4.94313508,34.9440294 4.9965408,35.3523984 5.19401753,35.6949424 L6.69795587,38.2996585 C7.11427713,39.0156351 8.03110189,39.260288 8.7470791,38.8479035 L11.4770791,37.2719035 C12.200376,36.8543519 13.1069795,36.9302031 13.7508374,37.462138 L14.8210499,38.3463136 C16.1898549,39.4774943 17.737648,40.3725891 19.3990866,40.9941596 L20.6990866,41.4791596 C21.4813437,41.7710017 22,42.5180761 22,43.353 L22,46.5 C22,47.3308348 22.6679761,48 23.493,48 L26.5007228,48.0000099 C27.328845,47.9974107 27.99906,47.3258525 28,46.5 L28,43.353 C28,42.5185702 28.5180515,41.771829 29.2996486,41.4796319 L30.599003,40.9938734 C32.261836,40.3715765 33.8093225,39.4764853 35.1790197,38.3444304 L36.2490197,37.4614304 C36.8927697,36.9301861 37.798736,36.8545694 38.5216462,37.271745 L41.25566,38.8494877 C41.598961,39.0483965 42.0072504,39.1026814 42.3905986,39.0003857 C42.7739468,38.89809 43.1009089,38.6476046 43.2989825,38.3049424 L44.8027541,35.7005546 C45.2145268,34.9820366 44.9679661,34.0658286 44.2529318,33.6520114 L41.5269318,32.0780114 C40.8046988,31.6609921 40.4170478,30.8395862 40.5542512,30.016968 L40.7821577,28.6505288 C40.9272286,27.7792134 41,26.8950523 41,26 C41,25.1046153 40.9271742,24.2195703 40.7822512,23.350032 L40.5542512,21.983032 C40.4170478,21.1604138 40.8046988,20.3390079 41.5269318,19.9219886 L44.2556492,18.3464224 C44.5995302,18.148583 44.8506728,17.8221709 44.9537689,17.4390708 C45.0568649,17.0559706 45.0034592,16.6476016 44.8059825,16.3050576 L43.3020441,13.7003415 C42.8857229,12.9843649 41.9688981,12.739712 41.2529209,13.1520965 L38.5229209,14.7280965 C37.799624,15.1456481 36.8930205,15.0697969 36.2491626,14.537862 L35.1789501,13.6536864 C33.8101451,12.5225057 32.262352,11.6274109 30.6021792,11.0063122 L29.3021792,10.5223122 C28.5192618,10.230826 28,9.48341836 28,8.648 L28,5.5 C28,4.66916515 27.3320239,4 26.507,4 L23.4992772,3.99999015 C22.671155,4.00258933 22.00094,4.67414748 22,5.5 L22,8.647 C22,9.48142977 21.4819485,10.228171 20.7003514,10.5203681 L19.400997,11.0061266 C17.738164,11.6284235 16.1906775,12.5235147 14.822142,13.6546103 C14.8121128,13.6628994 14.4553446,13.9573166 13.7518374,14.537862 Z"/> <g transform="rotate(15 -59.137 82.348)"> <circle cx="8" cy="8" r="8" fill="#FFFFFF" transform="translate(.035 6.008)"/> <path fill="#6B4FBB" d="M7.40192379,14.7679492 C2.98364579,14.7679492 -0.598076211,11.1862272 -0.598076211,6.76794919 C-0.598076211,2.34967119 2.98364579,-1.23205081 7.40192379,-1.23205081 C11.8202018,-1.23205081 15.4019238,2.34967119 15.4019238,6.76794919 C15.4019238,11.1862272 11.8202018,14.7679492 7.40192379,14.7679492 Z M7.40192379,10.7679492 C9.61106279,10.7679492 11.4019238,8.97708819 11.4019238,6.76794919 C11.4019238,4.55881019 9.61106279,2.76794919 7.40192379,2.76794919 C5.19278479,2.76794919 3.40192379,4.55881019 3.40192379,6.76794919 C3.40192379,8.97708819 5.19278479,10.7679492 7.40192379,10.7679492 Z"/> @@ -37,7 +37,7 @@ </g> <g fill-rule="nonzero" transform="rotate(15 -402.968 460.884)"> <path fill="#FFFFFF" d="M9.82,8.53730769 C11.1889728,7.39547918 12.7368384,6.49195101 14.4,5.86384615 L14.4,4.44230769 C14.4,1.98934615 16.376,0 18.7944,0 L21.2056,0 C22.3719078,0.00042809204 23.4902431,0.468773604 24.314422,1.30193769 C25.1386009,2.13510179 25.6010613,3.26478579 25.6,4.44230769 L25.6,5.86384615 C27.2784,6.49626923 28.824,7.40653846 30.18,8.53730769 L31.4024,7.82492308 C33.5052912,6.60101478 36.192603,7.32608729 37.4104,9.44596154 L38.616,11.5540385 C39.1987431,12.5740464 39.3561196,13.7860498 39.0534853,14.9232439 C38.750851,16.060438 38.0130146,17.0296006 37.0024,17.6173846 L35.7824,18.3289615 C35.9256,19.1980385 36,20.0897308 36,21 C36,21.9102692 35.9256,22.8019615 35.7824,23.6710385 L37.0024,24.3818077 C39.1040303,25.6085057 39.8258195,28.3210992 38.616,30.4459615 L37.4104,32.5540385 C36.8268675,33.573657 35.8659065,34.317347 34.739079,34.6213801 C33.6122515,34.9254132 32.4119396,34.7648634 31.4024,34.1750769 L30.18,33.4626923 C28.8110272,34.6045208 27.2631616,35.508049 25.6,36.1361538 L25.6,37.5576923 C25.6,40.0106538 23.624,42 21.2056,42 L18.7944,42 C17.6280922,41.9995719 16.5097569,41.5312264 15.685578,40.6980623 C14.8613991,39.8648982 14.3989387,38.7352142 14.4,37.5576923 L14.4,36.1361538 C12.7368384,35.508049 11.1889728,34.6045208 9.82,33.4626923 L8.5976,34.1750769 C6.49470875,35.3989852 3.80739703,34.6739127 2.5896,32.5540385 L1.384,30.4459615 C0.8012569,29.4259536 0.643880418,28.2139502 0.946514692,27.0767561 C1.24914897,25.939562 1.98698538,24.9703994 2.9976,24.3826154 L4.2176,23.6710385 C4.07240963,22.7882521 3.99962928,21.8948738 4,21 C4,20.0897308 4.0744,19.1980385 4.2176,18.3289615 L2.9976,17.6181923 C0.895969702,16.3914943 0.174180473,13.6789008 1.384,11.5540385 L2.5896,9.44596154 C3.17313247,8.42634297 4.13409345,7.682653 5.260921,7.37861991 C6.38774855,7.07458682 7.58806043,7.23513658 8.5976,7.82492308 L9.82,8.53730769 Z"/> - <path class="animated spin infinite" fill="#FEE1D3" d="M14.0000007,5.6038043 L14.0000013,4.44005609 C14.0029906,1.78475013 16.1390906,-0.376211234 18.7944,-0.384615385 L21.2056,-0.384615385 C23.8595941,-0.384615385 26,1.78021801 26,4.44230769 L26,5.60295806 C27.5208716,6.20655954 28.9434678,7.03621848 30.2204219,8.06411282 L31.1970056,7.49492104 C33.4941909,6.15907529 36.4301298,6.95005805 37.7609369,9.26076474 L38.9671983,11.3699991 C39.5988409,12.4761812 39.768854,13.7886936 39.4405746,15.0202941 C39.1116282,16.2543969 38.308799,17.3078735 37.2096539,17.946304 L36.2175721,18.5246428 C36.3390841,19.3401617 36.4,20.1667594 36.4,21 C36.4,21.8329668 36.339124,22.6588262 36.2175401,23.4753391 L37.2113882,24.0547082 C39.4944154,25.3886826 40.276605,28.3232105 38.9665369,30.6311583 L37.7604568,32.7400742 C37.1252608,33.8495148 36.0768547,34.6604208 34.8452776,34.9922248 C33.6111681,35.324711 32.2964469,35.1482289 31.195569,34.5042428 L30.2192355,33.9354047 C28.9426535,34.9630196 27.5206806,35.7924453 25.9999993,36.3961957 L25.9999987,37.5599439 C25.9970094,40.2152499 23.8609094,42.3762112 21.2056,42.3846154 L18.7944,42.3846154 C16.1404059,42.3846154 14,40.219782 14,37.5576923 L14,36.3970419 C12.4791284,35.7934405 11.0565322,34.9637815 9.77957815,33.9358872 L8.80299442,34.505079 C6.50580915,35.8409247 3.56987021,35.049942 2.23906313,32.7392353 L1.03280169,30.6300009 C0.401159146,29.5238188 0.231145999,28.2113064 0.559425405,26.9797059 C0.888371786,25.7456031 1.69120101,24.6921265 2.79034606,24.053696 L3.78242779,23.4753573 C3.66091587,22.6598457 3.60000002,21.8333228 3.60000019,21.0008678 C3.59964068,20.1722851 3.66061719,19.3449468 3.78254167,18.5247085 L2.78861183,17.9452918 C0.505584602,16.6113174 -0.276605002,13.6767895 1.03346313,11.3688417 L2.23954317,9.25992583 C2.87473915,8.15048519 3.92314533,7.33957919 5.15472238,7.00777521 C6.38883187,6.67528896 7.70355311,6.85177112 8.80443097,7.49575721 L9.78076186,8.06459377 C11.0573465,7.03698045 12.4793194,6.20755475 14.0000007,5.6038043 Z M11.2634746,12.0326234 C10.617233,12.5716613 9.7026973,12.6485026 8.97556903,12.2248582 L6.78774825,10.9501716 C6.60754053,10.8447551 6.39506809,10.8162338 6.19527576,10.8700606 C5.99295099,10.9245697 5.8183659,11.0596053 5.71133687,11.246543 L4.50892658,13.3490215 C4.28085652,13.7508163 4.41776119,14.2644394 4.80485394,14.4906191 L6.98565394,15.7619268 C7.70254629,16.1798426 8.08690703,16.9970357 7.95165511,17.8157512 L7.76948523,18.9184706 C7.65638664,19.6061109 7.59969735,20.3020342 7.6,21 C7.6,21.7031066 7.65662064,22.3978283 7.76925511,23.0801334 L7.95165511,24.1842488 C8.08690703,25.0029643 7.70254629,25.8201574 6.98565394,26.2380732 L4.80213007,27.5109659 C4.61772321,27.6180778 4.48116147,27.7972748 4.42448029,28.0099246 C4.36713215,28.2250767 4.39688141,28.454743 4.50573687,28.6453801 L5.70831165,30.7481858 C5.93243371,31.1373303 6.41410538,31.2670993 6.79049373,31.0482253 L8.97449373,29.7753023 C9.7016554,29.3514832 10.6163433,29.4282639 11.2626746,29.9673766 L12.1188867,30.6815536 C13.1796505,31.566598 14.3786867,32.2666727 15.6649769,32.7525215 L16.7049769,33.1442523 C17.4841581,33.4377419 18,34.1832625 18,35.0158846 L18,37.5576923 C18,38.02074 18.3597694,38.3846154 18.7944,38.3846154 L21.1992624,38.3846254 C21.6372484,38.3832375 21.9994819,38.0167881 22,37.5576923 L22,35.0158846 C22,34.18376 22.5152346,33.4385758 23.2937506,33.1447321 L24.3331012,32.7524389 C25.620867,32.2658727 26.8196661,31.5658006 27.8813806,30.679856 L28.7373806,29.9666637 C29.3836087,29.4282468 30.2976553,29.3517028 31.024431,29.7751418 L33.2122517,31.0498284 C33.3924595,31.1552449 33.6049319,31.1837662 33.8047242,31.1299394 C34.007049,31.0754303 34.1816341,30.9403947 34.2886631,30.753457 L35.4910734,28.6509785 C35.7191435,28.2491837 35.5822388,27.7355606 35.1951461,27.5093809 L33.0143461,26.2380732 C32.2974537,25.8201574 31.913093,25.0029643 32.0483449,24.1842488 L32.2306531,23.0806893 C32.3434217,22.3968737 32.4,21.7028459 32.4,21 C32.4,20.2968934 32.3433794,19.6021717 32.2307449,18.9198666 L32.0483449,17.8157512 C31.913093,16.9970357 32.2974537,16.1798426 33.0143461,15.7619268 L35.1978699,14.4890341 C35.3822768,14.3819222 35.5188385,14.2027252 35.5755197,13.9900754 C35.6328679,13.7749233 35.6031186,13.545257 35.4942631,13.3546199 L34.2916883,11.2518142 C34.0675663,10.8626697 33.5858946,10.7329007 33.2095063,10.9517747 L31.0255063,12.2246977 C30.2983446,12.6485168 29.3836567,12.5717361 28.7373254,12.0326234 L27.8811133,11.3184464 C26.8203495,10.433402 25.6213133,9.73332732 24.3362966,9.24795765 L23.2962966,8.85703457 C22.5164499,8.56389992 22,7.81804293 22,6.98492308 L22,4.44230769 C22,3.97925995 21.6402306,3.61538462 21.2056,3.61538462 L18.8007376,3.61537457 C18.3627516,3.61676247 18.0005181,3.98321188 18,4.44230769 L18,6.98411538 C18,7.81623999 17.4847654,8.56142419 16.7062494,8.85526793 L15.6668988,9.24756113 C14.379133,9.73412728 13.1803339,10.4341994 12.1197785,11.3191775 C12.1108094,11.3266617 11.8253748,11.564477 11.2634746,12.0326234 Z"/> + <path class="animated spin-ccw infinite" fill="#FEE1D3" d="M14.0000007,5.6038043 L14.0000013,4.44005609 C14.0029906,1.78475013 16.1390906,-0.376211234 18.7944,-0.384615385 L21.2056,-0.384615385 C23.8595941,-0.384615385 26,1.78021801 26,4.44230769 L26,5.60295806 C27.5208716,6.20655954 28.9434678,7.03621848 30.2204219,8.06411282 L31.1970056,7.49492104 C33.4941909,6.15907529 36.4301298,6.95005805 37.7609369,9.26076474 L38.9671983,11.3699991 C39.5988409,12.4761812 39.768854,13.7886936 39.4405746,15.0202941 C39.1116282,16.2543969 38.308799,17.3078735 37.2096539,17.946304 L36.2175721,18.5246428 C36.3390841,19.3401617 36.4,20.1667594 36.4,21 C36.4,21.8329668 36.339124,22.6588262 36.2175401,23.4753391 L37.2113882,24.0547082 C39.4944154,25.3886826 40.276605,28.3232105 38.9665369,30.6311583 L37.7604568,32.7400742 C37.1252608,33.8495148 36.0768547,34.6604208 34.8452776,34.9922248 C33.6111681,35.324711 32.2964469,35.1482289 31.195569,34.5042428 L30.2192355,33.9354047 C28.9426535,34.9630196 27.5206806,35.7924453 25.9999993,36.3961957 L25.9999987,37.5599439 C25.9970094,40.2152499 23.8609094,42.3762112 21.2056,42.3846154 L18.7944,42.3846154 C16.1404059,42.3846154 14,40.219782 14,37.5576923 L14,36.3970419 C12.4791284,35.7934405 11.0565322,34.9637815 9.77957815,33.9358872 L8.80299442,34.505079 C6.50580915,35.8409247 3.56987021,35.049942 2.23906313,32.7392353 L1.03280169,30.6300009 C0.401159146,29.5238188 0.231145999,28.2113064 0.559425405,26.9797059 C0.888371786,25.7456031 1.69120101,24.6921265 2.79034606,24.053696 L3.78242779,23.4753573 C3.66091587,22.6598457 3.60000002,21.8333228 3.60000019,21.0008678 C3.59964068,20.1722851 3.66061719,19.3449468 3.78254167,18.5247085 L2.78861183,17.9452918 C0.505584602,16.6113174 -0.276605002,13.6767895 1.03346313,11.3688417 L2.23954317,9.25992583 C2.87473915,8.15048519 3.92314533,7.33957919 5.15472238,7.00777521 C6.38883187,6.67528896 7.70355311,6.85177112 8.80443097,7.49575721 L9.78076186,8.06459377 C11.0573465,7.03698045 12.4793194,6.20755475 14.0000007,5.6038043 Z M11.2634746,12.0326234 C10.617233,12.5716613 9.7026973,12.6485026 8.97556903,12.2248582 L6.78774825,10.9501716 C6.60754053,10.8447551 6.39506809,10.8162338 6.19527576,10.8700606 C5.99295099,10.9245697 5.8183659,11.0596053 5.71133687,11.246543 L4.50892658,13.3490215 C4.28085652,13.7508163 4.41776119,14.2644394 4.80485394,14.4906191 L6.98565394,15.7619268 C7.70254629,16.1798426 8.08690703,16.9970357 7.95165511,17.8157512 L7.76948523,18.9184706 C7.65638664,19.6061109 7.59969735,20.3020342 7.6,21 C7.6,21.7031066 7.65662064,22.3978283 7.76925511,23.0801334 L7.95165511,24.1842488 C8.08690703,25.0029643 7.70254629,25.8201574 6.98565394,26.2380732 L4.80213007,27.5109659 C4.61772321,27.6180778 4.48116147,27.7972748 4.42448029,28.0099246 C4.36713215,28.2250767 4.39688141,28.454743 4.50573687,28.6453801 L5.70831165,30.7481858 C5.93243371,31.1373303 6.41410538,31.2670993 6.79049373,31.0482253 L8.97449373,29.7753023 C9.7016554,29.3514832 10.6163433,29.4282639 11.2626746,29.9673766 L12.1188867,30.6815536 C13.1796505,31.566598 14.3786867,32.2666727 15.6649769,32.7525215 L16.7049769,33.1442523 C17.4841581,33.4377419 18,34.1832625 18,35.0158846 L18,37.5576923 C18,38.02074 18.3597694,38.3846154 18.7944,38.3846154 L21.1992624,38.3846254 C21.6372484,38.3832375 21.9994819,38.0167881 22,37.5576923 L22,35.0158846 C22,34.18376 22.5152346,33.4385758 23.2937506,33.1447321 L24.3331012,32.7524389 C25.620867,32.2658727 26.8196661,31.5658006 27.8813806,30.679856 L28.7373806,29.9666637 C29.3836087,29.4282468 30.2976553,29.3517028 31.024431,29.7751418 L33.2122517,31.0498284 C33.3924595,31.1552449 33.6049319,31.1837662 33.8047242,31.1299394 C34.007049,31.0754303 34.1816341,30.9403947 34.2886631,30.753457 L35.4910734,28.6509785 C35.7191435,28.2491837 35.5822388,27.7355606 35.1951461,27.5093809 L33.0143461,26.2380732 C32.2974537,25.8201574 31.913093,25.0029643 32.0483449,24.1842488 L32.2306531,23.0806893 C32.3434217,22.3968737 32.4,21.7028459 32.4,21 C32.4,20.2968934 32.3433794,19.6021717 32.2307449,18.9198666 L32.0483449,17.8157512 C31.913093,16.9970357 32.2974537,16.1798426 33.0143461,15.7619268 L35.1978699,14.4890341 C35.3822768,14.3819222 35.5188385,14.2027252 35.5755197,13.9900754 C35.6328679,13.7749233 35.6031186,13.545257 35.4942631,13.3546199 L34.2916883,11.2518142 C34.0675663,10.8626697 33.5858946,10.7329007 33.2095063,10.9517747 L31.0255063,12.2246977 C30.2983446,12.6485168 29.3836567,12.5717361 28.7373254,12.0326234 L27.8811133,11.3184464 C26.8203495,10.433402 25.6213133,9.73332732 24.3362966,9.24795765 L23.2962966,8.85703457 C22.5164499,8.56389992 22,7.81804293 22,6.98492308 L22,4.44230769 C22,3.97925995 21.6402306,3.61538462 21.2056,3.61538462 L18.8007376,3.61537457 C18.3627516,3.61676247 18.0005181,3.98321188 18,4.44230769 L18,6.98411538 C18,7.81623999 17.4847654,8.56142419 16.7062494,8.85526793 L15.6668988,9.24756113 C14.379133,9.73412728 13.1803339,10.4341994 12.1197785,11.3191775 C12.1108094,11.3266617 11.8253748,11.564477 11.2634746,12.0326234 Z"/> <g transform="rotate(15 -47.892 66.043)"> <ellipse cx="6.4" cy="6.462" fill="#FFFFFF" rx="6.4" ry="6.462" transform="translate(.028 4.853)"/> <path fill="#FC6D26" d="M5.92153903,11.9125743 C2.3834711,11.9125743 -0.478460969,9.0231237 -0.478460969,5.4664205 C-0.478460969,1.9097173 2.3834711,-0.979733345 5.92153903,-0.979733345 C9.45960696,-0.979733345 12.321539,1.9097173 12.321539,5.4664205 C12.321539,9.0231237 9.45960696,11.9125743 5.92153903,11.9125743 Z M5.92153903,8.71257435 C7.6854047,8.71257435 9.12153903,7.26263103 9.12153903,5.4664205 C9.12153903,3.67020997 7.6854047,2.22026666 5.92153903,2.22026666 C4.15767337,2.22026666 2.72153903,3.67020997 2.72153903,5.4664205 C2.72153903,7.26263103 4.15767337,8.71257435 5.92153903,8.71257435 Z"/> 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/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/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/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/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-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/feature-plantuml-restructured-text-captions.yml b/changelogs/unreleased/feature-plantuml-restructured-text-captions.yml new file mode 100644 index 00000000000..3d8d0f4fcd1 --- /dev/null +++ b/changelogs/unreleased/feature-plantuml-restructured-text-captions.yml @@ -0,0 +1,5 @@ +--- +title: 'Support uml:: and captions in reStructuredText' +merge_request: 15120 +author: Markus Koller +type: changed diff --git a/changelogs/unreleased/fix-user-tab-activity-mobile.yml b/changelogs/unreleased/fix-user-tab-activity-mobile.yml new file mode 100644 index 00000000000..a7e4fcb4355 --- /dev/null +++ b/changelogs/unreleased/fix-user-tab-activity-mobile.yml @@ -0,0 +1,5 @@ +--- +title: Fixed user profile activity tab being off-screen on mobile +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/jivl-mobile-friendly-table-runners.yml b/changelogs/unreleased/jivl-mobile-friendly-table-runners.yml new file mode 100644 index 00000000000..3448b003ee0 --- /dev/null +++ b/changelogs/unreleased/jivl-mobile-friendly-table-runners.yml @@ -0,0 +1,5 @@ +--- +title: Mobile-friendly table on Admin Runners +merge_request: +author: Takuya Noguchi +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-disable-unicorn-sampling-sidekiq.yml b/changelogs/unreleased/sh-disable-unicorn-sampling-sidekiq.yml new file mode 100644 index 00000000000..c4ed017dacd --- /dev/null +++ b/changelogs/unreleased/sh-disable-unicorn-sampling-sidekiq.yml @@ -0,0 +1,5 @@ +--- +title: Disable Unicorn sampling in Sidekiq since there are no Unicorn sockets to monitor +merge_request: +author: +type: performance 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/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-i18n-contributors-page.yml b/changelogs/unreleased/winh-i18n-contributors-page.yml new file mode 100644 index 00000000000..9b2611fc4fa --- /dev/null +++ b/changelogs/unreleased/winh-i18n-contributors-page.yml @@ -0,0 +1,5 @@ +--- +title: Make contributors page translatable +merge_request: 14915 +author: +type: other diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb index e1a59d8c152..2d8704622b6 100644 --- a/config/initializers/8_metrics.rb +++ b/config/initializers/8_metrics.rb @@ -123,7 +123,9 @@ def instrument_classes(instrumentation) end # rubocop:enable Metrics/AbcSize -Gitlab::Metrics::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start +unless Sidekiq.server? + Gitlab::Metrics::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start +end Gitlab::Application.configure do |config| # 0 should be Sentry to catch errors in this middleware 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/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/integration/plantuml.md b/doc/administration/integration/plantuml.md index 652ca9cf454..93c3642a1f1 100644 --- a/doc/administration/integration/plantuml.md +++ b/doc/administration/integration/plantuml.md @@ -56,29 +56,34 @@ that, login with an Admin account and do following: With PlantUML integration enabled and configured, we can start adding diagrams to our AsciiDoc snippets, wikis and repos using delimited blocks: -``` -[plantuml, format="png", id="myDiagram", width="200px"] --- -Bob->Alice : hello -Alice -> Bob : Go Away --- -``` +- **Markdown** + + ```plantuml + Bob -> Alice : hello + Alice -> Bob : Go Away + ``` -And in Markdown using fenced code blocks: +- **AsciiDoc** - ```plantuml - Bob -> Alice : hello + ``` + [plantuml, format="png", id="myDiagram", width="200px"] + -- + Bob->Alice : hello Alice -> Bob : Go Away + -- ``` -And in reStructuredText using a directive: +- **reStructuredText** -``` -.. plantuml:: + ``` + .. plantuml:: + :caption: Caption with **bold** and *italic* - Bob -> Alice: hello - Alice -> Bob: Go Away -``` + Bob -> Alice: hello + Alice -> Bob: Go Away + ``` + + You can also use the `uml::` directive for compatibility with [sphinxcontrib-plantuml](https://pypi.python.org/pypi/sphinxcontrib-plantuml), but please note that we currently only support the `caption` option. The above blocks will be converted to an HTML img tag with source pointing to the PlantUML instance. If the PlantUML server is correctly configured, this should 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/repository_storage_types.md b/doc/administration/repository_storage_types.md index 0cb2648cc1e..bc9b6253f1a 100644 --- a/doc/administration/repository_storage_types.md +++ b/doc/administration/repository_storage_types.md @@ -76,7 +76,7 @@ To migrate your existing projects to the new storage type, check the specific [r We are incrementally moving every storable object in GitLab to the Hashed Storage pattern. You can check the current coverage status below. -Not that things stored in S3 compatible endpoint, will not have the downsides mentioned earlier, if they are not +Note that things stored in an S3 compatible endpoint will not have the downsides mentioned earlier, if they are not prefixed with `#{namespace}/#{project_name}`, which is true for CI Cache and LFS Objects. | Storable Object | Legacy Storage | Hashed Storage | S3 Compatible | GitLab Version | 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 b503b7c06fd..535ed351366 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -43,7 +43,7 @@ future GitLab releases.** | **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. | | **CI_CONFIG_PATH** | 9.4 | 0.5 | The path to CI config file. Defaults to `.gitlab-ci.yml` | | **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled | -| **CI_DISPOSABLE_ENVIRONMENT** | all | 10.1 | Mark that job is executed in a disposable environment (something that is created only for this job and disposed of/destroyed after the execution - all executors except `shell` and `ssh`). If the environment is disposable, it is set to true, otherwise it is not defined at all. | +| **CI_DISPOSABLE_ENVIRONMENT** | all | 10.1 | Marks that the job is executed in a disposable environment (something that is created only for this job and disposed of/destroyed after the execution - all executors except `shell` and `ssh`). If the environment is disposable, it is set to true, otherwise it is not defined at all. | | **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job | | **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. | | **CI_ENVIRONMENT_URL** | 9.3 | all | The URL of the environment for this job | @@ -74,7 +74,7 @@ future GitLab releases.** | **CI_SERVER_NAME** | all | all | The name of CI server that is used to coordinate jobs | | **CI_SERVER_REVISION** | all | all | GitLab revision that is used to schedule jobs | | **CI_SERVER_VERSION** | all | all | GitLab version that is used to schedule jobs | -| **CI_SHARED_ENVIRONMENT** | all | 10.1 | Mark that job is executed in a shared environment (something that is persisted across CI invocations like `shell` or `ssh` executor). If the environment is shared, it is set to true, otherwise it is not defined at all. | +| **CI_SHARED_ENVIRONMENT** | all | 10.1 | Marks that the job is executed in a shared environment (something that is persisted across CI invocations like `shell` or `ssh` executor). If the environment is shared, it is set to true, otherwise it is not defined at all. | | **ARTIFACT_DOWNLOAD_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to download artifacts running a job | | **GET_SOURCES_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to fetch sources running a job | | **GITLAB_CI** | all | all | Mark that job is executed in GitLab CI environment | 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/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md index 7f08188bd65..ebb24ba0a7f 100644 --- a/doc/legal/corporate_contributor_license_agreement.md +++ b/doc/legal/corporate_contributor_license_agreement.md @@ -1,29 +1,2 @@ -# Corporate contributor license agreement - -You accept and agree to the following terms and conditions for Your present and future Contributions submitted to GitLab B.V.. Except for the license granted herein to GitLab B.V. and recipients of software distributed by GitLab B.V., You reserve all right, title, and interest in and to Your Contributions. - -1. Definitions. - - "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with GitLab B.V.. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - - "Contribution" shall mean the code, documentation or other original works of authorship, including any modifications or additions to an existing work, that is submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." - -2. Grant of Copyright License. - -Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. - -3. Grant of Patent License. - -Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. - -4. You represent that You are legally entitled to grant the above license. You represent further that each of Your employees is authorized to submit Contributions on Your behalf, but excluding employees that are designated in writing by You as "Not authorized to submit Contributions on behalf of [name of Your corporation here]." Such designations of exclusion for unauthorized employees are to be submitted via email to legal@gitlab.com. - -5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). - -6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. - -7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]". - -8. It is Your responsibility to notify GitLab.com when any change is required to the list of designated employees excluded from submitting Contributions on Your behalf per Section 4. Such notification should be sent via email to legal@gitlab.com. - -This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office. +This document has been replaced by a Developer Certificate of Origin and License, +as described in [Contributing.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md).
\ No newline at end of file diff --git a/doc/legal/individual_contributor_license_agreement.md b/doc/legal/individual_contributor_license_agreement.md index 59803aea080..ebb24ba0a7f 100644 --- a/doc/legal/individual_contributor_license_agreement.md +++ b/doc/legal/individual_contributor_license_agreement.md @@ -1,25 +1,2 @@ -# Individual contributor license agreement - -You accept and agree to the following terms and conditions for Your present and future Contributions submitted to GitLab B.V.. Except for the license granted herein to GitLab B.V. and recipients of software distributed by GitLab B.V., You reserve all right, title, and interest in and to Your Contributions. - -1. Definitions. - - "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with GitLab B.V.. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - - "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." - -2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. - -3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. - -4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to GitLab B.V., or that your employer has executed a separate Corporate CLA with GitLab B.V.. - -5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions. - -6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. - -7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [insert_name_here]". - -8. You agree to notify GitLab B.V. of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect. - -This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office. +This document has been replaced by a Developer Certificate of Origin and License, +as described in [Contributing.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md).
\ No newline at end of file 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/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 5561784ed0b..042cde3f01e 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -517,7 +517,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/README.md b/doc/university/README.md index c96b9f38890..55865ac23e8 100644 --- a/doc/university/README.md +++ b/doc/university/README.md @@ -55,10 +55,10 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project #### 1.5. Migrating from other Source Control -1. [Migrating from BitBucket/Stash](https://docs.gitlab.com/ee/workflow/importing/import_projects_from_bitbucket.html) -1. [Migrating from GitHub](https://docs.gitlab.com/ee/workflow/importing/import_projects_from_github.html) -1. [Migrating from SVN](https://docs.gitlab.com/ee/workflow/importing/migrating_from_svn.html) -1. [Migrating from Fogbugz](https://docs.gitlab.com/ee/workflow/importing/import_projects_from_fogbugz.html) +1. [Migrating from BitBucket/Stash](https://docs.gitlab.com/ee/user/project/import/bitbucket.html) +1. [Migrating from GitHub](https://docs.gitlab.com/ee/user/project/import/github.html) +1. [Migrating from SVN](https://docs.gitlab.com/ee/user/project/import/svn.html) +1. [Migrating from Fogbugz](https://docs.gitlab.com/ee/user/project/import/fogbugz.html) #### 1.6. GitLab Inc. @@ -80,13 +80,13 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project - Being part of our Great Community and Contributing to GitLab 1. [Getting Started with the GitLab Development Kit (GDK)](https://about.gitlab.com/2016/06/08/getting-started-with-gitlab-development-kit/) 1. [Contributing Technical Articles to the GitLab Blog](https://about.gitlab.com/2016/01/26/call-for-writers/) -1. [GitLab Training Workshops](https://about.gitlab.com/training) +1. [GitLab Training Workshops](https://docs.gitlab.com/ce/university/training/end-user/) +1. [GitLab Professional Services](https://about.gitlab.com/services/) #### 1.8 GitLab Training Material 1. [Git and GitLab Terminology](glossary/README.md) 1. [Git and GitLab Workshop - Slides](https://docs.google.com/presentation/d/1JzTYD8ij9slejV2-TO-NzjCvlvj6mVn9BORePXNJoMI/edit?usp=drive_web) -1. [Git and GitLab Revision](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/university/training/end-user) --- 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/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index df75e25e12b..5896f8f72a0 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -1140,6 +1140,18 @@ From this page, you can repeat delivery with the same data by clicking `Resend R >**Note:** If URL or secret token of the webhook were updated, data will be delivered to the new address. +### Receiving duplicate or multiple web hook requests triggered by one event + +When GitLab sends a webhook it expects a response in 10 seconds (set default value). If it does not receive one, it'll retry the webhook. +If the endpoint doesn't send its HTTP response within those 10 seconds, GitLab may decide the hook failed and retry it. + +If you are receiving multiple requests, you can try increasing the default value to wait for the HTTP response after sending the webhook +by uncommenting or adding the following setting to your `/etc/gitlab/gitlab.rb`: + +``` +gitlab_rails['webhook_timeout'] = 10 +``` + ## Example webhook receiver If you want to see GitLab's webhooks in action for testing purposes you can use 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/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/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/blob.rb b/lib/gitlab/git/blob.rb index a4336facee5..cc6c7609ec7 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -12,6 +12,12 @@ module Gitlab # blob data should use load_all_data!. MAX_DATA_DISPLAY_SIZE = 10.megabytes + # These limits are used as a heuristic to ignore files which can't be LFS + # pointers. The format of these is described in + # https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md#the-pointer + LFS_POINTER_MIN_SIZE = 120.bytes + LFS_POINTER_MAX_SIZE = 200.bytes + attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary class << self @@ -30,16 +36,7 @@ module Gitlab if is_enabled Gitlab::GitalyClient::BlobService.new(repository).get_blob(oid: sha, limit: MAX_DATA_DISPLAY_SIZE) else - blob = repository.lookup(sha) - - next unless blob.is_a?(Rugged::Blob) - - new( - id: blob.oid, - size: blob.size, - data: blob.content(MAX_DATA_DISPLAY_SIZE), - binary: blob.binary? - ) + rugged_raw(repository, sha, limit: MAX_DATA_DISPLAY_SIZE) end end end @@ -59,10 +56,25 @@ module Gitlab end end + # Find LFS blobs given an array of sha ids + # Returns array of Gitlab::Git::Blob + # Does not guarantee blob data will be set + def batch_lfs_pointers(repository, blob_ids) + blob_ids.lazy + .select { |sha| possible_lfs_blob?(repository, sha) } + .map { |sha| rugged_raw(repository, sha, limit: LFS_POINTER_MAX_SIZE) } + .select(&:lfs_pointer?) + .force + end + def binary?(data) EncodingHelper.detect_libgit2_binary?(data) end + def size_could_be_lfs?(size) + size.between?(LFS_POINTER_MIN_SIZE, LFS_POINTER_MAX_SIZE) + end + private # Recursive search of blob id by path @@ -167,6 +179,29 @@ module Gitlab end end end + + def rugged_raw(repository, sha, limit:) + blob = repository.lookup(sha) + + return unless blob.is_a?(Rugged::Blob) + + new( + id: blob.oid, + size: blob.size, + data: blob.content(limit), + binary: blob.binary? + ) + end + + # Efficient lookup to determine if object size + # and type make it a possible LFS blob without loading + # blob content into memory with repository.lookup(sha) + def possible_lfs_blob?(repository, sha) + object_header = repository.rugged.read_header(sha) + + object_header[:type] == :blob && + size_could_be_lfs?(object_header[:len]) + end end def initialize(options) @@ -226,7 +261,7 @@ module Gitlab # size # see https://github.com/github/git-lfs/blob/v1.1.0/docs/spec.md#the-pointer def lfs_pointer? - has_lfs_version_key? && lfs_oid.present? && lfs_size.present? + self.class.size_could_be_lfs?(size) && has_lfs_version_key? && lfs_oid.present? && lfs_size.present? end def lfs_oid 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/lfs_changes.rb b/lib/gitlab/git/lfs_changes.rb new file mode 100644 index 00000000000..2749e2e69e2 --- /dev/null +++ b/lib/gitlab/git/lfs_changes.rb @@ -0,0 +1,36 @@ +module Gitlab + module Git + class LfsChanges + def initialize(repository, newrev) + @repository = repository + @newrev = newrev + end + + def new_pointers(object_limit: nil, not_in: nil) + @new_pointers ||= begin + object_ids = new_objects(not_in: not_in) + object_ids = object_ids.take(object_limit) if object_limit + + Gitlab::Git::Blob.batch_lfs_pointers(@repository, object_ids) + end + end + + def all_pointers + object_ids = rev_list.all_objects(require_path: true) + + Gitlab::Git::Blob.batch_lfs_pointers(@repository, object_ids) + end + + private + + def new_objects(not_in:) + rev_list.new_objects(require_path: true, lazy: true, not_in: not_in) + end + + def rev_list + ::Gitlab::Git::RevList.new(path_to_repo: @repository.path_to_repo, + newrev: @newrev) + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index fc8af38d4d9..4f9eac92d9a 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -290,6 +290,14 @@ module Gitlab end end + def batch_existence(object_ids, existing: true) + filter_method = existing ? :select : :reject + + object_ids.public_send(filter_method) do |oid| # rubocop:disable GitlabSecurity/PublicSend + rugged.exists?(oid) + end + end + # Returns an Array of branch and tag names def ref_names branch_names + tag_names @@ -750,13 +758,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:) @@ -1169,10 +1177,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 @@ -1614,6 +1622,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/rev_list.rb b/lib/gitlab/git/rev_list.rb index 60b2a4ec411..e0c884aceaa 100644 --- a/lib/gitlab/git/rev_list.rb +++ b/lib/gitlab/git/rev_list.rb @@ -13,11 +13,31 @@ module Gitlab @path_to_repo = path_to_repo end - # This method returns an array of new references + # This method returns an array of new commit references def new_refs execute([*base_args, newrev, '--not', '--all']) end + # Finds newly added objects + # Returns an array of shas + # + # Can skip objects which do not have a path using required_path: true + # This skips commit objects and root trees, which might not be needed when + # looking for blobs + # + # Can return a lazy enumerator to limit work done on megabytes of data + def new_objects(require_path: nil, lazy: false, not_in: nil) + object_output = execute([*base_args, newrev, *not_in_refs(not_in), '--objects']) + + objects_from_output(object_output, require_path: require_path, lazy: lazy) + end + + def all_objects(require_path: nil) + object_output = execute([*base_args, '--all', '--objects']) + + objects_from_output(object_output, require_path: require_path, lazy: true) + end + # This methods returns an array of missed references # # Should become obsolete after https://gitlab.com/gitlab-org/gitaly/issues/348. @@ -27,6 +47,13 @@ module Gitlab private + def not_in_refs(references) + return ['--not', '--all'] unless references + return [] if references.empty? + + references.prepend('--not') + end + def execute(args) output, status = popen(args, nil, Gitlab::Git::Env.to_env_hash) @@ -44,6 +71,22 @@ module Gitlab 'rev-list' ] end + + def objects_from_output(object_output, require_path: nil, lazy: nil) + objects = object_output.lazy.map do |output_line| + sha, path = output_line.split(' ', 2) + + next if require_path && path.blank? + + sha + end.reject(&:nil?) + + if lazy + objects + else + objects.force + end + end end end 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/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index a2b50f2507e..da5505cb2fe 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -18,7 +18,7 @@ module Gitlab response = GitalyClient.call(@repository.storage, :commit_service, :list_files, request) response.flat_map do |msg| - msg.paths.map { |d| d.dup.force_encoding(Encoding::UTF_8) } + msg.paths.map { |d| EncodingHelper.encode!(d.dup) } end 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/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/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/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/package.json b/package.json index 0a1f5c8d081..e607981143d 100644 --- a/package.json +++ b/package.json @@ -64,10 +64,10 @@ "underscore": "^1.8.3", "url-loader": "^0.5.8", "visibilityjs": "^1.2.4", - "vue": "^2.2.6", + "vue": "^2.5.2", "vue-loader": "^11.3.4", "vue-resource": "^1.3.4", - "vue-template-compiler": "^2.2.6", + "vue-template-compiler": "^2.5.2", "vuex": "^3.0.0", "webpack": "^3.5.5", "webpack-bundle-analyzer": "^2.8.2", 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/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/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/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb index 01aca443f4a..39ac68af493 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 @@ -256,7 +256,7 @@ feature 'Dashboard Todos' do 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 diff --git a/spec/features/issues/filtered_search/recent_searches_spec.rb b/spec/features/issues/filtered_search/recent_searches_spec.rb index eef7988e2bd..4fff056cb70 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,7 @@ 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') + all('.filtered-search-history-dropdown-item', visible: false, count: 2)[0].trigger('click') wait_for_filtered_search('foo') expect(find('.filtered-search').value.strip).to eq('foo') @@ -90,12 +86,10 @@ 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) - - expect(items_before.count).to eq(1) + all('.filtered-search-history-dropdown-item', visible: false, count: 1) find('.filtered-search-history-clear-button', visible: false).trigger('click') - items_after = all('.filtered-search-history-dropdown-item', visible: false) + items_after = all('.filtered-search-history-dropdown-item', visible: false, count: 0) expect(items_after.count).to eq(0) end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index d4fd3a50008..9b94452fb0d 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -583,6 +583,16 @@ describe 'Issues' do expect(page.find_field("issue_description").value).not_to match /\n\n$/ end + + it "cancels a file upload correctly" do + dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false) + + click_button 'Cancel' + + 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/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb index bf21a719901..0ae43e226b9 100644 --- a/spec/features/merge_requests/mini_pipeline_graph_spec.rb +++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb @@ -83,7 +83,7 @@ feature 'Mini Pipeline Graph', :js do end before do - toggle.click + toggle.trigger('click') wait_for_requests end diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index 1cddd35fd8a..0166ab8be99 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') + 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') + 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/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb index 807a2189cc4..6f6d562c7b6 100644 --- a/spec/features/projects/commit/mini_pipeline_graph_spec.rb +++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb @@ -18,7 +18,7 @@ feature 'Mini Pipeline Graph in Commit View', :js do expect(page).to have_selector('.mr-widget-pipeline-graph') - first('.mini-pipeline-graph-dropdown-toggle').click + first('.mini-pipeline-graph-dropdown-toggle').trigger('click') wait_for_requests diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index acbc5b046e6..931811e0ee6 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -67,7 +67,7 @@ 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 @@ -86,8 +86,8 @@ 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 @@ -105,8 +105,8 @@ 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 @@ -124,8 +124,8 @@ 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 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/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js index cd19a0fae1e..59d4f7c45c6 100644 --- a/spec/javascripts/groups/components/app_spec.js +++ b/spec/javascripts/groups/components/app_spec.js @@ -431,9 +431,9 @@ describe('AppComponent', () => { }); it('should render groups tree', (done) => { - vm.groups = [mockParentGroupItem]; + vm.store.state.groups = [mockParentGroupItem]; vm.isLoading = false; - vm.pageInfo = mockPageInfo; + vm.store.state.pageInfo = mockPageInfo; Vue.nextTick(() => { expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined(); done(); 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/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/repo/components/repo_editor_spec.js b/spec/javascripts/repo/components/repo_editor_spec.js index 82f914b7c9d..979d2185076 100644 --- a/spec/javascripts/repo/components/repo_editor_spec.js +++ b/spec/javascripts/repo/components/repo_editor_spec.js @@ -17,6 +17,7 @@ describe('RepoEditor', () => { f.active = true; f.tempFile = true; vm.$store.state.openFiles.push(f); + vm.$store.getters.activeFile.html = 'testing'; vm.monaco = true; vm.$mount(); @@ -31,18 +32,25 @@ describe('RepoEditor', () => { it('renders an ide container', (done) => { Vue.nextTick(() => { expect(vm.shouldHideEditor).toBeFalsy(); + expect(vm.$el.textContent.trim()).toBe(''); + done(); }); }); describe('when open file is binary and not raw', () => { - it('does not render the IDE', (done) => { + beforeEach((done) => { vm.$store.getters.activeFile.binary = true; - Vue.nextTick(() => { - expect(vm.shouldHideEditor).toBeTruthy(); - done(); - }); + Vue.nextTick(done); + }); + + it('does not render the IDE', () => { + expect(vm.shouldHideEditor).toBeTruthy(); + }); + + it('shows activeFile html', () => { + expect(vm.$el.textContent.trim()).toBe('testing'); }); }); }); 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/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js index 60a5c2ae74e..65c49b9f30b 100644 --- a/spec/javascripts/vue_shared/components/markdown/field_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js @@ -42,12 +42,14 @@ describe('Markdown field component', () => { beforeEach(() => { spyOn(Vue.http, 'post').and.callFake(() => new Promise((resolve) => { - resolve({ - json() { - return { - body: '<p>markdown preview</p>', - }; - }, + setTimeout(() => { + resolve({ + json() { + return { + body: '<p>markdown preview</p>', + }; + }, + }); }); })); 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/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/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index 412a0093d97..c04a9688503 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -143,6 +143,16 @@ describe Gitlab::Git::Blob, seed_helper: true do expect(blob.loaded_size).to eq(blob_size) end end + + context 'when sha references a tree' do + it 'returns nil' do + tree = Gitlab::Git::Commit.find(repository, 'master').tree + + blob = Gitlab::Git::Blob.raw(repository, tree.oid) + + expect(blob).to be_nil + end + end end describe '.raw' do @@ -226,6 +236,51 @@ describe Gitlab::Git::Blob, seed_helper: true do end end + describe '.batch_lfs_pointers' do + let(:tree_object) { Gitlab::Git::Commit.find(repository, 'master').tree } + + let(:non_lfs_blob) do + Gitlab::Git::Blob.find( + repository, + 'master', + 'README.md' + ) + end + + let(:lfs_blob) do + Gitlab::Git::Blob.find( + repository, + '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', + 'files/lfs/image.jpg' + ) + end + + it 'returns a list of Gitlab::Git::Blob' do + blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id]) + + expect(blobs.count).to eq(1) + expect(blobs).to all( be_a(Gitlab::Git::Blob) ) + end + + it 'silently ignores tree objects' do + blobs = described_class.batch_lfs_pointers(repository, [tree_object.oid]) + + expect(blobs).to eq([]) + end + + it 'silently ignores non lfs objects' do + blobs = described_class.batch_lfs_pointers(repository, [non_lfs_blob.id]) + + expect(blobs).to eq([]) + end + + it 'avoids loading large blobs into memory' do + expect(repository).not_to receive(:lookup) + + described_class.batch_lfs_pointers(repository, [non_lfs_blob.id]) + end + end + describe 'encoding' do context 'file with russian text' do let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/russian.rb") } diff --git a/spec/lib/gitlab/git/lfs_changes_spec.rb b/spec/lib/gitlab/git/lfs_changes_spec.rb new file mode 100644 index 00000000000..c2d2c6e1bc8 --- /dev/null +++ b/spec/lib/gitlab/git/lfs_changes_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Gitlab::Git::LfsChanges do + let(:project) { create(:project, :repository) } + let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } + let(:blob_object_id) { '0c304a93cb8430108629bbbcaa27db3343299bc0' } + + subject { described_class.new(project.repository, newrev) } + + describe 'new_pointers' do + before do + allow_any_instance_of(Gitlab::Git::RevList).to receive(:new_objects).and_return([blob_object_id]) + end + + it 'uses rev-list to find new objects' do + rev_list = double + allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list) + + expect(rev_list).to receive(:new_objects).and_return([]) + + subject.new_pointers + end + + it 'filters new objects to find lfs pointers' do + expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, [blob_object_id]) + + subject.new_pointers(object_limit: 1) + end + + it 'limits new_objects using object_limit' do + expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, []) + + subject.new_pointers(object_limit: 0) + end + end + + describe 'all_pointers' do + it 'uses rev-list to find all objects' do + rev_list = double + allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list) + allow(rev_list).to receive(:all_objects).and_return([blob_object_id]) + + expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, [blob_object_id]) + + subject.all_pointers + end + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index b161d25b96c..bf6d199ebe2 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1163,6 +1163,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe "#ls_files" do let(:master_file_paths) { repository.ls_files("master") } + let(:utf8_file_paths) { repository.ls_files("ls-files-utf8") } let(:not_existed_branch) { repository.ls_files("not_existed_branch") } it "read every file paths of master branch" do @@ -1184,6 +1185,10 @@ describe Gitlab::Git::Repository, seed_helper: true do it "returns empty array when not existed branch" do expect(not_existed_branch.length).to equal(0) end + + it "returns valid utf-8 data" do + expect(utf8_file_paths.map { |file| file.force_encoding('utf-8') }).to all(be_valid_encoding) + end end describe "#copy_gitattributes" do @@ -1336,6 +1341,24 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#batch_existence' do + let(:refs) { ['deadbeef', SeedRepo::RubyBlob::ID, '909e6157199'] } + + it 'returns existing refs back' do + result = repository.batch_existence(refs) + + expect(result).to eq([SeedRepo::RubyBlob::ID]) + end + + context 'existing: true' do + it 'inverts meaning and returns non-existing refs' do + result = repository.batch_existence(refs, existing: false) + + expect(result).to eq(%w(deadbeef 909e6157199)) + end + end + end + describe '#local_branches' do before(:all) do @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') @@ -1609,39 +1632,57 @@ 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 - end - context 'with a non-existing source commit' do - let(:source_sha) { 'f001' } + context 'with a non-existing source commit' do + let(:source_sha) { 'f001' } - it 'throws an ArgumentError' do - expect { subject }.to raise_error(ArgumentError) + it 'throws an ArgumentError' do + expect { subject }.to raise_error(ArgumentError) + end end - end - context 'when the source sha is not a descendant of the branch head' do - let(:source_sha) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' } + 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) + 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) + expect(repository.commit(target_branch).id).to eq(branch_head) + end end end + + 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) + + subject + end + + it_behaves_like '#ff_merge' + end + + context 'without gitaly', :skip_gitaly_mock do + it_behaves_like '#ff_merge' + end end def create_remote_branch(repository, remote_name, branch_name, source_branch_name) diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb index c0eac98d718..643a4b2d03e 100644 --- a/spec/lib/gitlab/git/rev_list_spec.rb +++ b/spec/lib/gitlab/git/rev_list_spec.rb @@ -2,53 +2,82 @@ require 'spec_helper' describe Gitlab::Git::RevList do let(:project) { create(:project, :repository) } + let(:rev_list) { described_class.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) } before do - expect(Gitlab::Git::Env).to receive(:all).and_return({ + allow(Gitlab::Git::Env).to receive(:all).and_return({ GIT_OBJECT_DIRECTORY: 'foo', GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar' }) end - context "#new_refs" do - let(:rev_list) { described_class.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) } + def stub_popen_rev_list(*additional_args, output:) + expect(rev_list).to receive(:popen).with([ + Gitlab.config.git.bin_path, + "--git-dir=#{project.repository.path_to_repo}", + 'rev-list', + *additional_args + ], + nil, + { + 'GIT_OBJECT_DIRECTORY' => 'foo', + 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar' + }).and_return([output, 0]) + end + context "#new_refs" do it 'calls out to `popen`' do - expect(rev_list).to receive(:popen).with([ - Gitlab.config.git.bin_path, - "--git-dir=#{project.repository.path_to_repo}", - 'rev-list', - 'newrev', - '--not', - '--all' - ], - nil, - { - 'GIT_OBJECT_DIRECTORY' => 'foo', - 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar' - }).and_return(["sha1\nsha2", 0]) + stub_popen_rev_list('newrev', '--not', '--all', output: "sha1\nsha2") expect(rev_list.new_refs).to eq(%w[sha1 sha2]) end end + context '#new_objects' do + it 'fetches list of newly pushed objects using rev-list' do + stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2") + + expect(rev_list.new_objects).to eq(%w[sha1 sha2]) + end + + it 'can skip pathless objects' do + stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2 path/to/file") + + expect(rev_list.new_objects(require_path: true)).to eq(%w[sha2]) + end + + it 'can return a lazy enumerator' do + stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2") + + expect(rev_list.new_objects(lazy: true)).to be_a Enumerator::Lazy + end + + it 'can accept list of references to exclude' do + stub_popen_rev_list('newrev', '--not', 'master', '--objects', output: "sha1\nsha2") + + expect(rev_list.new_objects(not_in: ['master'])).to eq(%w[sha1 sha2]) + end + + it 'handles empty list of references to exclude as listing all known objects' do + stub_popen_rev_list('newrev', '--objects', output: "sha1\nsha2") + + expect(rev_list.new_objects(not_in: [])).to eq(%w[sha1 sha2]) + end + end + + context '#all_objects' do + it 'fetches list of all pushed objects using rev-list' do + stub_popen_rev_list('--all', '--objects', output: "sha1\nsha2") + + expect(rev_list.all_objects.force).to eq(%w[sha1 sha2]) + end + end + context "#missed_ref" do let(:rev_list) { described_class.new(oldrev: 'oldrev', newrev: 'newrev', path_to_repo: project.repository.path_to_repo) } it 'calls out to `popen`' do - expect(rev_list).to receive(:popen).with([ - Gitlab.config.git.bin_path, - "--git-dir=#{project.repository.path_to_repo}", - 'rev-list', - '--max-count=1', - 'oldrev', - '^newrev' - ], - nil, - { - 'GIT_OBJECT_DIRECTORY' => 'foo', - 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar' - }).and_return(["sha1\nsha2", 0]) + stub_popen_rev_list('--max-count=1', 'oldrev', '^newrev', output: "sha1\nsha2") expect(rev_list.missed_ref).to eq(%w[sha1 sha2]) 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/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..f75de0a0d88 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -547,6 +547,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 +592,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/user_spec.rb b/spec/models/user_spec.rb index 1c3c9068f12..fb03e320734 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) 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/spec_helper.rb b/spec/spec_helper.rb index 48cacba6a8a..0dc417b3cb6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -49,6 +49,7 @@ RSpec.configure do |config| config.include LoginHelpers, type: :feature config.include SearchHelpers, type: :feature 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/gitlab-git-test.git/objects/88/3e379fcaa5f818fca81cdbabd7a497794d6535 b/spec/support/gitlab-git-test.git/objects/88/3e379fcaa5f818fca81cdbabd7a497794d6535 Binary files differnew file mode 100644 index 00000000000..1c47f34b9a5 --- /dev/null +++ b/spec/support/gitlab-git-test.git/objects/88/3e379fcaa5f818fca81cdbabd7a497794d6535 diff --git a/spec/support/gitlab-git-test.git/objects/c8/b1ab16c858c67b680eea4644cf652485f555cf b/spec/support/gitlab-git-test.git/objects/c8/b1ab16c858c67b680eea4644cf652485f555cf Binary files differnew file mode 100644 index 00000000000..ca13c8df66a --- /dev/null +++ b/spec/support/gitlab-git-test.git/objects/c8/b1ab16c858c67b680eea4644cf652485f555cf diff --git a/spec/support/gitlab-git-test.git/objects/e3/7697aea12699f0b44544332a7c0f41ace5fb16 b/spec/support/gitlab-git-test.git/objects/e3/7697aea12699f0b44544332a7c0f41ace5fb16 new file mode 100644 index 00000000000..3be244dbda4 --- /dev/null +++ b/spec/support/gitlab-git-test.git/objects/e3/7697aea12699f0b44544332a7c0f41ace5fb16 @@ -0,0 +1,2 @@ +xK +0EgNI|ADt*^
mZ qGčY8ZK7"Fc%oHD9rZLsMJ2=ACmeFgVxI9H2XJrp6;N8z??>+zWƏBÞf}bN@K\SYiSC
\ No newline at end of file diff --git a/spec/support/gitlab-git-test.git/objects/eb/a0c153ed20d927bab00507f356043b6b4be31e b/spec/support/gitlab-git-test.git/objects/eb/a0c153ed20d927bab00507f356043b6b4be31e Binary files differnew file mode 100644 index 00000000000..2bf27fe5048 --- /dev/null +++ b/spec/support/gitlab-git-test.git/objects/eb/a0c153ed20d927bab00507f356043b6b4be31e diff --git a/spec/support/gitlab-git-test.git/objects/f6/5ad228d96e2a2ae7088e8557fe8906f6dd2b3f b/spec/support/gitlab-git-test.git/objects/f6/5ad228d96e2a2ae7088e8557fe8906f6dd2b3f Binary files differnew file mode 100644 index 00000000000..8ab8606c6be --- /dev/null +++ b/spec/support/gitlab-git-test.git/objects/f6/5ad228d96e2a2ae7088e8557fe8906f6dd2b3f 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/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/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/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/yarn.lock b/yarn.lock index 818878fe36c..ee00c1f4f3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6398,9 +6398,9 @@ vue-style-loader@^2.0.0: hash-sum "^1.0.2" loader-utils "^1.0.2" -vue-template-compiler@^2.2.6: - version "2.2.6" - resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.2.6.tgz#2e2928daf0cd0feca9dfc35a9729adeae173ec68" +vue-template-compiler@^2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.2.tgz#6f198ebc677b8f804315cd33b91e849315ae7177" dependencies: de-indent "^1.0.2" he "^1.1.0" @@ -6409,9 +6409,9 @@ vue-template-es2015-compiler@^1.2.2: version "1.5.1" resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.5.1.tgz#0c36cc57aa3a9ec13e846342cb14a72fcac8bd93" -vue@^2.2.6: - version "2.2.6" - resolved "https://registry.yarnpkg.com/vue/-/vue-2.2.6.tgz#451714b394dd6d4eae7b773c40c2034a59621aed" +vue@^2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.2.tgz#fd367a87bae7535e47f9dc5c9ec3b496e5feb5a4" vuex@^3.0.0: version "3.0.0" |