diff options
141 files changed, 2314 insertions, 1254 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 71ddebef662..eed1a50cc8f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -721,7 +721,7 @@ codequality: tags: [] before_script: [] services: - - docker:dind + - docker:stable-dind variables: SETUP_DB: "false" DOCKER_DRIVER: overlay2 @@ -82,16 +82,9 @@ gem 'net-ldap' # Git Wiki # Required manually in config/initializers/gollum.rb to control load order -# Before updating this gem, check if -# https://github.com/gollum/gollum-lib/pull/292 has been merged. -# If it has, then remove the monkey patch for update_page, rename_page and raw_data_in_committer -# in config/initializers/gollum.rb -gem 'gollum-lib', '~> 4.2', require: false - -# Before updating this gem, check if -# https://github.com/gollum/rugged_adapter/pull/28 has been merged. -# If it has, then remove the monkey patch for tree_entry in config/initializers/gollum.rb -gem 'gollum-rugged_adapter', '~> 0.4.4', require: false +gem 'gitlab-gollum-lib', '~> 4.2' + +gem 'gitlab-gollum-rugged_adapter', '~> 0.4.4', require: false # Language detection gem 'github-linguist', '~> 5.3.3', require: 'linguist' diff --git a/Gemfile.lock b/Gemfile.lock index 76e1a17155f..23b6c2423e6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -298,11 +298,22 @@ GEM escape_utils (~> 1.1.0) mime-types (>= 1.19) rugged (>= 0.25.1) - github-markup (1.6.1) + github-markup (1.7.0) gitlab-flowdock-git-hook (1.0.1) flowdock (~> 0.7) gitlab-grit (>= 2.4.1) multi_json + gitlab-gollum-lib (4.2.7.1) + gemojione (~> 3.2) + github-markup (~> 1.6) + gollum-grit_adapter (~> 1.0) + nokogiri (>= 1.6.1, < 2.0) + rouge (~> 2.1) + sanitize (~> 2.1) + stringex (~> 2.6) + gitlab-gollum-rugged_adapter (0.4.4) + mime-types (>= 1.15) + rugged (~> 0.25) gitlab-grit (2.8.2) charlock_holmes (~> 0.6) diff-lcs (~> 1.1) @@ -325,17 +336,6 @@ GEM activesupport (>= 4.2, < 5.2) gollum-grit_adapter (1.0.1) gitlab-grit (~> 2.7, >= 2.7.1) - gollum-lib (4.2.7) - gemojione (~> 3.2) - github-markup (~> 1.6) - gollum-grit_adapter (~> 1.0) - nokogiri (>= 1.6.1, < 2.0) - rouge (~> 2.1) - sanitize (~> 2.1) - stringex (~> 2.6) - gollum-rugged_adapter (0.4.4) - mime-types (>= 1.15) - rugged (~> 0.25) gon (6.1.0) actionpack (>= 3.0) json @@ -590,7 +590,7 @@ GEM orm_adapter (0.5.0) os (0.9.6) parallel (1.12.1) - parser (2.5.0.5) + parser (2.5.1.0) ast (~> 2.4.0) parslet (1.5.0) blankslate (~> 2.0) @@ -910,7 +910,7 @@ GEM state_machines-activerecord (0.5.1) activerecord (>= 4.1, < 6.0) state_machines-activemodel (>= 0.5.0) - stringex (2.7.1) + stringex (2.8.4) sys-filesystem (1.1.6) ffi sysexits (1.2.0) @@ -1067,12 +1067,12 @@ DEPENDENCIES gitaly-proto (~> 0.94.0) github-linguist (~> 5.3.3) gitlab-flowdock-git-hook (~> 1.0.1) + gitlab-gollum-lib (~> 4.2) + gitlab-gollum-rugged_adapter (~> 0.4.4) gitlab-markup (~> 1.6.2) gitlab-styles (~> 2.3) gitlab_omniauth-ldap (~> 2.0.4) goldiloader (~> 2.0) - gollum-lib (~> 4.2) - gollum-rugged_adapter (~> 0.4.4) gon (~> 6.1.0) google-api-client (~> 0.19.8) google-protobuf (= 3.5.1) diff --git a/app/assets/images/ext_snippet_icons/ext_snippet_icons.png b/app/assets/images/ext_snippet_icons/ext_snippet_icons.png Binary files differnew file mode 100644 index 00000000000..20380adc4e5 --- /dev/null +++ b/app/assets/images/ext_snippet_icons/ext_snippet_icons.png diff --git a/app/assets/images/ext_snippet_icons/logo.png b/app/assets/images/ext_snippet_icons/logo.png Binary files differnew file mode 100644 index 00000000000..794c9cc2dbc --- /dev/null +++ b/app/assets/images/ext_snippet_icons/logo.png diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js index c50ac667c20..2d5bae9a9c4 100644 --- a/app/assets/javascripts/feature_highlight/feature_highlight.js +++ b/app/assets/javascripts/feature_highlight/feature_highlight.js @@ -1,19 +1,19 @@ import $ from 'jquery'; -import _ from 'underscore'; import { getSelector, - togglePopover, inserted, - mouseenter, - mouseleave, } from './feature_highlight_helper'; +import { + togglePopover, + mouseenter, + debouncedMouseleave, +} from '../shared/popover'; export function setupFeatureHighlightPopover(id, debounceTimeout = 300) { const $selector = $(getSelector(id)); const $parent = $selector.parent(); const $popoverContent = $parent.siblings('.feature-highlight-popover-content'); const hideOnScroll = togglePopover.bind($selector, false); - const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout); $selector // Setup popover @@ -29,13 +29,10 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) { `, }) .on('mouseenter', mouseenter) - .on('mouseleave', debouncedMouseleave) + .on('mouseleave', debouncedMouseleave(debounceTimeout)) .on('inserted.bs.popover', inserted) .on('show.bs.popover', () => { - window.addEventListener('scroll', hideOnScroll); - }) - .on('hide.bs.popover', () => { - window.removeEventListener('scroll', hideOnScroll); + window.addEventListener('scroll', hideOnScroll, { once: true }); }) // Display feature highlight .removeAttr('disabled'); diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js index f480e72961c..d5b97ebb264 100644 --- a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js +++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js @@ -3,20 +3,10 @@ import axios from '../lib/utils/axios_utils'; import { __ } from '../locale'; import Flash from '../flash'; import LazyLoader from '../lazy_loader'; +import { togglePopover } from '../shared/popover'; export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`; -export function togglePopover(show) { - const isAlreadyShown = this.hasClass('js-popover-show'); - if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) { - return false; - } - this.popover(show ? 'show' : 'hide'); - this.toggleClass('disable-animation js-popover-show', show); - - return true; -} - export function dismiss(highlightId) { axios.post(this.attr('data-dismiss-endpoint'), { feature_name: highlightId, @@ -27,23 +17,6 @@ export function dismiss(highlightId) { this.hide(); } -export function mouseleave() { - if (!$('.popover:hover').length > 0) { - const $featureHighlight = $(this); - togglePopover.call($featureHighlight, false); - } -} - -export function mouseenter() { - const $featureHighlight = $(this); - - const showedPopover = togglePopover.call($featureHighlight, true); - if (showedPopover) { - $('.popover') - .on('mouseleave', mouseleave.bind($featureHighlight)); - } -} - export function inserted() { const popoverId = this.getAttribute('aria-describedby'); const highlightId = this.dataset.highlight; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index f536ce6344b..367c45f7e2d 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -37,9 +37,9 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => { const commitMsg = sprintf( __('Your changes have been committed. Commit %{commitId} %{commitStats}'), { - commitId: `<a href="${currentProject.web_url}/commit/${ + commitId: `<a href="${currentProject.web_url}/commit/${data.short_id}" class="commit-sha">${ data.short_id - }" class="commit-sha">${data.short_id}</a>`, + }</a>`, commitStats, }, false, @@ -54,9 +54,7 @@ export const checkCommitStatus = ({ rootState }) => .then(({ data }) => { const { id } = data.commit; const selectedBranch = - rootState.projects[rootState.currentProjectId].branches[ - rootState.currentBranchId - ]; + rootState.projects[rootState.currentProjectId].branches[rootState.currentBranchId]; if (selectedBranch.workingReference !== id) { return true; @@ -135,32 +133,15 @@ export const updateFilesAfterCommit = ( if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) { router.push( - `/project/${rootState.currentProjectId}/blob/${branch}/${ - rootGetters.activeFile.path - }`, + `/project/${rootState.currentProjectId}/blob/${branch}/${rootGetters.activeFile.path}`, ); } - - dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH); }; -export const commitChanges = ({ - commit, - state, - getters, - dispatch, - rootState, -}) => { +export const commitChanges = ({ commit, state, getters, dispatch, rootState }) => { const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH; - const payload = createCommitPayload( - getters.branchName, - newBranch, - state, - rootState, - ); - const getCommitStatus = newBranch - ? Promise.resolve(false) - : dispatch('checkCommitStatus'); + const payload = createCommitPayload(getters.branchName, newBranch, state, rootState); + const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus'); commit(types.UPDATE_LOADING, true); @@ -182,28 +163,29 @@ export const commitChanges = ({ if (!data.short_id) { flash(data.message, 'alert', document, null, false, true); - return; + return null; } dispatch('setLastCommitMessage', data); dispatch('updateCommitMessage', ''); - - if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) { - dispatch( - 'redirectToUrl', - createNewMergeRequestUrl( - rootState.projects[rootState.currentProjectId].web_url, - getters.branchName, - rootState.currentBranchId, - ), - { root: true }, - ); - } else { - dispatch('updateFilesAfterCommit', { - data, - branch: getters.branchName, - }); - } + return dispatch('updateFilesAfterCommit', { + data, + branch: getters.branchName, + }) + .then(() => { + if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) { + dispatch( + 'redirectToUrl', + createNewMergeRequestUrl( + rootState.projects[rootState.currentProjectId].web_url, + getters.branchName, + rootState.currentBranchId, + ), + { root: true }, + ); + } + }) + .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH)); }) .catch(err => { let errMsg = __('Error committing changes. Please try again.'); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 00a212f3d5f..3548c07aea8 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -7,11 +7,7 @@ import flash from './flash'; import BlobForkSuggestion from './blob/blob_fork_suggestion'; import initChangesDropdown from './init_changes_dropdown'; import bp from './breakpoints'; -import { - parseUrlPathname, - handleLocationHash, - isMetaClick, -} from './lib/utils/common_utils'; +import { parseUrlPathname, handleLocationHash, isMetaClick } from './lib/utils/common_utils'; import { getLocationHash } from './lib/utils/url_utility'; import initDiscussionTab from './image_diff/init_discussion_tab'; import Diff from './diff'; @@ -69,11 +65,10 @@ import Notes from './notes'; let location = window.location; export default class MergeRequestTabs { - constructor({ action, setUrl, stubLocation } = {}) { const mergeRequestTabs = document.querySelector('.js-tabs-affix'); const navbar = document.querySelector('.navbar-gitlab'); - const peek = document.getElementById('peek'); + const peek = document.getElementById('js-peek'); const paddingTop = 16; this.diffsLoaded = false; @@ -109,8 +104,7 @@ export default class MergeRequestTabs { .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) .on('click', '.js-show-tab', this.showTab); - $('.merge-request-tabs a[data-toggle="tab"]') - .on('click', this.clickTab); + $('.merge-request-tabs a[data-toggle="tab"]').on('click', this.clickTab); } // Used in tests @@ -119,8 +113,7 @@ export default class MergeRequestTabs { .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) .off('click', '.js-show-tab', this.showTab); - $('.merge-request-tabs a[data-toggle="tab"]') - .off('click', this.clickTab); + $('.merge-request-tabs a[data-toggle="tab"]').off('click', this.clickTab); } destroyPipelinesView() { @@ -183,10 +176,7 @@ export default class MergeRequestTabs { scrollToElement(container) { if (location.hash) { - const offset = 0 - ( - $('.navbar-gitlab').outerHeight() + - $('.js-tabs-affix').outerHeight() - ); + const offset = 0 - ($('.navbar-gitlab').outerHeight() + $('.js-tabs-affix').outerHeight()); const $el = $(`${container} ${location.hash}:not(.match)`); if ($el.length) { $.scrollTo($el[0], { offset }); @@ -240,9 +230,13 @@ export default class MergeRequestTabs { // Turbolinks' history. // // See https://github.com/rails/turbolinks/issues/363 - window.history.replaceState({ - url: newState, - }, document.title, newState); + window.history.replaceState( + { + url: newState, + }, + document.title, + newState, + ); return newState; } @@ -258,7 +252,8 @@ export default class MergeRequestTabs { this.toggleLoading(true); - axios.get(`${source}.json`) + axios + .get(`${source}.json`) .then(({ data }) => { document.querySelector('div#commits').innerHTML = data.html; localTimeAgo($('.js-timeago', 'div#commits')); @@ -303,7 +298,8 @@ export default class MergeRequestTabs { this.toggleLoading(true); - axios.get(`${urlPathname}.json${location.search}`) + axios + .get(`${urlPathname}.json${location.search}`) .then(({ data }) => { const $container = $('#diffs'); $container.html(data.html); @@ -332,8 +328,7 @@ export default class MergeRequestTabs { cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'), suggestionSections: $(el).find('.js-file-fork-suggestion-section'), actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'), - }) - .init(); + }).init(); }); // Scroll any linked note into view @@ -388,8 +383,7 @@ export default class MergeRequestTabs { resetViewContainer() { if (this.fixedLayoutPref !== null) { - $('.content-wrapper .container-fluid') - .toggleClass('container-limited', this.fixedLayoutPref); + $('.content-wrapper .container-fluid').toggleClass('container-limited', this.fixedLayoutPref); } } @@ -438,12 +432,11 @@ export default class MergeRequestTabs { const $diffTabs = $('#diff-notes-app'); - $tabs.off('affix.bs.affix affix-top.bs.affix') + $tabs + .off('affix.bs.affix affix-top.bs.affix') .affix({ offset: { - top: () => ( - $diffTabs.offset().top - $tabs.height() - $fixedNav.height() - ), + top: () => $diffTabs.offset().top - $tabs.height() - $fixedNav.height(), }, }) .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() })) diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index e6e3a66aa20..325fa570f37 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import flash from './flash'; +import { mouseenter, debouncedMouseleave, togglePopover } from './shared/popover'; export default class Milestone { constructor() { @@ -43,4 +44,25 @@ export default class Milestone { .catch(() => flash('Error loading milestone tab')); } } + + static initDeprecationMessage() { + const deprecationMesssageContainer = document.querySelector('.js-milestone-deprecation-message'); + + if (!deprecationMesssageContainer) return; + + const deprecationMessage = deprecationMesssageContainer.querySelector('.js-milestone-deprecation-message-template').innerHTML; + const $popover = $('.js-popover-link', deprecationMesssageContainer); + const hideOnScroll = togglePopover.bind($popover, false); + + $popover.popover({ + content: deprecationMessage, + html: true, + placement: 'bottom', + }) + .on('mouseenter', mouseenter) + .on('mouseleave', debouncedMouseleave()) + .on('show.bs.popover', () => { + window.addEventListener('scroll', hideOnScroll, { once: true }); + }); + } } diff --git a/app/assets/javascripts/pages/dashboard/milestones/show/index.js b/app/assets/javascripts/pages/dashboard/milestones/show/index.js index 397149aaa9e..8b529585898 100644 --- a/app/assets/javascripts/pages/dashboard/milestones/show/index.js +++ b/app/assets/javascripts/pages/dashboard/milestones/show/index.js @@ -6,4 +6,6 @@ document.addEventListener('DOMContentLoaded', () => { new Milestone(); // eslint-disable-line no-new new Sidebar(); // eslint-disable-line no-new new MountMilestoneSidebar(); // eslint-disable-line no-new + + Milestone.initDeprecationMessage(); }); diff --git a/app/assets/javascripts/pages/groups/milestones/show/index.js b/app/assets/javascripts/pages/groups/milestones/show/index.js index 88f40b5278e..74cc4ba42c1 100644 --- a/app/assets/javascripts/pages/groups/milestones/show/index.js +++ b/app/assets/javascripts/pages/groups/milestones/show/index.js @@ -1,3 +1,8 @@ import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; +import Milestone from '~/milestone'; -document.addEventListener('DOMContentLoaded', initMilestonesShow); +document.addEventListener('DOMContentLoaded', () => { + initMilestonesShow(); + + Milestone.initDeprecationMessage(); +}); diff --git a/app/assets/javascripts/pages/projects/snippets/show/index.js b/app/assets/javascripts/pages/projects/snippets/show/index.js index a134599cb04..c35b9c30058 100644 --- a/app/assets/javascripts/pages/projects/snippets/show/index.js +++ b/app/assets/javascripts/pages/projects/snippets/show/index.js @@ -1,11 +1,13 @@ import initNotes from '~/init_notes'; import ZenMode from '~/zen_mode'; -import LineHighlighter from '../../../../line_highlighter'; -import BlobViewer from '../../../../blob/viewer'; +import LineHighlighter from '~/line_highlighter'; +import BlobViewer from '~/blob/viewer'; +import snippetEmbed from '~/snippet/snippet_embed'; document.addEventListener('DOMContentLoaded', () => { new LineHighlighter(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new initNotes(); new ZenMode(); // eslint-disable-line no-new + snippetEmbed(); }); diff --git a/app/assets/javascripts/pages/snippets/show/index.js b/app/assets/javascripts/pages/snippets/show/index.js index f548b9fad65..26936110402 100644 --- a/app/assets/javascripts/pages/snippets/show/index.js +++ b/app/assets/javascripts/pages/snippets/show/index.js @@ -1,11 +1,13 @@ -import LineHighlighter from '../../../line_highlighter'; -import BlobViewer from '../../../blob/viewer'; -import ZenMode from '../../../zen_mode'; -import initNotes from '../../../init_notes'; +import LineHighlighter from '~/line_highlighter'; +import BlobViewer from '~/blob/viewer'; +import ZenMode from '~/zen_mode'; +import initNotes from '~/init_notes'; +import snippetEmbed from '~/snippet/snippet_embed'; document.addEventListener('DOMContentLoaded', () => { new LineHighlighter(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new initNotes(); new ZenMode(); // eslint-disable-line no-new + snippetEmbed(); }); diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index b3fcaf0ccd1..32cf3dba3c3 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -1,5 +1,4 @@ <script> - import $ from 'jquery'; /** * Renders each stage of the pipeline mini graph. @@ -13,8 +12,11 @@ * 3. Merge request widget * 4. Commit widget */ - import axios from '../../lib/utils/axios_utils'; + + import $ from 'jquery'; import Flash from '../../flash'; + import axios from '../../lib/utils/axios_utils'; + import eventHub from '../event_hub'; import Icon from '../../vue_shared/components/icon.vue'; import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; @@ -82,6 +84,7 @@ methods: { onClickStage() { if (!this.isDropdownOpen()) { + eventHub.$emit('clickedDropdown'); this.isLoading = true; this.fetchJobs(); } diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js new file mode 100644 index 00000000000..b384c7500e7 --- /dev/null +++ b/app/assets/javascripts/pipelines/constants.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export const CANCEL_REQUEST = 'CANCEL_REQUEST'; diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index 522a4277bd7..6d87f75ae8e 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -7,6 +7,7 @@ import SvgBlankState from '../components/blank_state.vue'; import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; import PipelinesTableComponent from '../components/pipelines_table.vue'; import eventHub from '../event_hub'; +import { CANCEL_REQUEST } from '../constants'; export default { components: { @@ -52,34 +53,58 @@ export default { }); eventHub.$on('postAction', this.postAction); + eventHub.$on('clickedDropdown', this.updateTable); }, beforeDestroy() { eventHub.$off('postAction', this.postAction); + eventHub.$off('clickedDropdown', this.updateTable); }, destroyed() { this.poll.stop(); }, methods: { + updateTable() { + // Cancel ongoing request + if (this.isMakingRequest) { + this.service.cancelationSource.cancel(CANCEL_REQUEST); + } + // Stop polling + this.poll.stop(); + // Update the table + return this.getPipelines() + .then(() => this.poll.restart()); + }, fetchPipelines() { if (!this.isMakingRequest) { this.isLoading = true; - this.service.getPipelines(this.requestData) - .then(response => this.successCallback(response)) - .catch(() => this.errorCallback()); + this.getPipelines(); } }, + getPipelines() { + return this.service.getPipelines(this.requestData) + .then(response => this.successCallback(response)) + .catch((error) => this.errorCallback(error)); + }, setCommonData(pipelines) { this.store.storePipelines(pipelines); this.isLoading = false; this.updateGraphDropdown = true; this.hasMadeRequest = true; + + // In case the previous polling request returned an error, we need to reset it + if (this.hasError) { + this.hasError = false; + } }, - errorCallback() { - this.hasError = true; - this.isLoading = false; - this.updateGraphDropdown = false; + errorCallback(error) { this.hasMadeRequest = true; + this.isLoading = false; + + if (error && error.message && error.message !== CANCEL_REQUEST) { + this.hasError = true; + this.updateGraphDropdown = false; + } }, setIsMakingRequest(isMakingRequest) { this.isMakingRequest = isMakingRequest; diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js index 001286f5d52..59c8b9c58e5 100644 --- a/app/assets/javascripts/pipelines/services/pipelines_service.js +++ b/app/assets/javascripts/pipelines/services/pipelines_service.js @@ -19,8 +19,13 @@ export default class PipelinesService { getPipelines(data = {}) { const { scope, page } = data; + const CancelToken = axios.CancelToken; + + this.cancelationSource = CancelToken.source(); + return axios.get(this.endpoint, { params: { scope, page }, + cancelToken: this.cancelationSource.token, }); } diff --git a/app/assets/javascripts/shared/popover.js b/app/assets/javascripts/shared/popover.js new file mode 100644 index 00000000000..3fc03553bdd --- /dev/null +++ b/app/assets/javascripts/shared/popover.js @@ -0,0 +1,33 @@ +import $ from 'jquery'; +import _ from 'underscore'; + +export function togglePopover(show) { + const isAlreadyShown = this.hasClass('js-popover-show'); + if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) { + return false; + } + this.popover(show ? 'show' : 'hide'); + this.toggleClass('disable-animation js-popover-show', show); + + return true; +} + +export function mouseleave() { + if (!$('.popover:hover').length > 0) { + const $popover = $(this); + togglePopover.call($popover, false); + } +} + +export function mouseenter() { + const $popover = $(this); + + const showedPopover = togglePopover.call($popover, true); + if (showedPopover) { + $('.popover').on('mouseleave', mouseleave.bind($popover)); + } +} + +export function debouncedMouseleave(debounceTimeout = 300) { + return _.debounce(mouseleave, debounceTimeout); +} diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js index 25f39e4fdb6..9f69f110d06 100644 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js +++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js @@ -1,12 +1,15 @@ +import { visitUrl } from './lib/utils/url_utility'; + /** * Helper function that finds the href of the fiven selector and updates the location. * * @param {String} selector */ -export default (selector) => { - const link = document.querySelector(selector).getAttribute('href'); +export default function findAndFollowLink(selector) { + const element = document.querySelector(selector); + const link = element && element.getAttribute('href'); if (link) { - window.location = link; + visitUrl(link); } -}; +} diff --git a/app/assets/javascripts/snippet/snippet_embed.js b/app/assets/javascripts/snippet/snippet_embed.js new file mode 100644 index 00000000000..81ec483f2d9 --- /dev/null +++ b/app/assets/javascripts/snippet/snippet_embed.js @@ -0,0 +1,23 @@ +export default () => { + const { protocol, host, pathname } = location; + const shareBtn = document.querySelector('.js-share-btn'); + const embedBtn = document.querySelector('.js-embed-btn'); + const snippetUrlArea = document.querySelector('.js-snippet-url-area'); + const embedAction = document.querySelector('.js-embed-action'); + const url = `${protocol}//${host + pathname}`; + + shareBtn.addEventListener('click', () => { + shareBtn.classList.add('is-active'); + embedBtn.classList.remove('is-active'); + snippetUrlArea.value = url; + embedAction.innerText = 'Share'; + }); + + embedBtn.addEventListener('click', () => { + embedBtn.classList.add('is-active'); + shareBtn.classList.remove('is-active'); + const scriptTag = `<script src="${url}.js"></script>`; + snippetUrlArea.value = scriptTag; + embedAction.innerText = 'Embed'; + }); +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue index 95c8b0a4c55..f012f9c6772 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue @@ -146,8 +146,8 @@ export default { </p> <p v-if="shouldShowMemoryGraph" - class="usage-info js-usage-info"> - {{ memoryChangeMessage }} + class="usage-info js-usage-info" + v-html="memoryChangeMessage"> </p> <p v-if="shouldShowLoadFailure" diff --git a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue index b06493e6c66..16304e4815d 100644 --- a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue +++ b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue @@ -9,7 +9,7 @@ lines: { type: Number, required: false, - default: 6, + default: 3, }, }, computed: { diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 0665622fe4a..f2950308019 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -37,7 +37,11 @@ /* * Code highlight */ -@import "highlight/**/*"; +@import "highlight/dark"; +@import "highlight/monokai"; +@import "highlight/solarized_dark"; +@import "highlight/solarized_light"; +@import "highlight/white"; /* * Styles for JS behaviors. diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 728f9a27aca..14cd32da9eb 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -187,12 +187,9 @@ a { animation: fadeInFull $fade-in-duration 1; } - .animation-container { - background: $repo-editor-grey; height: 40px; overflow: hidden; - position: relative; &.animation-container-small { height: 12px; @@ -205,60 +202,43 @@ a { } } - &::before { - animation-duration: 1s; - animation-fill-mode: forwards; - animation-iteration-count: infinite; - animation-name: blockTextShine; - animation-timing-function: linear; - background-image: $repo-editor-linear-gradient; - background-repeat: no-repeat; - background-size: 800px 45px; - content: ' '; - display: block; - height: 100%; + [class^="skeleton-line-"] { position: relative; - } - - div { - background: $white-light; - height: 6px; - left: 0; - position: absolute; - right: 0; - } - - .skeleton-line-1 { - left: 0; - top: 8px; - } - - .skeleton-line-2 { - left: 150px; - top: 0; + background-color: $theme-gray-100; height: 10px; - } + overflow: hidden; - .skeleton-line-3 { - left: 0; - top: 23px; - } + &:not(:last-of-type) { + margin-bottom: 4px; + } - .skeleton-line-4 { - left: 0; - top: 38px; + &::after { + content: ' '; + display: block; + animation: blockTextShine 1s linear infinite forwards; + background-repeat: no-repeat; + background-size: cover; + background-image: linear-gradient( + to right, + $theme-gray-100 0%, + $theme-gray-50 20%, + $theme-gray-100 40%, + $theme-gray-100 100% + ); + height: 10px; + } } +} - .skeleton-line-5 { - left: 200px; - top: 28px; - height: 10px; - } +$skeleton-line-widths: ( + 156px, + 235px, + 200px, +); - .skeleton-line-6 { - top: 14px; - left: 230px; - height: 10px; +@for $count from 1 through length($skeleton-line-widths) { + .skeleton-line-#{$count} { + width: nth($skeleton-line-widths, $count); } } diff --git a/app/assets/stylesheets/framework/banner.scss b/app/assets/stylesheets/framework/banner.scss index 6433b0c7855..02f3896d591 100644 --- a/app/assets/stylesheets/framework/banner.scss +++ b/app/assets/stylesheets/framework/banner.scss @@ -1,7 +1,7 @@ .banner-callout { display: flex; position: relative; - flex-wrap: wrap; + align-items: start; .banner-close { position: absolute; @@ -16,10 +16,25 @@ } .banner-graphic { - margin: 20px auto; + margin: 0 $gl-padding $gl-padding 0; } &.banner-non-empty-state { border-bottom: 1px solid $border-color; } + + @media (max-width: $screen-xs-max) { + justify-content: center; + flex-direction: column; + align-items: center; + + .banner-title, + .banner-buttons { + text-align: center; + } + + .banner-graphic { + margin-left: $gl-padding; + } + } } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 50d039348c4..02b8e5feee9 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -423,25 +423,43 @@ } } -.btn-link.btn-secondary-hover-link { - color: $gl-text-color-secondary; +.btn-link { + padding: 0; + background-color: transparent; + color: $blue-600; + font-weight: normal; + border-radius: 0; + border-color: transparent; &:hover, &:active, &:focus { - color: $gl-link-color; - text-decoration: none; + color: $blue-800; + text-decoration: underline; + background-color: transparent; + border-color: transparent; } -} -.btn-link.btn-primary-hover-link { - color: inherit; + &.btn-secondary-hover-link { + color: $gl-text-color-secondary; - &:hover, - &:active, - &:focus { - color: $gl-link-color; - text-decoration: none; + &:hover, + &:active, + &:focus { + color: $gl-link-color; + text-decoration: none; + } + } + + &.btn-primary-hover-link { + color: inherit; + + &:hover, + &:active, + &:focus { + color: $gl-link-color; + text-decoration: none; + } } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 4247fe3b271..f55bf0b7887 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -481,7 +481,8 @@ .dropdown-menu-selectable { li { - a { + a, + button { padding: 8px 40px; position: relative; diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss index b3720759129..bb185d2bd81 100644 --- a/app/assets/stylesheets/framework/snippets.scss +++ b/app/assets/stylesheets/framework/snippets.scss @@ -29,8 +29,10 @@ } .snippet-title { - font-size: 24px; + color: $gl-text-color; + font-size: 2em; font-weight: $gl-font-weight-bold; + min-height: $header-height; } .snippet-edited-ago { @@ -46,3 +48,26 @@ .snippet-scope-menu .btn-new { margin-top: 15px; } + +.snippet-embed-input { + height: 35px; +} + +.embed-snippet { + padding-right: 0; + padding-top: $gl-padding; + + .form-control { + cursor: auto; + width: 101%; + margin-left: -1px; + } + + .embed-toggle-list li button { + padding: 8px 40px; + } + + .embed-toggle { + height: 35px; + } +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 8ee1bb03d55..37223175199 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -714,20 +714,6 @@ $color-average-score: $orange-400; $color-low-score: $red-400; /* -Repo editor -*/ -$repo-editor-grey: #f6f7f9; -$repo-editor-grey-darker: #e9ebee; -$repo-editor-linear-gradient: linear-gradient( - to right, - $repo-editor-grey 0%, - $repo-editor-grey-darker, - 20%, - $repo-editor-grey 40%, - $repo-editor-grey 100% -); - -/* Performance Bar */ $perf-bar-text: #999; diff --git a/app/assets/stylesheets/highlight/embedded.scss b/app/assets/stylesheets/highlight/embedded.scss new file mode 100644 index 00000000000..44c8a1d39ec --- /dev/null +++ b/app/assets/stylesheets/highlight/embedded.scss @@ -0,0 +1,3 @@ +.code { + @import "white_base"; +} diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index c3d8f0c61a2..355c8d223f7 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -1,292 +1,3 @@ -/* https://github.com/aahan/pygments-github-style */ - -/* -* White Syntax Colors -*/ -$white-code-color: $gl-text-color; -$white-highlight: #fafe3d; -$white-pre-hll-bg: #f8eec7; -$white-hll-bg: #f8f8f8; -$white-over-bg: #ded7fc; -$white-expanded-border: #e0e0e0; -$white-expanded-bg: #f7f7f7; -$white-c: #998; -$white-err: #a61717; -$white-err-bg: #e3d2d2; -$white-cm: #998; -$white-cp: #999; -$white-c1: #998; -$white-cs: #999; -$white-gd: $black; -$white-gd-bg: #fdd; -$white-gd-x: $black; -$white-gd-x-bg: #faa; -$white-gr: #a00; -$white-gh: #999; -$white-gi: $black; -$white-gi-bg: #dfd; -$white-gi-x: $black; -$white-gi-x-bg: #afa; -$white-go: #888; -$white-gp: #555; -$white-gu: #800080; -$white-gt: #a00; -$white-kt: #458; -$white-m: #099; -$white-s: #d14; -$white-n: #333; -$white-na: teal; -$white-nb: #0086b3; -$white-nc: #458; -$white-no: teal; -$white-ni: purple; -$white-ne: #900; -$white-nf: #900; -$white-nn: #555; -$white-nt: navy; -$white-nv: teal; -$white-w: #bbb; -$white-mf: #099; -$white-mh: #099; -$white-mi: #099; -$white-mo: #099; -$white-sb: #d14; -$white-sc: #d14; -$white-sd: #d14; -$white-s2: #d14; -$white-se: #d14; -$white-sh: #d14; -$white-si: #d14; -$white-sx: #d14; -$white-sr: #009926; -$white-s1: #d14; -$white-ss: #990073; -$white-bp: #999; -$white-vc: teal; -$white-vg: teal; -$white-vi: teal; -$white-il: #099; -$white-gc-color: #999; -$white-gc-bg: #eaf2f5; - - -@mixin matchLine { - color: $black-transparent; - background-color: $gray-light; -} - .code.white { - // Line numbers - .line-numbers, - .diff-line-num { - background-color: $gray-light; - } - - .diff-line-num, - .diff-line-num a { - color: $black-transparent; - } - - // Code itself - pre.code, - .diff-line-num { - border-color: $white-normal; - } - - &, - pre.code, - .line_holder .line_content { - background-color: $white-light; - color: $white-code-color; - } - - // Diff line - .line_holder { - - &.match .line_content { - @include matchLine; - } - - .diff-line-num { - &.old { - background-color: $line-number-old; - border-color: $line-removed-dark; - - a { - color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%); - } - } - - &.new { - background-color: $line-number-new; - border-color: $line-added-dark; - - a { - color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%); - } - } - - &.is-over, - &.hll:not(.empty-cell).is-over { - background-color: $white-over-bg; - border-color: darken($white-over-bg, 5%); - - a { - color: darken($white-over-bg, 15%); - } - } - - &.hll:not(.empty-cell) { - background-color: $line-number-select; - border-color: $line-select-yellow-dark; - } - } - - &:not(.diff-expanded) + .diff-expanded, - &.diff-expanded + .line_holder:not(.diff-expanded) { - > .diff-line-num, - > .line_content { - border-top: 1px solid $white-expanded-border; - } - } - - &.diff-expanded { - > .diff-line-num, - > .line_content { - background: $white-expanded-bg; - border-color: $white-expanded-bg; - } - } - - .line_content { - &.old { - background-color: $line-removed; - - &::before { - color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%); - } - - span.idiff { - background-color: $line-removed-dark; - } - } - - &.new { - background-color: $line-added; - - &::before { - color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%); - } - - span.idiff { - background-color: $line-added-dark; - } - } - - &.match { - @include matchLine; - } - - &.hll:not(.empty-cell) { - background-color: $line-select-yellow; - } - } - } - - // highlight line via anchor - pre .hll { - background-color: $white-pre-hll-bg !important; - } - - // Search result highlight - span.highlight_word { - background-color: $white-highlight !important; - } - - // Links to URLs, emails, or dependencies - .line a { - color: $white-nb; - } - - .hll { background-color: $white-hll-bg; } - .c { color: $white-c; font-style: italic; } - .err { color: $white-err; background-color: $white-err-bg; } - .k { font-weight: $gl-font-weight-bold; } - .o { font-weight: $gl-font-weight-bold; } - .cm { color: $white-cm; font-style: italic; } - .cp { color: $white-cp; font-weight: $gl-font-weight-bold; } - .c1 { color: $white-c1; font-style: italic; } - .cs { color: $white-cs; font-weight: $gl-font-weight-bold; font-style: italic; } - - .gd { - color: $white-gd; - background-color: $white-gd-bg; - - .x { - color: $white-gd-x; - background-color: $white-gd-x-bg; - } - } - - .ge { font-style: italic; } - .gr { color: $white-gr; } - .gh { color: $white-gh; } - - .gi { - color: $white-gi; - background-color: $white-gi-bg; - - .x { - color: $white-gi-x; - background-color: $white-gi-x-bg; - } - } - - .go { color: $white-go; } - .gp { color: $white-gp; } - .gs { font-weight: $gl-font-weight-bold; } - .gu { color: $white-gu; font-weight: $gl-font-weight-bold; } - .gt { color: $white-gt; } - .kc { font-weight: $gl-font-weight-bold; } - .kd { font-weight: $gl-font-weight-bold; } - .kn { font-weight: $gl-font-weight-bold; } - .kp { font-weight: $gl-font-weight-bold; } - .kr { font-weight: $gl-font-weight-bold; } - .kt { color: $white-kt; font-weight: $gl-font-weight-bold; } - .m { color: $white-m; } - .s { color: $white-s; } - .n { color: $white-n; } - .na { color: $white-na; } - .nb { color: $white-nb; } - .nc { color: $white-nc; font-weight: $gl-font-weight-bold; } - .no { color: $white-no; } - .ni { color: $white-ni; } - .ne { color: $white-ne; font-weight: $gl-font-weight-bold; } - .nf { color: $white-nf; font-weight: $gl-font-weight-bold; } - .nn { color: $white-nn; } - .nt { color: $white-nt; } - .nv { color: $white-nv; } - .ow { font-weight: $gl-font-weight-bold; } - .w { color: $white-w; } - .mf { color: $white-mf; } - .mh { color: $white-mh; } - .mi { color: $white-mi; } - .mo { color: $white-mo; } - .sb { color: $white-sb; } - .sc { color: $white-sc; } - .sd { color: $white-sd; } - .s2 { color: $white-s2; } - .se { color: $white-se; } - .sh { color: $white-sh; } - .si { color: $white-si; } - .sx { color: $white-sx; } - .sr { color: $white-sr; } - .s1 { color: $white-s1; } - .ss { color: $white-ss; } - .bp { color: $white-bp; } - .vc { color: $white-vc; } - .vg { color: $white-vg; } - .vi { color: $white-vi; } - .il { color: $white-il; } - .gc { color: $white-gc-color; background-color: $white-gc-bg; } + @import "white_base"; } diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss new file mode 100644 index 00000000000..8cc5252648d --- /dev/null +++ b/app/assets/stylesheets/highlight/white_base.scss @@ -0,0 +1,290 @@ +/* https://github.com/aahan/pygments-github-style */ + +/* +* White Syntax Colors +*/ +$white-code-color: $gl-text-color; +$white-highlight: #fafe3d; +$white-pre-hll-bg: #f8eec7; +$white-hll-bg: #f8f8f8; +$white-over-bg: #ded7fc; +$white-expanded-border: #e0e0e0; +$white-expanded-bg: #f7f7f7; +$white-c: #998; +$white-err: #a61717; +$white-err-bg: #e3d2d2; +$white-cm: #998; +$white-cp: #999; +$white-c1: #998; +$white-cs: #999; +$white-gd: $black; +$white-gd-bg: #fdd; +$white-gd-x: $black; +$white-gd-x-bg: #faa; +$white-gr: #a00; +$white-gh: #999; +$white-gi: $black; +$white-gi-bg: #dfd; +$white-gi-x: $black; +$white-gi-x-bg: #afa; +$white-go: #888; +$white-gp: #555; +$white-gu: #800080; +$white-gt: #a00; +$white-kt: #458; +$white-m: #099; +$white-s: #d14; +$white-n: #333; +$white-na: teal; +$white-nb: #0086b3; +$white-nc: #458; +$white-no: teal; +$white-ni: purple; +$white-ne: #900; +$white-nf: #900; +$white-nn: #555; +$white-nt: navy; +$white-nv: teal; +$white-w: #bbb; +$white-mf: #099; +$white-mh: #099; +$white-mi: #099; +$white-mo: #099; +$white-sb: #d14; +$white-sc: #d14; +$white-sd: #d14; +$white-s2: #d14; +$white-se: #d14; +$white-sh: #d14; +$white-si: #d14; +$white-sx: #d14; +$white-sr: #009926; +$white-s1: #d14; +$white-ss: #990073; +$white-bp: #999; +$white-vc: teal; +$white-vg: teal; +$white-vi: teal; +$white-il: #099; +$white-gc-color: #999; +$white-gc-bg: #eaf2f5; + + +@mixin matchLine { + color: $black-transparent; + background-color: $gray-light; +} + + // Line numbers +.line-numbers, +.diff-line-num { + background-color: $gray-light; +} + +.diff-line-num, +.diff-line-num a { + color: $black-transparent; +} + +// Code itself +pre.code, +.diff-line-num { + border-color: $white-normal; +} + +&, +pre.code, +.line_holder .line_content { + background-color: $white-light; + color: $white-code-color; +} + +// Diff line +.line_holder { + + &.match .line_content { + @include matchLine; + } + + .diff-line-num { + &.old { + background-color: $line-number-old; + border-color: $line-removed-dark; + + a { + color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%); + } + } + + &.new { + background-color: $line-number-new; + border-color: $line-added-dark; + + a { + color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%); + } + } + + &.is-over, + &.hll:not(.empty-cell).is-over { + background-color: $white-over-bg; + border-color: darken($white-over-bg, 5%); + + a { + color: darken($white-over-bg, 15%); + } + } + + &.hll:not(.empty-cell) { + background-color: $line-number-select; + border-color: $line-select-yellow-dark; + } + } + + &:not(.diff-expanded) + .diff-expanded, + &.diff-expanded + .line_holder:not(.diff-expanded) { + > .diff-line-num, + > .line_content { + border-top: 1px solid $white-expanded-border; + } + } + + &.diff-expanded { + > .diff-line-num, + > .line_content { + background: $white-expanded-bg; + border-color: $white-expanded-bg; + } + } + + .line_content { + &.old { + background-color: $line-removed; + + &::before { + color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%); + } + + span.idiff { + background-color: $line-removed-dark; + } + } + + &.new { + background-color: $line-added; + + &::before { + color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%); + } + + span.idiff { + background-color: $line-added-dark; + } + } + + &.match { + @include matchLine; + } + + &.hll:not(.empty-cell) { + background-color: $line-select-yellow; + } + } +} + +// highlight line via anchor +pre .hll { + background-color: $white-pre-hll-bg !important; +} + + // Search result highlight +span.highlight_word { + background-color: $white-highlight !important; +} + + // Links to URLs, emails, or dependencies +.line a { + color: $white-nb; +} + +.hll { background-color: $white-hll-bg; } +.c { color: $white-c; font-style: italic; } +.err { color: $white-err; background-color: $white-err-bg; } +.k { font-weight: $gl-font-weight-bold; } +.o { font-weight: $gl-font-weight-bold; } +.cm { color: $white-cm; font-style: italic; } +.cp { color: $white-cp; font-weight: $gl-font-weight-bold; } +.c1 { color: $white-c1; font-style: italic; } +.cs { color: $white-cs; font-weight: $gl-font-weight-bold; font-style: italic; } + +.gd { + color: $white-gd; + background-color: $white-gd-bg; + + .x { + color: $white-gd-x; + background-color: $white-gd-x-bg; + } +} + +.ge { font-style: italic; } +.gr { color: $white-gr; } +.gh { color: $white-gh; } + +.gi { + color: $white-gi; + background-color: $white-gi-bg; + + .x { + color: $white-gi-x; + background-color: $white-gi-x-bg; + } +} + +.go { color: $white-go; } +.gp { color: $white-gp; } +.gs { font-weight: $gl-font-weight-bold; } +.gu { color: $white-gu; font-weight: $gl-font-weight-bold; } +.gt { color: $white-gt; } +.kc { font-weight: $gl-font-weight-bold; } +.kd { font-weight: $gl-font-weight-bold; } +.kn { font-weight: $gl-font-weight-bold; } +.kp { font-weight: $gl-font-weight-bold; } +.kr { font-weight: $gl-font-weight-bold; } +.kt { color: $white-kt; font-weight: $gl-font-weight-bold; } +.m { color: $white-m; } +.s { color: $white-s; } +.n { color: $white-n; } +.na { color: $white-na; } +.nb { color: $white-nb; } +.nc { color: $white-nc; font-weight: $gl-font-weight-bold; } +.no { color: $white-no; } +.ni { color: $white-ni; } +.ne { color: $white-ne; font-weight: $gl-font-weight-bold; } +.nf { color: $white-nf; font-weight: $gl-font-weight-bold; } +.nn { color: $white-nn; } +.nt { color: $white-nt; } +.nv { color: $white-nv; } +.ow { font-weight: $gl-font-weight-bold; } +.w { color: $white-w; } +.mf { color: $white-mf; } +.mh { color: $white-mh; } +.mi { color: $white-mi; } +.mo { color: $white-mo; } +.sb { color: $white-sb; } +.sc { color: $white-sc; } +.sd { color: $white-sd; } +.s2 { color: $white-s2; } +.se { color: $white-se; } +.sh { color: $white-sh; } +.si { color: $white-si; } +.sx { color: $white-sx; } +.sr { color: $white-sr; } +.s1 { color: $white-s1; } +.ss { color: $white-ss; } +.bp { color: $white-bp; } +.vc { color: $white-vc; } +.vg { color: $white-vg; } +.vi { color: $white-vi; } +.il { color: $white-il; } +.gc { color: $white-gc-color; background-color: $white-gc-bg; } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 8f9333b00ef..16e6d089d87 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -194,3 +194,38 @@ .issuable-row { background-color: $white-light; } + +.milestone-deprecation-message { + .popover { + padding: 0; + } + + .popover-content { + padding: 0; + } +} + +.milestone-popover-body { + padding: $gl-padding-8; + background-color: $gray-light; +} + +.milestone-popover-footer { + padding: $gl-padding-8 $gl-padding; + border-top: 1px solid $white-dark; +} + +.milestone-popover-instructions-list { + padding-left: 2em; + + > li { + padding-left: 1em; + } +} + +@media (max-width: $screen-xs-max) { + .milestone-banner-text, + .milestone-banner-link { + display: inline; + } +} diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 7427036e928..5015298e8ba 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -429,6 +429,7 @@ .projects-sidebar { display: flex; flex-direction: column; + height: 100%; .context-header { width: auto; @@ -438,8 +439,8 @@ .multi-file-commit-panel-inner { display: flex; - flex: 1; flex-direction: column; + height: 100%; } .multi-file-commit-panel-inner-scroll { diff --git a/app/assets/stylesheets/snippets.scss b/app/assets/stylesheets/snippets.scss new file mode 100644 index 00000000000..0d6b0735f70 --- /dev/null +++ b/app/assets/stylesheets/snippets.scss @@ -0,0 +1,156 @@ +@import "framework/variables"; + +.gitlab-embed-snippets { + @import "highlight/embedded"; + @import "framework/images"; + + $border-style: 1px solid $border-color; + + font-family: $regular_font; + font-size: $gl-font-size; + line-height: $code_line_height; + color: $gl-text-color; + margin: 20px; + font-weight: 200; + + .gl-snippet-icon { + display: inline-block; + background: url(asset_path('ext_snippet_icons/ext_snippet_icons.png')) no-repeat; + overflow: hidden; + text-align: left; + width: 16px; + height: 16px; + background-size: cover; + + &.gl-snippet-icon-doc_code { background-position: 0 0; } + &.gl-snippet-icon-doc_text { background-position: 0 -16px; } + &.gl-snippet-icon-download { background-position: 0 -32px; } + } + + .blob-viewer { + background-color: $white-light; + text-align: left; + } + + .file-content.code { + border: $border-style; + border-radius: 0 0 4px 4px; + display: flex; + box-shadow: none; + margin: 0; + padding: 0; + table-layout: fixed; + + .blob-content { + overflow-x: auto; + + pre { + padding: 10px; + border: 0; + border-radius: 0; + font-family: $monospace_font; + font-size: $code_font_size; + line-height: $code_line_height; + margin: 0; + overflow: auto; + overflow-y: hidden; + white-space: pre; + word-wrap: normal; + border-left: $border-style; + } + } + + .line-numbers { + padding: 10px; + text-align: right; + float: left; + + .diff-line-num { + font-family: $monospace_font; + display: block; + font-size: $code_font_size; + min-height: $code_line_height; + white-space: nowrap; + color: $black-transparent; + min-width: 30px; + } + + .diff-line-num:hover { + color: $almost-black; + cursor: pointer; + } + } + } + + .file-title-flex-parent { + display: flex; + align-items: center; + justify-content: space-between; + background-color: $gray-light; + border: $border-style; + border-bottom: 0; + padding: $gl-padding-top $gl-padding; + margin: 0; + border-radius: $border-radius-default $border-radius-default 0 0; + + .file-header-content { + .file-title-name { + font-weight: $gl-font-weight-bold; + } + + .gitlab-embedded-snippets-title { + text-decoration: none; + color: $gl-text-color; + + &:hover { + text-decoration: underline; + } + } + + .gitlab-logo { + display: inline-block; + padding-left: 5px; + text-decoration: none; + color: $gl-text-color-secondary; + + .logo-text { + background: image_url('ext_snippet_icons/logo.png') no-repeat left center; + background-size: 18px; + font-weight: $gl-font-weight-normal; + padding-left: 24px; + } + } + } + + img, + .gl-snippet-icon { + display: inline-block; + vertical-align: middle; + } + } + + .btn-group { + a.btn { + background-color: $white-light; + text-decoration: none; + padding: 7px 9px; + border: $border-style; + border-right: 0; + + &:hover { + background-color: $white-normal; + border-color: $border-white-normal; + text-decoration: none; + } + + &:first-child { + border-radius: 3px 0 0 3px; + } + + &:last-child { + border-radius: 0 3px 3px 0; + border-right: $border-style; + } + } + } +} diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 145f74d9e59..8958eab0423 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -57,22 +57,17 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController def application_setting_params params[:application_setting] ||= {} - import_sources = params[:application_setting][:import_sources] - if import_sources.nil? - params[:application_setting][:import_sources] = [] - else - import_sources.map! do |source| - source.to_str - end - end + if params[:application_setting].key?(:enabled_oauth_sign_in_sources) + enabled_oauth_sign_in_sources = params[:application_setting].delete(:enabled_oauth_sign_in_sources) + enabled_oauth_sign_in_sources&.delete("") - enabled_oauth_sign_in_sources = params[:application_setting].delete(:enabled_oauth_sign_in_sources) - - params[:application_setting][:disabled_oauth_sign_in_sources] = - AuthHelper.button_based_providers.map(&:to_s) - - Array(enabled_oauth_sign_in_sources) + params[:application_setting][:disabled_oauth_sign_in_sources] = + AuthHelper.button_based_providers.map(&:to_s) - + Array(enabled_oauth_sign_in_sources) + end + params[:application_setting][:import_sources]&.delete("") params[:application_setting][:restricted_visibility_levels]&.delete("") params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file] diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb index 9095cc7f783..120614739aa 100644 --- a/app/controllers/concerns/snippets_actions.rb +++ b/app/controllers/concerns/snippets_actions.rb @@ -17,6 +17,10 @@ module SnippetsActions end # rubocop:enable Gitlab/ModuleWithInstanceVariables + def js_request? + request.format.js? + end + private def convert_line_endings(content) diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 7c19aa7bb23..208a1d19862 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -5,6 +5,8 @@ class Projects::SnippetsController < Projects::ApplicationController include SnippetsActions include RendersBlob + skip_before_action :verify_authenticity_token, only: [:show], if: :js_request? + before_action :check_snippets_available! before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam] @@ -71,6 +73,7 @@ class Projects::SnippetsController < Projects::ApplicationController format.json do render_blob_json(blob) end + format.js { render 'shared/snippets/show'} end end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index be2d3f638ff..3d51520ddf4 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -6,6 +6,8 @@ class SnippetsController < ApplicationController include RendersBlob include PreviewMarkdown + skip_before_action :verify_authenticity_token, only: [:show], if: :js_request? + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] # Allow read snippet @@ -77,6 +79,8 @@ class SnippetsController < ApplicationController format.json do render_blob_json(blob) end + + format.js { render 'shared/snippets/show' } end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index b3b080e6dcf..3fbb32c5229 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -74,10 +74,12 @@ module ApplicationSettingsHelper css_class = 'btn' css_class << ' active' unless disabled checkbox_name = 'application_setting[enabled_oauth_sign_in_sources][]' + name = Gitlab::Auth::OAuth::Provider.label_for(source) label_tag(checkbox_name, class: css_class) do check_box_tag(checkbox_name, source, !disabled, - autocomplete: 'off') + Gitlab::Auth::OAuth::Provider.label_for(source) + autocomplete: 'off', + id: name.tr(' ', '_')) + name end end end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 08b7168b117..58372edff3c 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -43,6 +43,10 @@ module IconsHelper content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes) end + def external_snippet_icon(name) + content_tag(:span, "", class: "gl-snippet-icon gl-snippet-icon-#{name}") + end + def audit_icon(names, options = {}) case names when "standard" diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 00e7e4230b9..733832c1bbb 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -101,4 +101,39 @@ module SnippetsHelper # Return snippet with chunk array { snippet_object: snippet, snippet_chunks: snippet_chunks } end + + def snippet_embed + "<script src=\"#{url_for(only_path: false, overwrite_params: nil)}.js\"></script>" + end + + def embedded_snippet_raw_button + blob = @snippet.blob + return if blob.empty? || blob.raw_binary? || blob.stored_externally? + + snippet_raw_url = if @snippet.is_a?(PersonalSnippet) + raw_snippet_url(@snippet) + else + raw_project_snippet_url(@snippet.project, @snippet) + end + + link_to external_snippet_icon('doc_code'), snippet_raw_url, class: 'btn', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw' + end + + def embedded_snippet_download_button + download_url = if @snippet.is_a?(PersonalSnippet) + raw_snippet_url(@snippet, inline: false) + else + raw_project_snippet_url(@snippet.project, @snippet, inline: false) + end + + link_to external_snippet_icon('download'), download_url, class: 'btn', target: '_blank', title: 'Download', rel: 'noopener noreferrer' + end + + def public_snippet? + if @snippet.project_id? + can?(nil, :read_project_snippet, @snippet) + else + can?(nil, :read_personal_snippet, @snippet) + end + end end diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml index 2f06c1322f9..4d74568d69a 100644 --- a/app/views/admin/application_settings/_signin.html.haml +++ b/app/views/admin/application_settings/_signin.html.haml @@ -23,7 +23,8 @@ must be used to authenticate. - if omniauth_enabled? && button_based_providers.any? .form-group.row - = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'col-form-label col-sm-2' + = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2' + = hidden_field_tag 'application_setting[enabled_oauth_sign_in_sources][]' .col-sm-10 .btn-group{ data: { toggle: 'buttons' } } - oauth_providers_checkboxes.each do |source| diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml index 10e9f0b0303..c37a89237f0 100644 --- a/app/views/admin/application_settings/_visibility_and_access.html.haml +++ b/app/views/admin/application_settings/_visibility_and_access.html.haml @@ -32,6 +32,7 @@ .form-group.row = f.label :import_sources, class: 'col-form-label col-sm-2' .col-sm-10 + = hidden_field_tag 'application_setting[import_sources][]' - import_sources_checkboxes('import-sources-help').each do |source| .form-check= source %span.form-text.text-muted#import-sources-help diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 20cf024d703..78706492ba3 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -126,6 +126,7 @@ GitLab %span.float-right = Gitlab::VERSION + = "(#{Gitlab::REVISION})" %p GitLab Shell %span.float-right diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index d1c3c7ed294..e35ebdea435 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -20,7 +20,7 @@ - else %p Download the Google Authenticator application from App Store or Google Play Store and scan this code. - More information is available in the #{link_to('documentation', help_page_path('profile/two_factor_authentication'))}. + More information is available in the #{link_to('documentation', help_page_path('user/profile/account/two_factor_authentication'))}. .row.append-bottom-10 .col-md-4 = raw @qr_code diff --git a/app/views/projects/blob/_viewer.html.haml b/app/views/projects/blob/_viewer.html.haml index 3124443b4e4..9c760c81527 100644 --- a/app/views/projects/blob/_viewer.html.haml +++ b/app/views/projects/blob/_viewer.html.haml @@ -2,6 +2,7 @@ - render_error = viewer.render_error - rich_type = viewer.type == :rich ? viewer.partial_name : nil - load_async = local_assigns.fetch(:load_async, viewer.load_async? && render_error.nil?) +- external_embed = local_assigns.fetch(:external_embed, false) - viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_async .blob-viewer{ data: { type: viewer.type, rich_type: rich_type, url: viewer_url }, class: ('hidden' if hidden) } @@ -9,6 +10,8 @@ = render 'projects/blob/render_error', viewer: viewer - elsif load_async = render viewer.loading_partial_path, viewer: viewer + - elsif external_embed + = render 'projects/blob/viewers/highlight_embed', blob: viewer.blob - else - viewer.prepare! diff --git a/app/views/projects/blob/viewers/_highlight_embed.html.haml b/app/views/projects/blob/viewers/_highlight_embed.html.haml new file mode 100644 index 00000000000..9bd4ef6ad0b --- /dev/null +++ b/app/views/projects/blob/viewers/_highlight_embed.html.haml @@ -0,0 +1,7 @@ +.file-content.code.js-syntax-highlight + .line-numbers + - if blob.data.present? + - blob.data.each_line.each_with_index do |_, index| + %span.diff-line-num= index + 1 + .blob-content{ data: { blob_id: blob.id } } + = highlight(blob.path, blob.data, repository: nil, plain: blob.no_highlighting?) diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml index 934d65e8b42..e9ac192f5f7 100644 --- a/app/views/shared/_auto_devops_callout.html.haml +++ b/app/views/shared/_auto_devops_callout.html.haml @@ -1,14 +1,14 @@ -.js-autodevops-banner.banner-callout.banner-non-empty-state.append-bottom-20{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } } +.js-autodevops-banner.banner-callout.banner-non-empty-state.append-bottom-20.prepend-top-10{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } } .banner-graphic = custom_icon('icon_autodevops') - .prepend-top-10.prepend-left-10.append-bottom-10 - %h5= s_('AutoDevOps|Auto DevOps (Beta)') + .banner-body.prepend-left-10.append-bottom-10 + %h5.banner-title= s_('AutoDevOps|Auto DevOps (Beta)') %p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.') %p - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer') = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link } - .prepend-top-10 + .banner-buttons = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn js-close-callout' %button.btn-transparent.banner-close.close.js-close-callout{ type: 'button', diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml index 4466f9fcd06..21e1e1634b2 100644 --- a/app/views/shared/members/_group.html.haml +++ b/app/views/shared/members/_group.html.haml @@ -8,7 +8,7 @@ %strong = link_to group.full_name, group_path(group) .cgray - Joined #{time_ago_with_tooltip(group.created_at)} + Given access #{time_ago_with_tooltip(group_link.created_at)} - if group_link.expires? · %span{ class: ('text-warning' if group_link.expires_soon?) } diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index a96321bf4fb..8826a6aadc8 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -29,7 +29,7 @@ Requested = time_ago_with_tooltip(member.requested_at) - else - Joined #{time_ago_with_tooltip(member.created_at)} + Given access #{time_ago_with_tooltip(member.created_at)} - if member.expires? · %span{ class: "#{"text-warning" if member.expires_soon?} has-tooltip", title: member.expires_at.to_time.in_time_zone.to_s(:medium) } diff --git a/app/views/shared/milestones/_deprecation_message.html.haml b/app/views/shared/milestones/_deprecation_message.html.haml new file mode 100644 index 00000000000..4a8f90937ea --- /dev/null +++ b/app/views/shared/milestones/_deprecation_message.html.haml @@ -0,0 +1,14 @@ +.banner-callout.compact.milestone-deprecation-message.js-milestone-deprecation-message.prepend-top-20 + .banner-graphic= image_tag 'illustrations/milestone_removing-page.svg' + .banner-body.prepend-left-10.append-right-10 + %h5.banner-title.prepend-top-0= _('This page will be removed in a future release.') + %p.milestone-banner-text= _('Use group milestones to manage issues from multiple projects in the same milestone.') + = button_tag _('Promote these project milestones into a group milestone.'), class: 'btn btn-link js-popover-link text-align-left milestone-banner-link' + .milestone-banner-buttons.prepend-top-20= link_to _('Learn more'), help_page_url('user/project/milestones/index', anchor: 'promoting-project-milestones-to-group-milestones'), class: 'btn btn-default', target: '_blank' + + %template.js-milestone-deprecation-message-template + .milestone-popover-body + %ol.milestone-popover-instructions-list.append-bottom-0 + %li= _('Click any <strong>project name</strong> in the project list below to navigate to the project milestone.').html_safe + %li= _('Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone.').html_safe + .milestone-popover-footer= link_to _('Learn more'), help_page_url('user/project/milestones/index', anchor: 'promoting-project-milestones-to-group-milestones'), class: 'btn btn-link prepend-left-0', target: '_blank' diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index b47f2358606..6acf671c432 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -1,7 +1,8 @@ -- page_title @milestone.title +- page_title milestone.title - @breadcrumb_link = dashboard_milestone_path(milestone.safe_title, title: milestone.title) - group = local_assigns[:group] +- is_dynamic_milestone = milestone.legacy_group_milestone? || milestone.dashboard_milestone? .detail-page-header %a.btn.btn-secondary.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: "#" } @@ -31,21 +32,23 @@ - else = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" += render 'shared/milestones/deprecation_message' if is_dynamic_milestone + .detail-page-description.milestone-detail %h2.title = markdown_field(milestone, :title) - - if @milestone.group_milestone? && @milestone.description.present? + - if milestone.group_milestone? && milestone.description.present? %div .description .wiki - = markdown_field(@milestone, :description) + = markdown_field(milestone, :description) - if milestone.complete?(current_user) && milestone.active? .alert.alert-success.prepend-top-default - close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.' %span All issues for this milestone are closed. #{close_msg} -- if @milestone.legacy_group_milestone? || @milestone.dashboard_milestone? +- if is_dynamic_milestone .table-holder %table.table %thead @@ -68,7 +71,7 @@ Open %td = ms.expires_at -- elsif @milestone.group_milestone? +- elsif milestone.group_milestone? %br View = link_to 'Issues', issues_group_path(@group, milestone_title: milestone.title) diff --git a/app/views/shared/snippets/_embed.html.haml b/app/views/shared/snippets/_embed.html.haml new file mode 100644 index 00000000000..2d93e51a2d9 --- /dev/null +++ b/app/views/shared/snippets/_embed.html.haml @@ -0,0 +1,24 @@ +- blob = @snippet.blob +.gitlab-embed-snippets + .js-file-title.file-title-flex-parent + .file-header-content + = external_snippet_icon('doc_text') + + %strong.file-title-name + %a.gitlab-embedded-snippets-title{ href: url_for(only_path: false, overwrite_params: nil) } + = blob.name + + %small + = number_to_human_size(blob.raw_size) + %a.gitlab-logo{ href: url_for(only_path: false, overwrite_params: nil), title: 'view on gitlab' } + on + %span.logo-text + GitLab + + .file-actions.hidden-xs + .btn-group{ role: "group" }< + = embedded_snippet_raw_button + + = embedded_snippet_download_button + %article.file-holder.snippet-file-content + = render 'projects/blob/viewer', viewer: @snippet.blob.simple_viewer, load_async: false, external_embed: true diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 52fe3be92fe..c75cc8d32f0 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -19,11 +19,32 @@ %h2.snippet-title.prepend-top-0.append-bottom-0 = markdown_field(@snippet, :title) - - if @snippet.updated_at != @snippet.created_at - = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago', exclude_author: true) - if @snippet.description.present? .description .wiki = markdown_field(@snippet, :description) %textarea.hidden.js-task-list-field = @snippet.description + + - if @snippet.updated_at != @snippet.created_at + = edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago', exclude_author: true) + + - if public_snippet? + .embed-snippet + .input-group + .input-group-btn + %button.btn.embed-toggle{ 'data-toggle': 'dropdown', type: 'button' } + %span.js-embed-action= _("Embed") + = sprite_icon('angle-down', size: 12) + %ul.dropdown-menu.dropdown-menu-selectable.embed-toggle-list + %li + %button.js-embed-btn.btn.btn-transparent.is-active{ type: 'button' } + %strong.embed-toggle-list-item= _("Embed") + %li + %button.js-share-btn.btn.btn-transparent{ type: 'button' } + %strong.embed-toggle-list-item= _("Share") + %input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed } + .input-group-btn + %button.js-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '#snippet-url-area' } + = sprite_icon('duplicate', size: 16) + .clearfix diff --git a/app/views/shared/snippets/show.js.haml b/app/views/shared/snippets/show.js.haml new file mode 100644 index 00000000000..a9af732bbb5 --- /dev/null +++ b/app/views/shared/snippets/show.js.haml @@ -0,0 +1,2 @@ +document.write('#{escape_javascript(stylesheet_link_tag "#{stylesheet_url 'snippets'}")}'); +document.write('#{escape_javascript(render 'shared/snippets/embed')}'); diff --git a/changelogs/no-rm-rf-gitlab-basics.yml b/changelogs/no-rm-rf-gitlab-basics.yml new file mode 100644 index 00000000000..d5aa1091b45 --- /dev/null +++ b/changelogs/no-rm-rf-gitlab-basics.yml @@ -0,0 +1,5 @@ +--- + title: Do not use '-f' with 'rm' in gitlab-basics docs + merge_request: 18027 + author: Elias Werberich + type: changed diff --git a/changelogs/unreleased/30739-fix-joined-information-on-project-members-page.yml b/changelogs/unreleased/30739-fix-joined-information-on-project-members-page.yml new file mode 100644 index 00000000000..f2d5b503661 --- /dev/null +++ b/changelogs/unreleased/30739-fix-joined-information-on-project-members-page.yml @@ -0,0 +1,5 @@ +--- +title: Fix `joined` information on project members page +merge_request: 18290 +author: Fabian Schneider +type: fixed diff --git a/changelogs/unreleased/44582-clear-pipeline-status-cache.yml b/changelogs/unreleased/44582-clear-pipeline-status-cache.yml new file mode 100644 index 00000000000..1777f2ffaab --- /dev/null +++ b/changelogs/unreleased/44582-clear-pipeline-status-cache.yml @@ -0,0 +1,5 @@ +--- +title: Now `rake cache:clear` will also clear pipeline status cache +merge_request: 18257 +author: +type: fixed diff --git a/changelogs/unreleased/8088_embedded_snippets_support.yml b/changelogs/unreleased/8088_embedded_snippets_support.yml new file mode 100644 index 00000000000..7bd77a69dbd --- /dev/null +++ b/changelogs/unreleased/8088_embedded_snippets_support.yml @@ -0,0 +1,5 @@ +--- +title: Adds Embedded Snippets Support +merge_request: 15695 +author: haseebeqx +type: added diff --git a/changelogs/unreleased/ab-45247-project-lookups-validation.yml b/changelogs/unreleased/ab-45247-project-lookups-validation.yml new file mode 100644 index 00000000000..cd5ebdebc58 --- /dev/null +++ b/changelogs/unreleased/ab-45247-project-lookups-validation.yml @@ -0,0 +1,5 @@ +--- +title: Validate project path prior to hitting the database. +merge_request: 18322 +author: +type: performance diff --git a/changelogs/unreleased/ash-mckenzie-include-sha-with-version.yml b/changelogs/unreleased/ash-mckenzie-include-sha-with-version.yml new file mode 100644 index 00000000000..b49c48e0fe1 --- /dev/null +++ b/changelogs/unreleased/ash-mckenzie-include-sha-with-version.yml @@ -0,0 +1,5 @@ +--- +title: git SHA is now displayed alongside the GitLab version on the Admin Dashboard +merge_request: +author: +type: added diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-comments-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-comments-feature.yml new file mode 100644 index 00000000000..e7077f27555 --- /dev/null +++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-comments-feature.yml @@ -0,0 +1,5 @@ +--- +title: Replace the `project/commits/comments.feature` spinach test with an rspec analog +merge_request: 18356 +author: "@blackst0ne" +type: other diff --git a/changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml b/changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml new file mode 100644 index 00000000000..3e1ac7b795d --- /dev/null +++ b/changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml @@ -0,0 +1,5 @@ +--- +title: Add deprecation message to dynamic milestone pages +merge_request: 17505 +author: +type: added diff --git a/changelogs/unreleased/docs-for-failure-reason-tooltip.yml b/changelogs/unreleased/docs-for-failure-reason-tooltip.yml new file mode 100644 index 00000000000..ef37654b189 --- /dev/null +++ b/changelogs/unreleased/docs-for-failure-reason-tooltip.yml @@ -0,0 +1,5 @@ +--- +title: Add documentation for Pipelines failure reasons +merge_request: 18352 +author: +type: other diff --git a/changelogs/unreleased/feature-add-language-in-repository-to-api.yml b/changelogs/unreleased/feature-add-language-in-repository-to-api.yml new file mode 100644 index 00000000000..bd9bd377212 --- /dev/null +++ b/changelogs/unreleased/feature-add-language-in-repository-to-api.yml @@ -0,0 +1,5 @@ +--- +title: 'API: add languages of project GET /projects/:id/languages' +merge_request: 17770 +author: Roger Rüttimann +type: added diff --git a/changelogs/unreleased/fix-references-in-group-context.yml b/changelogs/unreleased/fix-references-in-group-context.yml new file mode 100644 index 00000000000..b436c2089ed --- /dev/null +++ b/changelogs/unreleased/fix-references-in-group-context.yml @@ -0,0 +1,5 @@ +--- +title: Ignore project internal references in group context +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fj-change-gollum-gems-to-custom-ones.yml b/changelogs/unreleased/fj-change-gollum-gems-to-custom-ones.yml new file mode 100644 index 00000000000..53883e8d907 --- /dev/null +++ b/changelogs/unreleased/fj-change-gollum-gems-to-custom-ones.yml @@ -0,0 +1,5 @@ +--- +title: Replacing gollum libraries for gitlab custom libs +merge_request: 18343 +author: +type: other diff --git a/changelogs/unreleased/ide-mr-changes-alert-box.yml b/changelogs/unreleased/ide-mr-changes-alert-box.yml new file mode 100644 index 00000000000..fec2719c2b1 --- /dev/null +++ b/changelogs/unreleased/ide-mr-changes-alert-box.yml @@ -0,0 +1,5 @@ +--- +title: Removed alert box in IDE when redirecting to new merge request +merge_request: +author: +type: fixed diff --git a/config/application.rb b/config/application.rb index 13501d4bdb5..ad7338763f7 100644 --- a/config/application.rb +++ b/config/application.rb @@ -113,6 +113,7 @@ module Gitlab config.assets.precompile << "performance_bar.css" config.assets.precompile << "lib/ace.js" config.assets.precompile << "test.css" + config.assets.precompile << "snippets.css" config.assets.precompile << "locale/**/app.js" # Import gitlab-svgs directly from vendored directory diff --git a/config/initializers/deprecations.rb b/config/initializers/deprecations.rb new file mode 100644 index 00000000000..f3f47b2ccf0 --- /dev/null +++ b/config/initializers/deprecations.rb @@ -0,0 +1,5 @@ +deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab') + +if Gitlab.com? || Rails.env.development? + ActiveSupport::Deprecation.deprecate_methods(Gitlab::GitalyClient::StorageSettings, :legacy_disk_path, deprecator: deprecator) +end diff --git a/config/initializers/gollum.rb b/config/initializers/gollum.rb index 6dfaceb8427..81e0577a7c9 100644 --- a/config/initializers/gollum.rb +++ b/config/initializers/gollum.rb @@ -7,139 +7,6 @@ module Gollum end require "gollum-lib" -module Gollum - class Committer - # Patch for UTF-8 path - def method_missing(name, *args) - index.send(name, *args) - end - end - - class Wiki - def pages(treeish = nil, limit: nil) - tree_list((treeish || @ref), limit: limit) - end - - def tree_list(ref, limit: nil) - if (sha = @access.ref_to_sha(ref)) - commit = @access.commit(sha) - tree_map_for(sha).inject([]) do |list, entry| - next list unless @page_class.valid_page_name?(entry.name) - - list << entry.page(self, commit) - break list if limit && list.size >= limit - - list - end - else - [] - end - end - - # Remove if https://github.com/gollum/gollum-lib/pull/292 has been merged - def update_page(page, name, format, data, commit = {}) - name = name ? ::File.basename(name) : page.name - format ||= page.format - dir = ::File.dirname(page.path) - dir = '' if dir == '.' - filename = (rename = page.name != name) ? Gollum::Page.cname(name) : page.filename_stripped - - multi_commit = !!commit[:committer] - committer = multi_commit ? commit[:committer] : Committer.new(self, commit) - - if !rename && page.format == format - committer.add(page.path, normalize(data)) - else - committer.delete(page.path) - committer.add_to_index(dir, filename, format, data) - end - - committer.after_commit do |index, _sha| - @access.refresh - index.update_working_dir(dir, page.filename_stripped, page.format) - index.update_working_dir(dir, filename, format) - end - - multi_commit ? committer : committer.commit - end - - # Remove if https://github.com/gollum/gollum-lib/pull/292 has been merged - def rename_page(page, rename, commit = {}) - return false if page.nil? - return false if rename.nil? || rename.empty? - - (target_dir, target_name) = ::File.split(rename) - (source_dir, source_name) = ::File.split(page.path) - source_name = page.filename_stripped - - # File.split gives us relative paths with ".", commiter.add_to_index doesn't like that. - target_dir = '' if target_dir == '.' - source_dir = '' if source_dir == '.' - target_dir = target_dir.gsub(/^\//, '') # rubocop:disable Style/RegexpLiteral - - # if the rename is a NOOP, abort - if source_dir == target_dir && source_name == target_name - return false - end - - multi_commit = !!commit[:committer] - committer = multi_commit ? commit[:committer] : Committer.new(self, commit) - - # This piece only works for multi_commit - # If we are in a commit batch and one of the previous operations - # has updated the page, any information we ask to the page can be outdated. - # Therefore, we should ask first to the current committer tree to see if - # there is any updated change. - raw_data = raw_data_in_committer(committer, source_dir, page.filename) || - raw_data_in_committer(committer, source_dir, "#{target_name}.#{Page.format_to_ext(page.format)}") || - page.raw_data - - committer.delete(page.path) - committer.add_to_index(target_dir, target_name, page.format, raw_data) - - committer.after_commit do |index, _sha| - @access.refresh - index.update_working_dir(source_dir, source_name, page.format) - index.update_working_dir(target_dir, target_name, page.format) - end - - multi_commit ? committer : committer.commit - end - - # Remove if https://github.com/gollum/gollum-lib/pull/292 has been merged - def raw_data_in_committer(committer, dir, filename) - data = nil - - [*dir.split(::File::SEPARATOR), filename].each do |key| - data = data ? data[key] : committer.tree[key] - break unless data - end - - data - end - end - - module Git - class Git - def tree_entry(commit, path) - pathname = Pathname.new(path) - tmp_entry = nil - - pathname.each_filename do |dir| - tmp_entry = if tmp_entry.nil? - commit.tree[dir] - else - @repo.lookup(tmp_entry[:oid])[dir] - end - - return nil unless tmp_entry - end - tmp_entry - end - end - end -end - Rails.application.configure do config.after_initialize do Gollum::Page.per_page = Kaminari.config.default_per_page diff --git a/doc/README.md b/doc/README.md index 178e6567845..080341e4f4f 100644 --- a/doc/README.md +++ b/doc/README.md @@ -183,7 +183,7 @@ instant how code changes impact your production environment. ### Git and GitLab - [Git](topics/git/index.md): Getting started with Git, branching strategies, Git LFS, advanced use. -- [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf): Download a PDF describing the most used Git operations. +- [Git cheatsheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf): Download a PDF describing the most used Git operations. - [GitLab Flow](workflow/gitlab_flow.md): explore the best of Git with the GitLab Flow strategy. ## Administrator documentation diff --git a/doc/api/projects.md b/doc/api/projects.md index a0cb5aa0820..7ffe380e275 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -915,6 +915,29 @@ Example response: } ``` +## Languages + +Get languages used in a project with percentage value. + +``` +GET /projects/:id/languages +``` + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/languages" +``` + +Example response: + +```json +{ + "Ruby": 66.69, + "JavaScript": 22.98, + "HTML": 7.91, + "CoffeeScript": 2.42 +} +``` + ## Archive a project Archives the project if the user is either admin or the project owner of this project. This action is diff --git a/doc/api/todos.md b/doc/api/todos.md index dd4c737b729..27e623007cc 100644 --- a/doc/api/todos.md +++ b/doc/api/todos.md @@ -15,7 +15,7 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `action` | string | no | The action to be filtered. Can be `assigned`, `mentioned`, `build_failed`, `marked`, or `approval_required`. | +| `action` | string | no | The action to be filtered. Can be `assigned`, `mentioned`, `build_failed`, `marked`, `approval_required`, `unmergeable` or `directly_addressed`. | | `author_id` | integer | no | The ID of an author | | `project_id` | integer | no | The ID of a project | | `state` | string | no | The state of the todo. Can be either `pending` or `done` | diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index 183808641c0..07b144f6ddd 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -101,12 +101,12 @@ In order to do that, follow the steps: --registration-token REGISTRATION_TOKEN \ --executor docker \ --description "My Docker Runner" \ - --docker-image "docker:latest" \ + --docker-image "docker:stable" \ --docker-privileged ``` The above command will register a new Runner to use the special - `docker:latest` image which is provided by Docker. **Notice that it's using + `docker:stable` image which is provided by Docker. **Notice that it's using the `privileged` mode to start the build and service containers.** If you want to use [docker-in-docker] mode, you always have to use `privileged = true` in your Docker containers. @@ -120,7 +120,7 @@ In order to do that, follow the steps: executor = "docker" [runners.docker] tls_verify = false - image = "docker:latest" + image = "docker:stable" privileged = true disable_cache = false volumes = ["/cache"] @@ -132,7 +132,7 @@ In order to do that, follow the steps: `docker:dind` service): ```yaml - image: docker:latest + image: docker:stable # When using dind, it's wise to use the overlayfs driver for # improved performance. @@ -201,12 +201,12 @@ In order to do that, follow the steps: --registration-token REGISTRATION_TOKEN \ --executor docker \ --description "My Docker Runner" \ - --docker-image "docker:latest" \ + --docker-image "docker:stable" \ --docker-volumes /var/run/docker.sock:/var/run/docker.sock ``` The above command will register a new Runner to use the special - `docker:latest` image which is provided by Docker. **Notice that it's using + `docker:stable` image which is provided by Docker. **Notice that it's using the Docker daemon of the Runner itself, and any containers spawned by docker commands will be siblings of the Runner rather than children of the runner.** This may have complications and limitations that are unsuitable for your workflow. @@ -220,7 +220,7 @@ In order to do that, follow the steps: executor = "docker" [runners.docker] tls_verify = false - image = "docker:latest" + image = "docker:stable" privileged = false disable_cache = false volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"] @@ -232,7 +232,7 @@ In order to do that, follow the steps: include the `docker:dind` service as when using the Docker in Docker executor): ```yaml - image: docker:latest + image: docker:stable before_script: - docker info @@ -286,7 +286,7 @@ any image that's used with the `--cache-from` argument must first be pulled Here's a simple `.gitlab-ci.yml` file showing how Docker caching can be utilized: ```yaml -image: docker:latest +image: docker:stable services: - docker:dind @@ -388,7 +388,7 @@ could look like: ```yaml build: - image: docker:latest + image: docker:stable services: - docker:dind stage: build @@ -434,7 +434,7 @@ when needed. Changes to `master` also get tagged as `latest` and deployed using an application-specific deploy script: ```yaml -image: docker:latest +image: docker:stable services: - docker:dind diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index bc5d3840368..7c0f837ea9c 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -86,7 +86,7 @@ services](#accessing-the-services). ### How the health check of services works Services are designed to provide additional functionality which is **network accessible**. -It may be a database like MySQL, or Redis, and even `docker:dind` which +It may be a database like MySQL, or Redis, and even `docker:stable-dind` which allows you to use Docker in Docker. It can be practically anything that is required for the CI/CD job to proceed and is accessed by network. diff --git a/doc/ci/examples/browser_performance.md b/doc/ci/examples/browser_performance.md index 691370d7195..0dab07a7f80 100644 --- a/doc/ci/examples/browser_performance.md +++ b/doc/ci/examples/browser_performance.md @@ -17,7 +17,7 @@ performance: variables: URL: https://example.com services: - - docker:dind + - docker:stable-dind script: - mkdir gitlab-exporter - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js @@ -94,7 +94,7 @@ performance: stage: performance image: docker:git services: - - docker:dind + - docker:stable-dind dependencies: - review script: diff --git a/doc/ci/img/job_failure_reason.png b/doc/ci/img/job_failure_reason.png Binary files differnew file mode 100644 index 00000000000..a60ce1fb21c --- /dev/null +++ b/doc/ci/img/job_failure_reason.png diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md index 301cccc80a3..f14e0527998 100644 --- a/doc/ci/pipelines.md +++ b/doc/ci/pipelines.md @@ -73,6 +73,21 @@ cancel the job, retry it, or erase the job trace. ![Pipelines example](img/pipelines.png) +## Seeing the failure reason for jobs + +> [Introduced][ce-5742] in GitLab 10.7. + +When a pipeline fails or is allowed to fail, there are several places where you +can quickly check the reason it failed: + +- **In the pipeline graph** present on the pipeline detail view. +- **In the pipeline widgets** present in the merge requests and commit pages. +- **In the job views** present in the global and detailed views of a job. + +In any case, if you hover over the failed job you can see the reason it failed. + +![Pipeline detail](img/job_failure_reason.png) + ## Pipeline graphs > [Introduced][ce-5742] in GitLab 8.11. @@ -263,4 +278,5 @@ runners will not use regular runners, they must be tagged accordingly. [ce-6242]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6242 [ce-7931]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7931 [ce-9760]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9760 +[ce-17782]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17782 [regexp]: https://gitlab.com/gitlab-org/gitlab-ce/blob/2f3dc314f42dbd79813e6251792853bc231e69dd/app/models/commit_status.rb#L99 diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index bd7ffb2befb..623e7d662a3 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -308,7 +308,9 @@ except master. ## `only` and `except` (complex) -> Introduced in GitLab 10.0 +> `refs` and `kubernetes` policies introduced in GitLab 10.0 + +> `variables` policy introduced in 10.7 CAUTION: **Warning:** This an _alpha_ feature, and it it subject to change at any time without diff --git a/doc/gitlab-basics/command-line-commands.md b/doc/gitlab-basics/command-line-commands.md index 2a531193adf..c9766040234 100644 --- a/doc/gitlab-basics/command-line-commands.md +++ b/doc/gitlab-basics/command-line-commands.md @@ -71,7 +71,7 @@ rm NAME-OF-FILE ### Remove a directory and all of its contents ``` -rm -rf NAME-OF-DIRECTORY +rm -r NAME-OF-DIRECTORY ``` ### View history in the command line diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md index 1f53e12d5f8..3feef8cbd19 100644 --- a/doc/install/kubernetes/gitlab_runner_chart.md +++ b/doc/install/kubernetes/gitlab_runner_chart.md @@ -80,7 +80,7 @@ runners: image: ubuntu:16.04 ## Run all containers with the privileged flag enabled - ## This will allow the docker:dind image to run if you need to run Docker + ## This will allow the docker:stable-dind image to run if you need to run Docker ## commands. Please read the docs before turning this on: ## ref: https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-dind ## @@ -147,7 +147,7 @@ enable privileged mode in `values.yaml`: ```yaml runners: ## Run all containers with the privileged flag enabled - ## This will allow the docker:dind image to run if you need to run Docker + ## This will allow the docker:stable-dind image to run if you need to run Docker ## commands. Please read the docs before turning this on: ## ref: https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-dind ## diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 89b8ea209b3..fb2ce27bf49 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -383,12 +383,12 @@ into your project to enable staging and canary deployments, and more. ### Custom buildpacks If the automatic buildpack detection fails for your project, or if you want to -use a custom buildpack, you can override the buildpack using a project variable -or a `.buildpack` file in your project: +use a custom buildpack, you can override the buildpack(s) using a project variable +or a `.buildpacks` file in your project: - **Project variable** - Create a project variable `BUILDPACK_URL` with the URL of the buildpack to use. -- **`.buildpack` file** - Add a file in your project's repo called `.buildpack` +- **`.buildpacks` file** - Add a file in your project's repo called `.buildpacks` and add the URL of the buildpack to use on a line in the file. If you want to use multiple buildpacks, you can enter them in, one on each line. diff --git a/doc/user/project/integrations/custom_issue_tracker.md b/doc/user/project/integrations/custom_issue_tracker.md index 731291ebe84..6fc083170b6 100644 --- a/doc/user/project/integrations/custom_issue_tracker.md +++ b/doc/user/project/integrations/custom_issue_tracker.md @@ -15,8 +15,8 @@ in the table below. Once you have configured and enabled Custom Issue Tracker Service you'll see a link on the GitLab project pages that takes you to that custom issue tracker. - ## Referencing issues -Issues are referenced with `#<ID>`, where `<ID>` is a number (example `#143`). -So with the example above, `#143` would refer to `https://customissuetracker.com/project-name/143`.
\ No newline at end of file +- Issues are referenced with `ANYTHING-<ID>`, where `ANYTHING` can be any string and `<ID>` is a number used in the target project of the custom integration (example `PROJECT-143`). +- `ANYTHING` is a placeholder to differentiate against GitLab issues, which are referenced with `#<ID>`. You can use a project name or project key to replace it for example. +- So with the example above, `PROJECT-143` would refer to `https://customissuetracker.com/project-name/143`.
\ No newline at end of file diff --git a/features/project/commits/comments.feature b/features/project/commits/comments.feature deleted file mode 100644 index fafb54b183a..00000000000 --- a/features/project/commits/comments.feature +++ /dev/null @@ -1,51 +0,0 @@ -@project_commits -Feature: Project Commits Comments - Background: - Given I sign in as a user - And I own project "Shop" - And I visit project commit page - - @javascript - Scenario: I can comment on a commit - Given I leave a comment like "XML attached" - Then I should see a comment saying "XML attached" - - @javascript - Scenario: I can't cancel the main form - Then I should not see the cancel comment button - - @javascript - Scenario: I can preview with text - Given I write a comment like ":+1: Nice" - Then The comment preview tab should be display rendered Markdown - - @javascript - Scenario: I preview a comment - Given I preview a comment text like "Bug fixed :smile:" - Then I should see the comment preview - And I should not see the comment text field - - @javascript - Scenario: I can edit after preview - Given I preview a comment text like "Bug fixed :smile:" - Then I should see the comment write tab - - @javascript - Scenario: I have a reset form after posting from preview - Given I preview a comment text like "Bug fixed :smile:" - And I submit the comment - Then I should see an empty comment text field - And I should not see the comment preview - - @javascript - Scenario: I can delete a comment - Given I leave a comment like "XML attached" - Then I should see a comment saying "XML attached" - And I delete a comment - Then I should not see a comment saying "XML attached" - - @javascript - Scenario: I can edit a comment with +1 - Given I leave a comment like "XML attached" - And I edit the last comment with a +1 - Then I should see +1 in the description diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index cbe1cae096e..bf1b88c60d7 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -6,70 +6,12 @@ module SharedNote wait_for_requests if javascript_test? end - step 'I delete a comment' do - page.within('.main-notes-list') do - note = find('.note') - note.hover - - find('.more-actions').click - find('.more-actions .dropdown-menu li', match: :first) - - accept_confirm { find(".js-note-delete").click } - end - end - step 'I haven\'t written any comment text' do page.within(".js-main-target-form") do fill_in "note[note]", with: "" end end - step 'I leave a comment like "XML attached"' do - page.within(".js-main-target-form") do - fill_in "note[note]", with: "XML attached" - click_button "Comment" - end - - wait_for_requests - end - - step 'I preview a comment text like "Bug fixed :smile:"' do - page.within(".js-main-target-form") do - fill_in "note[note]", with: "Bug fixed :smile:" - find('.js-md-preview-button').click - end - end - - step 'I submit the comment' do - page.within(".js-main-target-form") do - click_button "Comment" - end - - wait_for_requests - end - - step 'I write a comment like ":+1: Nice"' do - page.within(".js-main-target-form") do - fill_in 'note[note]', with: ':+1: Nice' - end - end - - step 'I should not see a comment saying "XML attached"' do - expect(page).not_to have_css(".note") - end - - step 'I should not see the cancel comment button' do - page.within(".js-main-target-form") do - should_not have_link("Cancel") - end - end - - step 'I should not see the comment preview' do - page.within(".js-main-target-form") do - expect(find('.js-md-preview')).not_to be_visible - end - end - step 'The comment preview tab should say there is nothing to do' do page.within(".js-main-target-form") do find('.js-md-preview-button').click @@ -77,71 +19,7 @@ module SharedNote end end - step 'I should not see the comment text field' do - page.within(".js-main-target-form") do - expect(find('.js-note-text')).not_to be_visible - end - end - - step 'I should see a comment saying "XML attached"' do - page.within(".note") do - expect(page).to have_content("XML attached") - end - end - - step 'I should see an empty comment text field' do - page.within(".js-main-target-form") do - expect(page).to have_field("note[note]", with: "") - end - end - - step 'I should see the comment write tab' do - page.within(".js-main-target-form") do - expect(page).to have_css('.js-md-write-button', visible: true) - end - end - - step 'The comment preview tab should be display rendered Markdown' do - page.within(".js-main-target-form") do - find('.js-md-preview-button').click - expect(find('.js-md-preview')).to have_css('gl-emoji', visible: true) - end - end - - step 'I should see the comment preview' do - page.within(".js-main-target-form") do - expect(page).to have_css('.js-md-preview', visible: true) - end - end - step 'I should see no notes at all' do expect(page).not_to have_css('.note') end - - # Markdown - - step 'I edit the last comment with a +1' do - page.within(".main-notes-list") do - note = find('.note') - note.hover - - note.find('.js-note-edit').click - end - - page.find('.current-note-edit-form textarea') - - page.within(".current-note-edit-form") do - fill_in 'note[note]', with: '+1 Awesome!' - click_button 'Save comment' - end - wait_for_requests - end - - step 'I should see +1 in the description' do - page.within(".note") do - expect(page).to have_content("+1 Awesome!") - end - - wait_for_requests - end end diff --git a/features/support/capybara.rb b/features/support/capybara.rb index 4e2b3c67af5..8879c9ab650 100644 --- a/features/support/capybara.rb +++ b/features/support/capybara.rb @@ -21,13 +21,7 @@ Capybara.register_driver :chrome do |app| options.add_argument("no-sandbox") # Run headless by default unless CHROME_HEADLESS specified - unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i - options.add_argument("headless") - - # Chrome documentation says this flag is needed for now - # https://developers.google.com/web/updates/2017/04/headless-chrome#cli - options.add_argument("disable-gpu") - end + options.add_argument("headless") unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252 options.add_argument("disable-dev-shm-usage") if ENV['CI'] || ENV['CI_SERVER'] diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 61dab1dd5cb..b8657cd7ee4 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -103,9 +103,9 @@ module API end def find_project(id) - if id =~ /^\d+$/ + if id.is_a?(Integer) || id =~ /^\d+$/ Project.find_by(id: id) - else + elsif id.include?("/") Project.find_by_full_path(id) end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index d0a4a23e074..3ae6fbd1fa9 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -338,6 +338,11 @@ module API end end + desc 'Get languages in project repository' + get ':id/languages' do + user_project.repository.languages.map { |language| language.values_at(:label, :value) }.to_h + end + desc 'Remove a project' delete ":id" do authorize! :remove_project, user_project diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index a848154b2d4..60a12dca9d3 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -56,29 +56,29 @@ module Banzai # Implement in child class # Example: project.merge_requests.find - def find_object(project, id) + def find_object(parent_object, id) end # Override if the link reference pattern produces a different ID (global # ID vs internal ID, for instance) to the regular reference pattern. - def find_object_from_link(project, id) - find_object(project, id) + def find_object_from_link(parent_object, id) + find_object(parent_object, id) end # Implement in child class # Example: project_merge_request_url - def url_for_object(object, project) + def url_for_object(object, parent_object) end - def find_object_cached(project, id) - cached_call(:banzai_find_object, id, path: [object_class, project.id]) do - find_object(project, id) + def find_object_cached(parent_object, id) + cached_call(:banzai_find_object, id, path: [object_class, parent_object.id]) do + find_object(parent_object, id) end end - def find_object_from_link_cached(project, id) - cached_call(:banzai_find_object_from_link, id, path: [object_class, project.id]) do - find_object_from_link(project, id) + def find_object_from_link_cached(parent_object, id) + cached_call(:banzai_find_object_from_link, id, path: [object_class, parent_object.id]) do + find_object_from_link(parent_object, id) end end @@ -88,9 +88,9 @@ module Banzai end end - def url_for_object_cached(object, project) - cached_call(:banzai_url_for_object, object, path: [object_class, project.id]) do - url_for_object(object, project) + def url_for_object_cached(object, parent_object) + cached_call(:banzai_url_for_object, object, path: [object_class, parent_object.id]) do + url_for_object(object, parent_object) end end diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb index 99fa2d9d8fb..01b3b0dafb9 100644 --- a/lib/banzai/filter/commit_range_reference_filter.rb +++ b/lib/banzai/filter/commit_range_reference_filter.rb @@ -23,6 +23,8 @@ module Banzai end def find_object(project, id) + return unless project.is_a?(Project) + range = CommitRange.new(id, project) range.valid_commits? ? range : nil diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb index 43bf4fc6565..8cd92a1adba 100644 --- a/lib/banzai/filter/commit_reference_filter.rb +++ b/lib/banzai/filter/commit_reference_filter.rb @@ -17,6 +17,8 @@ module Banzai end def find_object(project, id) + return unless project.is_a?(Project) + if project && project.valid_repo? # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/43894 Gitlab::GitalyClient.allow_n_plus_1_calls { project.commit(id) } diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index 1cbada818fb..a5f38046a43 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -8,8 +8,8 @@ module Banzai Label end - def find_object(project, id) - find_labels(project).find(id) + def find_object(parent_object, id) + find_labels(parent_object).find(id) end def self.references_in(text, pattern = Label.reference_pattern) diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index 1a1d7dbeb3d..b144bd8cf54 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -12,10 +12,14 @@ module Banzai # 'regular' references, we need to use the global ID to disambiguate # between group and project milestones. def find_object(project, id) + return unless project.is_a?(Project) + find_milestone_with_finder(project, id: id) end def find_object_from_link(project, iid) + return unless project.is_a?(Project) + find_milestone_with_finder(project, iid: iid) end @@ -40,7 +44,7 @@ module Banzai project_path = full_project_path(namespace_ref, project_ref) project = parent_from_ref(project_path) - return unless project + return unless project && project.is_a?(Project) milestone_params = milestone_params(milestone_id, milestone_name) diff --git a/lib/banzai/filter/snippet_reference_filter.rb b/lib/banzai/filter/snippet_reference_filter.rb index 134a192c22b..881e10afb9f 100644 --- a/lib/banzai/filter/snippet_reference_filter.rb +++ b/lib/banzai/filter/snippet_reference_filter.rb @@ -12,6 +12,8 @@ module Banzai end def find_object(project, id) + return unless project.is_a?(Project) + project.snippets.find_by(id: id) end diff --git a/lib/banzai/reference_parser/commit_range_parser.rb b/lib/banzai/reference_parser/commit_range_parser.rb index a50e6f8ef8f..2920e886938 100644 --- a/lib/banzai/reference_parser/commit_range_parser.rb +++ b/lib/banzai/reference_parser/commit_range_parser.rb @@ -29,6 +29,8 @@ module Banzai end def find_object(project, id) + return unless project.is_a?(Project) + range = CommitRange.new(id, project) range.valid_commits? ? range : nil diff --git a/lib/gitlab.rb b/lib/gitlab.rb index aa9fd36d9ff..2526a870976 100644 --- a/lib/gitlab.rb +++ b/lib/gitlab.rb @@ -3,13 +3,18 @@ require_dependency 'gitlab/git' module Gitlab COM_URL = 'https://gitlab.com'.freeze APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))} + SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z} def self.com? - # Check `staging?` as well to keep parity with gitlab.com - Gitlab.config.gitlab.url == COM_URL || staging? + # Check `gl_subdomain?` as well to keep parity with gitlab.com + Gitlab.config.gitlab.url == COM_URL || gl_subdomain? end - def self.staging? - Gitlab.config.gitlab.url == 'https://staging.gitlab.com' + def self.gl_subdomain? + SUBDOMAIN_REGEX === Gitlab.config.gitlab.url + end + + def self.dev_env_or_com? + Rails.env.test? || Rails.env.development? || com? end end diff --git a/lib/gitlab/cache/ci/project_pipeline_status.rb b/lib/gitlab/cache/ci/project_pipeline_status.rb index dba37892863..add048d671e 100644 --- a/lib/gitlab/cache/ci/project_pipeline_status.rb +++ b/lib/gitlab/cache/ci/project_pipeline_status.rb @@ -40,7 +40,7 @@ module Gitlab end def self.cache_key_for_project(project) - "projects/#{project.id}/pipeline_status" + "#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:projects/#{project.id}/pipeline_status" end def self.update_for_pipeline(pipeline) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 36992cbcca0..11a421cb430 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -75,9 +75,6 @@ module Gitlab end end - # Full path to repo - attr_reader :path - # Directory name of repo attr_reader :name @@ -96,14 +93,13 @@ module Gitlab @relative_path = relative_path @gl_repository = gl_repository - storage_path = Gitlab.config.repositories.storages[@storage].legacy_disk_path @gitlab_projects = Gitlab::Git::GitlabProjects.new( storage, relative_path, global_hooks_path: Gitlab.config.gitlab_shell.hooks_path, logger: Rails.logger ) - @path = File.join(storage_path, @relative_path) + @name = @relative_path.split("/").last end @@ -111,6 +107,12 @@ module Gitlab path == other.path end + def path + @path ||= File.join( + Gitlab.config.repositories.storages[@storage].legacy_disk_path, @relative_path + ) + end + # Default branch in the repository def root_ref @root_ref ||= gitaly_migrate(:root_ref) do |is_enabled| @@ -139,12 +141,12 @@ module Gitlab end def exists? - Gitlab::GitalyClient.migrate(:repository_exists, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled| + Gitlab::GitalyClient.migrate(:repository_exists, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled| if enabled gitaly_repository_client.exists? else circuit_breaker.perform do - File.exist?(File.join(@path, 'refs')) + File.exist?(File.join(path, 'refs')) end end end @@ -1018,7 +1020,7 @@ module Gitlab if is_enabled gitaly_repository_client.info_attributes else - attributes_path = File.join(File.expand_path(@path), 'info', 'attributes') + attributes_path = File.join(File.expand_path(path), 'info', 'attributes') if File.exist?(attributes_path) File.read(attributes_path) @@ -1468,7 +1470,7 @@ module Gitlab return [] if empty? || safe_query.blank? - args = %W(ls-tree --full-tree -r #{ref || root_ref} --name-status | #{safe_query}) + args = %W(ls-tree -r --name-status --full-tree #{ref || root_ref} -- #{safe_query}) run_git(args).first.lines.map(&:strip) end diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake index 564aa141952..cb4d5abffbc 100644 --- a/lib/tasks/cache.rake +++ b/lib/tasks/cache.rake @@ -6,17 +6,22 @@ namespace :cache do desc "GitLab | Clear redis cache" task redis: :environment do Gitlab::Redis::Cache.with do |redis| - cursor = REDIS_SCAN_START_STOP - loop do - cursor, keys = redis.scan( - cursor, - match: "#{Gitlab::Redis::Cache::CACHE_NAMESPACE}*", - count: REDIS_CLEAR_BATCH_SIZE - ) + cache_key_pattern = %W[#{Gitlab::Redis::Cache::CACHE_NAMESPACE}* + projects/*/pipeline_status] - redis.del(*keys) if keys.any? + cache_key_pattern.each do |match| + cursor = REDIS_SCAN_START_STOP + loop do + cursor, keys = redis.scan( + cursor, + match: match, + count: REDIS_CLEAR_BATCH_SIZE + ) - break if cursor == REDIS_SCAN_START_STOP + redis.del(*keys) if keys.any? + + break if cursor == REDIS_SCAN_START_STOP + end end end end diff --git a/package.json b/package.json index f6907e1fd16..4a71a9cb409 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js" }, "dependencies": { - "@gitlab-org/gitlab-svgs": "^1.17.0", + "@gitlab-org/gitlab-svgs": "^1.18.0", "autosize": "^4.0.0", "axios": "^0.17.1", "babel-core": "^6.26.0", diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb index cc1b1e5039e..b4fc2aa326f 100644 --- a/spec/controllers/admin/application_settings_controller_spec.rb +++ b/spec/controllers/admin/application_settings_controller_spec.rb @@ -72,11 +72,10 @@ describe Admin::ApplicationSettingsController do expect(ApplicationSetting.current.restricted_visibility_levels).to eq([10, 20]) end - it 'falls back to defaults when settings are omitted' do - put :update, application_setting: {} + it 'updates the restricted_visibility_levels when empty array is passed' do + put :update, application_setting: { restricted_visibility_levels: [] } expect(response).to redirect_to(admin_application_settings_path) - expect(ApplicationSetting.current.default_project_visibility).to eq(Gitlab::VisibilityLevel::PRIVATE) expect(ApplicationSetting.current.restricted_visibility_levels).to be_empty end end diff --git a/spec/db/production/settings_spec.rb b/spec/db/production/settings_spec.rb index 79e67330854..c8d016070f5 100644 --- a/spec/db/production/settings_spec.rb +++ b/spec/db/production/settings_spec.rb @@ -2,10 +2,15 @@ require 'spec_helper' require 'rainbow/ext/string' describe 'seed production settings' do - include StubENV let(:settings_file) { Rails.root.join('db/fixtures/production/010_settings.rb') } let(:settings) { Gitlab::CurrentSettings.current_application_settings } + before do + # It's important to set this variable so that we don't save a memoized + # (supposed to be) in-memory record in `Gitlab::CurrentSettings.in_memory_application_settings` + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + end + context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do before do stub_env('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN', '013456789') diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index bca7e920de4..e1fafa71d5c 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -206,7 +206,7 @@ FactoryBot.define do options do { image: { name: 'ruby:2.1', entrypoint: '/bin/sh' }, - services: ['postgres', { name: 'docker:dind', entrypoint: '/bin/sh', command: 'sleep 30', alias: 'docker' }], + services: ['postgres', { name: 'docker:stable-dind', entrypoint: '/bin/sh', command: 'sleep 30', alias: 'docker' }], after_script: %w(ls date), artifacts: { name: 'artifacts_file', diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 846b8040be6..7853d2952ea 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -32,6 +32,29 @@ feature 'Admin updates settings' do expect(find('#application_setting_visibility_level_20')).not_to be_checked end + scenario 'Modify import sources' do + expect(Gitlab::CurrentSettings.import_sources).not_to be_empty + + page.within('.as-visibility-access') do + Gitlab::ImportSources.options.map do |name, _| + uncheck name + end + + click_button 'Save changes' + end + + expect(page).to have_content "Application settings saved successfully" + expect(Gitlab::CurrentSettings.import_sources).to be_empty + + page.within('.as-visibility-access') do + check "Repo by URL" + click_button 'Save changes' + end + + expect(page).to have_content "Application settings saved successfully" + expect(Gitlab::CurrentSettings.import_sources).to eq(['git']) + end + scenario 'Change Visibility and Access Controls' do page.within('.as-visibility-access') do uncheck 'Project export enabled' @@ -62,6 +85,26 @@ feature 'Admin updates settings' do expect(page).to have_content "Application settings saved successfully" end + scenario 'Modify oauth providers' do + expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to be_empty + + page.within('.as-signin') do + uncheck 'Google' + click_button 'Save changes' + end + + expect(page).to have_content "Application settings saved successfully" + expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to include('google_oauth2') + + page.within('.as-signin') do + check "Google" + click_button 'Save changes' + end + + expect(page).to have_content "Application settings saved successfully" + expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).not_to include('google_oauth2') + end + scenario 'Change Help page' do page.within('.as-help-page') do fill_in 'Help page text', with: 'Example text' @@ -211,16 +254,6 @@ feature 'Admin updates settings' do expect(find('#service_push_channel').value).to eq '#test_channel' end - context 'sign-in restrictions', :js do - it 'de-activates oauth sign-in source' do - page.within('.as-signin') do - find('input#application_setting_enabled_oauth_sign_in_sources_[value=gitlab]').send_keys(:return) - - expect(find('.btn', text: 'GitLab.com')).not_to have_css('.active') - end - end - end - scenario 'Change Keys settings' do page.within('.as-visibility-access') do select 'Are forbidden', from: 'RSA SSH keys' diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb index 19152bf1f0f..6c51e4bbe26 100644 --- a/spec/features/milestone_spec.rb +++ b/spec/features/milestone_spec.rb @@ -108,4 +108,18 @@ feature 'Milestone' do expect(page).to have_selector('.js-delete-milestone-button', count: 0) end end + + feature 'deprecation popover', :js do + it 'opens deprecation popover' do + milestone = create(:milestone, project: project) + + visit group_milestone_path(group, milestone, title: milestone.title) + + expect(page).to have_selector('.milestone-deprecation-message') + + find('.milestone-deprecation-message .js-popover-link').click + + expect(page).to have_selector('.milestone-deprecation-message .popover') + end + end end diff --git a/spec/features/projects/commit/user_comments_on_commit_spec.rb b/spec/features/projects/commit/user_comments_on_commit_spec.rb new file mode 100644 index 00000000000..5174f793367 --- /dev/null +++ b/spec/features/projects/commit/user_comments_on_commit_spec.rb @@ -0,0 +1,110 @@ +require "spec_helper" + +describe "User comments on commit", :js do + include Spec::Support::Helpers::Features::NotesHelpers + include RepoHelpers + + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + COMMENT_TEXT = "XML attached".freeze + + before do + sign_in(user) + project.add_developer(user) + + visit(project_commit_path(project, sample_commit.id)) + end + + context "when adding new comment" do + it "adds comment" do + EMOJI = ":+1:".freeze + + page.within(".js-main-target-form") do + expect(page).not_to have_link("Cancel") + + fill_in("note[note]", with: "#{COMMENT_TEXT} #{EMOJI}") + + # Check on `Preview` tab + click_link("Preview") + + expect(find(".js-md-preview")).to have_content(COMMENT_TEXT).and have_css("gl-emoji") + expect(page).not_to have_css(".js-note-text") + + # Check on `Write` tab + click_link("Write") + + expect(page).to have_field("note[note]", with: "#{COMMENT_TEXT} #{EMOJI}") + + # Submit comment from the `Preview` tab to get rid of a separate `it` block + # which would specially tests if everything gets cleared from the note form. + click_link("Preview") + click_button("Comment") + end + + wait_for_requests + + page.within(".note") do + expect(page).to have_content(COMMENT_TEXT).and have_css("gl-emoji") + end + + page.within(".js-main-target-form") do + expect(page).to have_field("note[note]", with: "").and have_no_css(".js-md-preview") + end + end + end + + context "when editing comment" do + before do + add_note(COMMENT_TEXT) + end + + it "edits comment" do + NEW_COMMENT_TEXT = "+1 Awesome!".freeze + + page.within(".main-notes-list") do + note = find(".note") + note.hover + + note.find(".js-note-edit").click + end + + page.find(".current-note-edit-form textarea") + + page.within(".current-note-edit-form") do + fill_in("note[note]", with: NEW_COMMENT_TEXT) + click_button("Save comment") + end + + wait_for_requests + + page.within(".note") do + expect(page).to have_content(NEW_COMMENT_TEXT) + end + end + end + + context "when deleting comment" do + before do + add_note(COMMENT_TEXT) + end + + it "deletes comment" do + page.within(".note") do + expect(page).to have_content(COMMENT_TEXT) + end + + page.within(".main-notes-list") do + note = find(".note") + note.hover + + find(".more-actions").click + find(".more-actions .dropdown-menu li", match: :first) + + accept_confirm { find(".js-note-delete").click } + end + + expect(page).not_to have_css(".note") + end + end +end diff --git a/spec/features/snippets/embedded_snippet_spec.rb b/spec/features/snippets/embedded_snippet_spec.rb new file mode 100644 index 00000000000..ab661f6fc69 --- /dev/null +++ b/spec/features/snippets/embedded_snippet_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe 'Embedded Snippets' do + let(:snippet) { create(:personal_snippet, :public, file_name: 'random_dir.rb', content: content) } + let(:content) { "require 'fileutils'\nFileUtils.mkdir_p 'some/random_dir'\n" } + + it 'loads snippet', :js do + script_url = "http://#{Capybara.current_session.server.host}:#{Capybara.current_session.server.port}/#{snippet_path(snippet, format: 'js')}" + embed_body = "<html><body><script src=\"#{script_url}\"></script></body></html>" + + rack_app = proc do + ['200', { 'Content-Type' => 'text/html' }, [embed_body]] + end + + server = Capybara::Server.new(rack_app) + server.boot + + visit("http://#{server.host}:#{server.port}/embedded_snippet.html") + + expect(page).to have_content("random_dir.rb") + expect(page).to have_content("require 'fileutils'") + expect(page).to have_link('Open raw') + expect(page).to have_link('Download') + end +end diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb index 2f23ed55d99..93d8e672f8c 100644 --- a/spec/helpers/icons_helper_spec.rb +++ b/spec/helpers/icons_helper_spec.rb @@ -162,4 +162,11 @@ describe IconsHelper do expect(file_type_icon_class('file', 0, 'CHANGELOG')).to eq 'file-text-o' end end + + describe '#external_snippet_icon' do + it 'returns external snippet icon' do + expect(external_snippet_icon('download').to_s) + .to eq("<span class=\"gl-snippet-icon gl-snippet-icon-download\"></span>") + end + end end diff --git a/spec/helpers/snippets_helper_spec.rb b/spec/helpers/snippets_helper_spec.rb new file mode 100644 index 00000000000..0323ffb641c --- /dev/null +++ b/spec/helpers/snippets_helper_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe SnippetsHelper do + include IconsHelper + + describe '#embedded_snippet_raw_button' do + it 'gives view raw button of embedded snippets for project snippets' do + @snippet = create(:project_snippet, :public) + + expect(embedded_snippet_raw_button.to_s).to eq("<a class=\"btn\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Open raw\" href=\"#{raw_project_snippet_url(@snippet.project, @snippet)}\">#{external_snippet_icon('doc_code')}</a>") + end + + it 'gives view raw button of embedded snippets for personal snippets' do + @snippet = create(:personal_snippet, :public) + + expect(embedded_snippet_raw_button.to_s).to eq("<a class=\"btn\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Open raw\" href=\"#{raw_snippet_url(@snippet)}\">#{external_snippet_icon('doc_code')}</a>") + end + end + + describe '#embedded_snippet_download_button' do + it 'gives download button of embedded snippets for project snippets' do + @snippet = create(:project_snippet, :public) + + expect(embedded_snippet_download_button.to_s).to eq("<a class=\"btn\" target=\"_blank\" title=\"Download\" rel=\"noopener noreferrer\" href=\"#{raw_project_snippet_url(@snippet.project, @snippet, inline: false)}\">#{external_snippet_icon('download')}</a>") + end + + it 'gives download button of embedded snippets for personal snippets' do + @snippet = create(:personal_snippet, :public) + + expect(embedded_snippet_download_button.to_s).to eq("<a class=\"btn\" target=\"_blank\" title=\"Download\" rel=\"noopener noreferrer\" href=\"#{raw_snippet_url(@snippet, inline: false)}\">#{external_snippet_icon('download')}</a>") + end + end +end diff --git a/spec/initializers/gollum_spec.rb b/spec/initializers/gollum_spec.rb deleted file mode 100644 index adf824a8947..00000000000 --- a/spec/initializers/gollum_spec.rb +++ /dev/null @@ -1,62 +0,0 @@ -require 'spec_helper' - -describe 'gollum' do - let(:project) { create(:project) } - let(:user) { project.owner } - let(:wiki) { ProjectWiki.new(project, user) } - let(:gollum_wiki) { Gollum::Wiki.new(wiki.repository.path) } - - before do - create_page(page_name, 'content1') - end - - after do - destroy_page(page_name) - end - - context 'with simple paths' do - let(:page_name) { 'page1' } - - it 'returns the entry hash if it matches the file name' do - expect(tree_entry(page_name)).not_to be_nil - end - - it 'returns nil if the path does not fit completely' do - expect(tree_entry("foo/#{page_name}")).to be_nil - end - end - - context 'with complex paths' do - let(:page_name) { '/foo/bar/page2' } - - it 'returns the entry hash if it matches the file name' do - expect(tree_entry(page_name)).not_to be_nil - end - - it 'returns nil if the path does not fit completely' do - expect(tree_entry("foo1/bar/page2")).to be_nil - expect(tree_entry("foo/bar1/page2")).to be_nil - end - end - - def tree_entry(name) - gollum_wiki.repo.git.tree_entry(wiki_commits[0].commit, name + '.md') - end - - def wiki_commits - gollum_wiki.repo.commits - end - - def commit_details - Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit") - end - - def create_page(name, content) - wiki.wiki.write_page(name, :markdown, content, commit_details) - end - - def destroy_page(name) - page = wiki.find_page(name).page - wiki.delete_page(page, "test commit") - end -end diff --git a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js index 480c138b9db..2ab6a0077b5 100644 --- a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js +++ b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js @@ -3,12 +3,11 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import { getSelector, - togglePopover, dismiss, - mouseleave, - mouseenter, inserted, } from '~/feature_highlight/feature_highlight_helper'; +import { togglePopover } from '~/shared/popover'; + import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; describe('feature highlight helper', () => { @@ -19,110 +18,6 @@ describe('feature highlight helper', () => { }); }); - describe('togglePopover', () => { - describe('togglePopover(true)', () => { - it('returns true when popover is shown', () => { - const context = { - hasClass: () => false, - popover: () => {}, - toggleClass: () => {}, - }; - - expect(togglePopover.call(context, true)).toEqual(true); - }); - - it('returns false when popover is already shown', () => { - const context = { - hasClass: () => true, - }; - - expect(togglePopover.call(context, true)).toEqual(false); - }); - - it('shows popover', (done) => { - const context = { - hasClass: () => false, - popover: () => {}, - toggleClass: () => {}, - }; - - spyOn(context, 'popover').and.callFake((method) => { - expect(method).toEqual('show'); - done(); - }); - - togglePopover.call(context, true); - }); - - it('adds disable-animation and js-popover-show class', (done) => { - const context = { - hasClass: () => false, - popover: () => {}, - toggleClass: () => {}, - }; - - spyOn(context, 'toggleClass').and.callFake((classNames, show) => { - expect(classNames).toEqual('disable-animation js-popover-show'); - expect(show).toEqual(true); - done(); - }); - - togglePopover.call(context, true); - }); - }); - - describe('togglePopover(false)', () => { - it('returns true when popover is hidden', () => { - const context = { - hasClass: () => true, - popover: () => {}, - toggleClass: () => {}, - }; - - expect(togglePopover.call(context, false)).toEqual(true); - }); - - it('returns false when popover is already hidden', () => { - const context = { - hasClass: () => false, - }; - - expect(togglePopover.call(context, false)).toEqual(false); - }); - - it('hides popover', (done) => { - const context = { - hasClass: () => true, - popover: () => {}, - toggleClass: () => {}, - }; - - spyOn(context, 'popover').and.callFake((method) => { - expect(method).toEqual('hide'); - done(); - }); - - togglePopover.call(context, false); - }); - - it('removes disable-animation and js-popover-show class', (done) => { - const context = { - hasClass: () => true, - popover: () => {}, - toggleClass: () => {}, - }; - - spyOn(context, 'toggleClass').and.callFake((classNames, show) => { - expect(classNames).toEqual('disable-animation js-popover-show'); - expect(show).toEqual(false); - done(); - }); - - togglePopover.call(context, false); - }); - }); - }); - describe('dismiss', () => { let mock; const context = { @@ -163,56 +58,6 @@ describe('feature highlight helper', () => { }); }); - describe('mouseleave', () => { - it('calls hide popover if .popover:hover is false', () => { - const fakeJquery = { - length: 0, - }; - - spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn)); - spyOn(togglePopover, 'call'); - mouseleave(); - expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), false); - }); - - it('does not call hide popover if .popover:hover is true', () => { - const fakeJquery = { - length: 1, - }; - - spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn)); - spyOn(togglePopover, 'call'); - mouseleave(); - expect(togglePopover.call).not.toHaveBeenCalledWith(false); - }); - }); - - describe('mouseenter', () => { - const context = {}; - - it('shows popover', () => { - spyOn(togglePopover, 'call').and.returnValue(false); - mouseenter.call(context); - expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), true); - }); - - it('registers mouseleave event if popover is showed', (done) => { - spyOn(togglePopover, 'call').and.returnValue(true); - spyOn($.fn, 'on').and.callFake((eventName) => { - expect(eventName).toEqual('mouseleave'); - done(); - }); - mouseenter.call(context); - }); - - it('does not register mouseleave event if popover is not showed', () => { - spyOn(togglePopover, 'call').and.returnValue(false); - const spy = spyOn($.fn, 'on').and.callFake(() => {}); - mouseenter.call(context); - expect(spy).not.toHaveBeenCalled(); - }); - }); - describe('inserted', () => { it('registers click event callback', (done) => { const context = { diff --git a/spec/javascripts/feature_highlight/feature_highlight_spec.js b/spec/javascripts/feature_highlight/feature_highlight_spec.js index d2dd39d49d1..ec46d4f905a 100644 --- a/spec/javascripts/feature_highlight/feature_highlight_spec.js +++ b/spec/javascripts/feature_highlight/feature_highlight_spec.js @@ -1,6 +1,6 @@ import $ from 'jquery'; -import * as featureHighlightHelper from '~/feature_highlight/feature_highlight_helper'; import * as featureHighlight from '~/feature_highlight/feature_highlight'; +import * as popover from '~/shared/popover'; import axios from '~/lib/utils/axios_utils'; import MockAdapter from 'axios-mock-adapter'; @@ -29,7 +29,6 @@ describe('feature highlight', () => { mock = new MockAdapter(axios); mock.onGet('/test').reply(200); spyOn(window, 'addEventListener'); - spyOn(window, 'removeEventListener'); featureHighlight.setupFeatureHighlightPopover('test', 0); }); @@ -45,14 +44,14 @@ describe('feature highlight', () => { }); it('setup mouseenter', () => { - const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call'); + const toggleSpy = spyOn(popover.togglePopover, 'call'); $(selector).trigger('mouseenter'); expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), true); }); it('setup debounced mouseleave', (done) => { - const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call'); + const toggleSpy = spyOn(popover.togglePopover, 'call'); $(selector).trigger('mouseleave'); // Even though we've set the debounce to 0ms, setTimeout is needed for the debounce @@ -64,12 +63,7 @@ describe('feature highlight', () => { it('setup show.bs.popover', () => { $(selector).trigger('show.bs.popover'); - expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function)); - }); - - it('setup hide.bs.popover', () => { - $(selector).trigger('hide.bs.popover'); - expect(window.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function)); + expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), { once: true }); }); it('removes disabled attribute', () => { @@ -85,7 +79,7 @@ describe('feature highlight', () => { it('toggles when clicked', () => { $(selector).trigger('mouseenter'); const popoverId = $(selector).attr('aria-describedby'); - const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call'); + const toggleSpy = spyOn(popover.togglePopover, 'call'); $(`#${popoverId} .dismiss-feature-highlight`).click(); diff --git a/spec/javascripts/ide/components/repo_loading_file_spec.js b/spec/javascripts/ide/components/repo_loading_file_spec.js index 8f9644216bc..7c20b8302f9 100644 --- a/spec/javascripts/ide/components/repo_loading_file_spec.js +++ b/spec/javascripts/ide/components/repo_loading_file_spec.js @@ -27,7 +27,7 @@ describe('RepoLoadingFile', () => { const lines = [...container.querySelectorAll(':scope > div')]; expect(container).toBeTruthy(); - expect(lines.length).toEqual(6); + expect(lines.length).toEqual(3); assertLines(lines); }); } diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js index 90ded940227..1946a0c547c 100644 --- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js @@ -133,10 +133,7 @@ describe('IDE commit module actions', () => { store .dispatch('commit/checkCommitStatus') .then(() => { - expect(service.getBranchData).toHaveBeenCalledWith( - 'abcproject', - 'master', - ); + expect(service.getBranchData).toHaveBeenCalledWith('abcproject', 'master'); done(); }) @@ -230,9 +227,7 @@ describe('IDE commit module actions', () => { branch, }) .then(() => { - expect( - store.state.projects.abcproject.branches.master.workingReference, - ).toBe(data.id); + expect(store.state.projects.abcproject.branches.master.workingReference).toBe(data.id); }) .then(done) .catch(done.fail); @@ -317,26 +312,7 @@ describe('IDE commit module actions', () => { branch, }) .then(() => { - expect(router.push).toHaveBeenCalledWith( - `/project/abcproject/blob/master/${f.path}`, - ); - }) - .then(done) - .catch(done.fail); - }); - - it('resets stores commit actions', done => { - store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH; - - store - .dispatch('commit/updateFilesAfterCommit', { - data, - branch, - }) - .then(() => { - expect(store.state.commit.commitAction).not.toBe( - consts.COMMIT_TO_NEW_BRANCH, - ); + expect(router.push).toHaveBeenCalledWith(`/project/abcproject/blob/master/${f.path}`); }) .then(done) .catch(done.fail); @@ -448,33 +424,60 @@ describe('IDE commit module actions', () => { store .dispatch('commit/commitChanges') .then(() => { - expect(store.state.openFiles[0].lastCommit.message).toBe( - 'test message', - ); + expect(store.state.openFiles[0].lastCommit.message).toBe('test message'); done(); }) .catch(done.fail); }); - it('redirects to new merge request page', done => { - spyOn(eventHub, '$on'); - - store.state.commit.commitAction = '3'; + it('resets stores commit actions', done => { + store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH; store .dispatch('commit/commitChanges') .then(() => { - expect(urlUtils.visitUrl).toHaveBeenCalledWith( - `webUrl/merge_requests/new?merge_request[source_branch]=${ - store.getters['commit/newBranchName'] - }&merge_request[target_branch]=master`, - ); - - done(); + expect(store.state.commit.commitAction).not.toBe(consts.COMMIT_TO_NEW_BRANCH); }) + .then(done) .catch(done.fail); }); + + describe('merge request', () => { + it('redirects to new merge request page', done => { + spyOn(eventHub, '$on'); + + store.state.commit.commitAction = '3'; + + store + .dispatch('commit/commitChanges') + .then(() => { + expect(urlUtils.visitUrl).toHaveBeenCalledWith( + `webUrl/merge_requests/new?merge_request[source_branch]=${ + store.getters['commit/newBranchName'] + }&merge_request[target_branch]=master`, + ); + + done(); + }) + .catch(done.fail); + }); + + it('resets changed files before redirecting', done => { + spyOn(eventHub, '$on'); + + store.state.commit.commitAction = '3'; + + store + .dispatch('commit/commitChanges') + .then(() => { + expect(store.state.changedFiles.length).toBe(0); + + done(); + }) + .catch(done.fail); + }); + }); }); describe('failed', () => { diff --git a/spec/javascripts/pipelines/mock_data.js b/spec/javascripts/pipelines/mock_data.js new file mode 100644 index 00000000000..59092e0f041 --- /dev/null +++ b/spec/javascripts/pipelines/mock_data.js @@ -0,0 +1,326 @@ +export const pipelineWithStages = { + id: 20333396, + user: { + id: 128633, + name: 'Rémy Coutable', + username: 'rymai', + state: 'active', + avatar_url: + 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon', + web_url: 'https://gitlab.com/rymai', + path: '/rymai', + }, + active: true, + coverage: '58.24', + source: 'push', + created_at: '2018-04-11T14:04:53.881Z', + updated_at: '2018-04-11T14:05:00.792Z', + path: '/gitlab-org/gitlab-ee/pipelines/20333396', + flags: { + latest: true, + stuck: false, + auto_devops: false, + yaml_errors: false, + retryable: false, + cancelable: true, + failure_reason: false, + }, + details: { + status: { + icon: 'status_running', + text: 'running', + label: 'running', + group: 'running', + has_details: true, + details_path: '/gitlab-org/gitlab-ee/pipelines/20333396', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_running-2eb56be2871937954b2ba6d6f4ee9fdf7e5e1c146ac45f7be98119ccaca1aca9.ico', + }, + duration: null, + finished_at: null, + stages: [ + { + name: 'build', + title: 'build: skipped', + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + has_details: true, + details_path: '/gitlab-org/gitlab-ee/pipelines/20333396#build', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_skipped-a2eee568a5bffdb494050c7b62dde241de9189280836288ac8923d369f16222d.ico', + }, + path: '/gitlab-org/gitlab-ee/pipelines/20333396#build', + dropdown_path: '/gitlab-org/gitlab-ee/pipelines/20333396/stage.json?stage=build', + }, + { + name: 'prepare', + title: 'prepare: passed', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/gitlab-org/gitlab-ee/pipelines/20333396#prepare', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_success-26f59841becbef8c6fe414e9e74471d8bfd6a91b5855c19fe7f5923a40a7da47.ico', + }, + path: '/gitlab-org/gitlab-ee/pipelines/20333396#prepare', + dropdown_path: '/gitlab-org/gitlab-ee/pipelines/20333396/stage.json?stage=prepare', + }, + { + name: 'test', + title: 'test: running', + status: { + icon: 'status_running', + text: 'running', + label: 'running', + group: 'running', + has_details: true, + details_path: '/gitlab-org/gitlab-ee/pipelines/20333396#test', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_running-2eb56be2871937954b2ba6d6f4ee9fdf7e5e1c146ac45f7be98119ccaca1aca9.ico', + }, + path: '/gitlab-org/gitlab-ee/pipelines/20333396#test', + dropdown_path: '/gitlab-org/gitlab-ee/pipelines/20333396/stage.json?stage=test', + }, + { + name: 'post-test', + title: 'post-test: created', + status: { + icon: 'status_created', + text: 'created', + label: 'created', + group: 'created', + has_details: true, + details_path: '/gitlab-org/gitlab-ee/pipelines/20333396#post-test', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico', + }, + path: '/gitlab-org/gitlab-ee/pipelines/20333396#post-test', + dropdown_path: '/gitlab-org/gitlab-ee/pipelines/20333396/stage.json?stage=post-test', + }, + { + name: 'pages', + title: 'pages: created', + status: { + icon: 'status_created', + text: 'created', + label: 'created', + group: 'created', + has_details: true, + details_path: '/gitlab-org/gitlab-ee/pipelines/20333396#pages', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico', + }, + path: '/gitlab-org/gitlab-ee/pipelines/20333396#pages', + dropdown_path: '/gitlab-org/gitlab-ee/pipelines/20333396/stage.json?stage=pages', + }, + { + name: 'post-cleanup', + title: 'post-cleanup: created', + status: { + icon: 'status_created', + text: 'created', + label: 'created', + group: 'created', + has_details: true, + details_path: '/gitlab-org/gitlab-ee/pipelines/20333396#post-cleanup', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico', + }, + path: '/gitlab-org/gitlab-ee/pipelines/20333396#post-cleanup', + dropdown_path: '/gitlab-org/gitlab-ee/pipelines/20333396/stage.json?stage=post-cleanup', + }, + ], + artifacts: [ + { + name: 'gitlab:assets:compile', + expired: false, + expire_at: '2018-05-12T14:22:54.730Z', + path: '/gitlab-org/gitlab-ee/-/jobs/62411438/artifacts/download', + keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411438/artifacts/keep', + browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411438/artifacts/browse', + }, + { + name: 'rspec-mysql 12 28', + expired: false, + expire_at: '2018-05-12T14:22:45.136Z', + path: '/gitlab-org/gitlab-ee/-/jobs/62411397/artifacts/download', + keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411397/artifacts/keep', + browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411397/artifacts/browse', + }, + { + name: 'rspec-mysql 6 28', + expired: false, + expire_at: '2018-05-12T14:22:41.523Z', + path: '/gitlab-org/gitlab-ee/-/jobs/62411391/artifacts/download', + keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411391/artifacts/keep', + browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411391/artifacts/browse', + }, + { + name: 'rspec-pg geo 0 1', + expired: false, + expire_at: '2018-05-12T14:22:13.287Z', + path: '/gitlab-org/gitlab-ee/-/jobs/62411353/artifacts/download', + keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411353/artifacts/keep', + browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411353/artifacts/browse', + }, + { + name: 'rspec-mysql 0 28', + expired: false, + expire_at: '2018-05-12T14:22:06.834Z', + path: '/gitlab-org/gitlab-ee/-/jobs/62411385/artifacts/download', + keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411385/artifacts/keep', + browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411385/artifacts/browse', + }, + { + name: 'spinach-mysql 0 2', + expired: false, + expire_at: '2018-05-12T14:21:51.409Z', + path: '/gitlab-org/gitlab-ee/-/jobs/62411423/artifacts/download', + keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411423/artifacts/keep', + browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411423/artifacts/browse', + }, + { + name: 'karma', + expired: false, + expire_at: '2018-05-12T14:21:20.934Z', + path: '/gitlab-org/gitlab-ee/-/jobs/62411440/artifacts/download', + keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411440/artifacts/keep', + browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411440/artifacts/browse', + }, + { + name: 'spinach-pg 0 2', + expired: false, + expire_at: '2018-05-12T14:20:01.028Z', + path: '/gitlab-org/gitlab-ee/-/jobs/62411419/artifacts/download', + keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411419/artifacts/keep', + browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411419/artifacts/browse', + }, + { + name: 'spinach-pg 1 2', + expired: false, + expire_at: '2018-05-12T14:19:04.336Z', + path: '/gitlab-org/gitlab-ee/-/jobs/62411421/artifacts/download', + keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411421/artifacts/keep', + browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411421/artifacts/browse', + }, + { + name: 'sast', + expired: null, + expire_at: null, + path: '/gitlab-org/gitlab-ee/-/jobs/62411442/artifacts/download', + browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411442/artifacts/browse', + }, + { + name: 'codequality', + expired: false, + expire_at: '2018-04-18T14:16:24.484Z', + path: '/gitlab-org/gitlab-ee/-/jobs/62411441/artifacts/download', + keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411441/artifacts/keep', + browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411441/artifacts/browse', + }, + { + name: 'cache gems', + expired: null, + expire_at: null, + path: '/gitlab-org/gitlab-ee/-/jobs/62411447/artifacts/download', + browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411447/artifacts/browse', + }, + { + name: 'dependency_scanning', + expired: null, + expire_at: null, + path: '/gitlab-org/gitlab-ee/-/jobs/62411443/artifacts/download', + browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411443/artifacts/browse', + }, + { + name: 'compile-assets', + expired: false, + expire_at: '2018-04-18T14:12:07.638Z', + path: '/gitlab-org/gitlab-ee/-/jobs/62411334/artifacts/download', + keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411334/artifacts/keep', + browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411334/artifacts/browse', + }, + { + name: 'setup-test-env', + expired: false, + expire_at: '2018-04-18T14:10:27.024Z', + path: '/gitlab-org/gitlab-ee/-/jobs/62411336/artifacts/download', + keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411336/artifacts/keep', + browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411336/artifacts/browse', + }, + { + name: 'retrieve-tests-metadata', + expired: false, + expire_at: '2018-05-12T14:06:35.926Z', + path: '/gitlab-org/gitlab-ee/-/jobs/62411333/artifacts/download', + keep_path: '/gitlab-org/gitlab-ee/-/jobs/62411333/artifacts/keep', + browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411333/artifacts/browse', + }, + ], + manual_actions: [ + { + name: 'package-and-qa', + path: '/gitlab-org/gitlab-ee/-/jobs/62411330/play', + playable: true, + }, + { + name: 'review-docs-deploy', + path: '/gitlab-org/gitlab-ee/-/jobs/62411332/play', + playable: true, + }, + ], + }, + ref: { + name: 'master', + path: '/gitlab-org/gitlab-ee/commits/master', + tag: false, + branch: true, + }, + commit: { + id: 'e6a2885c503825792cb8a84a8731295e361bd059', + short_id: 'e6a2885c', + title: "Merge branch 'ce-to-ee-2018-04-11' into 'master'", + created_at: '2018-04-11T14:04:39.000Z', + parent_ids: [ + '5d9b5118f6055f72cff1a82b88133609912f2c1d', + '6fdc6ee76a8062fe41b1a33f7c503334a6ebdc02', + ], + message: + "Merge branch 'ce-to-ee-2018-04-11' into 'master'\n\nCE upstream - 2018-04-11 12:26 UTC\n\nSee merge request gitlab-org/gitlab-ee!5326", + author_name: 'Rémy Coutable', + author_email: 'remy@rymai.me', + authored_date: '2018-04-11T14:04:39.000Z', + committer_name: 'Rémy Coutable', + committer_email: 'remy@rymai.me', + committed_date: '2018-04-11T14:04:39.000Z', + author: { + id: 128633, + name: 'Rémy Coutable', + username: 'rymai', + state: 'active', + avatar_url: + 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon', + web_url: 'https://gitlab.com/rymai', + path: '/rymai', + }, + author_gravatar_url: + 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon', + commit_url: + 'https://gitlab.com/gitlab-org/gitlab-ee/commit/e6a2885c503825792cb8a84a8731295e361bd059', + commit_path: '/gitlab-org/gitlab-ee/commit/e6a2885c503825792cb8a84a8731295e361bd059', + }, + cancel_path: '/gitlab-org/gitlab-ee/pipelines/20333396/cancel', + triggered_by: null, + triggered: [], +}; + +export const stageReply = { + html: + '\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="karma - failed \u0026lt;br\u0026gt; (script failure)" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62402048"\u003e\u003cspan class="ci-status-icon ci-status-icon-failed"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_failed"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003ekarma\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62402048/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="codequality - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398081"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003ecodequality\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398081/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="db:check-schema-pg - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398066"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edb:check-schema-pg\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398066/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="db:migrate:reset-mysql - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398065"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edb:migrate:reset-mysql\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398065/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="db:migrate:reset-pg - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398064"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edb:migrate:reset-pg\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398064/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="db:rollback-mysql - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398070"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edb:rollback-mysql\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398070/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="db:rollback-pg - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398069"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edb:rollback-pg\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398069/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="dependency_scanning - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398083"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edependency_scanning\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398083/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="docs lint - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398061"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edocs lint\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398061/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="downtime_check - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398062"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edowntime_check\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398062/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="ee_compat_check - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398063"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003eee_compat_check\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398063/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="gitlab:assets:compile - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398075"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003egitlab:assets:compile\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398075/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="gitlab:setup-mysql - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398073"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003egitlab:setup-mysql\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398073/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="gitlab:setup-pg - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398071"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003egitlab:setup-pg\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398071/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="gitlab_git_test - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398086"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003egitlab_git_test\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398086/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="migration:path-mysql - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398068"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003emigration:path-mysql\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398068/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="migration:path-pg - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398067"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003emigration:path-pg\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398067/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="qa:internal - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398084"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003eqa:internal\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398084/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="qa:selectors - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398085"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003eqa:selectors\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398085/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 0 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398020"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 0 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398020/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 1 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398022"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 1 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398022/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 10 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398033"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 10 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398033/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 11 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398034"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 11 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398034/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 12 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398035"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 12 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398035/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 13 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398036"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 13 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398036/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 14 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398037"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 14 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398037/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 15 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398038"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 15 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398038/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 16 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398039"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 16 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398039/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 17 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398040"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 17 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398040/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 18 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398041"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 18 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398041/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 19 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398042"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 19 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398042/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 2 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398024"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 2 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398024/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 20 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398043"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 20 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398043/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 21 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398044"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 21 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398044/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 22 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398046"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 22 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398046/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 23 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398047"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 23 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398047/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 24 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398048"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 24 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398048/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 25 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398049"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 25 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398049/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 26 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398050"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 26 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398050/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 27 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398051"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 27 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398051/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 3 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398025"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 3 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398025/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 4 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398027"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 4 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398027/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 5 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398028"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 5 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398028/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 6 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398029"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 6 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398029/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 7 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398030"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 7 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398030/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 8 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398031"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 8 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398031/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 9 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398032"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 9 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398032/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 0 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397981"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 0 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397981/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 1 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397985"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 1 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397985/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 10 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398000"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 10 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398000/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 11 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398001"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 11 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398001/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 12 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398002"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 12 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398002/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 13 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398003"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 13 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398003/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 14 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398004"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 14 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398004/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 15 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398006"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 15 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398006/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 16 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398007"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 16 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398007/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 17 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398008"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 17 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398008/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 18 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398009"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 18 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398009/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 19 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398010"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 19 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398010/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 2 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397986"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 2 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397986/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 20 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398012"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 20 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398012/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 21 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398013"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 21 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398013/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 22 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398014"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 22 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398014/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 23 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398015"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 23 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398015/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 24 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398016"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 24 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398016/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 25 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398017"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 25 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398017/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 26 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398018"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 26 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398018/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 27 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398019"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 27 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398019/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 3 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397988"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 3 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397988/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 4 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397989"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 4 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397989/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 5 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397991"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 5 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397991/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 6 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397993"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 6 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397993/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 7 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397994"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 7 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397994/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 8 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397995"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 8 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397995/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 9 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397996"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 9 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397996/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="sast - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398082"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003esast\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398082/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="spinach-mysql 0 2 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398058"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003espinach-mysql 0 2\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398058/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="spinach-mysql 1 2 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398059"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003espinach-mysql 1 2\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398059/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="spinach-pg 0 2 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398053"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003espinach-pg 0 2\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398053/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="spinach-pg 1 2 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398056"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003espinach-pg 1 2\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398056/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="static-analysis - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398060"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003estatic-analysis\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398060/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n', +}; diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js index d79544f83ad..ff17602da2b 100644 --- a/spec/javascripts/pipelines/pipelines_spec.js +++ b/spec/javascripts/pipelines/pipelines_spec.js @@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils'; import pipelinesComp from '~/pipelines/components/pipelines.vue'; import Store from '~/pipelines/stores/pipelines_store'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { pipelineWithStages, stageReply } from './mock_data'; describe('Pipelines', () => { const jsonFixtureName = 'pipelines/pipelines.json'; @@ -668,4 +669,79 @@ describe('Pipelines', () => { }); }); }); + + describe('updates results when a staged is clicked', () => { + beforeEach(() => { + const copyPipeline = Object.assign({}, pipelineWithStages); + copyPipeline.id += 1; + mock + .onGet('twitter/flight/pipelines.json').reply(200, { + pipelines: [pipelineWithStages], + count: { + all: 1, + finished: 1, + pending: 0, + running: 0, + }, + }, { + 'POLL-INTERVAL': 100, + }) + .onGet(pipelineWithStages.details.stages[0].dropdown_path) + .reply(200, stageReply); + + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, + }); + }); + + describe('when a request is being made', () => { + it('stops polling, cancels the request, fetches pipelines & restarts polling', (done) => { + spyOn(vm.poll, 'stop'); + spyOn(vm.poll, 'restart'); + spyOn(vm, 'getPipelines').and.returnValue(Promise.resolve()); + spyOn(vm.service.cancelationSource, 'cancel').and.callThrough(); + + setTimeout(() => { + vm.isMakingRequest = true; + return vm.$nextTick() + .then(() => { + vm.$el.querySelector('.js-builds-dropdown-button').click(); + }) + .then(() => { + expect(vm.service.cancelationSource.cancel).toHaveBeenCalled(); + expect(vm.poll.stop).toHaveBeenCalled(); + + setTimeout(() => { + expect(vm.getPipelines).toHaveBeenCalled(); + expect(vm.poll.restart).toHaveBeenCalled(); + done(); + }, 0); + }); + }, 0); + }); + }); + + describe('when no request is being made', () => { + it('stops polling, fetches pipelines & restarts polling', (done) => { + spyOn(vm.poll, 'stop'); + spyOn(vm.poll, 'restart'); + spyOn(vm, 'getPipelines').and.returnValue(Promise.resolve()); + + setTimeout(() => { + vm.$el.querySelector('.js-builds-dropdown-button').click(); + + expect(vm.poll.stop).toHaveBeenCalled(); + + setTimeout(() => { + expect(vm.getPipelines).toHaveBeenCalled(); + expect(vm.poll.restart).toHaveBeenCalled(); + done(); + }, 0); + }, 0); + }); + }); + }); }); diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js index c2ed2e9a31b..be1632e7206 100644 --- a/spec/javascripts/pipelines/stage_spec.js +++ b/spec/javascripts/pipelines/stage_spec.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import stage from '~/pipelines/components/stage.vue'; +import eventHub from '~/pipelines/event_hub'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Pipelines stage component', () => { @@ -43,13 +44,15 @@ describe('Pipelines stage component', () => { mock.onGet('path.json').reply(200, { html: 'foo' }); }); - it('should render the received data', done => { + it('should render the received data and emit `clickedDropdown` event', done => { + spyOn(eventHub, '$emit'); component.$el.querySelector('button').click(); setTimeout(() => { expect( component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(), ).toEqual('foo'); + expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown'); done(); }, 0); }); diff --git a/spec/javascripts/shared/popover_spec.js b/spec/javascripts/shared/popover_spec.js new file mode 100644 index 00000000000..1d574c9424b --- /dev/null +++ b/spec/javascripts/shared/popover_spec.js @@ -0,0 +1,162 @@ +import $ from 'jquery'; +import { + togglePopover, + mouseleave, + mouseenter, +} from '~/shared/popover'; + +describe('popover', () => { + describe('togglePopover', () => { + describe('togglePopover(true)', () => { + it('returns true when popover is shown', () => { + const context = { + hasClass: () => false, + popover: () => {}, + toggleClass: () => {}, + }; + + expect(togglePopover.call(context, true)).toEqual(true); + }); + + it('returns false when popover is already shown', () => { + const context = { + hasClass: () => true, + }; + + expect(togglePopover.call(context, true)).toEqual(false); + }); + + it('shows popover', (done) => { + const context = { + hasClass: () => false, + popover: () => {}, + toggleClass: () => {}, + }; + + spyOn(context, 'popover').and.callFake((method) => { + expect(method).toEqual('show'); + done(); + }); + + togglePopover.call(context, true); + }); + + it('adds disable-animation and js-popover-show class', (done) => { + const context = { + hasClass: () => false, + popover: () => {}, + toggleClass: () => {}, + }; + + spyOn(context, 'toggleClass').and.callFake((classNames, show) => { + expect(classNames).toEqual('disable-animation js-popover-show'); + expect(show).toEqual(true); + done(); + }); + + togglePopover.call(context, true); + }); + }); + + describe('togglePopover(false)', () => { + it('returns true when popover is hidden', () => { + const context = { + hasClass: () => true, + popover: () => {}, + toggleClass: () => {}, + }; + + expect(togglePopover.call(context, false)).toEqual(true); + }); + + it('returns false when popover is already hidden', () => { + const context = { + hasClass: () => false, + }; + + expect(togglePopover.call(context, false)).toEqual(false); + }); + + it('hides popover', (done) => { + const context = { + hasClass: () => true, + popover: () => {}, + toggleClass: () => {}, + }; + + spyOn(context, 'popover').and.callFake((method) => { + expect(method).toEqual('hide'); + done(); + }); + + togglePopover.call(context, false); + }); + + it('removes disable-animation and js-popover-show class', (done) => { + const context = { + hasClass: () => true, + popover: () => {}, + toggleClass: () => {}, + }; + + spyOn(context, 'toggleClass').and.callFake((classNames, show) => { + expect(classNames).toEqual('disable-animation js-popover-show'); + expect(show).toEqual(false); + done(); + }); + + togglePopover.call(context, false); + }); + }); + }); + + describe('mouseleave', () => { + it('calls hide popover if .popover:hover is false', () => { + const fakeJquery = { + length: 0, + }; + + spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn)); + spyOn(togglePopover, 'call'); + mouseleave(); + expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), false); + }); + + it('does not call hide popover if .popover:hover is true', () => { + const fakeJquery = { + length: 1, + }; + + spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn)); + spyOn(togglePopover, 'call'); + mouseleave(); + expect(togglePopover.call).not.toHaveBeenCalledWith(false); + }); + }); + + describe('mouseenter', () => { + const context = {}; + + it('shows popover', () => { + spyOn(togglePopover, 'call').and.returnValue(false); + mouseenter.call(context); + expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), true); + }); + + it('registers mouseleave event if popover is showed', (done) => { + spyOn(togglePopover, 'call').and.returnValue(true); + spyOn($.fn, 'on').and.callFake((eventName) => { + expect(eventName).toEqual('mouseleave'); + done(); + }); + mouseenter.call(context); + }); + + it('does not register mouseleave event if popover is not showed', () => { + spyOn(togglePopover, 'call').and.returnValue(false); + const spy = spyOn($.fn, 'on').and.callFake(() => {}); + mouseenter.call(context); + expect(spy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/shortcuts_dashboard_navigation_spec.js b/spec/javascripts/shortcuts_dashboard_navigation_spec.js new file mode 100644 index 00000000000..888b49004bf --- /dev/null +++ b/spec/javascripts/shortcuts_dashboard_navigation_spec.js @@ -0,0 +1,24 @@ +import findAndFollowLink from '~/shortcuts_dashboard_navigation'; +import * as urlUtility from '~/lib/utils/url_utility'; + +describe('findAndFollowLink', () => { + it('visits a link when the selector exists', () => { + const href = '/some/path'; + const locationSpy = spyOn(urlUtility, 'visitUrl'); + + setFixtures(`<a class="my-shortcut" href="${href}">link</a>`); + + findAndFollowLink('.my-shortcut'); + + expect(locationSpy).toHaveBeenCalledWith(href); + }); + + it('does not throw an exception when the selector does not exist', () => { + const locationSpy = spyOn(urlUtility, 'visitUrl'); + + // this should not throw an exception + findAndFollowLink('.this-selector-does-not-exist'); + + expect(locationSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js index d9c03296857..91e81a0675a 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js @@ -51,8 +51,7 @@ const createComponent = () => { const messages = { loadingMetrics: 'Loading deployment statistics', - hasMetrics: - '<a href="/root/acets-review-apps/environments/15/metrics"> Memory </a> usage is <b> unchanged </b> at 0MB', + hasMetrics: 'Memory usage is unchanged at 0MB', loadFailed: 'Failed to load deployment statistics', metricsUnavailable: 'Deployment statistics are not available currently', }; diff --git a/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js b/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js index bbd50863069..34487885cf0 100644 --- a/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js +++ b/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js @@ -14,8 +14,8 @@ describe('Skeleton loading container', () => { vm.$destroy(); }); - it('renders 6 skeleton lines by default', () => { - expect(vm.$el.querySelector('.skeleton-line-6')).not.toBeNull(); + it('renders 3 skeleton lines by default', () => { + expect(vm.$el.querySelector('.skeleton-line-3')).not.toBeNull(); }); it('renders in full mode by default', () => { diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb index 3c4deba4712..58a49124ce6 100644 --- a/spec/lib/api/helpers_spec.rb +++ b/spec/lib/api/helpers_spec.rb @@ -3,6 +3,48 @@ require 'spec_helper' describe API::Helpers do subject { Class.new.include(described_class).new } + describe '#find_project' do + let(:project) { create(:project) } + + shared_examples 'project finder' do + context 'when project exists' do + it 'returns requested project' do + expect(subject.find_project(existing_id)).to eq(project) + end + + it 'returns nil' do + expect(subject.find_project(non_existing_id)).to be_nil + end + end + end + + context 'when ID is used as an argument' do + let(:existing_id) { project.id } + let(:non_existing_id) { (Project.maximum(:id) || 0) + 1 } + + it_behaves_like 'project finder' + end + + context 'when PATH is used as an argument' do + let(:existing_id) { project.full_path } + let(:non_existing_id) { 'something/else' } + + it_behaves_like 'project finder' + + context 'with an invalid PATH' do + let(:non_existing_id) { 'undefined' } # path without slash + + it_behaves_like 'project finder' + + it 'does not hit the database' do + expect(Project).not_to receive(:find_by_full_path) + + subject.find_project(non_existing_id) + end + end + end + end + describe '#find_namespace' do let(:namespace) { create(:namespace) } diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb index a41a28a56f1..e1af5a15371 100644 --- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb @@ -233,4 +233,20 @@ describe Banzai::Filter::CommitRangeReferenceFilter do expect(reference_filter(act).to_html).to eq exp end end + + context 'group context' do + let(:context) { { project: nil, group: create(:group) } } + + it 'ignores internal references' do + exp = act = "See #{range.to_reference}" + + expect(reference_filter(act, context).to_html).to eq exp + end + + it 'links to a full-path reference' do + reference = "#{project.full_path}@#{commit1.short_id}...#{commit2.short_id}" + + expect(reference_filter("See #{reference}", context).css('a').first.text).to eql(reference) + end + end end diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb index b18af806118..d6c9e9e4b19 100644 --- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb @@ -238,4 +238,20 @@ describe Banzai::Filter::CommitReferenceFilter do expect(doc.text).to eq("See (#{commit.reference_link_text(project)} (builds).patch)") end end + + context 'group context' do + let(:context) { { project: nil, group: create(:group) } } + + it 'ignores internal references' do + exp = act = "See #{commit.id}" + + expect(reference_filter(act, context).to_html).to eq exp + end + + it 'links to a valid reference' do + act = "See #{project.full_path}@#{commit.id}" + + expect(reference_filter(act, context).css('a').first.text).to eql("#{project.full_path}@#{commit.short_id}") + 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 6a9087d2e59..f8fa9b2d13d 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -343,14 +343,22 @@ describe Banzai::Filter::MilestoneReferenceFilter do end context 'group context' do + let(:context) { { project: nil, group: create(:group) } } + let(:milestone) { create(:milestone, project: project) } + 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) } ) + result = reference_filter("See #{reference}", context) expect(result.css('a').first.attr('href')).to eq(urls.milestone_url(milestone)) end + + it 'ignores internal references' do + exp = act = "See %#{milestone.iid}" + + expect(reference_filter(act, context).to_html).to eq exp + end end context 'when milestone is open' do diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb index e068e02d4fc..21cf092428d 100644 --- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb @@ -210,5 +210,11 @@ describe Banzai::Filter::SnippetReferenceFilter do expect(result.css('a').first.attr('href')).to eq(urls.project_snippet_url(project, snippet)) end + + it 'ignores internal references' do + exp = act = "See $#{snippet.id}" + + expect(reference_filter(act, project: nil, group: create(:group)).to_html).to eq exp + end end end diff --git a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb index 23e16fe0213..ff3b82cc482 100644 --- a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb @@ -107,12 +107,9 @@ describe Banzai::ReferenceParser::CommitRangeParser do describe '#find_object' do let(:range) { double(:range) } - before do - expect(CommitRange).to receive(:new).and_return(range) - end - context 'when the range has valid commits' do it 'returns the commit range' do + expect(CommitRange).to receive(:new).and_return(range) expect(range).to receive(:valid_commits?).and_return(true) expect(subject.find_object(project, '123..456')).to eq(range) @@ -121,10 +118,19 @@ describe Banzai::ReferenceParser::CommitRangeParser do context 'when the range does not have any valid commits' do it 'returns nil' do + expect(CommitRange).to receive(:new).and_return(range) expect(range).to receive(:valid_commits?).and_return(false) expect(subject.find_object(project, '123..456')).to be_nil end end + + context 'group context' do + it 'returns nil' do + group = create(:group) + + expect(subject.find_object(group, '123..456')).to be_nil + end + end end end diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb index 16704ff5e77..18658588a40 100644 --- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb +++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do let!(:project) { create(:project, :repository) } let(:pipeline_status) { described_class.new(project) } - let(:cache_key) { "projects/#{project.id}/pipeline_status" } + let(:cache_key) { described_class.cache_key_for_project(project) } describe '.load_for_project' do it "loads the status" do diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb index f97136f0191..bd443a5d9e7 100644 --- a/spec/lib/gitlab_spec.rb +++ b/spec/lib/gitlab_spec.rb @@ -14,6 +14,12 @@ describe Gitlab do expect(described_class.com?).to eq true end + it 'is true when on other gitlab subdomain' do + stub_config_setting(url: 'https://example.gitlab.com') + + expect(described_class.com?).to eq true + end + it 'is false when not on GitLab.com' do stub_config_setting(url: 'http://example.com') diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 2ec29a79e93..17272cb00e5 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1,6 +1,18 @@ # -*- coding: utf-8 -*- require 'spec_helper' +shared_examples 'languages and percentages JSON response' do + let(:expected_languages) { project.repository.languages.map { |language| language.values_at(:label, :value)}.to_h } + + it 'returns expected language values' do + get api("/projects/#{project.id}/languages", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq(expected_languages) + expect(json_response.count).to be > 1 + end +end + describe API::Projects do let(:user) { create(:user) } let(:user2) { create(:user) } @@ -1694,6 +1706,42 @@ describe API::Projects do end end + describe 'GET /projects/:id/languages' do + context 'with an authorized user' do + it_behaves_like 'languages and percentages JSON response' do + let(:project) { project3 } + end + + it 'returns not_found(404) for not existing project' do + get api("/projects/9999999999/languages", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with not authorized user' do + it 'returns not_found for existing but unauthorized project' do + get api("/projects/#{project3.id}/languages", user3) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'without user' do + let(:project_public) { create(:project, :public, :repository) } + + it_behaves_like 'languages and percentages JSON response' do + let(:project) { project_public } + end + + it 'returns not_found for existing but unauthorized project' do + get api("/projects/#{project3.id}/languages", nil) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + describe 'DELETE /projects/:id' do context 'when authenticated as user' do it 'removes project' do diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 28d51ac86c6..17c7a511857 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -406,7 +406,7 @@ describe API::Runner do expect(json_response['image']).to eq({ 'name' => 'ruby:2.1', 'entrypoint' => '/bin/sh' }) expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil, 'alias' => nil, 'command' => nil }, - { 'name' => 'docker:dind', 'entrypoint' => '/bin/sh', + { 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh', 'alias' => 'docker', 'command' => 'sleep 30' }]) expect(json_response['steps']).to eq(expected_steps) expect(json_response['artifacts']).to eq(expected_artifacts) diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 8603b7f3e2c..9ddcc5f2fbf 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -7,6 +7,16 @@ require 'selenium-webdriver' # Give CI some extra time timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30 +# Define an error class for JS console messages +JSConsoleError = Class.new(StandardError) + +# Filter out innocuous JS console messages +JS_CONSOLE_FILTER = Regexp.union([ + '"[HMR] Waiting for update signal from WDS..."', + '"[WDS] Hot Module Replacement enabled."', + "Download the Vue Devtools extension" +]) + Capybara.register_driver :chrome do |app| capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( # This enables access to logs with `page.driver.manage.get_log(:browser)` @@ -25,13 +35,7 @@ Capybara.register_driver :chrome do |app| options.add_argument("no-sandbox") # Run headless by default unless CHROME_HEADLESS specified - unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i - options.add_argument("headless") - - # Chrome documentation says this flag is needed for now - # https://developers.google.com/web/updates/2017/04/headless-chrome#cli - options.add_argument("disable-gpu") - end + options.add_argument("headless") unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252 options.add_argument("disable-dev-shm-usage") if ENV['CI'] || ENV['CI_SERVER'] @@ -78,6 +82,15 @@ RSpec.configure do |config| end config.after(:example, :js) do |example| + # when a test fails, display any messages in the browser's console + if example.exception + console = page.driver.browser.manage.logs.get(:browser)&.reject { |log| log.message =~ JS_CONSOLE_FILTER } + if console.present? + message = "Unexpected browser console output:\n" + console.map(&:message).join("\n") + raise JSConsoleError, message + end + end + # prevent localStorage from introducing side effects based on test order unless ['', 'about:blank', 'data:,'].include? Capybara.current_session.driver.browser.current_url execute_script("localStorage.clear();") diff --git a/spec/tasks/cache/clear/redis_spec.rb b/spec/tasks/cache/clear/redis_spec.rb new file mode 100644 index 00000000000..cca2b864e9b --- /dev/null +++ b/spec/tasks/cache/clear/redis_spec.rb @@ -0,0 +1,19 @@ +require 'rake_helper' + +describe 'clearing redis cache' do + before do + Rake.application.rake_require 'tasks/cache' + end + + describe 'clearing pipeline status cache' do + let(:pipeline_status) { create(:ci_pipeline).project.pipeline_status } + + before do + allow(pipeline_status).to receive(:loaded).and_return(nil) + end + + it 'clears pipeline status cache' do + expect { run_rake_task('cache:clear:redis') }.to change { pipeline_status.has_cache? } + end + end +end diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb index b4359d819a0..099baacf019 100644 --- a/spec/views/admin/dashboard/index.html.haml_spec.rb +++ b/spec/views/admin/dashboard/index.html.haml_spec.rb @@ -18,4 +18,10 @@ describe 'admin/dashboard/index.html.haml' do expect(rendered).to have_content 'GitLab Workhorse' expect(rendered).to have_content Gitlab::Workhorse.version end + + it "includes revision of GitLab" do + render + + expect(rendered).to have_content "#{Gitlab::VERSION} (#{Gitlab::REVISION})" + end end diff --git a/spec/views/shared/milestones/_top.html.haml.rb b/spec/views/shared/milestones/_top.html.haml.rb new file mode 100644 index 00000000000..516d81c87ac --- /dev/null +++ b/spec/views/shared/milestones/_top.html.haml.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe 'shared/milestones/_top.html.haml' do + set(:group) { create(:group) } + let(:project) { create(:project, group: group) } + let(:milestone) { create(:milestone, project: project) } + + before do + allow(milestone).to receive(:milestones) { [] } + end + + it 'renders a deprecation message for a legacy milestone' do + allow(milestone).to receive(:legacy_group_milestone?) { true } + + render 'shared/milestones/top', milestone: milestone + + expect(rendered).to have_css('.milestone-deprecation-message') + end + + it 'renders a deprecation message for a dashboard milestone' do + allow(milestone).to receive(:dashboard_milestone?) { true } + + render 'shared/milestones/top', milestone: milestone + + expect(rendered).to have_css('.milestone-deprecation-message') + end + + it 'does not render a deprecation message for a non-legacy and non-dashboard milestone' do + assign :group, group + + render 'shared/milestones/top', milestone: milestone + + expect(rendered).not_to have_css('.milestone-deprecation-message') + end +end diff --git a/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml b/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml index 4d5b6484d6e..ff7c87c29f0 100644 --- a/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml @@ -7,7 +7,7 @@ image: "chef/chefdk" services: - - docker:dind + - docker:stable-dind variables: DOCKER_HOST: "tcp://docker:2375" diff --git a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml index eeefadaa019..58d48d1284b 100644 --- a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml @@ -2,7 +2,7 @@ image: docker:latest services: - - docker:dind + - docker:stable-dind before_script: - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY diff --git a/yarn.lock b/yarn.lock index fde06adf67e..f35d625762c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -54,9 +54,9 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" -"@gitlab-org/gitlab-svgs@^1.17.0": - version "1.17.0" - resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.17.0.tgz#d0c74d9e44c127ccfad16941f352088b86f86c89" +"@gitlab-org/gitlab-svgs@^1.18.0": + version "1.18.0" + resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.18.0.tgz#7829f0e6de0647dace54c1fcd597ee3424afb233" "@types/jquery@^2.0.40": version "2.0.48" |