diff options
264 files changed, 3890 insertions, 2114 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 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index fcdb2e109f6..ee74734aa22 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -4.0.0 +4.1.0 @@ -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..bbe3c9e49b8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -206,7 +206,7 @@ GEM railties (>= 3.0.0) faraday (0.12.2) multipart-post (>= 1.2, < 3) - faraday_middleware (0.11.0.1) + faraday_middleware (0.12.2) faraday (>= 0.7.4, < 1.0) faraday_middleware-multi_json (0.0.6) faraday_middleware @@ -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 e77318fef46..3f84f4b9499 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/notes.js b/app/assets/javascripts/notes.js index ac70ddb3ff4..b0573510ff9 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1190,12 +1190,12 @@ export default class Notes { addForm = false; let lineTypeSelector = ''; rowCssToAdd = - '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content discussion-notes"></div></td></tr>'; + '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>'; // In parallel view, look inside the correct left/right pane if (this.isParallelView()) { lineTypeSelector = `.${lineType}`; rowCssToAdd = - '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content discussion-notes"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content discussion-notes"></div></td></tr>'; + '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>'; } const notesContentSelector = `.notes_content${lineTypeSelector} .content`; let notesContent = targetRow.find(notesContentSelector); diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 648fa6ff804..396a675b4ac 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -317,10 +317,10 @@ Please check your network connection and try again.`; <note-signed-out-widget v-if="!isLoggedIn" /> <discussion-locked-widget issuable-type="issue" - v-else-if="!canCreateNote" + v-else-if="isLocked(getNoteableData) && !canCreateNote" /> <ul - v-else + v-else-if="canCreateNote" class="notes notes-form timeline"> <li class="timeline-entry"> <div class="timeline-entry-inner"> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index a7e2d857013..626b0799581 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -40,6 +40,10 @@ export default { type: Boolean, required: true, }, + canAwardEmoji: { + type: Boolean, + required: true, + }, canDelete: { type: Boolean, required: true, @@ -74,9 +78,6 @@ export default { shouldShowActionsDropdown() { return this.currentUserId && (this.canEdit || this.canReportAsAbuse); }, - canAddAwardEmoji() { - return this.currentUserId; - }, isAuthoredByCurrentUser() { return this.authorId === this.currentUserId; }, @@ -149,7 +150,7 @@ export default { </button> </div> <div - v-if="canAddAwardEmoji" + v-if="canAwardEmoji" class="note-actions-item"> <a v-tooltip diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index 6cb8229e268..e8fd155a1ee 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -28,6 +28,10 @@ export default { type: Number, required: true, }, + canAwardEmoji: { + type: Boolean, + required: true, + }, }, computed: { ...mapGetters(['getUserData']), @@ -67,9 +71,6 @@ export default { isAuthoredByMe() { return this.noteAuthorId === this.getUserData.id; }, - isLoggedIn() { - return this.getUserData.id; - }, }, created() { this.emojiSmiling = emojiSmiling; @@ -156,7 +157,7 @@ export default { return title; }, handleAward(awardName) { - if (!this.isLoggedIn) { + if (!this.canAwardEmoji) { return; } @@ -208,7 +209,7 @@ export default { </span> </button> <div - v-if="isLoggedIn" + v-if="canAwardEmoji" class="award-menu-holder"> <button v-tooltip diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index 069f94c5845..0cb626c14f4 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -112,6 +112,7 @@ export default { :note-author-id="note.author.id" :awards="note.award_emoji" :toggle-award-path="note.toggle_award_path" + :can-award-emoji="note.current_user.can_award_emoji" /> <note-attachment v-if="note.attachment" diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 476b15aca4a..e0f883a8e08 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -258,7 +258,9 @@ Please check your network connection and try again.`; :key="note.id" /> </ul> - <div class="discussion-reply-holder"> + <div + :class="{ 'is-replying': isReplying }" + class="discussion-reply-holder"> <template v-if="!isReplying && canReply"> <div class="btn-group-justified discussion-with-resolve-btn" diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 3554027d2b4..566f5c68e66 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -177,6 +177,7 @@ export default { :note-id="note.id" :access-level="note.human_access" :can-edit="note.current_user.can_edit" + :can-award-emoji="note.current_user.can_award_emoji" :can-delete="note.current_user.can_edit" :can-report-as-abuse="canReportAsAbuse" :report-abuse-path="note.report_abuse_path" 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/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js index be37df36be8..628913483c6 100644 --- a/app/assets/javascripts/pages/projects/edit/index.js +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -1,12 +1,12 @@ import initSettingsPanels from '~/settings_panels'; import setupProjectEdit from '~/project_edit'; import initConfirmDangerModal from '~/confirm_danger_modal'; -import ProjectNew from '../shared/project_new'; +import initProjectLoadingSpinner from '../shared/save_project_loader'; import projectAvatar from '../shared/project_avatar'; import initProjectPermissionsSettings from '../shared/permissions'; document.addEventListener('DOMContentLoaded', () => { - new ProjectNew(); // eslint-disable-line no-new + initProjectLoadingSpinner(); setupProjectEdit(); // Initialize expandable settings panels initSettingsPanels(); diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js index ea6fd961393..7db644e2477 100644 --- a/app/assets/javascripts/pages/projects/new/index.js +++ b/app/assets/javascripts/pages/projects/new/index.js @@ -1,9 +1,9 @@ -import ProjectNew from '../shared/project_new'; +import initProjectLoadingSpinner from '../shared/save_project_loader'; import initProjectVisibilitySelector from '../../../project_visibility'; import initProjectNew from '../../../projects/project_new'; document.addEventListener('DOMContentLoaded', () => { - new ProjectNew(); // eslint-disable-line no-new + initProjectLoadingSpinner(); initProjectVisibilitySelector(); initProjectNew.bindEvents(); }); diff --git a/app/assets/javascripts/pages/projects/shared/project_new.js b/app/assets/javascripts/pages/projects/shared/project_new.js deleted file mode 100644 index 56d5574aa2f..00000000000 --- a/app/assets/javascripts/pages/projects/shared/project_new.js +++ /dev/null @@ -1,152 +0,0 @@ -/* eslint-disable func-names, no-var, no-underscore-dangle, prefer-template, prefer-arrow-callback*/ - -import $ from 'jquery'; -import VisibilitySelect from '../../../visibility_select'; - -function highlightChanges($elm) { - $elm.addClass('highlight-changes'); - setTimeout(() => $elm.removeClass('highlight-changes'), 10); -} - -export default class ProjectNew { - constructor() { - this.toggleSettings = this.toggleSettings.bind(this); - this.$selects = $('.features select'); - this.$repoSelects = this.$selects.filter('.js-repo-select'); - this.$projectSelects = this.$selects.not('.js-repo-select'); - - $('.project-edit-container').on('ajax:before', () => { - $('.project-edit-container').hide(); - return $('.save-project-loader').show(); - }); - - this.initVisibilitySelect(); - - this.toggleSettings(); - this.toggleSettingsOnclick(); - this.toggleRepoVisibility(); - } - - initVisibilitySelect() { - const visibilityContainer = document.querySelector('.js-visibility-select'); - if (!visibilityContainer) return; - const visibilitySelect = new VisibilitySelect(visibilityContainer); - visibilitySelect.init(); - - const $visibilitySelect = $(visibilityContainer).find('select'); - let projectVisibility = $visibilitySelect.val(); - const PROJECT_VISIBILITY_PRIVATE = '0'; - - $visibilitySelect.on('change', () => { - const newProjectVisibility = $visibilitySelect.val(); - - if (projectVisibility !== newProjectVisibility) { - this.$projectSelects.each((idx, select) => { - const $select = $(select); - const $options = $select.find('option'); - const values = $.map($options, e => e.value); - - // if switched to "private", limit visibility options - if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) { - if ($select.val() !== values[0] && $select.val() !== values[1]) { - $select.val(values[1]).trigger('change'); - highlightChanges($select); - } - $options.slice(2).disable(); - } - - // if switched from "private", increase visibility for non-disabled options - if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) { - $options.enable(); - if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) { - $select.val(values[values.length - 1]).trigger('change'); - highlightChanges($select); - } - } - }); - - projectVisibility = newProjectVisibility; - } - }); - } - - toggleSettings() { - this.$selects.each(function () { - var $select = $(this); - var className = $select.data('field') - .replace(/_/g, '-') - .replace('access-level', 'feature'); - ProjectNew._showOrHide($select, '.' + className); - }); - } - - toggleSettingsOnclick() { - this.$selects.on('change', this.toggleSettings); - } - - static _showOrHide(checkElement, container) { - const $container = $(container); - - if ($(checkElement).val() !== '0') { - return $container.show(); - } - return $container.hide(); - } - - toggleRepoVisibility() { - var $repoAccessLevel = $('.js-repo-access-level select'); - var $lfsEnabledOption = $('.js-lfs-enabled select'); - var containerRegistry = document.querySelectorAll('.js-container-registry')[0]; - var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled'); - var prevSelectedVal = parseInt($repoAccessLevel.val(), 10); - - this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']") - .nextAll() - .hide(); - - $repoAccessLevel - .off('change') - .on('change', function () { - var selectedVal = parseInt($repoAccessLevel.val(), 10); - - this.$repoSelects.each(function () { - var $this = $(this); - var repoSelectVal = parseInt($this.val(), 10); - - $this.find('option').enable(); - - if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) { - $this.val(selectedVal).trigger('change'); - highlightChanges($this); - } - - $this.find("option[value='" + selectedVal + "']").nextAll().disable(); - }); - - if (selectedVal) { - this.$repoSelects.removeClass('disabled'); - - if ($lfsEnabledOption.length) { - $lfsEnabledOption.removeClass('disabled'); - highlightChanges($lfsEnabledOption); - } - if (containerRegistry) { - containerRegistry.style.display = ''; - } - } else { - this.$repoSelects.addClass('disabled'); - - if ($lfsEnabledOption.length) { - $lfsEnabledOption.val('false').addClass('disabled'); - highlightChanges($lfsEnabledOption); - } - if (containerRegistry) { - containerRegistry.style.display = 'none'; - containerRegistryCheckbox.checked = false; - } - } - - prevSelectedVal = selectedVal; - }.bind(this)); - } -} diff --git a/app/assets/javascripts/pages/projects/shared/save_project_loader.js b/app/assets/javascripts/pages/projects/shared/save_project_loader.js new file mode 100644 index 00000000000..aa3589ac88d --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/save_project_loader.js @@ -0,0 +1,12 @@ +import $ from 'jquery'; + +export default function initProjectLoadingSpinner() { + const $formContainer = $('.project-edit-container'); + const $loadingSpinner = $('.save-project-loader'); + + // show loading spinner when saving + $formContainer.on('ajax:before', () => { + $formContainer.hide(); + $loadingSpinner.show(); + }); +} 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/visibility_select.js b/app/assets/javascripts/visibility_select.js deleted file mode 100644 index 0c928d0d5f6..00000000000 --- a/app/assets/javascripts/visibility_select.js +++ /dev/null @@ -1,21 +0,0 @@ -export default class VisibilitySelect { - constructor(container) { - if (!container) throw new Error('VisibilitySelect requires a container element as argument 1'); - this.container = container; - this.helpBlock = this.container.querySelector('.help-block'); - this.select = this.container.querySelector('select'); - } - - init() { - if (this.select) { - this.updateHelpText(); - this.select.addEventListener('change', this.updateHelpText.bind(this)); - } else { - this.helpBlock.textContent = this.container.querySelector('.js-locked').dataset.helpBlock; - } - } - - updateHelpText() { - this.helpBlock.textContent = this.select.querySelector('option:checked').dataset.description; - } -} 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 6b89387ab5f..48f20029383 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -422,25 +422,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 cc74cb72795..cc5fac6816d 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 30c15c231d5..606d4675f19 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/diff.scss b/app/assets/stylesheets/pages/diff.scss index 679f783b1b6..7f037582ca0 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -813,7 +813,6 @@ } .discussion-notes { - padding: 0 $gl-padding $gl-padding; min-height: 35px; &:first-child { diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index e5afa8fffcb..3af8d80daab 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/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 8720f821ce9..4a528bc2bb1 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -173,7 +173,11 @@ } .discussion-form { - padding-top: $gl-padding-top; + background-color: $white-light; +} + +.discussion-form-container { + padding: $gl-padding-top $gl-padding $gl-padding; } .discussion-notes .disabled-comment { @@ -233,7 +237,12 @@ .discussion-body, .diff-file { .discussion-reply-holder { - padding-top: $gl-padding; + background-color: $white-light; + padding: 10px 16px; + + &.is-replying { + padding-bottom: $gl-padding; + } } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 9d9cbecc958..81e98f358a8 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -47,7 +47,7 @@ ul.notes { } .timeline-entry-inner { - padding: $gl-padding 0; + padding: $gl-padding $gl-btn-padding; border-bottom: 1px solid $white-normal; } @@ -94,6 +94,12 @@ ul.notes { } } + &.note-discussion { + .timeline-entry-inner { + padding: $gl-padding 10px; + } + } + .editing-spinner { display: none; } @@ -346,8 +352,6 @@ ul.notes { } .discussion-notes { - background-color: $white-light; - &:not(:first-child) { border-top: 1px solid $white-normal; margin-top: 20px; @@ -359,6 +363,10 @@ ul.notes { } } + .notes { + background-color: $white-light; + } + a code { top: 0; margin-right: 0; @@ -639,6 +647,8 @@ ul.notes { border-bottom: 1px solid $white-normal; .timeline-entry-inner { + padding-left: $gl-padding; + padding-right: $gl-padding; border-bottom: 0; } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 8d5eb2e8c5a..2c840cb407a 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -344,7 +344,6 @@ svg { vertical-align: middle; - margin-right: 3px; } .stage-column { @@ -495,17 +494,12 @@ svg { fill: $gl-text-color-secondary; position: relative; - left: 1px; top: -1px; - width: 16px; - height: 16px; } &.play { svg { - width: 16px; - height: 16px; - left: 3px; + left: 2px; } } } diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index a414deb8921..5f46e69a56d 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/checks_collaboration.rb b/app/controllers/concerns/checks_collaboration.rb new file mode 100644 index 00000000000..81367663a06 --- /dev/null +++ b/app/controllers/concerns/checks_collaboration.rb @@ -0,0 +1,21 @@ +module ChecksCollaboration + def can_collaborate_with_project?(project, ref: nil) + return true if can?(current_user, :push_code, project) + + can_create_merge_request = + can?(current_user, :create_merge_request_in, project) && + current_user.already_forked?(project) + + can_create_merge_request || + user_access(project).can_push_to_branch?(ref) + end + + # rubocop:disable Gitlab/ModuleWithInstanceVariables + # enabling this so we can easily cache the user access value as it might be + # used across multiple calls in the view + def user_access(project) + @user_access ||= {} + @user_access[project] ||= Gitlab::UserAccess.new(current_user, project: project) + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables +end 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/application_controller.rb b/app/controllers/projects/application_controller.rb index 6d9b42a2c04..032bb2267e7 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -1,5 +1,6 @@ class Projects::ApplicationController < ApplicationController include RoutableActions + include ChecksCollaboration skip_before_action :authenticate_user! before_action :project @@ -31,14 +32,6 @@ class Projects::ApplicationController < ApplicationController @repository ||= project.repository end - def can_collaborate_with_project?(project = nil, ref: nil) - project ||= @project - - can?(current_user, :push_code, project) || - (current_user && current_user.already_forked?(project)) || - user_access(project).can_push_to_branch?(ref) - end - def authorize_action!(action) unless can?(current_user, action, project) return access_denied! @@ -91,9 +84,4 @@ class Projects::ApplicationController < ApplicationController def check_issues_available! return render_404 unless @project.feature_available?(:issues, current_user) end - - def user_access(project) - @user_access ||= {} - @user_access[project] ||= Gitlab::UserAccess.new(current_user, project: project) - end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index b14939c4216..767e492f566 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -20,7 +20,7 @@ class Projects::IssuesController < Projects::ApplicationController before_action :authorize_update_issuable!, only: [:edit, :update, :move] # Allow create a new branch and empty WIP merge request from current issue - before_action :authorize_create_merge_request!, only: [:create_merge_request] + before_action :authorize_create_merge_request_from!, only: [:create_merge_request] respond_to :html diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index a90030a8312..4a377fefc62 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -5,7 +5,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap skip_before_action :merge_request before_action :whitelist_query_limiting, only: [:create] - before_action :authorize_create_merge_request! + before_action :authorize_create_merge_request_from! before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path] before_action :build_merge_request, except: [:create] 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/finders/merge_request_target_project_finder.rb b/app/finders/merge_request_target_project_finder.rb index f358938344e..188ec447a94 100644 --- a/app/finders/merge_request_target_project_finder.rb +++ b/app/finders/merge_request_target_project_finder.rb @@ -12,6 +12,7 @@ class MergeRequestTargetProjectFinder if @source_project.fork_network @source_project.fork_network.projects .public_or_visible_to_user(current_user) + .non_archived .with_feature_available_for_user(:merge_requests, current_user) else Project.where(id: source_project) 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/blob_helper.rb b/app/helpers/blob_helper.rb index 2b440e4d584..866b8773db6 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -59,7 +59,7 @@ module BlobHelper button_tag label, class: "#{common_classes} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' } elsif can_modify_blob?(blob, project, ref) button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal' - elsif can?(current_user, :fork_project, project) + elsif can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project) edit_fork_button_tag(common_classes, project, label, edit_modify_file_fork_params(action), action) end end @@ -280,7 +280,7 @@ module BlobHelper options << link_to("submit an issue", new_project_issue_path(project)) end - merge_project = can?(current_user, :create_merge_request, project) ? project : (current_user && current_user.fork_of(project)) + merge_project = merge_request_source_project_for_project(@project) if merge_project options << link_to("create a merge request", project_new_merge_request_path(project)) end @@ -334,7 +334,7 @@ module BlobHelper # Web IDE (Beta) requires the user to have this feature enabled elsif !current_user || (current_user && can_modify_blob?(blob, project, ref)) edit_link_tag(text, edit_path, common_classes) - elsif current_user && can?(current_user, :fork_project, project) + elsif can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project) edit_fork_button_tag(common_classes, project, text, edit_blob_fork_params(edit_path)) end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 7cc56de24e4..98894b86551 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -163,7 +163,7 @@ module CommitsHelper tooltip = "#{action.capitalize} this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip btn_class = "btn btn-#{btn_class}" unless btn_class.nil? - if can_collaborate_with_project? + if can_collaborate_with_project?(@project) link_to action.capitalize, "#modal-#{action}-commit", 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" elsif can?(current_user, :fork_project, @project) continue_params = { diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb index 8bf96c0905f..2df5b5d1695 100644 --- a/app/helpers/compare_helper.rb +++ b/app/helpers/compare_helper.rb @@ -3,7 +3,7 @@ module CompareHelper from.present? && to.present? && from != to && - can?(current_user, :create_merge_request, project) && + can?(current_user, :create_merge_request_from, project) && project.repository.branch_exists?(from) && project.repository.branch_exists?(to) end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index c5522ff7a69..2f304b040c7 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/issues_helper.rb b/app/helpers/issues_helper.rb index 0f25d401406..96dc7ae1185 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -82,8 +82,8 @@ module IssuesHelper names.to_sentence end - def award_state_class(awards, current_user) - if !current_user + def award_state_class(awardable, awards, current_user) + if !can?(current_user, :award_emoji, awardable) "disabled" elsif current_user && awards.find { |a| a.user_id == current_user.id } "active" @@ -126,6 +126,17 @@ module IssuesHelper link_to link_text, path end + def show_new_issue_link?(project) + return false unless project + return false if project.archived? + + # We want to show the link to users that are not signed in, that way they + # get directed to the sign-in/sign-up flow and afterwards to the new issue page. + return true unless current_user + + can?(current_user, :create_issue, project) + end + # Required for Banzai::Filter::IssueReferenceFilter module_function :url_for_issue module_function :url_for_internal_issue diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index fb4fe1c40b7..c19c5b9cc82 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -138,6 +138,18 @@ module MergeRequestsHelper end end + def merge_request_source_project_for_project(project = @project) + unless can?(current_user, :create_merge_request_in, project) + return nil + end + + if can?(current_user, :create_merge_request_from, project) + project + else + current_user.fork_of(project) + end + end + def merge_params_ee(merge_request) {} end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 27ed48fdbc7..7f67574a428 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -6,10 +6,6 @@ module NotesHelper end end - def note_editable?(note) - Ability.can_edit_note?(current_user, note) - end - def note_supports_quick_actions?(note) Notes::QuickActionsService.supported?(note) end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 15f48e43a28..a64b2acdd77 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -157,40 +157,6 @@ module ProjectsHelper current_user&.recent_push(@project) end - def project_feature_access_select(field) - # Don't show option "everyone with access" if project is private - options = project_feature_options - - level = @project.project_feature.public_send(field) # rubocop:disable GitlabSecurity/PublicSend - - if @project.private? - disabled_option = ProjectFeature::ENABLED - highest_available_option = ProjectFeature::PRIVATE if level == disabled_option - end - - options = options_for_select( - options.invert, - selected: highest_available_option || level, - disabled: disabled_option - ) - - content_tag :div, class: "select-wrapper" do - concat( - content_tag( - :select, - options, - name: "project[project_feature_attributes][#{field}]", - id: "project_project_feature_attributes_#{field}", - class: "pull-right form-control select-control #{repo_children_classes(field)} ", - data: { field: field } - ) - ) - concat( - icon('chevron-down') - ) - end.html_safe - end - def link_to_autodeploy_doc link_to _('About auto deploy'), help_page_path('ci/autodeploy/index'), target: '_blank' end @@ -274,16 +240,6 @@ module ProjectsHelper private - def repo_children_classes(field) - needs_repo_check = [:merge_requests_access_level, :builds_access_level] - return unless needs_repo_check.include?(field) - - classes = "project-repo-select js-repo-select" - classes << " disabled" unless @project.feature_available?(:repository, current_user) - - classes - end - def get_project_nav_tabs(project, current_user) nav_tabs = [:home] @@ -447,14 +403,6 @@ module ProjectsHelper filtered_message.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]") end - def project_feature_options - { - ProjectFeature::DISABLED => s_('ProjectFeature|Disabled'), - ProjectFeature::PRIVATE => s_('ProjectFeature|Only team members'), - ProjectFeature::ENABLED => s_('ProjectFeature|Everyone with access') - } - end - def project_child_container_class(view_path) view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}" end @@ -463,20 +411,6 @@ module ProjectsHelper IssuesFinder.new(current_user, project_id: project.id).execute end - def visibility_select_options(project, selected_level) - level_options = Gitlab::VisibilityLevel.values.each_with_object([]) do |level, level_options| - next if restricted_levels.include?(level) - - level_options << [ - visibility_level_label(level), - { data: { description: visibility_level_description(level, project) } }, - level - ] - end - - options_for_select(level_options, selected_level) - end - def restricted_levels return [] if current_user.admin? 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/models/ability.rb b/app/models/ability.rb index 6dae49f38dc..618d4af4272 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -46,10 +46,6 @@ class Ability end end - def can_edit_note?(user, note) - allowed?(user, :edit_note, note) - end - def allowed?(user, action, subject = :global, opts = {}) if subject.is_a?(Hash) opts, subject = subject, :global diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index d8394415362..fce37e7f78e 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -79,11 +79,7 @@ module Awardable end def user_can_award?(current_user, name) - if user_authored?(current_user) - !awardable_votes?(normalize_name(name)) - else - true - end + awardable_by_user?(current_user, name) && Ability.allowed?(current_user, :award_emoji, self) end def user_authored?(current_user) @@ -119,4 +115,12 @@ module Awardable def normalize_name(name) Gitlab::Emoji.normalize_emoji_name(name) end + + def awardable_by_user?(current_user, name) + if user_authored?(current_user) + !awardable_votes?(normalize_name(name)) + else + true + end + end end diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb index cbec735c2dd..96a43006642 100644 --- a/app/models/internal_id.rb +++ b/app/models/internal_id.rb @@ -23,9 +23,12 @@ class InternalId < ActiveRecord::Base # # The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL). # As such, the increment is atomic and safe to be called concurrently. - def increment_and_save! + # + # If a `maximum_iid` is passed in, this overrides the incremented value if it's + # greater than that. This can be used to correct the increment value if necessary. + def increment_and_save!(maximum_iid) lock! - self.last_value = (last_value || 0) + 1 + self.last_value = [(last_value || 0) + 1, (maximum_iid || 0) + 1].max save! last_value end @@ -89,7 +92,16 @@ class InternalId < ActiveRecord::Base # and increment its last value # # Note this will acquire a ROW SHARE lock on the InternalId record - (lookup || create_record).increment_and_save! + + # Note we always calculate the maximum iid present here and + # pass it in to correct the InternalId entry if it's last_value is off. + # + # This can happen in a transition phase where both `AtomicInternalId` and + # `NonatomicInternalId` code runs (e.g. during a deploy). + # + # This is subject to be cleaned up with the 10.8 release: + # https://gitlab.com/gitlab-org/gitlab-ce/issues/45389. + (lookup || create_record).increment_and_save!(maximum_iid) end end @@ -115,11 +127,15 @@ class InternalId < ActiveRecord::Base InternalId.create!( **scope, usage: usage_value, - last_value: init.call(subject) || 0 + last_value: maximum_iid ) end rescue ActiveRecord::RecordNotUnique lookup end + + def maximum_iid + @maximum_iid ||= init.call(subject) || 0 + end end end diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 1ab391a5a9d..808a81cbbf9 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -11,7 +11,7 @@ module Ci end condition(:owner_of_job) do - can?(:developer_access) && @subject.triggered_by?(@user) + @subject.triggered_by?(@user) end rule { protected_ref }.policy do @@ -19,6 +19,6 @@ module Ci prevent :erase_build end - rule { can?(:master_access) | owner_of_job }.enable :erase_build + rule { can?(:admin_build) | (can?(:update_build) & owner_of_job) }.enable :erase_build end end diff --git a/app/policies/ci/pipeline_schedule_policy.rb b/app/policies/ci/pipeline_schedule_policy.rb index dc7a4aed577..ecba0488d3c 100644 --- a/app/policies/ci/pipeline_schedule_policy.rb +++ b/app/policies/ci/pipeline_schedule_policy.rb @@ -7,23 +7,17 @@ module Ci end condition(:owner_of_schedule) do - can?(:developer_access) && pipeline_schedule.owned_by?(@user) + pipeline_schedule.owned_by?(@user) end - condition(:non_owner_of_schedule) do - !pipeline_schedule.owned_by?(@user) - end - - rule { can?(:developer_access) }.policy do - enable :play_pipeline_schedule - end + rule { can?(:create_pipeline) }.enable :play_pipeline_schedule - rule { can?(:master_access) | owner_of_schedule }.policy do + rule { can?(:admin_pipeline) | (can?(:update_build) & owner_of_schedule) }.policy do enable :update_pipeline_schedule enable :admin_pipeline_schedule end - rule { can?(:master_access) & non_owner_of_schedule }.policy do + rule { can?(:admin_pipeline_schedule) & ~owner_of_schedule }.policy do enable :take_ownership_pipeline_schedule end diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb index e86d1c8f98e..b431d376e3d 100644 --- a/app/policies/issuable_policy.rb +++ b/app/policies/issuable_policy.rb @@ -18,9 +18,7 @@ class IssuablePolicy < BasePolicy rule { locked & ~is_project_member }.policy do prevent :create_note - prevent :update_note prevent :admin_note prevent :resolve_note - prevent :edit_note end end diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb index d4cb5a77e63..077a6761ee6 100644 --- a/app/policies/note_policy.rb +++ b/app/policies/note_policy.rb @@ -1,26 +1,21 @@ class NotePolicy < BasePolicy delegate { @subject.project } - delegate { @subject.noteable if @subject.noteable.lockable? } + delegate { @subject.noteable if DeclarativePolicy.has_policy?(@subject.noteable) } condition(:is_author) { @user && @subject.author == @user } - condition(:for_merge_request, scope: :subject) { @subject.for_merge_request? } condition(:is_noteable_author) { @user && @subject.noteable.author_id == @user.id } condition(:editable, scope: :subject) { @subject.editable? } - rule { ~editable | anonymous }.prevent :edit_note - - rule { is_author | admin }.enable :edit_note - rule { can?(:master_access) }.enable :edit_note + rule { ~editable }.prevent :admin_note rule { is_author }.policy do enable :read_note - enable :update_note enable :admin_note enable :resolve_note end - rule { for_merge_request & is_noteable_author }.policy do + rule { is_noteable_author }.policy do enable :resolve_note end end diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb index cac0530b9f7..c1a84727cfa 100644 --- a/app/policies/personal_snippet_policy.rb +++ b/app/policies/personal_snippet_policy.rb @@ -25,4 +25,6 @@ class PersonalSnippetPolicy < BasePolicy end rule { anonymous }.prevent :comment_personal_snippet + + rule { can?(:comment_personal_snippet) }.enable :award_emoji end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 21bb0934dee..3529d0aa60c 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -1,12 +1,26 @@ class ProjectPolicy < BasePolicy - def self.create_read_update_admin(name) - [ - :"create_#{name}", - :"read_#{name}", - :"update_#{name}", - :"admin_#{name}" - ] - end + extend ClassMethods + + READONLY_FEATURES_WHEN_ARCHIVED = %i[ + issue + list + merge_request + label + milestone + project_snippet + wiki + note + pipeline + pipeline_schedule + build + trigger + environment + deployment + commit_status + container_image + pages + cluster + ].freeze desc "User is a project owner" condition :owner do @@ -15,7 +29,7 @@ class ProjectPolicy < BasePolicy end desc "Project has public builds enabled" - condition(:public_builds, scope: :subject) { project.public_builds? } + condition(:public_builds, scope: :subject, score: 0) { project.public_builds? } # For guest access we use #team_member? so we can use # project.members, which gets cached in subject scope. @@ -35,7 +49,7 @@ class ProjectPolicy < BasePolicy condition(:master) { team_access_level >= Gitlab::Access::MASTER } desc "Project is public" - condition(:public_project, scope: :subject) { project.public? } + condition(:public_project, scope: :subject, score: 0) { project.public? } desc "Project is visible to internal users" condition(:internal_access) do @@ -46,7 +60,7 @@ class ProjectPolicy < BasePolicy condition(:group_member, scope: :subject) { project_group_member? } desc "Project is archived" - condition(:archived, scope: :subject) { project.archived? } + condition(:archived, scope: :subject, score: 0) { project.archived? } condition(:default_issues_tracker, scope: :subject) { project.default_issues_tracker? } @@ -56,10 +70,10 @@ class ProjectPolicy < BasePolicy end desc "Project has an external wiki" - condition(:has_external_wiki, scope: :subject) { project.has_external_wiki? } + condition(:has_external_wiki, scope: :subject, score: 0) { project.has_external_wiki? } desc "Project has request access enabled" - condition(:request_access_enabled, scope: :subject) { project.request_access_enabled } + condition(:request_access_enabled, scope: :subject, score: 0) { project.request_access_enabled } desc "Has merge requests allowing pushes to user" condition(:has_merge_requests_allowing_pushes, scope: :subject) do @@ -126,6 +140,7 @@ class ProjectPolicy < BasePolicy rule { can?(:guest_access) }.policy do enable :read_project + enable :create_merge_request_in enable :read_board enable :read_list enable :read_wiki @@ -140,6 +155,7 @@ class ProjectPolicy < BasePolicy enable :create_note enable :upload_file enable :read_cycle_analytics + enable :award_emoji end # These abilities are not allowed to admins that are not members of the project, @@ -197,7 +213,7 @@ class ProjectPolicy < BasePolicy enable :create_pipeline enable :update_pipeline enable :create_pipeline_schedule - enable :create_merge_request + enable :create_merge_request_from enable :create_wiki enable :push_code enable :resolve_note @@ -208,7 +224,7 @@ class ProjectPolicy < BasePolicy end rule { can?(:master_access) }.policy do - enable :delete_protected_branch + enable :push_to_delete_protected_branch enable :update_project_snippet enable :update_environment enable :update_deployment @@ -231,37 +247,50 @@ class ProjectPolicy < BasePolicy end rule { archived }.policy do - prevent :create_merge_request prevent :push_code - prevent :delete_protected_branch - prevent :update_merge_request - prevent :admin_merge_request + prevent :push_to_delete_protected_branch + prevent :request_access + prevent :upload_file + prevent :resolve_note + prevent :create_merge_request_from + prevent :create_merge_request_in + prevent :award_emoji + + READONLY_FEATURES_WHEN_ARCHIVED.each do |feature| + prevent(*create_update_admin_destroy(feature)) + end + end + + rule { issues_disabled }.policy do + prevent(*create_read_update_admin_destroy(:issue)) end rule { merge_requests_disabled | repository_disabled }.policy do - prevent(*create_read_update_admin(:merge_request)) + prevent :create_merge_request_in + prevent :create_merge_request_from + prevent(*create_read_update_admin_destroy(:merge_request)) end rule { issues_disabled & merge_requests_disabled }.policy do - prevent(*create_read_update_admin(:label)) - prevent(*create_read_update_admin(:milestone)) + prevent(*create_read_update_admin_destroy(:label)) + prevent(*create_read_update_admin_destroy(:milestone)) end rule { snippets_disabled }.policy do - prevent(*create_read_update_admin(:project_snippet)) + prevent(*create_read_update_admin_destroy(:project_snippet)) end rule { wiki_disabled & ~has_external_wiki }.policy do - prevent(*create_read_update_admin(:wiki)) + prevent(*create_read_update_admin_destroy(:wiki)) prevent(:download_wiki_code) end rule { builds_disabled | repository_disabled }.policy do - prevent(*create_read_update_admin(:build)) - prevent(*(create_read_update_admin(:pipeline) - [:read_pipeline])) - prevent(*create_read_update_admin(:pipeline_schedule)) - prevent(*create_read_update_admin(:environment)) - prevent(*create_read_update_admin(:deployment)) + prevent(*create_update_admin_destroy(:pipeline)) + prevent(*create_read_update_admin_destroy(:build)) + prevent(*create_read_update_admin_destroy(:pipeline_schedule)) + prevent(*create_read_update_admin_destroy(:environment)) + prevent(*create_read_update_admin_destroy(:deployment)) end rule { repository_disabled }.policy do @@ -272,7 +301,7 @@ class ProjectPolicy < BasePolicy end rule { container_registry_disabled }.policy do - prevent(*create_read_update_admin(:container_image)) + prevent(*create_read_update_admin_destroy(:container_image)) end rule { anonymous & ~public_project }.prevent_all @@ -314,13 +343,6 @@ class ProjectPolicy < BasePolicy enable :read_pipeline_schedule end - rule { issues_disabled }.policy do - prevent :create_issue - prevent :update_issue - prevent :admin_issue - prevent :read_issue - end - # These rules are included to allow maintainers of projects to push to certain # to run pipelines for the branches they have access to. rule { can?(:public_access) & has_merge_requests_allowing_pushes }.policy do diff --git a/app/policies/project_policy/class_methods.rb b/app/policies/project_policy/class_methods.rb new file mode 100644 index 00000000000..60e5aba00ba --- /dev/null +++ b/app/policies/project_policy/class_methods.rb @@ -0,0 +1,19 @@ +class ProjectPolicy + module ClassMethods + def create_read_update_admin_destroy(name) + [ + :"read_#{name}", + *create_update_admin_destroy(name) + ] + end + + def create_update_admin_destroy(name) + [ + :"create_#{name}", + :"update_#{name}", + :"admin_#{name}", + :"destroy_#{name}" + ] + end + end +end diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index 9f3f2637183..4b4132af2d0 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -3,6 +3,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated include GitlabRoutingHelper include MarkupHelper include TreeHelper + include ChecksCollaboration include Gitlab::Utils::StrongMemoize presents :merge_request @@ -152,11 +153,11 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated end def can_revert_on_current_merge_request? - user_can_collaborate_with_project? && cached_can_be_reverted? + can_collaborate_with_project?(project) && cached_can_be_reverted? end def can_cherry_pick_on_current_merge_request? - user_can_collaborate_with_project? && can_be_cherry_picked? + can_collaborate_with_project?(project) && can_be_cherry_picked? end def can_push_to_source_branch? @@ -195,12 +196,6 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated end.sort.to_sentence end - def user_can_collaborate_with_project? - can?(current_user, :push_code, project) || - (current_user && current_user.already_forked?(project)) || - can_push_to_source_branch? - end - def user_can_fork_project? can?(current_user, :fork_project, project) end diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb index b5e2334b6e3..840fdbcbf14 100644 --- a/app/serializers/issue_entity.rb +++ b/app/serializers/issue_entity.rb @@ -29,6 +29,10 @@ class IssueEntity < IssuableEntity expose :can_update do |issue| can?(request.current_user, :update_issue, issue) end + + expose :can_award_emoji do |issue| + can?(request.current_user, :award_emoji, issue) + end end expose :create_note_path do |issue| diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb index c964aa9c99b..06d603b277e 100644 --- a/app/serializers/note_entity.rb +++ b/app/serializers/note_entity.rb @@ -15,7 +15,11 @@ class NoteEntity < API::Entities::Note expose :current_user do expose :can_edit do |note| - Ability.can_edit_note?(request.current_user, note) + Ability.allowed?(request.current_user, :admin_note, note) + end + + expose :can_award_emoji do |note| + Ability.allowed?(request.current_user, :award_emoji, note) end end diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index c57a2445341..fe1ac70781e 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -71,8 +71,8 @@ module MergeRequests params.delete(:source_project_id) params.delete(:target_project_id) - unless can?(current_user, :read_project, @source_project) && - can?(current_user, :read_project, @project) + unless can?(current_user, :create_merge_request_from, @source_project) && + can?(current_user, :create_merge_request_in, @project) raise Gitlab::Access::AccessDeniedError end diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml index 864e64b5fa9..48331c40bca 100644 --- a/app/views/admin/application_settings/_signin.html.haml +++ b/app/views/admin/application_settings/_signin.html.haml @@ -24,6 +24,7 @@ - if omniauth_enabled? && button_based_providers.any? .form-group = 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 cbc779548f6..a75dd90fe6b 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 = f.label :import_sources, class: 'control-label col-sm-2' .col-sm-10 + = hidden_field_tag 'application_setting[import_sources][]' - import_sources_checkboxes('import-sources-help').each do |source| .checkbox= source %span.help-block#import-sources-help diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 05c41082882..bbf0e0fb95c 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -126,6 +126,7 @@ GitLab %span.pull-right = Gitlab::VERSION + = "(#{Gitlab::REVISION})" %p GitLab Shell %span.pull-right diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index c47b8a88f56..aeba9788fda 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -101,7 +101,7 @@ - if @project.archived? %li %span.light archived: - %strong repository is read-only + %strong project is read-only %li %span.light access: diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index 5f07d2720c2..4b3c52af16a 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -3,13 +3,13 @@ .awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } } - awards_sort(grouped_emojis).each do |emoji, awards| %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", - class: [(award_state_class(awards, current_user)), (award_user_authored_class(emoji) if user_authored)], + class: [(award_state_class(awardable, awards, current_user)), (award_user_authored_class(emoji) if user_authored)], data: { placement: "bottom", title: award_user_list(awards, current_user) } } = emoji_icon(emoji) %span.award-control-text.js-counter = awards.count - - if current_user + - if can?(current_user, :award_emoji, awardable) .award-menu-holder.js-award-holder %button.btn.award-control.has-tooltip.js-add-award{ type: 'button', 'aria-label': 'Add reaction', diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index eb32f393310..6f53f5ac1ae 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -19,8 +19,8 @@ %li.dropdown-bold-header GitLab - if @project&.persisted? - - create_project_issue = can?(current_user, :create_issue, @project) - - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) + - create_project_issue = show_new_issue_link?(@project) + - merge_project = merge_request_source_project_for_project(@project) - create_project_snippet = can?(current_user, :create_project_snippet, @project) - if create_project_issue || merge_project || create_project_snippet %li.dropdown-bold-header This project diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 93f674b9d3c..196db08cebd 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -13,7 +13,7 @@ .nav-icon-container = sprite_icon('project') %span.nav-item-name - Overview + Project %ul.sidebar-sub-level-items = nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 1bd10018b40..d1eae05c46c 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/_last_push.html.haml b/app/views/projects/_last_push.html.haml index 6a1035d2dc7..f6d396c8127 100644 --- a/app/views/projects/_last_push.html.haml +++ b/app/views/projects/_last_push.html.haml @@ -13,6 +13,7 @@ #{time_ago_with_tooltip(event.created_at)} - .flex-right - = link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn btn-info btn-sm qa-create-merge-request" do - #{ _('Create merge request') } + - if can?(current_user, :create_merge_request_in, event.project.default_merge_request_target) + .flex-right + = link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn btn-info btn-sm qa-create-merge-request" do + #{ _('Create merge request') } diff --git a/app/views/projects/_visibility_select.html.haml b/app/views/projects/_visibility_select.html.haml deleted file mode 100644 index 4026b9e3c46..00000000000 --- a/app/views/projects/_visibility_select.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- if can_change_visibility_level?(@project, current_user) - .select-wrapper - = form.select(model_method, visibility_select_options(@project, selected_level), {}, class: 'form-control visibility-select select-control') - = icon('chevron-down') -- else - .info.js-locked{ data: { help_block: visibility_level_description(@project.visibility_level, @project) } } - = visibility_level_icon(@project.visibility_level) - %strong - = visibility_level_label(@project.visibility_level) 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/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 883dfb3e6c8..71176acd12d 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -4,7 +4,7 @@ - diverging_commit_counts = @repository.diverging_commit_counts(branch) - number_commits_behind = diverging_commit_counts[:behind] - number_commits_ahead = diverging_commit_counts[:ahead] -- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) +- merge_project = merge_request_source_project_for_project(@project) %li{ class: "branch-item js-branch-#{branch.name}" } .branch-info .branch-title @@ -61,7 +61,7 @@ title: s_('Branches|The default branch cannot be deleted') } = icon("trash-o") - elsif protected_branch?(@project, branch) - - if can?(current_user, :delete_protected_branch, @project) + - if can?(current_user, :push_to_delete_protected_branch, @project) %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip", title: s_('Branches|Delete protected branch'), data: { toggle: "modal", diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 18e948ce35a..2e86a7d36d7 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -1,13 +1,17 @@ -- if current_user +- can_create_issue = show_new_issue_link?(@project) +- can_create_project_snippet = can?(current_user, :create_project_snippet, @project) +- can_push_code = can?(current_user, :push_code, @project) +- create_mr_from_new_fork = can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project) +- merge_project = merge_request_source_project_for_project(@project) + +- show_menu = can_create_issue || can_create_project_snippet || can_push_code || create_mr_from_new_fork || merge_project + +- if show_menu .project-action-button.dropdown.inline %a.btn.dropdown-toggle.has-tooltip{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...') } = icon('plus') = icon("caret-down") %ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown - - can_create_issue = can?(current_user, :create_issue, @project) - - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - - can_create_project_snippet = can?(current_user, :create_project_snippet, @project) - - if can_create_issue || merge_project || can_create_project_snippet %li.dropdown-header= _('This project') @@ -20,17 +24,17 @@ - if can_create_project_snippet %li= link_to _('New snippet'), new_project_snippet_path(@project) - - if can?(current_user, :push_code, @project) + - if can_push_code %li.dropdown-header= _('This repository') - - if can?(current_user, :push_code, @project) + - if can_push_code %li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master') - unless @project.empty_repo? %li= link_to _('New branch'), new_project_branch_path(@project) %li= link_to _('New tag'), new_project_tag_path(@project) - - elsif current_user && current_user.already_forked?(@project) + - elsif can_collaborate_with_project?(@project) %li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master') - - elsif can?(current_user, :fork_project, @project) + - elsif create_mr_from_new_fork - continue_params = { to: project_new_blob_path(@project, @project.default_branch || 'master'), notice: edit_in_new_fork_notice, notice_now: edit_in_new_fork_notice_now } diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/projects/clusters/_empty_state.html.haml index 112dde66ff7..5f49d03b1bb 100644 --- a/app/views/projects/clusters/_empty_state.html.haml +++ b/app/views/projects/clusters/_empty_state.html.haml @@ -7,5 +7,6 @@ - link_to_help_page = link_to(_('Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') %p= s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page} - .text-center - = link_to s_('ClusterIntegration|Add Kubernetes cluster'), new_project_cluster_path(@project), class: 'btn btn-success' + - if can?(current_user, :create_cluster, @project) + .text-center + = link_to s_('ClusterIntegration|Add Kubernetes cluster'), new_project_cluster_path(@project), class: 'btn btn-success' diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 74c5317428c..213c4c90a0e 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -1,3 +1,5 @@ +- can_collaborate = can_collaborate_with_project?(@project) + .page-content-header.js-commit-box{ 'data-commit-path' => branches_project_commit_path(@project, @commit.id) } .header-main-content = render partial: 'signature', object: @commit.signature @@ -32,12 +34,13 @@ %li.visible-xs-block.visible-sm-block = link_to project_tree_path(@project, @commit) do #{ _('Browse Files') } - - unless @commit.has_been_reverted?(current_user) + - if can_collaborate && !@commit.has_been_reverted?(current_user) %li.clearfix = revert_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false) - %li.clearfix - = cherry_pick_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false) - - if can_collaborate_with_project? + - if can_collaborate + %li.clearfix + = cherry_pick_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false) + - if can?(current_user, :push_code, @project) %li.clearfix = link_to s_("CreateTag|Tag"), new_project_tag_path(@project, ref: @commit) %li.divider diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index abb292f8f27..541ae905246 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -17,6 +17,6 @@ .limited-width-notes = render "shared/notes/notes_with_form", :autocomplete => true - - if can_collaborate_with_project? + - if can_collaborate_with_project?(@project) - %w(revert cherry-pick).each do |type| = render "projects/commit/change", type: type, commit: @commit, title: @commit.title diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 99eeb9551e3..0994498c6be 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -114,17 +114,18 @@ Archive project - if @project.archived? %p - Unarchiving the project will mark its repository as active. The project can be committed to. + Unarchiving the project will restore people's ability to make changes to it. + The repository can be committed to, and issues, comments and other entities can be created. %strong Once active this project shows up in the search and on the dashboard. = link_to 'Unarchive project', unarchive_project_path(@project), - data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." }, + data: { confirm: "Are you sure that you want to unarchive this project?" }, method: :post, class: "btn btn-success" - else %p - Archiving the project will mark its repository as read-only. It is hidden from the dashboard and doesn't show up in searches. - %strong Archived projects cannot be committed to! + Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches. + %strong The repository cannot be committed to, and no issues, comments or other entities can be created. = link_to 'Archive project', archive_project_path(@project), - data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." }, + data: { confirm: "Are you sure that you want to archive this project?" }, method: :post, class: "btn btn-warning" .sub-section.rename-respository %h4.warning-title diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml index 0d39edb7bfd..dd1a836fa20 100644 --- a/app/views/projects/issues/_nav_btns.html.haml +++ b/app/views/projects/issues/_nav_btns.html.haml @@ -2,9 +2,10 @@ = icon('rss') - if @can_bulk_update = button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle" -= link_to "New issue", new_project_issue_path(@project, - issue: { assignee_id: finder.assignee.try(:id), - milestone_id: finder.milestones.first.try(:id) }), - class: "btn btn-new", - title: "New issue", - id: "new_issue_link" +- if show_new_issue_link?(@project) + = link_to "New issue", new_project_issue_path(@project, + issue: { assignee_id: finder.assignee.try(:id), + milestone_id: finder.milestones.first.try(:id) }), + class: "btn btn-new", + title: "New issue", + id: "new_issue_link" diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index 36e24037214..4b8bf578b28 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -1,8 +1,8 @@ -- can_create_merge_request = can?(current_user, :create_merge_request, @project) -- data_action = can_create_merge_request ? 'create-mr' : 'create-branch' -- value = can_create_merge_request ? 'Create merge request' : 'Create branch' - - if can?(current_user, :push_code, @project) + - can_create_merge_request = can?(current_user, :create_merge_request_in, @project) + - data_action = can_create_merge_request ? 'create-mr' : 'create-branch' + - value = can_create_merge_request ? 'Create merge request' : 'Create branch' + - can_create_path = can_create_branch_project_issue_path(@project, @issue) - create_mr_path = create_merge_request_project_issue_path(@project, @issue, branch_name: @issue.to_branch_name, ref: @project.default_branch) - create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid) diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index ec7e87219f5..f1fc1c2316d 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -7,6 +7,7 @@ - can_update_issue = can?(current_user, :update_issue, @issue) - can_report_spam = @issue.submittable_as_spam_by?(current_user) +- can_create_issue = show_new_issue_link?(@project) .detail-page-header .detail-page-header-body @@ -42,16 +43,18 @@ %li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' - if can_report_spam %li= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam' - - if can_update_issue || can_report_spam - %li.divider - %li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link' + - if can_create_issue + - if can_update_issue || can_report_spam + %li.divider + %li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link' = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue - if can_report_spam = link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam' - = link_to new_project_issue_path(@project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do - New issue + - if can_create_issue + = link_to new_project_issue_path(@project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do + New issue .issue-details.issuable-details .detail-page-description.content-block diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index b2c0d9e1cfa..623380c9c61 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -1,6 +1,6 @@ - @no_container = true - @can_bulk_update = can?(current_user, :admin_merge_request, @project) -- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) +- merge_project = merge_request_source_project_for_project(@project) - new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project - page_title "Merge Requests" diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml index 5ea653ccad5..b4fe1cabdfd 100644 --- a/app/views/projects/notes/_actions.html.haml +++ b/app/views/projects/notes/_actions.html.haml @@ -36,7 +36,7 @@ %template{ 'v-else' => '' } = render 'shared/icons/icon_resolve_discussion.svg' -- if current_user +- if can?(current_user, :award_emoji, note) - if note.emoji_awardable? - user_authored = note.user_authored?(current_user) .note-actions-item diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 94331a16abd..e28accd5b43 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -24,7 +24,7 @@ .text-warning.center.prepend-top-20 %p = icon("exclamation-triangle fw") - #{ _('Archived project! Repository is read-only') } + #{ _('Archived project! Repository and other project resources are read-only') } - view_path = @project.default_view diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 3d5f92f9aaa..98b4d6339da 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -31,6 +31,6 @@ = link_to edit_project_tag_release_path(@project, tag.name), class: 'btn has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do = icon("pencil") - - if can?(current_user, :admin_project, @project) - = link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip prepend-left-10 #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do - = icon("trash-o") + - if can?(current_user, :admin_project, @project) + = link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip prepend-left-10 #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do + = icon("trash-o") diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index dfe2c37ed8e..7a3469cdd26 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -28,7 +28,7 @@ = icon('history') .btn-container.controls-item = render 'projects/buttons/download', project: @project, ref: @tag.name - - if can?(current_user, :admin_project, @project) + - if can?(current_user, :push_code, @project) && can?(current_user, :admin_project, @project) .btn-container.controls-item-full = link_to project_tag_path(@project, @tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, @tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: @tag.name } } do %i.fa.fa-trash-o diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 5ef5e9c09a2..8587d3b0c0d 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -1,3 +1,6 @@ +- can_collaborate = can_collaborate_with_project?(@project) +- can_create_mr_from_fork = can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project) + .tree-ref-container .tree-ref-holder = render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true @@ -15,7 +18,7 @@ %li = link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path)) - - if current_user + - if can_collaborate || can_create_mr_from_fork %li %a.btn.add-to-tree{ addtotree_toggle_attributes } = sprite_icon('plus', size: 16, css_class: 'pull-left') @@ -35,7 +38,7 @@ %li = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do #{ _('New directory') } - - elsif can?(current_user, :fork_project, @project) + - elsif can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project) %li - continue_params = { to: project_new_blob_path(@project, @id), notice: edit_in_new_fork_notice, @@ -61,23 +64,25 @@ = link_to fork_path, method: :post do #{ _('New directory') } - %li.divider - %li.dropdown-header - #{ _('This repository') } - %li - = link_to new_project_branch_path(@project) do - #{ _('New branch') } - %li - = link_to new_project_tag_path(@project) do - #{ _('New tag') } + - if can?(current_user, :push_code, @project) + %li.divider + %li.dropdown-header + #{ _('This repository') } + %li + = link_to new_project_branch_path(@project) do + #{ _('New branch') } + %li + = link_to new_project_tag_path(@project) do + #{ _('New tag') } .tree-controls = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn' = render 'projects/find_file_link' - = succeed " " do - = link_to ide_edit_path(@project, @id, ""), class: 'btn btn-default' do - = _('Web IDE') + - if can_collaborate + = succeed " " do + = link_to ide_edit_path(@project, @id, ""), class: 'btn btn-default' do + = _('Web IDE') = render 'projects/buttons/download', project: @project, ref: @ref 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/_label.html.haml b/app/views/shared/_label.html.haml index 56403907844..836df57a3a2 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -47,20 +47,20 @@ class: 'text-danger' .pull-right.hidden-xs.hidden-sm - - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group) - %button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'), - disabled: true, - type: 'button', - data: { url: promote_project_label_path(label.project, label), - label_title: label.title, - label_color: label.color, - label_text_color: label.text_color, - group_name: label.project.group.name, - target: '#promote-label-modal', - container: 'body', - toggle: 'modal' } } - = sprite_icon('level-up') - if can?(current_user, :admin_label, label) + - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group) + %button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'), + disabled: true, + type: 'button', + data: { url: promote_project_label_path(label.project, label), + label_title: label.title, + label_color: label.color, + label_text_color: label.text_color, + group_name: label.project.group.name, + target: '#promote-label-modal', + container: 'body', + toggle: 'modal' } } + = sprite_icon('level-up') = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do %span.sr-only Edit = sprite_icon('pencil') diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml index 5868c52566d..fc634856061 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 ba57d922c6d..1c139827acf 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/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml index a942ebc328b..ee134480705 100644 --- a/app/views/shared/milestones/_sidebar.html.haml +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -72,7 +72,7 @@ .title.hide-collapsed Issues %span.badge= milestone.issues_visible_to_user(current_user).count - - if project && can?(current_user, :create_issue, project) + - if show_new_issue_link?(project) = link_to new_project_issue_path(project, issue: { milestone_id: milestone.id }), class: "pull-right", title: "New Issue" do New issue .value.hide-collapsed.bold diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index f302299eb24..797ff034bb2 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-default.btn-grouped.pull-right.visible-xs-block.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/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml index 725bf916592..71c0d740bc8 100644 --- a/app/views/shared/notes/_form.html.haml +++ b/app/views/shared/notes/_form.html.haml @@ -24,20 +24,21 @@ -# DiffNote = f.hidden_field :position - = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do - = render 'projects/zen', f: f, - attr: :note, - classes: 'note-textarea js-note-text', - placeholder: "Write a comment or drag your files here...", - supports_quick_actions: supports_quick_actions, - supports_autocomplete: supports_autocomplete - = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions - .error-alert - - .note-form-actions.clearfix - = render partial: 'shared/notes/comment_button' - - = yield(:note_actions) - - %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } } - Discard draft + .discussion-form-container + = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do + = render 'projects/zen', f: f, + attr: :note, + classes: 'note-textarea js-note-text', + placeholder: "Write a comment or drag your files here...", + supports_quick_actions: supports_quick_actions, + supports_autocomplete: supports_autocomplete + = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions + .error-alert + + .note-form-actions.clearfix + = render partial: 'shared/notes/comment_button' + + = yield(:note_actions) + + %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } } + Discard draft diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml index bf359774ead..893a7f26ebd 100644 --- a/app/views/shared/notes/_note.html.haml +++ b/app/views/shared/notes/_note.html.haml @@ -2,7 +2,7 @@ - return if note.cross_reference_not_visible_for?(current_user) - show_image_comment_badge = local_assigns.fetch(:show_image_comment_badge, false) -- note_editable = note_editable?(note) +- note_editable = can?(current_user, :admin_note, note) - note_counter = local_assigns.fetch(:note_counter, 0) %li.timeline-entry{ id: dom_id(note), 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 12df79a28c7..836230ae8ee 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/44870-remove-extra-space-around-comment-form-on-merge-requests.yml b/changelogs/unreleased/44870-remove-extra-space-around-comment-form-on-merge-requests.yml deleted file mode 100644 index 53e4ebdb996..00000000000 --- a/changelogs/unreleased/44870-remove-extra-space-around-comment-form-on-merge-requests.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Refactor and tweak margin for note forms on Issuable -merge_request: 18120 -author: Takuya Noguchi -type: fixed diff --git a/changelogs/unreleased/45287-align-icons.yml b/changelogs/unreleased/45287-align-icons.yml new file mode 100644 index 00000000000..0a1cccf9ca6 --- /dev/null +++ b/changelogs/unreleased/45287-align-icons.yml @@ -0,0 +1,5 @@ +--- +title: Align action icons in pipeline graph +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/45397-update-faraday_middleware-to-0-12-2.yml b/changelogs/unreleased/45397-update-faraday_middleware-to-0-12-2.yml new file mode 100644 index 00000000000..3370ec3feba --- /dev/null +++ b/changelogs/unreleased/45397-update-faraday_middleware-to-0-12-2.yml @@ -0,0 +1,5 @@ +--- +title: Update faraday_middlewar to 0.12.2 +merge_request: 18397 +author: Takuya Noguchi +type: security 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/changelogs/unreleased/rename-overview-project-sidenav.yml b/changelogs/unreleased/rename-overview-project-sidenav.yml new file mode 100644 index 00000000000..3632ef25c00 --- /dev/null +++ b/changelogs/unreleased/rename-overview-project-sidenav.yml @@ -0,0 +1,5 @@ +--- +title: Renamed Overview to Project in the contextual navigation at a project level +merge_request: 18295 +author: Constance Okoghenun +type: changed 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/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb index 30244ee4431..bcfdd058a1c 100644 --- a/db/fixtures/development/10_merge_requests.rb +++ b/db/fixtures/development/10_merge_requests.rb @@ -4,7 +4,7 @@ Gitlab::Seeder.quiet do # Limit the number of merge requests per project to avoid long seeds MAX_NUM_MERGE_REQUESTS = 10 - Project.all.reject(&:empty_repo?).each do |project| + Project.non_archived.with_merge_requests_enabled.reject(&:empty_repo?).each do |project| branches = project.repository.branch_names.sample(MAX_NUM_MERGE_REQUESTS * 2) branches.each do |branch_name| @@ -21,7 +21,11 @@ Gitlab::Seeder.quiet do assignee: project.team.users.sample } - MergeRequests::CreateService.new(project, project.team.users.sample, params).execute + # Only create MRs with users that are allowed to create MRs + developer = project.team.developers.sample + break unless developer + + MergeRequests::CreateService.new(project, developer, params).execute print '.' end end 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/examples/container_scanning.md b/doc/ci/examples/container_scanning.md index c58efc7392a..dc34f4acd75 100644 --- a/doc/ci/examples/container_scanning.md +++ b/doc/ci/examples/container_scanning.md @@ -30,6 +30,7 @@ sast:container: - mv clair-scanner_linux_amd64 clair-scanner - chmod +x clair-scanner - touch clair-whitelist.yml + - while( ! wget -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; done - ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-sast-container-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true artifacts: paths: [gl-sast-container-report.json] 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.  +## 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 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 68aa64b3834..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 @@ -869,37 +871,29 @@ skip the download step. - Introduced in GitLab Runner v0.7.0 for non-Windows platforms. - Windows support was added in GitLab Runner v.1.0.0. - From GitLab 9.2, caches are restored before artifacts. -- Currently not all executors are supported. +- Not all executors are [supported](https://docs.gitlab.com/runner/executors/#compatibility-chart). - Job artifacts are only collected for successful jobs by default. `artifacts` is used to specify a list of files and directories which should be -attached to the job after success. You can only use paths that are within the -project workspace. To pass artifacts between different jobs, see [dependencies](#dependencies). -Below are some examples. +attached to the job after success. -Send all files in `binaries` and `.config`: +The artifacts will be sent to GitLab after the job finishes successfully and will +be available for download in the GitLab UI. -```yaml -artifacts: - paths: - - binaries/ - - .config -``` +[Read more about artifacts.](../../user/project/pipelines/job_artifacts.md) -Send all Git untracked files: +### `artifacts:paths` -```yaml -artifacts: - untracked: true -``` +You can only use paths that are within the project workspace. To pass artifacts +between different jobs, see [dependencies](#dependencies). -Send all Git untracked files and files in `binaries`: +Send all files in `binaries` and `.config`: ```yaml artifacts: - untracked: true paths: - binaries/ + - .config ``` To disable artifact passing, define the job with empty [dependencies](#dependencies): @@ -933,11 +927,6 @@ release-job: - tags ``` -The artifacts will be sent to GitLab after the job finishes successfully and will -be available for download in the GitLab UI. - -[Read more about artifacts.](../../user/project/pipelines/job_artifacts.md) - ### `artifacts:name` > Introduced in GitLab 8.6 and GitLab Runner v1.1.0. @@ -954,26 +943,30 @@ To create an archive with a name of the current job: job: artifacts: name: "$CI_JOB_NAME" + paths: + - binaries/ ``` To create an archive with a name of the current branch or tag including only -the files that are untracked by Git: +the binaries directory: ```yaml job: artifacts: name: "$CI_COMMIT_REF_NAME" - untracked: true + paths: + - binaries/ ``` To create an archive with a name of the current job and the current branch or -tag including only the files that are untracked by Git: +tag including only the binaries directory: ```yaml job: artifacts: name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME" - untracked: true + paths: + - binaries/ ``` To create an archive with a name of the current [stage](#stages) and branch name: @@ -982,7 +975,8 @@ To create an archive with a name of the current [stage](#stages) and branch name job: artifacts: name: "$CI_JOB_STAGE-$CI_COMMIT_REF_NAME" - untracked: true + paths: + - binaries/ ``` --- @@ -994,7 +988,8 @@ If you use **Windows Batch** to run your shell scripts you need to replace job: artifacts: name: "%CI_JOB_STAGE%-%CI_COMMIT_REF_NAME%" - untracked: true + paths: + - binaries/ ``` If you use **Windows PowerShell** to run your shell scripts you need to replace @@ -1004,7 +999,33 @@ If you use **Windows PowerShell** to run your shell scripts you need to replace job: artifacts: name: "$env:CI_JOB_STAGE-$env:CI_COMMIT_REF_NAME" - untracked: true + paths: + - binaries/ +``` + +### `artifacts:untracked` + +`artifacts:untracked` is used to add all Git untracked files as artifacts (along +to the paths defined in `artifacts:paths`). + +NOTE: **Note:** +To exclude the folders/files which should not be a part of `untracked` just +add them to `.gitignore`. + +Send all Git untracked files: + +```yaml +artifacts: + untracked: true +``` + +Send all Git untracked files and files in `binaries`: + +```yaml +artifacts: + untracked: true + paths: + - binaries/ ``` ### `artifacts:when` diff --git a/doc/development/new_fe_guide/development/components.md b/doc/development/new_fe_guide/development/components.md index 637099d1e83..899efb398cd 100644 --- a/doc/development/new_fe_guide/development/components.md +++ b/doc/development/new_fe_guide/development/components.md @@ -1,3 +1,21 @@ # Components -> TODO: Add content +## Graphs + +We have a lot of graphing libraries in our codebase to render graphs. In an effort to improve maintainability, new graphs should use [D3.js](https://d3js.org/). If a new graph is fairly simple, consider implementing it in SVGs or HTML5 canvas. + +We chose D3 as our library going forward because of the following features: + +* [Tree shaking webpack capabilities.](https://github.com/d3/d3/blob/master/CHANGES.md#changes-in-d3-40) +* [Compatible with vue.js as well as vanilla javascript.](https://github.com/d3/d3/blob/master/CHANGES.md#changes-in-d3-40) + +D3 is very popular across many projects outside of GitLab: + +* [The New York Times](https://archive.nytimes.com/www.nytimes.com/interactive/2012/02/13/us/politics/2013-budget-proposal-graphic.html) +* [plot.ly](https://plot.ly/) +* [Droptask](https://www.droptask.com/) + +Within GitLab, D3 has been used for the following notable features + +* [Prometheus graphs](https://docs.gitlab.com/ee/user/project/integrations/prometheus.html) +* Contribution calendars 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/installation.md b/doc/install/installation.md index 3cf6f7b7ddf..85174b64ff7 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -301,7 +301,7 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-6-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-7-stable gitlab **Note:** You can change `10-6-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! 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/update/10.6-to-10.7.md b/doc/update/10.6-to-10.7.md new file mode 100644 index 00000000000..4a76ae14d2e --- /dev/null +++ b/doc/update/10.6-to-10.7.md @@ -0,0 +1,361 @@ +--- +comments: false +--- + +# From 10.6 to 10.7 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + +```bash +sudo service gitlab stop +``` + +### 2. Backup + +NOTE: If you installed GitLab from source, make sure `rsync` is installed. + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Update Ruby + +NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be +sure to upgrade your interpreter if necessary. + +You can check which version you are running with `ruby -v`. + +Download and compile Ruby: + +```bash +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.6.tar.gz +echo '4e6a0f828819e15d274ae58485585fc8b7caace0 ruby-2.3.6.tar.gz' | shasum -c - && tar xzf ruby-2.3.6.tar.gz +cd ruby-2.3.6 +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. Update Node + +GitLab utilizes [webpack](http://webpack.js.org) to compile frontend assets. +This requires a minimum version of node v6.0.0. + +You can check which version you are running with `node -v`. If you are running +a version older than `v6.0.0` you will need to update to a newer version. You +can find instructions to install from community maintained packages or compile +from source at the nodejs.org website. + +<https://nodejs.org/en/download/> + +GitLab also requires the use of yarn `>= v1.2.0` to manage JavaScript +dependencies. + +```bash +curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update +sudo apt-get install yarn +``` + +More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). + +### 5. Update Go + +NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go +1.5.x through 1.7.x. Be sure to upgrade your installation if necessary. + +You can check which version you are running with `go version`. + +Download and install Go: + +```bash +# Remove former Go installation folder +sudo rm -rf /usr/local/go + +curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz +echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ + sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.8.3.linux-amd64.tar.gz +``` + +### 6. Get latest code + +```bash +cd /home/git/gitlab + +sudo -u git -H git fetch --all --prune +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +sudo -u git -H git checkout -- locale +``` + +For GitLab Community Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 10-7-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 10-7-stable-ee +``` + +### 7. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) +sudo -u git -H bin/compile +``` + +### 8. Update gitlab-workhorse + +Install and compile gitlab-workhorse. GitLab-Workhorse uses +[GNU Make](https://www.gnu.org/software/make/). +If you are not using Linux you may have to run `gmake` instead of +`make` below. + +```bash +cd /home/git/gitlab-workhorse + +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION) +sudo -u git -H make +``` + +### 9. Update Gitaly + +#### New Gitaly configuration options required + +In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`. + +```shell +echo ' +[gitaly-ruby] +dir = "/home/git/gitaly/ruby" + +[gitlab-shell] +dir = "/home/git/gitlab-shell" +' | sudo -u git tee -a /home/git/gitaly/config.toml +``` + +#### Check Gitaly configuration + +Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly +configuration file may contain syntax errors. The block name +`[[storages]]`, which may occur more than once in your `config.toml` +file, should be `[[storage]]` instead. + +```shell +sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml +``` + +#### Compile Gitaly + +```shell +cd /home/git/gitaly +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION) +sudo -u git -H make +``` + +### 10. Update MySQL permissions + +If you are using MySQL you need to grant the GitLab user the necessary +permissions on the database: + +```bash +mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';" +``` + +If you use MySQL with replication, or just have MySQL configured with binary logging, +you will need to also run the following on all of your MySQL servers: + +```bash +mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;" +``` + +You can make this setting permanent by adding it to your `my.cnf`: + +``` +log_bin_trust_function_creators=1 +``` + +### 11. Update configuration files + +#### New configuration options for `gitlab.yml` + +There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +cd /home/git/gitlab + +git diff origin/10-6-stable:config/gitlab.yml.example origin/10-7-stable:config/gitlab.yml.example +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +cd /home/git/gitlab + +# For HTTPS configurations +git diff origin/10-6-stable:lib/support/nginx/gitlab-ssl origin/10-7-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/10-6-stable:lib/support/nginx/gitlab origin/10-7-stable:lib/support/nginx/gitlab +``` + +If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx +configuration as GitLab application no longer handles setting it. + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-7-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-7-stable/config/initializers/smtp_settings.rb.sample#L13 + +#### Init script + +There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`: + +```sh +cd /home/git/gitlab + +git diff origin/10-6-stable:lib/support/init.d/gitlab.default.example origin/10-7-stable:lib/support/init.d/gitlab.default.example +``` + +Ensure you're still up-to-date with the latest init script changes: + +```bash +cd /home/git/gitlab + +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +For Ubuntu 16.04.1 LTS: + +```bash +sudo systemctl daemon-reload +``` + +### 12. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Compile GetText PO files + +sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production + +# Update node dependencies and recompile assets +sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production + +# Clean up cache +sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production +``` + +**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). + +### 13. Start application + +```bash +sudo service gitlab start +sudo service nginx restart +``` + +### 14. Check application status + +Check if GitLab and its environment are configured correctly: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` + +To make sure you didn't miss anything run a more thorough check: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (10.5) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 10.5 to 10.6](10.5-to-10.6.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. + +[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-7-stable/config/gitlab.yml.example +[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-7-stable/lib/support/init.d/gitlab.default.example diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index eacfe2baa27..159109e8954 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -14,6 +14,10 @@ The comment area supports [Markdown] and [quick actions]. One can edit their own comment at any time, and anyone with [Master access level][permissions] or higher can also edit a comment made by someone else. +You could also reply to the notification email in order to reply to a comment, +provided that [Reply by email] is configured by your GitLab admin. This also +supports [Markdown] and [quick actions] as if replied from the web. + Apart from the standard comments, you also have the option to create a comment in the form of a resolvable or threaded discussion. @@ -283,3 +287,4 @@ edit existing comments. Non-team members are restricted from adding or editing c [markdown]: ../markdown.md [quick actions]: ../project/quick_actions.md [permissions]: ../permissions.md +[Reply by email]: ../../administration/reply_by_email.md 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/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index 888dd0e143a..a387c1e443e 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -57,15 +57,20 @@ Here you can run housekeeping, archive, rename, transfer, or remove a project. NOTE: **Note:** Only project Owners and Admin users have the [permissions] to archive a project. -An archived project will be hidden by default in the project listings. +Archiving a project makes it read-only for all users and indicates that it is +no longer actively maintained. Projects that have been archived can also be +unarchived. + +When a project is archived, the repository, issues, merge requests and all +other features are read-only. Archived projects are also hidden +in project listings. + +To archive a project: 1. Navigate to your project's **Settings > General > Advanced settings**. -1. Under "Archive project", hit the **Archive project** button. +1. In the Archive project section, click the **Archive project** button. 1. Confirm the action when asked to. -An archived project can be fully restored and will therefore retain its -repository and all associated resources whilst in an archived state. - #### Renaming a repository NOTE: **Note:** 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/merge_requests.rb b/lib/api/merge_requests.rb index 3264a26f7d2..d4cc18f622b 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -189,7 +189,7 @@ module API post ":id/merge_requests" do Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42316') - authorize! :create_merge_request, user_project + authorize! :create_merge_request_from, user_project mr_params = declared_params(include_missing: false) mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) 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/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb index ce216497996..9b0f70e2bfe 100644 --- a/lib/api/v3/merge_requests.rb +++ b/lib/api/v3/merge_requests.rb @@ -93,7 +93,7 @@ module API post ":id/merge_requests" do Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42126') - authorize! :create_merge_request, user_project + authorize! :create_merge_request_from, user_project mr_params = declared_params(include_missing: false) mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present? 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/email/handler/create_merge_request_handler.rb b/lib/gitlab/email/handler/create_merge_request_handler.rb index 3436306e122..2f864f2082b 100644 --- a/lib/gitlab/email/handler/create_merge_request_handler.rb +++ b/lib/gitlab/email/handler/create_merge_request_handler.rb @@ -23,7 +23,8 @@ module Gitlab def execute raise ProjectNotFound unless project - validate_permission!(:create_merge_request) + validate_permission!(:create_merge_request_in) + validate_permission!(:create_merge_request_from) verify_record!( record: create_merge_request, 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/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 24393f96d96..69952cbb47c 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -51,7 +51,7 @@ module Gitlab return false unless can_access_git? if protected?(ProtectedBranch, project, ref) - user.can?(:delete_protected_branch, project) + user.can?(:push_to_delete_protected_branch, project) else user.can?(:push_code, project) 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 7bac672a0b2..45bea12fd9b 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/controllers/concerns/checks_collaboration_spec.rb b/spec/controllers/concerns/checks_collaboration_spec.rb new file mode 100644 index 00000000000..1bd764290ae --- /dev/null +++ b/spec/controllers/concerns/checks_collaboration_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe ChecksCollaboration do + include ProjectForksHelper + + let(:helper) do + fake_class = Class.new(ApplicationController) do + include ChecksCollaboration + end + + fake_class.new + end + + describe '#can_collaborate_with_project?' do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + + before do + allow(helper).to receive(:current_user).and_return(user) + allow(helper).to receive(:can?) do |user, ability, subject| + Ability.allowed?(user, ability, subject) + end + end + + it 'is true if the user can push to the project' do + project.add_developer(user) + + expect(helper.can_collaborate_with_project?(project)).to be_truthy + end + + it 'is true when the user can push to a branch of the project' do + fake_access = double('Gitlab::UserAccess') + expect(fake_access).to receive(:can_push_to_branch?).with('a-branch').and_return(true) + expect(Gitlab::UserAccess).to receive(:new).with(user, project: project).and_return(fake_access) + + expect(helper.can_collaborate_with_project?(project, ref: 'a-branch')).to be_truthy + end + + context 'when the user has forked the project' do + before do + fork_project(project, user, namespace: user.namespace) + end + + it 'is true' do + expect(helper.can_collaborate_with_project?(project)).to be_truthy + end + + it 'is false when the project is archived' do + project.archived = true + + expect(helper.can_collaborate_with_project?(project)).to be_falsy + end + end + end +end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 01b5506b64b..ca86b0bc737 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -938,7 +938,7 @@ describe Projects::IssuesController do end describe 'POST create_merge_request' do - let(:project) { create(:project, :repository) } + let(:project) { create(:project, :repository, :public) } before do project.add_developer(user) @@ -955,6 +955,22 @@ describe Projects::IssuesController do expect(response).to match_response_schema('merge_request') end + it 'is not available when the project is archived' do + project.update!(archived: true) + + create_merge_request + + expect(response).to have_gitlab_http_status(404) + end + + it 'is not available for users who cannot create merge requests' do + sign_in(create(:user)) + + create_merge_request + + expect(response).to have_gitlab_http_status(404) + end + def create_merge_request post :create_merge_request, namespace_id: project.namespace.to_param, project_id: project.to_param, 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/award_emoji.rb b/spec/factories/award_emoji.rb index a0abbbce686..d37e2bf511e 100644 --- a/spec/factories/award_emoji.rb +++ b/spec/factories/award_emoji.rb @@ -4,6 +4,10 @@ FactoryBot.define do user awardable factory: :issue + after(:create) do |award, evaluator| + award.awardable.project.add_guest(evaluator.user) + end + trait :upvote trait :downvote do name "thumbsdown" 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_broadcast_messages_spec.rb b/spec/features/admin/admin_broadcast_messages_spec.rb index 9cb351282a0..430a8d22b0f 100644 --- a/spec/features/admin/admin_broadcast_messages_spec.rb +++ b/spec/features/admin/admin_broadcast_messages_spec.rb @@ -45,7 +45,7 @@ feature 'Admin Broadcast Messages' do page.within('.broadcast-message-preview') do expect(page).to have_selector('strong', text: 'Markdown') - expect(page).to have_selector('gl-emoji[data-name="tada"]') + expect(page).to have_emoji('tada') end end end 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/groups/show_spec.rb b/spec/features/groups/show_spec.rb index 4ffadbbcd35..3a0424d60f8 100644 --- a/spec/features/groups/show_spec.rb +++ b/spec/features/groups/show_spec.rb @@ -98,7 +98,7 @@ feature 'Group show page' do it 'shows the project info' do expect(page).to have_content(project.title) - expect(page).to have_selector('gl-emoji[data-name="smile"]') + expect(page).to have_emoji('smile') end end end diff --git a/spec/features/merge_request/user_awards_emoji_spec.rb b/spec/features/merge_request/user_awards_emoji_spec.rb index 2f24cfbd9e3..859a4c65562 100644 --- a/spec/features/merge_request/user_awards_emoji_spec.rb +++ b/spec/features/merge_request/user_awards_emoji_spec.rb @@ -35,6 +35,14 @@ describe 'Merge request > User awards emoji', :js do expect(page).to have_selector('.emoji-menu', count: 1) end + + describe 'the project is archived' do + let(:project) { create(:project, :public, :repository, :archived) } + + it 'does not see award menu button' do + expect(page).not_to have_selector('.js-award-holder') + end + end end describe 'logged out' do diff --git a/spec/features/merge_request/user_cherry_picks_spec.rb b/spec/features/merge_request/user_cherry_picks_spec.rb index 494096b21c0..61d1bdaa95a 100644 --- a/spec/features/merge_request/user_cherry_picks_spec.rb +++ b/spec/features/merge_request/user_cherry_picks_spec.rb @@ -40,6 +40,14 @@ describe 'Merge request > User cherry-picks', :js do expect(page).to have_link 'Cherry-pick' end + + it 'hides the cherry pick button for an archived project' do + project.update!(archived: true) + + visit project_merge_request_path(project, merge_request) + + expect(page).not_to have_link 'Cherry-pick' + end end end end 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/awards/user_interacts_with_awards_in_issue_spec.rb b/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb index adff0a10f0e..12e07647ecd 100644 --- a/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb +++ b/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb @@ -99,6 +99,74 @@ describe 'User interacts with awards in an issue', :js do click_button('Comment') end - expect(page).to have_selector('gl-emoji[data-name="smile"]') + expect(page).to have_emoji('smile') + end + + context 'when a project is archived' do + let(:project) { create(:project, :archived) } + + it 'hides the add award button' do + page.within('.awards') do + expect(page).not_to have_css('.js-add-award') + end + end + end + + context 'awards on a note' do + let!(:note) { create(:note, noteable: issue, project: issue.project) } + let!(:award_emoji) { create(:award_emoji, awardable: note, name: '100') } + + it 'shows the award on the note' do + page.within('.note-awards') do + expect(page).to have_emoji('100') + end + end + + it 'allows adding a vote to an award' do + page.within('.note-awards') do + find('gl-emoji[data-name="100"]').click + end + wait_for_requests + + expect(note.reload.award_emoji.size).to eq(2) + end + + it 'allows adding a new emoji' do + page.within('.note-actions') do + find('a.js-add-award').click + end + page.within('.emoji-menu-content') do + find('gl-emoji[data-name="8ball"]').click + end + wait_for_requests + + page.within('.note-awards') do + expect(page).to have_emoji('8ball') + end + expect(note.reload.award_emoji.size).to eq(2) + end + + context 'when the project is archived' do + let(:project) { create(:project, :archived) } + + it 'hides the buttons for adding new emoji' do + page.within('.note-awards') do + expect(page).not_to have_css('.award-menu-holder') + end + + page.within('.note-actions') do + expect(page).not_to have_css('a.js-add-award') + end + end + + it 'does not allow toggling existing emoji' do + page.within('.note-awards') do + find('gl-emoji[data-name="100"]').click + end + wait_for_requests + + expect(note.reload.award_emoji.size).to eq(1) + end + end end end diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb index 2a9d9e6416c..b7ce1b9993a 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -195,6 +195,26 @@ describe 'Branches' do expect(page).to have_content("Protected branches can be managed in project settings") end end + + it 'shows the merge request button' do + visit project_branches_path(project) + + page.within first('.all-branches li') do + expect(page).to have_content 'Merge request' + end + end + + context 'when the project is archived' do + let(:project) { create(:project, :public, :repository, :archived) } + + it 'does not show the merge request button when the project is archived' do + visit project_branches_path(project) + + page.within first('.all-branches li') do + expect(page).not_to have_content 'Merge request' + end + end + end end context 'logged out' do @@ -204,7 +224,7 @@ describe 'Branches' do it 'does not show merge request button' do page.within first('.all-branches li') do - expect(page).not_to have_content 'Merge Request' + expect(page).not_to have_content 'Merge request' end end end diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb index c4c399e3058..1df45865d6f 100644 --- a/spec/features/projects/commit/cherry_pick_spec.rb +++ b/spec/features/projects/commit/cherry_pick_spec.rb @@ -89,4 +89,15 @@ describe 'Cherry-pick Commits' do expect(page).to have_content('The commit has been successfully cherry-picked.') end end + + context 'when the project is archived' do + let(:project) { create(:project, :repository, :archived, namespace: group) } + + it 'does not show the cherry-pick link' do + find('.header-action-buttons a.dropdown-toggle').click + + expect(page).not_to have_text("Cherry-pick") + expect(page).not_to have_css("a[href='#modal-cherry-pick-commit']") + 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/projects/commit/user_reverts_commit_spec.rb b/spec/features/projects/commit/user_reverts_commit_spec.rb index 221f1d7757e..42844a03ea6 100644 --- a/spec/features/projects/commit/user_reverts_commit_spec.rb +++ b/spec/features/projects/commit/user_reverts_commit_spec.rb @@ -10,13 +10,16 @@ describe 'User reverts a commit', :js do sign_in(user) visit(project_commit_path(project, sample_commit.id)) + end + def click_revert find('.header-action-buttons .dropdown').click find('a[href="#modal-revert-commit"]').click end context 'without creating a new merge request' do before do + click_revert page.within('#modal-revert-commit') do uncheck('create_merge_request') click_button('Revert') @@ -44,6 +47,10 @@ describe 'User reverts a commit', :js do end context 'with creating a new merge request' do + before do + click_revert + end + it 'reverts a commit' do page.within('#modal-revert-commit') do click_button('Revert') @@ -53,4 +60,14 @@ describe 'User reverts a commit', :js do expect(page).to have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master") end end + + context 'when the project is archived' do + let(:project) { create(:project, :repository, :archived, namespace: user.namespace) } + + it 'does not show the revert link' do + find('.header-action-buttons .dropdown').click + + expect(page).not_to have_link('Revert') + end + end end diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb index 523a9f3f4fe..dc6e4fd27cb 100644 --- a/spec/features/projects/files/user_edits_files_spec.rb +++ b/spec/features/projects/files/user_edits_files_spec.rb @@ -12,6 +12,23 @@ describe 'Projects > Files > User edits files' do sign_in(user) end + shared_examples 'unavailable for an archived project' do + it 'does not show the edit link for an archived project', :js do + project.update!(archived: true) + visit project_tree_path(project, project.repository.root_ref) + + click_link('.gitignore') + + aggregate_failures 'available edit buttons' do + expect(page).not_to have_text('Edit') + expect(page).not_to have_text('Web IDE') + + expect(page).not_to have_text('Replace') + expect(page).not_to have_text('Delete') + end + end + end + context 'when an user has write access' do before do project.add_master(user) @@ -85,6 +102,8 @@ describe 'Projects > Files > User edits files' do expect(page).to have_css('.line_holder.new') end + + it_behaves_like 'unavailable for an archived project' end context 'when an user does not have write access' do @@ -168,6 +187,10 @@ describe 'Projects > Files > User edits files' do expect(page).to have_content("From #{forked_project.full_path}") expect(page).to have_content("into #{project2.full_path}") end + + it_behaves_like 'unavailable for an archived project' do + let(:project) { project2 } + end end end end diff --git a/spec/features/projects/issues/user_views_issue_spec.rb b/spec/features/projects/issues/user_views_issue_spec.rb index f7f2cde3d64..4093876c289 100644 --- a/spec/features/projects/issues/user_views_issue_spec.rb +++ b/spec/features/projects/issues/user_views_issue_spec.rb @@ -6,11 +6,27 @@ describe "User views issue" do set(:issue) { create(:issue, project: project, description: "# Description header", author: user) } before do - project.add_guest(user) + project.add_developer(user) sign_in(user) visit(project_issue_path(project, issue)) end it { expect(page).to have_header_with_correct_id_and_link(1, "Description header", "description-header") } + + it 'shows the merge request and issue actions', :aggregate_failures do + expect(page).to have_link('New issue') + expect(page).to have_button('Create merge request') + expect(page).to have_link('Close issue') + end + + context 'when the project is archived' do + let(:project) { create(:project, :public, :archived) } + + it 'hides the merge request and issue actions', :aggregate_failures do + expect(page).not_to have_link('New issue') + expect(page).not_to have_button('Create merge request') + expect(page).not_to have_link('Close issue') + end + end end diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb index 40689964b91..b571d5a0e26 100644 --- a/spec/features/projects/merge_request_button_spec.rb +++ b/spec/features/projects/merge_request_button_spec.rb @@ -45,6 +45,18 @@ feature 'Merge Request button' do end end end + + context 'when the project is archived' do + it 'hides the link' do + project.update!(archived: true) + + visit url + + within("#content-body") do + expect(page).not_to have_link(label) + end + end + end end context 'logged in as non-member' do diff --git a/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb b/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb index a41d683dbbb..f3e97bc9eb2 100644 --- a/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb +++ b/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb @@ -56,4 +56,12 @@ describe 'User reverts a merge request', :js do expect(page).to have_content('The merge request has been successfully reverted. You can now submit a merge request to get this change into the original branch.') end + + it 'cannot revert a merge requests for an archived project' do + project.update!(archived: true) + + visit(merge_request_path(merge_request)) + + expect(page).not_to have_link('Revert') + end end diff --git a/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb index bf95dbb7d09..115e548b691 100644 --- a/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb +++ b/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb @@ -94,6 +94,18 @@ describe 'User views open merge requests' do end include_examples 'shows merge requests' + + it 'shows the new merge request button' do + expect(page).to have_link('New merge request') + end + + context 'when the project is archived' do + let(:project) { create(:project, :public, :repository, :archived) } + + it 'hides the new merge request button' do + expect(page).not_to have_link('New merge request') + end + end end end diff --git a/spec/features/projects/show/user_sees_collaboration_links_spec.rb b/spec/features/projects/show/user_sees_collaboration_links_spec.rb new file mode 100644 index 00000000000..7b3711531c6 --- /dev/null +++ b/spec/features/projects/show/user_sees_collaboration_links_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe 'Projects > Show > Collaboration links' do + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + before do + project.add_developer(user) + sign_in(user) + end + + it 'shows all the expected links' do + visit project_path(project) + + # The navigation bar + page.within('.header-new') do + aggregate_failures 'dropdown links in the navigation bar' do + expect(page).to have_link('New issue') + expect(page).to have_link('New merge request') + expect(page).to have_link('New snippet', href: new_project_snippet_path(project)) + end + end + + # The project header + page.within('.project-home-panel') do + aggregate_failures 'dropdown links in the project home panel' do + expect(page).to have_link('New issue') + expect(page).to have_link('New merge request') + expect(page).to have_link('New snippet') + expect(page).to have_link('New file') + expect(page).to have_link('New branch') + expect(page).to have_link('New tag') + end + end + + # The dropdown above the tree + page.within('.repo-breadcrumb') do + aggregate_failures 'dropdown links above the repo tree' do + expect(page).to have_link('New file') + expect(page).to have_link('Upload file') + expect(page).to have_link('New directory') + expect(page).to have_link('New branch') + expect(page).to have_link('New tag') + end + end + + # The Web IDE + expect(page).to have_link('Web IDE') + end + + it 'hides the links when the project is archived' do + project.update!(archived: true) + + visit project_path(project) + + page.within('.header-new') do + aggregate_failures 'dropdown links' do + expect(page).not_to have_link('New issue') + expect(page).not_to have_link('New merge request') + expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project)) + end + end + + page.within('.project-home-panel') do + aggregate_failures 'dropdown links' do + expect(page).not_to have_link('New issue') + expect(page).not_to have_link('New merge request') + expect(page).not_to have_link('New snippet') + expect(page).not_to have_link('New file') + expect(page).not_to have_link('New branch') + expect(page).not_to have_link('New tag') + end + end + + page.within('.repo-breadcrumb') do + aggregate_failures 'dropdown links' do + expect(page).not_to have_link('New file') + expect(page).not_to have_link('Upload file') + expect(page).not_to have_link('New directory') + expect(page).not_to have_link('New branch') + expect(page).not_to have_link('New tag') + end + end + + expect(page).not_to have_link('Web IDE') + end +end diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb index fb0d8c766fe..47c5a8161d9 100644 --- a/spec/features/projects/user_uses_shortcuts_spec.rb +++ b/spec/features/projects/user_uses_shortcuts_spec.rb @@ -11,12 +11,12 @@ describe 'User uses shortcuts', :js do visit(project_path(project)) end - context 'when navigating to the Overview pages' do + context 'when navigating to the Project pages' do it 'redirects to the details page' do find('body').native.send_key('g') find('body').native.send_key('p') - expect(page).to have_active_navigation('Overview') + expect(page).to have_active_navigation('Project') expect(page).to have_active_sub_navigation('Details') end @@ -24,7 +24,7 @@ describe 'User uses shortcuts', :js do find('body').native.send_key('g') find('body').native.send_key('e') - expect(page).to have_active_navigation('Overview') + expect(page).to have_active_navigation('Project') expect(page).to have_active_sub_navigation('Activity') 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/finders/merge_request_target_project_finder_spec.rb b/spec/finders/merge_request_target_project_finder_spec.rb index c81bfd7932c..f302cf80ce8 100644 --- a/spec/finders/merge_request_target_project_finder_spec.rb +++ b/spec/finders/merge_request_target_project_finder_spec.rb @@ -19,6 +19,12 @@ describe MergeRequestTargetProjectFinder do expect(finder.execute).to contain_exactly(forked_project) end + + it 'does not contain archived projects' do + base_project.update!(archived: true) + + expect(finder.execute).to contain_exactly(other_fork, forked_project) + end end context 'public projects' do 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/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index aeef5352333..8bb2e234e9a 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -96,13 +96,32 @@ describe IssuesHelper do describe '#award_state_class' do let!(:upvote) { create(:award_emoji) } + let(:awardable) { upvote.awardable } + let(:user) { upvote.user } + + before do + allow(helper).to receive(:can?) do |*args| + Ability.allowed?(*args) + end + end it "returns disabled string for unauthenticated user" do - expect(award_state_class(AwardEmoji.all, nil)).to eq("disabled") + expect(helper.award_state_class(awardable, AwardEmoji.all, nil)).to eq("disabled") + end + + it "returns disabled for a user that does not have access to the awardable" do + expect(helper.award_state_class(awardable, AwardEmoji.all, build(:user))).to eq("disabled") end it "returns active string for author" do - expect(award_state_class(AwardEmoji.all, upvote.user)).to eq("active") + expect(helper.award_state_class(awardable, AwardEmoji.all, upvote.user)).to eq("active") + end + + it "is blank for a user that has access to the awardable" do + user = build(:user) + expect(helper).to receive(:can?).with(user, :award_emoji, awardable).and_return(true) + + expect(helper.award_state_class(awardable, AwardEmoji.all, user)).to be_blank end end @@ -144,4 +163,26 @@ describe IssuesHelper do end end end + + describe '#show_new_issue_link?' do + before do + allow(helper).to receive(:current_user) + end + + it 'is false when no project there is no project' do + expect(helper.show_new_issue_link?(nil)).to be_falsey + end + + it 'is true when there is a project and no logged in user' do + expect(helper.show_new_issue_link?(build(:project))).to be_truthy + end + + it 'is true when the current user does not have access to the project' do + project = build(:project) + allow(helper).to receive(:current_user).and_return(project.owner) + + expect(helper).to receive(:can?).with(project.owner, :create_issue, project).and_return(true) + expect(helper.show_new_issue_link?(project)).to be_truthy + end + end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index ce96e90e2d7..46c55da24f8 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -322,74 +322,6 @@ describe ProjectsHelper do end end - describe "#project_feature_access_select" do - let(:project) { create(:project, :public) } - let(:user) { create(:user) } - - context "when project is internal or public" do - it "shows all options" do - helper.instance_variable_set(:@project, project) - result = helper.project_feature_access_select(:issues_access_level) - expect(result).to include("Disabled") - expect(result).to include("Only team members") - expect(result).to include("Everyone with access") - end - end - - context "when project is private" do - before do - project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - end - - it "shows only allowed options" do - helper.instance_variable_set(:@project, project) - result = helper.project_feature_access_select(:issues_access_level) - expect(result).to include("Disabled") - expect(result).to include("Only team members") - expect(result).to have_selector('option[disabled]', text: "Everyone with access") - end - end - - context "when project moves from public to private" do - before do - project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - end - - it "shows the highest allowed level selected" do - helper.instance_variable_set(:@project, project) - result = helper.project_feature_access_select(:issues_access_level) - - expect(result).to include("Disabled") - expect(result).to include("Only team members") - expect(result).to have_selector('option[disabled]', text: "Everyone with access") - expect(result).to have_selector('option[selected]', text: "Only team members") - end - end - end - - describe "#visibility_select_options" do - let(:project) { create(:project, :repository) } - let(:user) { create(:user) } - - before do - allow(helper).to receive(:current_user).and_return(user) - - stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) - end - - it "does not include the Public restricted level" do - expect(helper.send(:visibility_select_options, project, Gitlab::VisibilityLevel::PRIVATE)).not_to include('Public') - end - - it "includes the Internal level" do - expect(helper.send(:visibility_select_options, project, Gitlab::VisibilityLevel::PRIVATE)).to include('Internal') - end - - it "includes the Private level" do - expect(helper.send(:visibility_select_options, project, Gitlab::VisibilityLevel::PRIVATE)).to include('Private') - end - end - describe '#get_project_nav_tabs' do let(:project) { create(:project) } let(:user) { create(:user) } 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/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js index ab81aabb992..1dfe890e05e 100644 --- a/spec/javascripts/notes/components/note_actions_spec.js +++ b/spec/javascripts/notes/components/note_actions_spec.js @@ -3,7 +3,7 @@ import store from '~/notes/stores'; import noteActions from '~/notes/components/note_actions.vue'; import { userDataMock } from '../mock_data'; -describe('issse_note_actions component', () => { +describe('issue_note_actions component', () => { let vm; let Component; @@ -24,6 +24,7 @@ describe('issse_note_actions component', () => { authorId: 26, canDelete: true, canEdit: true, + canAwardEmoji: true, canReportAsAbuse: true, noteId: 539, reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', @@ -70,6 +71,7 @@ describe('issse_note_actions component', () => { authorId: 26, canDelete: false, canEdit: false, + canAwardEmoji: false, canReportAsAbuse: false, noteId: 539, reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', diff --git a/spec/javascripts/notes/components/note_awards_list_spec.js b/spec/javascripts/notes/components/note_awards_list_spec.js index 15995ec5a05..1c30d8691b1 100644 --- a/spec/javascripts/notes/components/note_awards_list_spec.js +++ b/spec/javascripts/notes/components/note_awards_list_spec.js @@ -29,6 +29,7 @@ describe('note_awards_list component', () => { awards: awardsMock, noteAuthorId: 2, noteId: 545, + canAwardEmoji: true, toggleAwardPath: '/gitlab-org/gitlab-ce/notes/545/toggle_award_emoji', }, }).$mount(); @@ -43,14 +44,45 @@ describe('note_awards_list component', () => { expect(vm.$el.querySelector('.js-awards-block button [data-name="cartwheel_tone3"]')).toBeDefined(); }); - it('should be possible to remove awareded emoji', () => { + it('should be possible to remove awarded emoji', () => { spyOn(vm, 'handleAward').and.callThrough(); + spyOn(vm, 'toggleAwardRequest').and.callThrough(); vm.$el.querySelector('.js-awards-block button').click(); expect(vm.handleAward).toHaveBeenCalledWith('flag_tz'); + expect(vm.toggleAwardRequest).toHaveBeenCalled(); }); it('should be possible to add new emoji', () => { expect(vm.$el.querySelector('.js-add-award')).toBeDefined(); }); + + describe('when the user cannot award emoji', () => { + beforeEach(() => { + const Component = Vue.extend(awardsNote); + + vm = new Component({ + store, + propsData: { + awards: awardsMock, + noteAuthorId: 2, + noteId: 545, + canAwardEmoji: false, + toggleAwardPath: '/gitlab-org/gitlab-ce/notes/545/toggle_award_emoji', + }, + }).$mount(); + }); + + it('should not be possible to remove awarded emoji', () => { + spyOn(vm, 'toggleAwardRequest').and.callThrough(); + + vm.$el.querySelector('.js-awards-block button').click(); + + expect(vm.toggleAwardRequest).not.toHaveBeenCalled(); + }); + + it('should not be possible to add new emoji', () => { + expect(vm.$el.querySelector('.js-add-award')).toBeNull(); + }); + }); }); diff --git a/spec/javascripts/notes/components/note_body_spec.js b/spec/javascripts/notes/components/note_body_spec.js index 0ff804f0e55..4e551496ff0 100644 --- a/spec/javascripts/notes/components/note_body_spec.js +++ b/spec/javascripts/notes/components/note_body_spec.js @@ -18,6 +18,7 @@ describe('issue_note_body component', () => { propsData: { note, canEdit: true, + canAwardEmoji: true, }, }).$mount(); }); diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 24388fba219..bfe3a65feee 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -9,6 +9,7 @@ export const notesDataMock = { totalNotes: 1, closePath: '/twitter/flight/issues/9.json?issue%5Bstate_event%5D=close', reopenPath: '/twitter/flight/issues/9.json?issue%5Bstate_event%5D=reopen', + canAwardEmoji: true, }; export const userDataMock = { @@ -30,6 +31,7 @@ export const noteableDataMock = { current_user: { can_create_note: true, can_update: true, + can_award_emoji: true, }, description: '', due_date: null, @@ -86,7 +88,10 @@ export const individualNote = { human_access: 'Owner', note: 'sdfdsaf', note_html: "<p dir='auto'>sdfdsaf</p>", - current_user: { can_edit: true }, + current_user: { + can_edit: true, + can_award_emoji: true, + }, discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', emoji_awardable: true, award_emoji: [ @@ -129,6 +134,7 @@ export const note = { note_html: '<p dir="auto">Vel id placeat reprehenderit sit numquam.</p>', current_user: { can_edit: true, + can_award_emoji: true, }, discussion_id: 'd3842a451b7f3d9a5dfce329515127b2d29a4cd0', emoji_awardable: true, @@ -187,6 +193,7 @@ export const discussionMock = { note_html: "<p dir='auto'>THIS IS A DICUSSSION!</p>", current_user: { can_edit: true, + can_award_emoji: true, }, discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', emoji_awardable: true, @@ -231,6 +238,7 @@ export const discussionMock = { }, current_user: { can_edit: true, + can_award_emoji: true, }, discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', emoji_awardable: true, @@ -275,6 +283,7 @@ export const discussionMock = { }, current_user: { can_edit: true, + can_award_emoji: true, }, discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', emoji_awardable: true, @@ -365,6 +374,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = { note_html: '\u003cp dir="auto"\u003esdfdsaf\u003c/p\u003e', current_user: { can_edit: true, + can_award_emoji: true, }, discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', emoji_awardable: true, @@ -425,6 +435,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = { note_html: '\u003cp dir="auto"\u003eNew note!\u003c/p\u003e', current_user: { can_edit: true, + can_award_emoji: true, }, discussion_id: '70d5c92a4039a36c70100c6691c18c27e4b0a790', emoji_awardable: true, @@ -478,6 +489,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = { }, current_user: { can_edit: true, + can_award_emoji: true, }, discussion_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052', emoji_awardable: true, @@ -527,6 +539,7 @@ export const DISCUSSION_NOTE_RESPONSE_MAP = { note_html: '\u003cp dir="auto"\u003eAdding a comment\u003c/p\u003e', current_user: { can_edit: true, + can_award_emoji: true, }, discussion_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052', emoji_awardable: true, 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/visibility_select_spec.js b/spec/javascripts/visibility_select_spec.js deleted file mode 100644 index 82714cb69bd..00000000000 --- a/spec/javascripts/visibility_select_spec.js +++ /dev/null @@ -1,98 +0,0 @@ -import VisibilitySelect from '~/visibility_select'; - -(() => { - describe('VisibilitySelect', function () { - const lockedElement = document.createElement('div'); - lockedElement.dataset.helpBlock = 'lockedHelpBlock'; - - const checkedElement = document.createElement('div'); - checkedElement.dataset.description = 'checkedDescription'; - - const mockElements = { - container: document.createElement('div'), - select: document.createElement('div'), - '.help-block': document.createElement('div'), - '.js-locked': lockedElement, - 'option:checked': checkedElement, - }; - - beforeEach(function () { - spyOn(Element.prototype, 'querySelector').and.callFake(selector => mockElements[selector]); - }); - - describe('constructor', function () { - beforeEach(function () { - this.visibilitySelect = new VisibilitySelect(mockElements.container); - }); - - it('sets the container member', function () { - expect(this.visibilitySelect.container).toEqual(mockElements.container); - }); - - it('queries and sets the helpBlock member', function () { - expect(Element.prototype.querySelector).toHaveBeenCalledWith('.help-block'); - expect(this.visibilitySelect.helpBlock).toEqual(mockElements['.help-block']); - }); - - it('queries and sets the select member', function () { - expect(Element.prototype.querySelector).toHaveBeenCalledWith('select'); - expect(this.visibilitySelect.select).toEqual(mockElements.select); - }); - - describe('if there is no container element provided', function () { - it('throws an error', function () { - expect(() => new VisibilitySelect()).toThrowError('VisibilitySelect requires a container element as argument 1'); - }); - }); - }); - - describe('init', function () { - describe('if there is a select', function () { - beforeEach(function () { - this.visibilitySelect = new VisibilitySelect(mockElements.container); - }); - - it('calls updateHelpText', function () { - spyOn(VisibilitySelect.prototype, 'updateHelpText'); - this.visibilitySelect.init(); - expect(this.visibilitySelect.updateHelpText).toHaveBeenCalled(); - }); - - it('adds a change event listener', function () { - spyOn(this.visibilitySelect.select, 'addEventListener'); - this.visibilitySelect.init(); - expect(this.visibilitySelect.select.addEventListener.calls.argsFor(0)).toContain('change'); - }); - }); - - describe('if there is no select', function () { - beforeEach(function () { - mockElements.select = undefined; - this.visibilitySelect = new VisibilitySelect(mockElements.container); - this.visibilitySelect.init(); - }); - - it('updates the helpBlock text to the locked `data-help-block` messaged', function () { - expect(this.visibilitySelect.helpBlock.textContent) - .toEqual(lockedElement.dataset.helpBlock); - }); - - afterEach(function () { - mockElements.select = document.createElement('div'); - }); - }); - }); - - describe('updateHelpText', function () { - beforeEach(function () { - this.visibilitySelect = new VisibilitySelect(mockElements.container); - this.visibilitySelect.init(); - }); - - it('updates the helpBlock text to the selected options `data-description`', function () { - expect(this.visibilitySelect.helpBlock.textContent) - .toEqual(checkedElement.dataset.description); - }); - }); - }); -})(); 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/models/ability_spec.rb b/spec/models/ability_spec.rb index cd175dba6da..199f49d0bf2 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -7,62 +7,6 @@ describe Ability do end end - describe '.can_edit_note?' do - let(:project) { create(:project) } - let(:note) { create(:note_on_issue, project: project) } - - context 'using an anonymous user' do - it 'returns false' do - expect(described_class.can_edit_note?(nil, note)).to be_falsy - end - end - - context 'using a system note' do - it 'returns false' do - system_note = create(:note, system: true) - user = create(:user) - - expect(described_class.can_edit_note?(user, system_note)).to be_falsy - end - end - - context 'using users with different access levels' do - let(:user) { create(:user) } - - it 'returns true for the author' do - expect(described_class.can_edit_note?(note.author, note)).to be_truthy - end - - it 'returns false for a guest user' do - project.add_guest(user) - - expect(described_class.can_edit_note?(user, note)).to be_falsy - end - - it 'returns false for a developer' do - project.add_developer(user) - - expect(described_class.can_edit_note?(user, note)).to be_falsy - end - - it 'returns true for a master' do - project.add_master(user) - - expect(described_class.can_edit_note?(user, note)).to be_truthy - end - - it 'returns true for a group owner' do - group = create(:group) - project.project_group_links.create( - group: group, - group_access: Gitlab::Access::MASTER) - group.add_owner(user) - - expect(described_class.can_edit_note?(user, note)).to be_truthy - end - end - end - describe '.users_that_can_read_project' do context 'using a public project' do it 'returns all the users' do diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb index 34f923d3f0c..a980cff28fb 100644 --- a/spec/models/concerns/awardable_spec.rb +++ b/spec/models/concerns/awardable_spec.rb @@ -46,6 +46,31 @@ describe Awardable do end end + describe '#user_can_award?' do + let(:user) { create(:user) } + + before do + issue.project.add_guest(user) + end + + it 'does not allow upvoting or downvoting your own issue' do + issue.update!(author: user) + + expect(issue.user_can_award?(user, AwardEmoji::DOWNVOTE_NAME)).to be_falsy + expect(issue.user_can_award?(user, AwardEmoji::UPVOTE_NAME)).to be_falsy + end + + it 'is truthy when the user is allowed to award emoji' do + expect(issue.user_can_award?(user, AwardEmoji::UPVOTE_NAME)).to be_truthy + end + + it 'is falsy when the project is archived' do + issue.project.update!(archived: true) + + expect(issue.user_can_award?(user, AwardEmoji::UPVOTE_NAME)).to be_falsy + end + end + describe "#toggle_award_emoji" do it "adds an emoji if it isn't awarded yet" do expect { issue.toggle_award_emoji("thumbsup", award_emoji.user) }.to change { AwardEmoji.count }.by(1) diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb index 581fd0293cc..8ef91e8fab5 100644 --- a/spec/models/internal_id_spec.rb +++ b/spec/models/internal_id_spec.rb @@ -5,7 +5,7 @@ describe InternalId do let(:usage) { :issues } let(:issue) { build(:issue, project: project) } let(:scope) { { project: project } } - let(:init) { ->(s) { s.project.issues.size } } + let(:init) { ->(s) { s.project.issues.maximum(:iid) } } context 'validations' do it { is_expected.to validate_presence_of(:usage) } @@ -39,6 +39,29 @@ describe InternalId do end end + context 'with an InternalId record present and existing issues with a higher internal id' do + # This can happen if the old NonatomicInternalId is still in use + before do + issues = Array.new(rand(1..10)).map { create(:issue, project: project) } + + issue = issues.last + issue.iid = issues.map { |i| i.iid }.max + 1 + issue.save + end + + let(:maximum_iid) { project.issues.map { |i| i.iid }.max } + + it 'updates last_value to the maximum internal id present' do + subject + + expect(described_class.find_by(project: project, usage: described_class.usages[usage.to_s]).last_value).to eq(maximum_iid + 1) + end + + it 'returns next internal id correctly' do + expect(subject).to eq(maximum_iid + 1) + end + end + context 'with concurrent inserts on table' do it 'looks up the record if it was created concurrently' do args = { **scope, usage: described_class.usages[usage.to_s] } @@ -81,7 +104,8 @@ describe InternalId do describe '#increment_and_save!' do let(:id) { create(:internal_id) } - subject { id.increment_and_save! } + let(:maximum_iid) { nil } + subject { id.increment_and_save!(maximum_iid) } it 'returns incremented iid' do value = id.last_value @@ -102,5 +126,14 @@ describe InternalId do expect(subject).to eq(1) end end + + context 'with maximum_iid given' do + let(:id) { create(:internal_id, last_value: 1) } + let(:maximum_iid) { id.last_value + 10 } + + it 'returns maximum_iid instead' do + expect(subject).to eq(12) + end + end end end diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb index 58d36a2c84e..e8096358f7d 100644 --- a/spec/policies/note_policy_spec.rb +++ b/spec/policies/note_policy_spec.rb @@ -18,7 +18,6 @@ describe NotePolicy, mdoels: true do context 'when the project is public' do context 'when the note author is not a project member' do it 'can edit a note' do - expect(policies).to be_allowed(:update_note) expect(policies).to be_allowed(:admin_note) expect(policies).to be_allowed(:resolve_note) expect(policies).to be_allowed(:read_note) @@ -29,7 +28,6 @@ describe NotePolicy, mdoels: true do it 'can edit note' do policies = policies(create(:project_snippet, project: project)) - expect(policies).to be_allowed(:update_note) expect(policies).to be_allowed(:admin_note) expect(policies).to be_allowed(:resolve_note) expect(policies).to be_allowed(:read_note) @@ -47,7 +45,6 @@ describe NotePolicy, mdoels: true do end it 'can edit a note' do - expect(policies).to be_allowed(:update_note) expect(policies).to be_allowed(:admin_note) expect(policies).to be_allowed(:resolve_note) expect(policies).to be_allowed(:read_note) @@ -56,7 +53,6 @@ describe NotePolicy, mdoels: true do context 'when the note author is not a project member' do it 'can not edit a note' do - expect(policies).to be_disallowed(:update_note) expect(policies).to be_disallowed(:admin_note) expect(policies).to be_disallowed(:resolve_note) end diff --git a/spec/policies/personal_snippet_policy_spec.rb b/spec/policies/personal_snippet_policy_spec.rb index 50bb0899eba..3809692b373 100644 --- a/spec/policies/personal_snippet_policy_spec.rb +++ b/spec/policies/personal_snippet_policy_spec.rb @@ -27,6 +27,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_allowed(:read_personal_snippet) is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end end @@ -37,6 +38,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_allowed(:read_personal_snippet) is_expected.to be_allowed(:comment_personal_snippet) + is_expected.to be_allowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end end @@ -47,6 +49,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_allowed(:read_personal_snippet) is_expected.to be_allowed(:comment_personal_snippet) + is_expected.to be_allowed(:award_emoji) is_expected.to be_allowed(*author_permissions) end end @@ -61,6 +64,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_disallowed(:read_personal_snippet) is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end end @@ -71,6 +75,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_allowed(:read_personal_snippet) is_expected.to be_allowed(:comment_personal_snippet) + is_expected.to be_allowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end end @@ -81,6 +86,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_disallowed(:read_personal_snippet) is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end end @@ -91,6 +97,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_allowed(:read_personal_snippet) is_expected.to be_allowed(:comment_personal_snippet) + is_expected.to be_allowed(:award_emoji) is_expected.to be_allowed(*author_permissions) end end @@ -105,6 +112,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_disallowed(:read_personal_snippet) is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end end @@ -115,6 +123,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_disallowed(:read_personal_snippet) is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end end @@ -125,6 +134,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_disallowed(:read_personal_snippet) is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end end @@ -135,6 +145,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_allowed(:read_personal_snippet) is_expected.to be_allowed(:comment_personal_snippet) + is_expected.to be_allowed(:award_emoji) is_expected.to be_allowed(*author_permissions) end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 905d82b3bb1..8b9c4ac0b4b 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -14,7 +14,8 @@ describe ProjectPolicy do read_project read_board read_list read_wiki read_issue read_project_for_iids read_issue_iid read_merge_request_iid read_label read_milestone read_project_snippet read_project_member read_note - create_project create_issue create_note upload_file + create_project create_issue create_note upload_file create_merge_request_in + award_emoji ] end @@ -35,7 +36,7 @@ describe ProjectPolicy do %i[ admin_milestone admin_merge_request update_merge_request create_commit_status update_commit_status create_build update_build create_pipeline - update_pipeline create_merge_request create_wiki push_code + update_pipeline create_merge_request_from create_wiki push_code resolve_note create_container_image update_container_image create_environment create_deployment ] @@ -43,7 +44,7 @@ describe ProjectPolicy do let(:base_master_permissions) do %i[ - delete_protected_branch update_project_snippet update_environment + push_to_delete_protected_branch update_project_snippet update_environment update_deployment admin_project_snippet admin_project_member admin_note admin_wiki admin_project admin_commit_status admin_build admin_container_image @@ -136,13 +137,66 @@ describe ProjectPolicy do end end + context 'merge requests feature' do + subject { described_class.new(owner, project) } + + it 'disallows all permissions when the feature is disabled' do + project.project_feature.update(merge_requests_access_level: ProjectFeature::DISABLED) + + mr_permissions = [:create_merge_request_from, :read_merge_request, + :update_merge_request, :admin_merge_request, + :create_merge_request_in] + + expect_disallowed(*mr_permissions) + end + end + + shared_examples 'archived project policies' do + let(:feature_write_abilities) do + described_class::READONLY_FEATURES_WHEN_ARCHIVED.flat_map do |feature| + described_class.create_update_admin_destroy(feature) + end + end + + let(:other_write_abilities) do + %i[ + create_merge_request_in + create_merge_request_from + push_to_delete_protected_branch + push_code + request_access + upload_file + resolve_note + award_emoji + ] + end + + context 'when the project is archived' do + before do + project.archived = true + end + + it 'disables write actions on all relevant project features' do + expect_disallowed(*feature_write_abilities) + end + + it 'disables some other important write actions' do + expect_disallowed(*other_write_abilities) + end + + it 'does not disable other other abilities' do + expect_allowed(*(regular_abilities - feature_write_abilities - other_write_abilities)) + end + end + end + shared_examples 'project policies as anonymous' do context 'abilities for public projects' do context 'when a project has pending invites' do let(:group) { create(:group, :public) } let(:project) { create(:project, :public, namespace: group) } - let(:user_permissions) { [:create_project, :create_issue, :create_note, :upload_file] } - let(:anonymous_permissions) { guest_permissions - user_permissions } + let(:user_permissions) { [:create_merge_request_in, :create_project, :create_issue, :create_note, :upload_file, :award_emoji] } + let(:anonymous_permissions) { guest_permissions - user_permissions } subject { described_class.new(nil, project) } @@ -154,6 +208,10 @@ describe ProjectPolicy do expect_allowed(*anonymous_permissions) expect_disallowed(*user_permissions) end + + it_behaves_like 'archived project policies' do + let(:regular_abilities) { anonymous_permissions } + end end end @@ -184,6 +242,10 @@ describe ProjectPolicy do expect_disallowed(*owner_permissions) end + it_behaves_like 'archived project policies' do + let(:regular_abilities) { guest_permissions } + end + context 'public builds enabled' do it do expect_allowed(*guest_permissions) @@ -224,12 +286,15 @@ describe ProjectPolicy do it do expect_allowed(*guest_permissions) expect_allowed(*reporter_permissions) - expect_allowed(*reporter_permissions) expect_allowed(*team_member_reporter_permissions) expect_disallowed(*developer_permissions) expect_disallowed(*master_permissions) expect_disallowed(*owner_permissions) end + + it_behaves_like 'archived project policies' do + let(:regular_abilities) { reporter_permissions } + end end end @@ -247,6 +312,10 @@ describe ProjectPolicy do expect_disallowed(*master_permissions) expect_disallowed(*owner_permissions) end + + it_behaves_like 'archived project policies' do + let(:regular_abilities) { developer_permissions } + end end end @@ -264,6 +333,10 @@ describe ProjectPolicy do expect_allowed(*master_permissions) expect_disallowed(*owner_permissions) end + + it_behaves_like 'archived project policies' do + let(:regular_abilities) { master_permissions } + end end end @@ -281,6 +354,10 @@ describe ProjectPolicy do expect_allowed(*master_permissions) expect_allowed(*owner_permissions) end + + it_behaves_like 'archived project policies' do + let(:regular_abilities) { owner_permissions } + end end end @@ -298,6 +375,10 @@ describe ProjectPolicy do expect_allowed(*master_permissions) expect_allowed(*owner_permissions) end + + it_behaves_like 'archived project policies' do + let(:regular_abilities) { owner_permissions } + end end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 3764aec0c71..f64623d7018 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -861,7 +861,7 @@ describe API::MergeRequests do expect(json_response['title']).to eq('Test merge_request') end - it 'returns 422 when target project has disabled merge requests' do + it 'returns 403 when target project has disabled merge requests' do project.project_feature.update(merge_requests_access_level: 0) post api("/projects/#{forked_project.id}/merge_requests", user2), @@ -871,7 +871,7 @@ describe API::MergeRequests do author: user2, target_project_id: project.id - expect(response).to have_gitlab_http_status(422) + expect(response).to have_gitlab_http_status(403) end it "returns 400 when source_branch is missing" do 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/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb index 6b748369f0d..be70cb24dce 100644 --- a/spec/requests/api/v3/merge_requests_spec.rb +++ b/spec/requests/api/v3/merge_requests_spec.rb @@ -340,7 +340,7 @@ describe API::MergeRequests do expect(json_response['title']).to eq('Test merge_request') end - it "returns 422 when target project has disabled merge requests" do + it "returns 403 when target project has disabled merge requests" do project.project_feature.update(merge_requests_access_level: 0) post v3_api("/projects/#{forked_project.id}/merge_requests", user2), @@ -350,7 +350,7 @@ describe API::MergeRequests do author: user2, target_project_id: project.id - expect(response).to have_gitlab_http_status(422) + expect(response).to have_gitlab_http_status(403) end it "returns 400 when source_branch is missing" do diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 44a83c436cb..736a50b2c15 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe MergeRequests::CreateService do + include ProjectForksHelper + let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:assignee) { create(:user) } @@ -300,7 +302,7 @@ describe MergeRequests::CreateService do end context 'when source and target projects are different' do - let(:target_project) { create(:project) } + let(:target_project) { fork_project(project, nil, repository: true) } let(:opts) do { @@ -334,6 +336,26 @@ describe MergeRequests::CreateService do .to raise_error Gitlab::Access::AccessDeniedError end end + + context 'when the user has access to both projects' do + before do + target_project.add_developer(user) + project.add_developer(user) + end + + it 'creates the merge request' do + merge_request = described_class.new(project, user, opts).execute + + expect(merge_request).to be_persisted + end + + it 'does not create the merge request when the target project is archived' do + target_project.update!(archived: true) + + expect { described_class.new(project, user, opts).execute } + .to raise_error Gitlab::Access::AccessDeniedError + end + end end context 'when user sets source project id' do 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/support/matchers/have_emoji.rb b/spec/support/matchers/have_emoji.rb new file mode 100644 index 00000000000..23fb8e9c1c4 --- /dev/null +++ b/spec/support/matchers/have_emoji.rb @@ -0,0 +1,5 @@ +RSpec::Matchers.define :have_emoji do |emoji_name| + match do |actual| + expect(actual).to have_selector("gl-emoji[data-name='#{emoji_name}']") + end +end 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/projects/buttons/_dropdown.html.haml_spec.rb b/spec/views/projects/buttons/_dropdown.html.haml_spec.rb index d0e692635b9..8b9aab30286 100644 --- a/spec/views/projects/buttons/_dropdown.html.haml_spec.rb +++ b/spec/views/projects/buttons/_dropdown.html.haml_spec.rb @@ -8,7 +8,8 @@ describe 'projects/buttons/_dropdown' do assign(:project, project) allow(view).to receive(:current_user).and_return(user) - allow(view).to receive(:can?).and_return(true) + allow(view).to receive(:can?).with(user, :push_code, project).and_return(true) + allow(view).to receive(:can_collaborate_with_project?).and_return(true) end context 'empty repository' do diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb index 448b925cf34..2fdd28a3be4 100644 --- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb +++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb @@ -7,6 +7,7 @@ describe 'projects/commit/_commit_box.html.haml' do before do assign(:project, project) assign(:commit, project.commit) + allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:can_collaborate_with_project?).and_return(false) end @@ -47,7 +48,8 @@ describe 'projects/commit/_commit_box.html.haml' do context 'viewing a commit' do context 'as a developer' do before do - expect(view).to receive(:can_collaborate_with_project?).and_return(true) + project.add_developer(user) + allow(view).to receive(:can_collaborate_with_project?).and_return(true) end it 'has a link to create a new tag' do @@ -58,10 +60,6 @@ describe 'projects/commit/_commit_box.html.haml' do end context 'as a non-developer' do - before do - expect(view).to receive(:can_collaborate_with_project?).and_return(false) - end - it 'does not have a link to create a new tag' do render 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/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml index 4810035a9e3..4f4ed80d101 100644 --- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml @@ -315,6 +315,7 @@ production: mv clair-scanner_linux_amd64 clair-scanner chmod +x clair-scanner touch clair-whitelist.yml + while( ! wget -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; done ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-sast-container-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true } 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/vendor/licenses.csv b/vendor/licenses.csv index 03115292f02..ca88f867fe5 100644 --- a/vendor/licenses.csv +++ b/vendor/licenses.csv @@ -4,7 +4,7 @@ @babel/template,7.0.0-beta.32,MIT @babel/traverse,7.0.0-beta.32,MIT @babel/types,7.0.0-beta.32,MIT -@gitlab-org/gitlab-svgs,1.8.0,SEE LICENSE IN LICENSE +@gitlab-org/gitlab-svgs,1.17.0,SEE LICENSE IN LICENSE @types/jquery,2.0.48,MIT JSONStream,1.3.2,MIT RedCloth,4.3.2,MIT @@ -27,10 +27,11 @@ activejob,4.2.10,MIT activemodel,4.2.10,MIT activerecord,4.2.10,MIT activesupport,4.2.10,MIT -acts-as-taggable-on,4.0.0,MIT +acts-as-taggable-on,5.0.0,MIT address,1.0.3,MIT addressable,2.5.2,Apache 2.0 addressparser,1.0.1,MIT +aes_key_wrap,1.0.1,MIT after,0.8.2,MIT agent-base,2.1.1,MIT ajv,4.11.8,MIT @@ -80,8 +81,8 @@ array-unique,0.3.2,MIT arraybuffer.slice,0.0.7,MIT arrify,1.0.1,MIT asana,0.6.0,MIT -asciidoctor,1.5.3,MIT -asciidoctor-plantuml,0.0.7,MIT +asciidoctor,1.5.6.2,MIT +asciidoctor-plantuml,0.0.8,MIT asn1,0.2.3,MIT asn1.js,4.10.1,MIT assert,1.4.1,MIT @@ -205,8 +206,8 @@ better-assert,1.0.2,MIT bfj-node4,5.2.1,MIT big.js,3.1.3,MIT binary-extensions,1.11.0,MIT -bindata,2.4.1,ruby -bitsyntax,0.0.4,Unknown +bindata,2.4.3,ruby +bitsyntax,0.0.4,UNKNOWN bl,1.1.2,MIT blackst0ne-mermaid,7.1.0-fixed,MIT blob,0.0.4,MIT* @@ -274,7 +275,7 @@ chalk,1.1.3,MIT chalk,2.3.0,MIT chalk,2.3.1,MIT chardet,0.4.2,MIT -charlock_holmes,0.7.5,MIT +charlock_holmes,0.7.6,MIT chart.js,1.0.2,MIT check-types,7.3.0,MIT chokidar,1.7.0,MIT @@ -314,7 +315,7 @@ combine-lists,1.0.1,MIT combine-source-map,0.7.2,MIT combine-source-map,0.8.0,MIT combined-stream,1.0.6,MIT -commander,2.14.1,MIT +commander,2.15.1,MIT commondir,1.0.1,MIT commonmarker,0.17.8,MIT component-bind,1.0.0,MIT* @@ -352,6 +353,7 @@ core-js,2.5.3,MIT core-util-is,1.0.2,MIT cosmiconfig,2.1.1,MIT crack,0.4.3,MIT +crass,1.0.3,MIT create-ecdh,4.0.0,MIT create-error-class,3.0.2,MIT create-hash,1.1.3,MIT @@ -457,13 +459,13 @@ document-register-element,1.3.0,MIT dom-serialize,2.2.1,MIT dom-serializer,0.1.0,MIT domain-browser,1.1.7,MIT -domain_name,0.5.20161021,"Simplified BSD,New BSD,Mozilla Public License 2.0" +domain_name,0.5.20170404,"Simplified BSD,New BSD,Mozilla Public License 2.0" domelementtype,1.1.3,Simplified BSD domelementtype,1.3.0,Simplified BSD domhandler,2.4.1,Simplified BSD domutils,1.6.2,Simplified BSD -doorkeeper,4.2.6,MIT -doorkeeper-openid_connect,1.2.0,MIT +doorkeeper,4.3.1,MIT +doorkeeper-openid_connect,1.3.0,MIT dot-prop,4.2.0,MIT double-ended-queue,2.1.0-0,MIT dropzone,4.2.0,MIT @@ -543,7 +545,7 @@ eventemitter3,1.2.0,MIT events,1.1.1,MIT eventsource,0.1.6,MIT evp_bytestokey,1.0.3,MIT -excon,0.57.1,MIT +excon,0.60.0,MIT execa,0.7.0,MIT execjs,2.6.0,MIT exit-hook,1.1.1,MIT @@ -571,7 +573,7 @@ fast-deep-equal,1.0.0,MIT fast-json-stable-stringify,2.0.0,MIT fast-levenshtein,2.0.6,MIT fast_blank,1.0.0,MIT -fast_gettext,1.4.0,"MIT,ruby" +fast_gettext,1.6.0,"MIT,ruby" fastparse,1.1.1,MIT faye-websocket,0.10.0,MIT faye-websocket,0.11.1,MIT @@ -594,15 +596,15 @@ find-up,1.1.2,MIT find-up,2.1.0,MIT flat-cache,1.2.2,MIT flatten,1.0.2,MIT -flipper,0.11.0,MIT -flipper-active_record,0.11.0,MIT -flipper-active_support_cache_store,0.11.0,MIT +flipper,0.13.0,MIT +flipper-active_record,0.13.0,MIT +flipper-active_support_cache_store,0.13.0,MIT flowdock,0.7.1,MIT flush-write-stream,1.0.2,MIT fog-aliyun,0.2.0,MIT -fog-aws,1.4.0,MIT -fog-core,1.44.3,MIT -fog-google,0.5.3,MIT +fog-aws,2.0.1,MIT +fog-core,1.45.0,MIT +fog-google,1.3.3,MIT fog-json,1.0.2,MIT fog-local,0.3.1,MIT fog-openstack,0.1.21,MIT @@ -646,8 +648,8 @@ get-value,2.0.6,MIT get_process_mem,0.2.0,MIT getpass,0.1.7,MIT gettext_i18n_rails,1.8.0,MIT -gettext_i18n_rails_js,1.2.0,MIT -gitaly-proto,0.88.0,MIT +gettext_i18n_rails_js,1.3.0,MIT +gitaly-proto,0.94.0,MIT github-linguist,5.3.3,MIT github-markup,1.6.1,MIT gitlab-flowdock-git-hook,1.0.1,MIT @@ -669,12 +671,13 @@ globals,9.18.0,MIT globby,5.0.0,MIT globby,6.1.0,MIT globby,7.1.1,MIT +goldiloader,2.0.1,MIT gollum-grit_adapter,1.0.1,MIT gollum-lib,4.2.7,MIT gollum-rugged_adapter,0.4.4,MIT gon,6.1.0,MIT good-listener,1.2.2,MIT -google-api-client,0.13.6,Apache 2.0 +google-api-client,0.19.8,Apache 2.0 google-protobuf,3.5.1,New BSD googleapis-common-protos-types,1.0.1,Apache 2.0 googleauth,0.6.2,Apache 2.0 @@ -682,7 +685,7 @@ got,6.7.1,MIT got,7.1.0,MIT gpgme,2.0.13,LGPL-2.1+ graceful-fs,4.1.11,ISC -grape,1.0.0,MIT +grape,1.0.2,MIT grape-entity,0.6.0,MIT grape-route-helpers,2.1.0,MIT grape_logging,1.7.0,MIT @@ -716,7 +719,7 @@ hash-base,2.0.2,MIT hash-base,3.0.4,MIT hash-sum,1.0.2,MIT hash.js,1.1.3,MIT -hashie,3.5.6,MIT +hashie,3.5.7,MIT hashie-forbidden_attributes,0.1.1,MIT hawk,3.1.3,New BSD hawk,6.0.2,New BSD @@ -733,16 +736,16 @@ hosted-git-info,2.2.0,ISC hpack.js,2.1.6,MIT html-comment-regex,1.1.1,MIT html-entities,1.2.0,MIT -html-pipeline,1.11.0,MIT +html-pipeline,2.7.1,MIT html2text,0.2.0,MIT htmlentities,4.3.4,MIT htmlescape,1.1.1,MIT htmlparser2,3.9.2,MIT -http,0.9.8,MIT +http,2.2.2,MIT http-cookie,1.0.3,MIT http-deceiver,1.2.7,MIT http-errors,1.6.2,MIT -http-form_data,1.0.1,MIT +http-form_data,1.0.3,MIT http-proxy,1.16.2,MIT http-proxy-agent,1.0.0,MIT http-proxy-middleware,0.17.4,MIT @@ -750,13 +753,13 @@ http-signature,1.1.1,MIT http-signature,1.2.0,MIT http_parser.rb,0.6.0,MIT httparty,0.13.7,MIT -httpclient,2.8.2,ruby +httpclient,2.8.3,ruby httpntlm,1.6.1,MIT httpreq,0.4.24,MIT https-browserify,0.0.1,MIT https-browserify,1.0.0,MIT https-proxy-agent,1.0.0,MIT -i18n,0.9.1,MIT +i18n,0.9.5,MIT ice_nine,0.11.2,MIT iconv-lite,0.4.15,MIT iconv-lite,0.4.19,MIT @@ -881,7 +884,6 @@ jed,1.1.1,MIT jira-ruby,1.4.1,MIT jquery,3.3.1,MIT jquery-atwho-rails,1.3.2,MIT -jquery-rails,4.3.1,MIT jquery-ujs,1.2.2,MIT jquery.waitforimages,2.2.0,MIT js-base64,2.1.9,New BSD @@ -893,7 +895,7 @@ jsbn,0.1.1,MIT jsesc,0.5.0,MIT jsesc,1.3.0,MIT json,1.8.6,ruby -json-jwt,1.7.2,MIT +json-jwt,1.9.2,MIT json-loader,0.5.7,MIT json-schema,0.2.3,BSD json-schema-traverse,0.3.1,MIT @@ -927,7 +929,7 @@ kind-of,3.2.2,MIT kind-of,4.0.0,MIT kind-of,5.1.0,MIT kind-of,6.0.2,MIT -kubeclient,2.2.0,MIT +kubeclient,3.0.0,MIT labeled-stream-splicer,2.0.0,MIT latest-version,3.1.0,MIT lazy-cache,1.0.4,MIT @@ -938,7 +940,7 @@ lexical-scope,1.2.0,MIT libbase64,0.1.0,MIT libmime,3.0.0,MIT libqp,1.1.0,MIT -licensee,8.7.0,MIT +licensee,8.9.2,MIT lie,3.1.1,MIT little-plugger,1.1.4,MIT load-json-file,1.1.0,MIT @@ -980,7 +982,7 @@ loggly,1.1.1,MIT loglevel,1.4.1,MIT lograge,0.5.1,MIT longest,1.0.1,MIT -loofah,2.0.3,MIT +loofah,2.2.2,MIT loose-envify,1.3.1,MIT loud-rejection,1.6.0,MIT lowercase-keys,1.0.0,MIT @@ -994,7 +996,7 @@ mailgun-js,0.7.15,MIT make-dir,1.0.0,MIT map-cache,0.2.2,MIT map-obj,1.0.1,MIT -map-stream,0.1.0,Unknown +map-stream,0.1.0,UNKNOWN map-visit,1.0.0,MIT marked,0.3.12,MIT match-at,0.1.1,MIT @@ -1022,7 +1024,7 @@ mime-types-data,3.2016.0521,MIT mimemagic,0.3.0,MIT mimic-fn,1.1.0,MIT mimic-response,1.0.0,MIT -mini_mime,0.1.4,MIT +mini_mime,1.0.0,MIT mini_portile2,2.3.0,MIT minimalistic-assert,1.0.0,ISC minimalistic-crypto-utils,1.0.1,MIT @@ -1047,7 +1049,7 @@ multi_xml,0.6.0,MIT multicast-dns,6.1.1,MIT multicast-dns-service-types,1.1.0,MIT multipart-post,2.0.0,MIT -mustermann,1.0.0,MIT +mustermann,1.0.2,MIT mustermann-grape,1.0.0,MIT mute-stream,0.0.5,ISC mute-stream,0.0.7,ISC @@ -1058,7 +1060,7 @@ nanomatch,1.2.9,MIT natural-compare,1.4.0,MIT negotiator,0.6.1,MIT net-ldap,0.16.0,MIT -net-ssh,4.1.0,MIT +net-ssh,4.2.0,MIT netmask,1.0.6,MIT netrc,0.11.0,MIT node-forge,0.6.33,New BSD @@ -1088,7 +1090,7 @@ null-check,1.0.0,MIT num2fraction,1.2.2,MIT number-is-nan,1.0.1,MIT numerizer,0.1.1,MIT -oauth,0.5.1,MIT +oauth,0.5.4,MIT oauth-sign,0.8.2,Apache 2.0 oauth2,1.4.0,MIT object-assign,4.1.1,MIT @@ -1099,25 +1101,25 @@ object-visit,1.0.1,MIT object.omit,2.0.1,MIT object.pick,1.3.0,MIT obuf,1.1.1,MIT -octokit,4.6.2,MIT -oj,2.17.5,MIT -omniauth,1.4.2,MIT -omniauth-auth0,1.4.1,MIT +octokit,4.8.0,MIT +omniauth,1.8.1,MIT +omniauth-auth0,2.0.0,MIT omniauth-authentiq,0.3.1,MIT omniauth-azure-oauth2,0.0.9,MIT omniauth-cas3,1.1.4,MIT omniauth-facebook,4.0.0,MIT omniauth-github,1.1.2,MIT omniauth-gitlab,1.0.2,MIT -omniauth-google-oauth2,0.5.2,MIT +omniauth-google-oauth2,0.5.3,MIT +omniauth-jwt,0.0.2,MIT omniauth-kerberos,0.3.0,MIT omniauth-multipassword,0.4.2,MIT omniauth-oauth,1.1.0,MIT -omniauth-oauth2,1.4.0,MIT +omniauth-oauth2,1.5.0,MIT omniauth-oauth2-generic,0.2.2,MIT -omniauth-saml,1.7.0,MIT +omniauth-saml,1.10.0,MIT omniauth-shibboleth,1.2.1,MIT -omniauth-twitter,1.2.1,MIT +omniauth-twitter,1.4.0,MIT omniauth_crowd,2.2.3,MIT on-finished,2.3.0,MIT on-headers,1.0.1,MIT @@ -1181,7 +1183,6 @@ pause-stream,0.0.11,Apache 2.0 pbkdf2,3.0.14,MIT peek,1.0.1,MIT peek-gc,0.0.2,MIT -peek-host,1.0.0,MIT peek-mysql2,1.1.0,MIT peek-performance_bar,1.3.1,MIT peek-pg,1.3.0,MIT @@ -1248,8 +1249,8 @@ premailer,1.10.4,New BSD premailer-rails,1.9.7,MIT prepend-http,1.0.4,MIT preserve,0.2.0,MIT +prettier,1.11.1,MIT prettier,1.8.2,MIT -prettier,1.9.2,MIT prismjs,1.6.0,MIT private,0.1.8,MIT process,0.11.10,MIT @@ -1283,18 +1284,18 @@ querystring,0.2.0,MIT querystring-es3,0.2.1,MIT querystringify,0.0.4,MIT querystringify,1.0.0,MIT -rack,1.6.8,MIT +rack,1.6.9,MIT rack-accept,0.4.5,MIT rack-attack,4.4.1,MIT rack-cors,1.0.2,MIT rack-oauth2,1.2.3,MIT -rack-protection,1.5.3,MIT +rack-protection,2.0.1,MIT rack-proxy,0.6.0,MIT rack-test,0.6.3,MIT rails,4.2.10,MIT rails-deprecated_sanitizer,1.0.3,MIT -rails-dom-testing,1.0.8,MIT -rails-html-sanitizer,1.0.3,MIT +rails-dom-testing,1.0.9,MIT +rails-html-sanitizer,1.0.4,MIT rails-i18n,4.0.9,MIT railties,4.2.10,MIT rainbow,2.2.2,MIT @@ -1331,7 +1332,7 @@ readdirp,2.1.0,MIT readline2,1.0.1,MIT recaptcha,3.0.0,MIT rechoir,0.6.2,MIT -recursive-open-struct,1.0.0,MIT +recursive-open-struct,1.0.5,MIT recursive-readdir,2.2.1,MIT redcarpet,3.4.0,MIT redent,1.0.0,MIT @@ -1383,7 +1384,7 @@ resolve-from,1.0.1,MIT resolve-from,3.0.0,MIT resolve-url,0.2.1,MIT responders,2.3.0,MIT -rest-client,2.0.0,MIT +rest-client,2.0.2,MIT restore-cursor,1.0.1,MIT restore-cursor,2.0.0,MIT ret,0.1.15,MIT @@ -1399,13 +1400,13 @@ rqrcode,0.7.0,MIT rqrcode-rails3,0.1.7,MIT ruby-enum,0.7.2,MIT ruby-fogbugz,0.2.1,MIT -ruby-prof,0.16.2,Simplified BSD -ruby-saml,1.4.1,MIT +ruby-prof,0.17.0,Simplified BSD +ruby-saml,1.7.2,MIT ruby_parser,3.9.0,MIT rubyntlm,0.6.2,MIT rubypants,0.2.0,BSD rufus-scheduler,3.4.0,MIT -rugged,0.26.0,MIT +rugged,0.27.0,MIT run-async,0.1.0,MIT run-async,2.3.0,MIT run-queue,1.0.3,ISC @@ -1436,7 +1437,7 @@ semver,5.3.0,ISC semver,5.5.0,ISC semver-diff,2.1.0,MIT send,0.16.1,MIT -sentry-raven,2.5.3,Apache 2.0 +sentry-raven,2.7.2,Apache 2.0 serialize-javascript,1.4.0,New BSD serve-index,1.9.0,MIT serve-static,1.13.1,MIT @@ -1507,9 +1508,9 @@ srcset,1.0.0,MIT sshkey,1.9.0,MIT sshpk,1.13.1,MIT ssri,5.2.4,ISC -state_machines,0.4.0,MIT -state_machines-activemodel,0.4.0,MIT -state_machines-activerecord,0.4.0,MIT +state_machines,0.5.0,MIT +state_machines-activemodel,0.5.1,MIT +state_machines-activerecord,0.5.1,MIT static-extend,0.1.2,MIT statuses,1.3.1,MIT statuses,1.4.0,MIT @@ -1603,7 +1604,7 @@ tweetnacl,0.14.5,Unlicense type-check,0.3.2,MIT type-is,1.6.16,MIT typedarray,0.0.6,MIT -tzinfo,1.2.4,MIT +tzinfo,1.2.5,MIT u2f,0.2.1,MIT uber,0.1.0,MIT uglifier,2.7.2,MIT @@ -1618,7 +1619,7 @@ undefsafe,2.0.2,MIT underscore,1.7.0,MIT underscore,1.8.3,MIT unf,0.1.4,BSD -unf_ext,0.0.7.4,MIT +unf_ext,0.0.7.5,MIT unicorn,5.1.0,ruby unicorn-worker-killer,0.4.4,ruby union-value,1.0.0,MIT diff --git a/yarn.lock b/yarn.lock index c4d682016a0..55a86a9a577 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" |