diff options
Diffstat (limited to 'app')
484 files changed, 4492 insertions, 1846 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 25fe2ae553e..cd800d75f7a 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -15,6 +15,7 @@ const Api = { mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes', mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions', groupLabelsPath: '/groups/:namespace_path/-/labels', + templatesPath: '/api/:version/templates/:key', licensePath: '/api/:version/templates/licenses/:key', gitignorePath: '/api/:version/templates/gitignores/:key', gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key', @@ -265,6 +266,12 @@ const Api = { }); }, + templates(key, params = {}) { + const url = Api.buildUrl(this.templatesPath).replace(':key', key); + + return axios.get(url, { params }); + }, + buildUrl(url) { let urlRoot = ''; if (gon.relative_url_root != null) { diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index e34db893989..5b0c4285339 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -109,8 +109,6 @@ export class AwardsHandler { } const $menu = $(`.${this.menuClass}`); - const $thumbsBtn = $menu.find('[data-name="thumbsup"], [data-name="thumbsdown"]').parent(); - const $userAuthored = this.isUserAuthored($addBtn); if ($menu.length) { if ($menu.is('.is-visible')) { $addBtn.removeClass('is-active'); @@ -134,9 +132,6 @@ export class AwardsHandler { }, 200); }); } - - $thumbsBtn.toggleClass('disabled', $userAuthored); - $thumbsBtn.prop('disabled', $userAuthored); } // Create the emoji menu with the first category of emojis. @@ -364,10 +359,6 @@ export class AwardsHandler { return $emojiButton.hasClass('active'); } - isUserAuthored($button) { - return $button.hasClass('js-user-authored'); - } - decrementCounter($emojiButton, emoji) { const counter = $('.js-counter', $emojiButton); const counterNumber = parseInt(counter.text(), 10); @@ -474,20 +465,16 @@ export class AwardsHandler { } postEmoji($emojiButton, awardUrl, emoji, callback) { - if (this.isUserAuthored($emojiButton)) { - this.userAuthored($emojiButton); - } else { - axios - .post(awardUrl, { - name: emoji, - }) - .then(({ data }) => { - if (data.ok) { - callback(); - } - }) - .catch(() => flash(__('Something went wrong on our end.'))); - } + axios + .post(awardUrl, { + name: emoji, + }) + .then(({ data }) => { + if (data.ok) { + callback(); + } + }) + .catch(() => flash(__('Something went wrong on our end.'))); } findEmojiIcon(votesBlock, emoji) { diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue index 7a13f74c570..b3f25da87ce 100644 --- a/app/assets/javascripts/badges/components/badge_form.vue +++ b/app/assets/javascripts/badges/components/badge_form.vue @@ -23,6 +23,11 @@ export default { required: true, }, }, + data() { + return { + wasValidated: false, + }; + }, computed: { ...mapState([ 'badgeInAddForm', @@ -39,16 +44,6 @@ export default { return this.badgeInAddForm; }, - canSubmit() { - return ( - this.badge !== null && - this.badge.imageUrl && - this.badge.imageUrl.trim() !== '' && - this.badge.linkUrl && - this.badge.linkUrl.trim() !== '' && - !this.isSaving - ); - }, helpText() { const placeholders = ['project_path', 'project_id', 'default_branch', 'commit_sha'] .map(placeholder => `<code>%{${placeholder}}</code>`) @@ -93,11 +88,18 @@ export default { }); }, }, - submitButtonLabel() { - if (this.isEditing) { - return s__('Badges|Save changes'); - } - return s__('Badges|Add badge'); + badgeImageUrlExample() { + const exampleUrl = + 'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/badge.svg'; + return sprintf(s__('Badges|e.g. %{exampleUrl}'), { + exampleUrl, + }); + }, + badgeLinkUrlExample() { + const exampleUrl = 'https://example.gitlab.com/%{project_path}'; + return sprintf(s__('Badges|e.g. %{exampleUrl}'), { + exampleUrl, + }); }, }, methods: { @@ -109,7 +111,9 @@ export default { this.stopEditing(); }, onSubmit() { - if (!this.canSubmit) { + const form = this.$el; + if (!form.checkValidity()) { + this.wasValidated = true; return Promise.resolve(); } @@ -117,6 +121,7 @@ export default { return this.saveBadge() .then(() => { createFlash(s__('Badges|The badge was saved.'), 'notice'); + this.wasValidated = false; }) .catch(error => { createFlash( @@ -129,6 +134,7 @@ export default { return this.addBadge() .then(() => { createFlash(s__('Badges|A new badge was added.'), 'notice'); + this.wasValidated = false; }) .catch(error => { createFlash( @@ -138,47 +144,58 @@ export default { }); }, }, - badgeImageUrlPlaceholder: - 'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/<badge>.svg', - badgeLinkUrlPlaceholder: 'https://example.gitlab.com/%{project_path}', }; </script> <template> <form - class="prepend-top-default append-bottom-default" + :class="{ 'was-validated': wasValidated }" + class="prepend-top-default append-bottom-default needs-validation" + novalidate @submit.prevent.stop="onSubmit" > <div class="form-group"> - <label for="badge-link-url">{{ s__('Badges|Link') }}</label> + <label + for="badge-link-url" + class="label-bold" + >{{ s__('Badges|Link') }}</label> + <p v-html="helpText"></p> <input id="badge-link-url" v-model="linkUrl" - :placeholder="$options.badgeLinkUrlPlaceholder" - type="text" + type="URL" class="form-control" + required @input="debouncedPreview" /> - <span - class="form-text text-muted" - v-html="helpText" - ></span> + <div class="invalid-feedback"> + {{ s__('Badges|Please fill in a valid URL') }} + </div> + <span class="form-text text-muted"> + {{ badgeLinkUrlExample }} + </span> </div> <div class="form-group"> - <label for="badge-image-url">{{ s__('Badges|Badge image URL') }}</label> + <label + for="badge-image-url" + class="label-bold" + >{{ s__('Badges|Badge image URL') }}</label> + <p v-html="helpText"></p> <input id="badge-image-url" v-model="imageUrl" - :placeholder="$options.badgeImageUrlPlaceholder" - type="text" + type="URL" class="form-control" + required @input="debouncedPreview" /> - <span - class="form-text text-muted" - v-html="helpText" - ></span> + <div class="invalid-feedback"> + {{ s__('Badges|Please fill in a valid URL') }} + </div> + <span class="form-text text-muted"> + {{ badgeImageUrlExample }} + </span> </div> <div class="form-group"> @@ -200,20 +217,32 @@ export default { >{{ s__('Badges|No image to preview') }}</p> </div> - <div class="row-content-block"> + <div + v-if="isEditing" + class="row-content-block" + > <loading-button - :disabled="!canSubmit" :loading="isSaving" - :label="submitButtonLabel" + :label="s__('Badges|Save changes')" type="submit" container-class="btn btn-success" /> <button - v-if="isEditing" class="btn btn-cancel" type="button" @click="onCancel" >{{ __('Cancel') }}</button> </div> + <div + v-else + class="form-group" + > + <loading-button + :loading="isSaving" + :label="s__('Badges|Add badge')" + type="submit" + container-class="btn btn-success" + /> + </div> </form> </template> diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue index 268968b63b3..d2ec0fbb2c0 100644 --- a/app/assets/javascripts/badges/components/badge_list.vue +++ b/app/assets/javascripts/badges/components/badge_list.vue @@ -28,7 +28,7 @@ export default { {{ s__('Badges|Your badges') }} <span v-show="!isLoading" - class="badge" + class="badge badge-pill" >{{ badges.length }}</span> </div> <loading-icon diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue index 98aa00af0d7..712d81d0430 100644 --- a/app/assets/javascripts/badges/components/badge_list_row.vue +++ b/app/assets/javascripts/badges/components/badge_list_row.vue @@ -43,13 +43,13 @@ export default { <badge :image-url="badge.renderedImageUrl" :link-url="badge.renderedLinkUrl" - class="table-section section-30" + class="table-section section-40" /> - <span class="table-section section-50 str-truncated">{{ badge.linkUrl }}</span> - <div class="table-section section-10"> - <span class="badge">{{ badgeKindText }}</span> + <span class="table-section section-30 str-truncated">{{ badge.linkUrl }}</span> + <div class="table-section section-15"> + <span class="badge badge-pill">{{ badgeKindText }}</span> </div> - <div class="table-section section-10 table-button-footer"> + <div class="table-section section-15 table-button-footer"> <div v-if="canEditBadge" class="table-action-buttons"> diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 3e610a4088c..bfc8d9b03ad 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -203,7 +203,7 @@ export default { this.showIssueForm = !this.showIssueForm; }, onScroll() { - if (!this.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) { + if (!this.list.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) { this.loadNextPage(); } }, diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index a9102743bf9..109e60cbde2 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -3,7 +3,7 @@ import $ from 'jquery'; import Vue from 'vue'; import Flash from '../../flash'; -import { __ } from '../../locale'; +import { sprintf, __ } from '../../locale'; import Sidebar from '../../right_sidebar'; import eventHub from '../../sidebar/event_hub'; import AssigneeTitle from '../../sidebar/components/assignees/assignee_title.vue'; @@ -55,8 +55,10 @@ gl.issueBoards.BoardSidebar = Vue.extend({ return this.issue.labels && this.issue.labels.length; }, labelDropdownTitle() { - return this.hasLabels ? - `${this.issue.labels[0].title} ${this.issue.labels.length - 1}+ more` : 'Label'; + return this.hasLabels ? sprintf(__('%{firstLabel} +%{labelCount} more'), { + firstLabel: this.issue.labels[0].title, + labelCount: this.issue.labels.length - 1 + }) : __('Label'); }, selectedLabels() { return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : ''; diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index 050cbd8db48..ad473404c29 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -1,6 +1,7 @@ /* eslint-disable no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len */ /* global ListIssue */ +import { __ } from '~/locale'; import ListLabel from '~/vue_shared/models/label'; import ListAssignee from '~/vue_shared/models/assignee'; import queryData from '../utils/query_data'; @@ -30,7 +31,7 @@ class List { this.id = obj.id; this._uid = this.guid(); this.position = obj.position; - this.title = obj.title; + this.title = obj.list_type === 'backlog' ? __('Open') : obj.title; this.type = obj.list_type; const typeInfo = this.getTypeInfo(this.type); diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js index 589eeee9695..742cf490ad2 100644 --- a/app/assets/javascripts/commons/polyfills.js +++ b/app/assets/javascripts/commons/polyfills.js @@ -8,6 +8,7 @@ import 'core-js/fn/object/assign'; import 'core-js/fn/promise'; import 'core-js/fn/string/code-point-at'; import 'core-js/fn/string/from-code-point'; +import 'core-js/fn/string/includes'; import 'core-js/fn/symbol'; import 'core-js/es6/map'; import 'core-js/es6/weak-map'; diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 7cc4e6a2c3a..b5b05df4d34 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -114,11 +114,15 @@ export default { this.adjustView(); }, methods: { - ...mapActions('diffs', ['setBaseConfig', 'fetchDiffFiles']), + ...mapActions('diffs', ['setBaseConfig', 'fetchDiffFiles', 'startRenderDiffsQueue']), fetchData() { - this.fetchDiffFiles().catch(() => { - createFlash(__('Something went wrong on our end. Please try again!')); - }); + this.fetchDiffFiles() + .then(() => { + requestIdleCallback(this.startRenderDiffsQueue, { timeout: 1000 }); + }) + .catch(() => { + createFlash(__('Something went wrong on our end. Please try again!')); + }); if (!this.isNotesFetched) { eventHub.$emit('fetchNotesData'); diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 7e7058d8d08..59e9ba08b8b 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -46,16 +46,25 @@ export default { showExpandMessage() { return this.isCollapsed && !this.isLoadingCollapsedDiff && !this.file.tooLarge; }, + showLoadingIcon() { + return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed); + }, }, methods: { ...mapActions('diffs', ['loadCollapsedDiff']), handleToggle() { const { collapsed, highlightedDiffLines, parallelDiffLines } = this.file; - if (collapsed && !highlightedDiffLines && !parallelDiffLines.length) { + if ( + collapsed && + !highlightedDiffLines && + parallelDiffLines !== undefined && + !parallelDiffLines.length + ) { this.handleLoadCollapsedDiff(); } else { this.file.collapsed = !this.file.collapsed; + this.file.renderIt = true; } }, handleLoadCollapsedDiff() { @@ -65,6 +74,7 @@ export default { .then(() => { this.isLoadingCollapsedDiff = false; this.file.collapsed = false; + this.file.renderIt = true; }) .catch(() => { this.isLoadingCollapsedDiff = false; @@ -121,12 +131,12 @@ export default { </div> <diff-content - v-if="!isCollapsed" + v-if="!isCollapsed && file.renderIt" :class="{ hidden: isCollapsed || file.tooLarge }" :diff-file="file" /> <loading-icon - v-if="isLoadingCollapsedDiff" + v-else-if="showLoadingIcon" class="diff-content loading" /> <div diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index 2fa8367f528..f68afa44837 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -25,3 +25,6 @@ export const CONTEXT_LINE_CLASS_NAME = 'diff-expanded'; export const UNFOLD_COUNT = 20; export const COUNT_OF_AVATARS_IN_GUTTER = 3; export const LENGTH_OF_AVATAR_TOOLTIP = 17; + +export const LINES_TO_BE_RENDERED_DIRECTLY = 100; +export const MAX_LINES_TO_BE_RENDERED = 2000; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 27001142257..4ab6ceb249a 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -29,6 +29,27 @@ export const fetchDiffFiles = ({ state, commit }) => { .then(handleLocationHash); }; +export const startRenderDiffsQueue = ({ state, commit }) => { + const checkItem = () => { + const nextFile = state.diffFiles.find( + file => !file.renderIt && (!file.collapsed || !file.text), + ); + if (nextFile) { + requestAnimationFrame(() => { + commit(types.RENDER_FILE, nextFile); + }); + requestIdleCallback( + () => { + checkItem(); + }, + { timeout: 1000 }, + ); + } + }; + + checkItem(); +}; + export const setInlineDiffViewType = ({ commit }) => { commit(types.SET_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE); diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index 2c8e1a1466f..c999d637d50 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -8,3 +8,4 @@ export const REMOVE_COMMENT_FORM_LINE = 'REMOVE_COMMENT_FORM_LINE'; export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES'; export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS'; export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES'; +export const RENDER_FILE = 'RENDER_FILE'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index a98b2be89a3..0522e32c410 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import _ from 'underscore'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { findDiffFile, addLineReferences, removeMatchLine, addContextLines } from './utils'; +import { LINES_TO_BE_RENDERED_DIRECTLY, MAX_LINES_TO_BE_RENDERED } from '../constants'; import * as types from './mutation_types'; export default { @@ -15,8 +16,48 @@ export default { }, [types.SET_DIFF_DATA](state, data) { + const diffData = convertObjectPropsToCamelCase(data, { deep: true }); + let showingLines = 0; + const filesLength = diffData.diffFiles.length; + let i; + for (i = 0; i < filesLength; i += 1) { + const file = diffData.diffFiles[i]; + if (file.parallelDiffLines) { + const linesLength = file.parallelDiffLines.length; + let u = 0; + for (u = 0; u < linesLength; u += 1) { + const line = file.parallelDiffLines[u]; + if (line.left) delete line.left.text; + if (line.right) delete line.right.text; + } + } + + if (file.highlightedDiffLines) { + const linesLength = file.highlightedDiffLines.length; + let u; + for (u = 0; u < linesLength; u += 1) { + const line = file.highlightedDiffLines[u]; + delete line.text; + } + } + + if (file.highlightedDiffLines) { + showingLines += file.parallelDiffLines.length; + } + Object.assign(file, { + renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY, + collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED, + }); + } + Object.assign(state, { - ...convertObjectPropsToCamelCase(data, { deep: true }), + ...diffData, + }); + }, + + [types.RENDER_FILE](state, file) { + Object.assign(file, { + renderIt: true, }); }, diff --git a/app/assets/javascripts/emoji/support/unicode_support_map.js b/app/assets/javascripts/emoji/support/unicode_support_map.js index 8c1861c56db..651169391fe 100644 --- a/app/assets/javascripts/emoji/support/unicode_support_map.js +++ b/app/assets/javascripts/emoji/support/unicode_support_map.js @@ -86,7 +86,7 @@ function generateUnicodeSupportMap(testMap) { canvas.height = numTestEntries * fontSize; ctx.fillStyle = '#000000'; ctx.textBaseline = 'middle'; - ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`; + ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`; // Write each emoji to the canvas vertically let writeIndex = 0; testMapKeys.forEach(testKey => { diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index a0af2875ab5..a29de9ae899 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -10,6 +10,7 @@ const hideFlash = (flashEl, fadeTransition = true) => { flashEl.addEventListener('transitionend', () => { flashEl.remove(); + window.dispatchEvent(new Event('resize')); if (document.body.classList.contains('flash-shown')) document.body.classList.remove('flash-shown'); }, { once: true, diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 2c8305aa0cc..6a5ab35a16a 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -78,13 +78,13 @@ export default { </script> <template> - <article class="ide"> + <article class="ide position-relative d-flex flex-column align-items-stretch"> <error-message v-if="errorMessage" :message="errorMessage" /> <div - class="ide-view" + class="ide-view flex-grow d-flex" > <find-file v-show="fileFindVisible" diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index 5611b37be7c..00ae5ea2c15 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -61,7 +61,7 @@ export default { <slot name="header"></slot> </header> <div - class="ide-tree-body" + class="ide-tree-body h-100" > <repo-file v-for="file in currentTree.tree" diff --git a/app/assets/javascripts/ide/components/new_dropdown/button.vue b/app/assets/javascripts/ide/components/new_dropdown/button.vue index ff114e47741..aa5fce59dbf 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/button.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/button.vue @@ -1,7 +1,11 @@ <script> import Icon from '~/vue_shared/components/icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; export default { + directives: { + tooltip, + }, components: { Icon, }, @@ -26,6 +30,11 @@ export default { default: true, }, }, + computed: { + tooltipTitle() { + return this.showLabel ? '' : this.label; + }, + }, methods: { clicked() { this.$emit('click'); @@ -36,7 +45,9 @@ export default { <template> <button + v-tooltip :aria-label="label" + :title="tooltipTitle" type="button" class="btn-blank" @click.stop.prevent="clicked" diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue index 5b1743bb30e..e2be805ed22 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue @@ -24,12 +24,6 @@ export default { default: null, }, }, - mounted() { - this.$refs.fileUpload.addEventListener('change', this.openFile); - }, - beforeDestroy() { - this.$refs.fileUpload.removeEventListener('change', this.openFile); - }, methods: { createFile(target, file, isText) { const { name } = file; @@ -85,6 +79,8 @@ export default { ref="fileUpload" type="file" class="hidden" + multiple + @change="openFile" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue index fef36eae7b1..39a1bd1f61b 100644 --- a/app/assets/javascripts/ide/components/preview/clientside.vue +++ b/app/assets/javascripts/ide/components/preview/clientside.vue @@ -2,6 +2,7 @@ import { mapActions, mapGetters, mapState } from 'vuex'; import _ from 'underscore'; import { Manager } from 'smooshpack'; +import { listen } from 'codesandbox-api'; import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import Navigator from './navigator.vue'; import { packageJsonPath } from '../../constants'; @@ -16,6 +17,7 @@ export default { return { manager: {}, loading: false, + sandpackReady: false, }; }, computed: { @@ -81,6 +83,10 @@ export default { } this.manager = {}; + if (this.listener) { + this.listener(); + } + clearTimeout(this.timeout); this.timeout = null; }, @@ -96,17 +102,29 @@ export default { return this.loadFileContent(this.mainEntry) .then(() => this.$nextTick()) - .then(() => + .then(() => { this.initManager('#ide-preview', this.sandboxOpts, { fileResolver: { isFile: p => Promise.resolve(!!this.entries[createPathWithExt(p)]), readFile: p => this.loadFileContent(createPathWithExt(p)).then(content => content), }, - }), - ); + }); + + this.listener = listen(e => { + switch (e.type) { + case 'done': + this.sandpackReady = true; + break; + default: + break; + } + }); + }); }, update() { - if (this.timeout) return; + if (!this.sandpackReady) return; + + clearTimeout(this.timeout); this.timeout = setTimeout(() => { if (_.isEmpty(this.manager)) { @@ -116,10 +134,7 @@ export default { } this.manager.updatePreview(this.sandboxOpts); - - clearTimeout(this.timeout); - this.timeout = null; - }, 500); + }, 250); }, initManager(el, opts, resolver) { this.manager = new Manager(el, opts, resolver); diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index f9badb01535..f55aa843444 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -133,7 +133,6 @@ export default { .then(() => this.getRawFileData({ path: this.file.path, - baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '', }), ) .then(() => { diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index dbdf0be2809..110eda83bb4 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -95,16 +95,18 @@ export default { return this.file.changed || this.file.tempFile || this.file.staged; }, }, + watch: { + 'file.active': function fileActiveWatch(active) { + if (this.file.type === 'blob' && active) { + this.scrollIntoView(); + } + }, + }, mounted() { if (this.hasPathAtCurrentRoute()) { this.scrollIntoView(true); } }, - updated() { - if (this.file.type === 'blob' && this.file.active) { - this.scrollIntoView(); - } - }, methods: { ...mapActions(['toggleTreeOpen']), clickFile() { diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index c6d7d218e81..3f6101e58f4 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -3,7 +3,6 @@ import VueRouter from 'vue-router'; import { join as joinPath } from 'path'; import flash from '~/flash'; import store from './stores'; -import { activityBarViews } from './constants'; Vue.use(VueRouter); @@ -74,98 +73,23 @@ router.beforeEach((to, from, next) => { projectId: to.params.project, }) .then(() => { - const fullProjectId = `${to.params.namespace}/${to.params.project}`; - + const basePath = to.params[0] || ''; + const projectId = `${to.params.namespace}/${to.params.project}`; const branchId = to.params.branchid; + const mergeRequestId = to.params.mrid; if (branchId) { - const basePath = to.params[0] || ''; - - store.dispatch('setCurrentBranchId', branchId); - - store.dispatch('getBranchData', { - projectId: fullProjectId, + store.dispatch('openBranch', { + projectId, branchId, + basePath, + }); + } else if (mergeRequestId) { + store.dispatch('openMergeRequest', { + projectId, + mergeRequestId, + targetProjectId: to.query.target_project, }); - - store - .dispatch('getFiles', { - projectId: fullProjectId, - branchId, - }) - .then(() => { - if (basePath) { - const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; - const treeEntryKey = Object.keys(store.state.entries).find( - key => key === path && !store.state.entries[key].pending, - ); - const treeEntry = store.state.entries[treeEntryKey]; - - if (treeEntry) { - store.dispatch('handleTreeEntryAction', treeEntry); - } - } - }) - .catch(e => { - throw e; - }); - } else if (to.params.mrid) { - store - .dispatch('getMergeRequestData', { - projectId: fullProjectId, - targetProjectId: to.query.target_project, - mergeRequestId: to.params.mrid, - }) - .then(mr => { - store.dispatch('updateActivityBarView', activityBarViews.review); - - store.dispatch('getBranchData', { - projectId: fullProjectId, - branchId: mr.source_branch, - }); - - return store.dispatch('getFiles', { - projectId: fullProjectId, - branchId: mr.source_branch, - }); - }) - .then(() => - store.dispatch('getMergeRequestVersions', { - projectId: fullProjectId, - targetProjectId: to.query.target_project, - mergeRequestId: to.params.mrid, - }), - ) - .then(() => - store.dispatch('getMergeRequestChanges', { - projectId: fullProjectId, - targetProjectId: to.query.target_project, - mergeRequestId: to.params.mrid, - }), - ) - .then(mrChanges => { - mrChanges.changes.forEach((change, ind) => { - const changeTreeEntry = store.state.entries[change.new_path]; - - if (changeTreeEntry) { - store.dispatch('setFileMrChange', { - file: changeTreeEntry, - mrChange: change, - }); - - if (ind < 10) { - store.dispatch('getFileData', { - path: change.new_path, - makeFileActive: ind === 0, - }); - } - } - }); - }) - .catch(e => { - flash('Error while loading the merge request. Please try again.'); - throw e; - }); } }) .catch(e => { diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 9e3f5da4676..28b9d0df201 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -54,9 +54,6 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => { commit(types.SET_FILE_ACTIVE, { path, active: true }); dispatch('scrollToTab'); - - commit(types.SET_CURRENT_PROJECT, file.projectId); - commit(types.SET_CURRENT_BRANCH, file.branchId); }; export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => { @@ -95,7 +92,7 @@ export const setFileMrChange = ({ commit }, { file, mrChange }) => { commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange }); }; -export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => { +export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) => { const file = state.entries[path]; return new Promise((resolve, reject) => { service @@ -103,6 +100,9 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) = .then(raw => { if (!(file.tempFile && !file.prevPath)) commit(types.SET_FILE_RAW_DATA, { file, raw }); if (file.mrChange && file.mrChange.new_file === false) { + const baseSha = + (getters.currentMergeRequest && getters.currentMergeRequest.baseCommitSha) || ''; + service .getBaseRawFileData(file, baseSha) .then(baseRaw => { @@ -125,7 +125,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) = action: payload => dispatch('getRawFileData', payload).then(() => dispatch('setErrorMessage', null)), actionText: __('Please try again'), - actionPayload: { path, baseSha }, + actionPayload: { path }, }); reject(); }); diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js index 1887b77b00b..187f8c75d07 100644 --- a/app/assets/javascripts/ide/stores/actions/merge_request.js +++ b/app/assets/javascripts/ide/stores/actions/merge_request.js @@ -1,6 +1,8 @@ -import { __ } from '../../../locale'; +import flash from '~/flash'; +import { __ } from '~/locale'; import service from '../../services'; import * as types from '../mutation_types'; +import { activityBarViews } from '../../constants'; export const getMergeRequestData = ( { commit, dispatch, state }, @@ -104,3 +106,67 @@ export const getMergeRequestVersions = ( resolve(state.projects[projectId].mergeRequests[mergeRequestId].versions); } }); + +export const openMergeRequest = ( + { dispatch, state }, + { projectId, targetProjectId, mergeRequestId } = {}, +) => + dispatch('getMergeRequestData', { + projectId, + targetProjectId, + mergeRequestId, + }) + .then(mr => { + dispatch('setCurrentBranchId', mr.source_branch); + + dispatch('getBranchData', { + projectId, + branchId: mr.source_branch, + }); + + return dispatch('getFiles', { + projectId, + branchId: mr.source_branch, + }); + }) + .then(() => + dispatch('getMergeRequestVersions', { + projectId, + targetProjectId, + mergeRequestId, + }), + ) + .then(() => + dispatch('getMergeRequestChanges', { + projectId, + targetProjectId, + mergeRequestId, + }), + ) + .then(mrChanges => { + if (mrChanges.changes.length) { + dispatch('updateActivityBarView', activityBarViews.review); + } + + mrChanges.changes.forEach((change, ind) => { + const changeTreeEntry = state.entries[change.new_path]; + + if (changeTreeEntry) { + dispatch('setFileMrChange', { + file: changeTreeEntry, + mrChange: change, + }); + + if (ind < 10) { + dispatch('getFileData', { + path: change.new_path, + makeFileActive: ind === 0, + }); + } + } + }); + }) + .catch(e => { + flash(__('Error while loading the merge request. Please try again.')); + throw e; + }); diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 501e25d452b..543dc6c0461 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -124,3 +124,35 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => { actionPayload: branchId, }); }; + +export const openBranch = ( + { dispatch, state }, + { projectId, branchId, basePath }, +) => { + dispatch('setCurrentBranchId', branchId); + + dispatch('getBranchData', { + projectId, + branchId, + }); + + return ( + dispatch('getFiles', { + projectId, + branchId, + }) + .then(() => { + if (basePath) { + const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; + const treeEntryKey = Object.keys(state.entries).find( + key => key === path && !state.entries[key].pending, + ); + const treeEntry = state.entries[treeEntryKey]; + + if (treeEntry) { + dispatch('handleTreeEntryAction', treeEntry); + } + } + }) + ); +}; diff --git a/app/assets/javascripts/ide/stores/modules/branches/actions.js b/app/assets/javascripts/ide/stores/modules/branches/actions.js index 74aa98ef9f9..f90c2d77f2b 100644 --- a/app/assets/javascripts/ide/stores/modules/branches/actions.js +++ b/app/assets/javascripts/ide/stores/modules/branches/actions.js @@ -33,7 +33,4 @@ export const fetchBranches = ({ dispatch, rootGetters }, { search = '' }) => { export const resetBranches = ({ commit }) => commit(types.RESET_BRANCHES); -export const openBranch = ({ rootState, dispatch }, id) => - dispatch('goToRoute', `/project/${rootState.currentProjectId}/edit/${id}`, { root: true }); - export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js new file mode 100644 index 00000000000..43237a29466 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js @@ -0,0 +1,82 @@ +import Api from '~/api'; +import { __ } from '~/locale'; +import * as types from './mutation_types'; + +export const requestTemplateTypes = ({ commit }) => commit(types.REQUEST_TEMPLATE_TYPES); +export const receiveTemplateTypesError = ({ commit, dispatch }) => { + commit(types.RECEIVE_TEMPLATE_TYPES_ERROR); + dispatch( + 'setErrorMessage', + { + text: __('Error loading template types.'), + action: () => + dispatch('fetchTemplateTypes').then(() => + dispatch('setErrorMessage', null, { root: true }), + ), + actionText: __('Please try again'), + }, + { root: true }, + ); +}; +export const receiveTemplateTypesSuccess = ({ commit }, templates) => + commit(types.RECEIVE_TEMPLATE_TYPES_SUCCESS, templates); + +export const fetchTemplateTypes = ({ dispatch, state }) => { + if (!Object.keys(state.selectedTemplateType).length) return Promise.reject(); + + dispatch('requestTemplateTypes'); + + return Api.templates(state.selectedTemplateType.key) + .then(({ data }) => dispatch('receiveTemplateTypesSuccess', data)) + .catch(() => dispatch('receiveTemplateTypesError')); +}; + +export const setSelectedTemplateType = ({ commit }, type) => + commit(types.SET_SELECTED_TEMPLATE_TYPE, type); + +export const receiveTemplateError = ({ dispatch }, template) => { + dispatch( + 'setErrorMessage', + { + text: __('Error loading template.'), + action: payload => + dispatch('fetchTemplateTypes', payload).then(() => + dispatch('setErrorMessage', null, { root: true }), + ), + actionText: __('Please try again'), + actionPayload: template, + }, + { root: true }, + ); +}; + +export const fetchTemplate = ({ dispatch, state }, template) => { + if (template.content) { + return dispatch('setFileTemplate', template); + } + + return Api.templates(`${state.selectedTemplateType.key}/${template.key || template.name}`) + .then(({ data }) => { + dispatch('setFileTemplate', data); + }) + .catch(() => dispatch('receiveTemplateError', template)); +}; + +export const setFileTemplate = ({ dispatch, commit, rootGetters }, template) => { + dispatch( + 'changeFileContent', + { path: rootGetters.activeFile.path, content: template.content }, + { root: true }, + ); + commit(types.SET_UPDATE_SUCCESS, true); +}; + +export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => { + const file = rootGetters.activeFile; + + dispatch('changeFileContent', { path: file.path, content: file.raw }, { root: true }); + commit(types.SET_UPDATE_SUCCESS, false); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js new file mode 100644 index 00000000000..38318fd49bf --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js @@ -0,0 +1,23 @@ +export const templateTypes = () => [ + { + name: '.gitlab-ci.yml', + key: 'gitlab_ci_ymls', + }, + { + name: '.gitignore', + key: 'gitignores', + }, + { + name: 'LICENSE', + key: 'licenses', + }, + { + name: 'Dockerfile', + key: 'dockerfiles', + }, +]; + +export const showFileTemplatesBar = (_, getters) => name => + getters.templateTypes.find(t => t.name === name); + +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/index.js b/app/assets/javascripts/ide/stores/modules/file_templates/index.js new file mode 100644 index 00000000000..dfa5ef54413 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/file_templates/index.js @@ -0,0 +1,12 @@ +import createState from './state'; +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; + +export default { + namespaced: true, + actions, + state: createState(), + getters, + mutations, +}; diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/mutation_types.js b/app/assets/javascripts/ide/stores/modules/file_templates/mutation_types.js new file mode 100644 index 00000000000..cf4499c0264 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/file_templates/mutation_types.js @@ -0,0 +1,7 @@ +export const REQUEST_TEMPLATE_TYPES = 'REQUEST_TEMPLATE_TYPES'; +export const RECEIVE_TEMPLATE_TYPES_ERROR = 'RECEIVE_TEMPLATE_TYPES_ERROR'; +export const RECEIVE_TEMPLATE_TYPES_SUCCESS = 'RECEIVE_TEMPLATE_TYPES_SUCCESS'; + +export const SET_SELECTED_TEMPLATE_TYPE = 'SET_SELECTED_TEMPLATE_TYPE'; + +export const SET_UPDATE_SUCCESS = 'SET_UPDATE_SUCCESS'; diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js b/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js new file mode 100644 index 00000000000..e413e61eaaa --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js @@ -0,0 +1,21 @@ +/* eslint-disable no-param-reassign */ +import * as types from './mutation_types'; + +export default { + [types.REQUEST_TEMPLATE_TYPES](state) { + state.isLoading = true; + }, + [types.RECEIVE_TEMPLATE_TYPES_ERROR](state) { + state.isLoading = false; + }, + [types.RECEIVE_TEMPLATE_TYPES_SUCCESS](state, templates) { + state.isLoading = false; + state.templates = templates; + }, + [types.SET_SELECTED_TEMPLATE_TYPE](state, type) { + state.selectedTemplateType = type; + }, + [types.SET_UPDATE_SUCCESS](state, success) { + state.updateSuccess = success; + }, +}; diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/state.js b/app/assets/javascripts/ide/stores/modules/file_templates/state.js new file mode 100644 index 00000000000..bd4b7d7bc52 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/file_templates/state.js @@ -0,0 +1,6 @@ +export default () => ({ + isLoading: false, + templates: [], + selectedTemplateType: {}, + updateSuccess: false, +}); diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 1eda5768709..0347f803757 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -146,13 +146,7 @@ export default { staged: false, prevPath: '', moved: false, - lastCommit: Object.assign(state.entries[file.path].lastCommit, { - id: lastCommit.commit.id, - url: lastCommit.commit_path, - message: lastCommit.commit.message, - author: lastCommit.commit.author_name, - updatedAt: lastCommit.commit.authored_date, - }), + lastCommitSha: lastCommit.commit.id, }); if (prevPath) { @@ -200,6 +194,7 @@ export default { }, [types.DELETE_ENTRY](state, path) { const entry = state.entries[path]; + const { tempFile = false } = entry; const parent = entry.parentPath ? state.entries[entry.parentPath] : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; @@ -209,7 +204,11 @@ export default { parent.tree = parent.tree.filter(f => f.path !== entry.path); if (entry.type === 'blob') { - state.changedFiles = state.changedFiles.concat(entry); + if (tempFile) { + state.changedFiles = state.changedFiles.filter(f => f.path !== path); + } else { + state.changedFiles = state.changedFiles.concat(entry); + } } }, [types.RENAME_ENTRY](state, { path, name, entryPath = null }) { diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index 0035d809062..eda8cdad908 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -87,7 +87,7 @@ class ImporterStatus { details = error.response.data.errors; } - flash(__(`An error occurred while importing project: ${details}`)); + flash(sprintf(__('An error occurred while importing project: %{details}'), { details })); }); } diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue new file mode 100644 index 00000000000..525c5eec91a --- /dev/null +++ b/app/assets/javascripts/jobs/components/artifacts_block.vue @@ -0,0 +1,98 @@ +<script> + import TimeagoTooltiop from '~/vue_shared/components/time_ago_tooltip.vue'; + + export default { + components: { + TimeagoTooltiop, + }, + props: { + // @build.artifacts_expired? + haveArtifactsExpired: { + type: Boolean, + required: true, + }, + // @build.has_expiring_artifacts? + willArtifactsExpire: { + type: Boolean, + required: true, + }, + expireAt: { + type: String, + required: false, + default: null, + }, + keepArtifactsPath: { + type: String, + required: false, + default: null, + }, + downloadArtifactsPath: { + type: String, + required: false, + default: null, + }, + browseArtifactsPath: { + type: String, + required: false, + default: null, + }, + }, + }; +</script> +<template> + <div class="block"> + <div class="title"> + {{ s__('Job|Job artifacts') }} + </div> + + <p + v-if="haveArtifactsExpired" + class="js-artifacts-removed build-detail-row" + > + {{ s__('Job|The artifacts were removed') }} + </p> + <p + v-else-if="willArtifactsExpire" + class="js-artifacts-will-be-removed build-detail-row" + > + {{ s__('Job|The artifacts will be removed') }} + </p> + + <timeago-tooltiop + v-if="expireAt" + :time="expireAt" + /> + + <div + class="btn-group d-flex" + role="group" + > + <a + v-if="keepArtifactsPath" + :href="keepArtifactsPath" + class="js-keep-artifacts btn btn-sm btn-default" + data-method="post" + > + {{ s__('Job|Keep') }} + </a> + + <a + v-if="downloadArtifactsPath" + :href="downloadArtifactsPath" + class="js-download-artifacts btn btn-sm btn-default" + download + rel="nofollow" + > + {{ s__('Job|Download') }} + </a> + + <a + v-if="browseArtifactsPath" + :href="browseArtifactsPath" + class="js-browse-artifacts btn btn-sm btn-default" + > + {{ s__('Job|Browse') }} + </a> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue new file mode 100644 index 00000000000..7f485295513 --- /dev/null +++ b/app/assets/javascripts/jobs/components/commit_block.vue @@ -0,0 +1,64 @@ +<script> +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; + +export default { + components: { + ClipboardButton, + }, + props: { + pipelineShortSha: { + type: String, + required: true, + }, + pipelineShaPath: { + type: String, + required: true, + }, + mergeRequestReference: { + type: String, + required: false, + default: null, + }, + mergeRequestPath: { + type: String, + required: false, + default: null, + }, + gitCommitTitlte: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <div class="block"> + <p> + {{ __('Commit') }} + + <a + :href="pipelineShaPath" + class="js-commit-sha commit-sha link-commit" + > + {{ pipelineShortSha }} + </a> + + <clipboard-button + :text="pipelineShortSha" + :title="__('Copy commit SHA to clipboard')" + /> + + <a + v-if="mergeRequestPath && mergeRequestReference" + :href="mergeRequestPath" + class="js-link-commit link-commit" + > + {{ mergeRequestReference }} + </a> + </p> + + <p class="build-light-text append-bottom-0"> + {{ gitCommitTitlte }} + </p> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue new file mode 100644 index 00000000000..4faf08387fb --- /dev/null +++ b/app/assets/javascripts/jobs/components/empty_state.vue @@ -0,0 +1,76 @@ +<script> + export default { + props: { + illustrationPath: { + type: String, + required: true, + }, + illustrationSizeClass: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + }, + content: { + type: String, + required: false, + default: null, + }, + action: { + type: Object, + required: false, + default: null, + validator(value) { + return ( + value === null || + (Object.prototype.hasOwnProperty.call(value, 'link') && + Object.prototype.hasOwnProperty.call(value, 'method') && + Object.prototype.hasOwnProperty.call(value, 'title')) + ); + }, + }, + }, + }; +</script> +<template> + <div class="row empty-state"> + <div class="col-12"> + <div + :class="illustrationSizeClass" + class="svg-content" + > + <img :src="illustrationPath" /> + </div> + </div> + + <div class="col-12"> + <div class="text-content"> + <h4 class="js-job-empty-state-title text-center"> + {{ title }} + </h4> + + <p + v-if="content" + class="js-job-empty-state-content" + > + {{ content }} + </p> + + <div + v-if="action" + class="text-center" + > + <a + :href="action.link" + :data-method="action.method" + class="js-job-empty-state-action btn btn-primary" + > + {{ action.title }} + </a> + </div> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/environments_block.vue b/app/assets/javascripts/jobs/components/environments_block.vue new file mode 100644 index 00000000000..ca6386595c7 --- /dev/null +++ b/app/assets/javascripts/jobs/components/environments_block.vue @@ -0,0 +1,118 @@ +<script> + import _ from 'underscore'; + import CiIcon from '~/vue_shared/components/ci_icon.vue'; + import { sprintf, __ } from '../../locale'; + + export default { + components: { + CiIcon, + }, + props: { + deploymentStatus: { + type: Object, + required: true, + }, + }, + computed: { + environment() { + let environmentText; + switch (this.deploymentStatus.status) { + case 'latest': + environmentText = sprintf( + __('This job is the most recent deployment to %{link}.'), + { link: this.environmentLink }, + false, + ); + break; + case 'out_of_date': + if (this.hasLastDeployment) { + environmentText = sprintf( + __( + 'This job is an out-of-date deployment to %{environmentLink}. View the most recent deployment %{deploymentLink}.', + ), + { + environmentLink: this.environmentLink, + deploymentLink: this.deploymentLink, + }, + false, + ); + } else { + environmentText = sprintf( + __('This job is an out-of-date deployment to %{environmentLink}.'), + { environmentLink: this.environmentLink }, + false, + ); + } + + break; + case 'failed': + environmentText = sprintf( + __('The deployment of this job to %{environmentLink} did not succeed.'), + { environmentLink: this.environmentLink }, + false, + ); + break; + case 'creating': + if (this.hasLastDeployment) { + environmentText = sprintf( + __( + 'This job is creating a deployment to %{environmentLink} and will overwrite the last %{deploymentLink}.', + ), + { + environmentLink: this.environmentLink, + deploymentLink: this.deploymentLink, + }, + false, + ); + } else { + environmentText = sprintf( + __('This job is creating a deployment to %{environmentLink}.'), + { environmentLink: this.environmentLink }, + false, + ); + } + break; + default: + break; + } + return environmentText; + }, + environmentLink() { + return sprintf( + '%{startLink}%{name}%{endLink}', + { + startLink: `<a href="${this.deploymentStatus.environment.path}">`, + name: _.escape(this.deploymentStatus.environment.name), + endLink: '</a>', + }, + false, + ); + }, + deploymentLink() { + return sprintf( + '%{startLink}%{name}%{endLink}', + { + startLink: `<a href="${this.lastDeployment.path}">`, + name: _.escape(this.lastDeployment.name), + endLink: '</a>', + }, + false, + ); + }, + hasLastDeployment() { + return this.deploymentStatus.environment.last_deployment; + }, + lastDeployment() { + return this.deploymentStatus.environment.last_deployment; + }, + }, + }; +</script> +<template> + <div class="prepend-top-default js-environment-container"> + <div class="environment-information"> + <ci-icon :status="deploymentStatus.icon" /> + <p v-html="environment"></p> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/erased_block.vue b/app/assets/javascripts/jobs/components/erased_block.vue new file mode 100644 index 00000000000..d688eebfa95 --- /dev/null +++ b/app/assets/javascripts/jobs/components/erased_block.vue @@ -0,0 +1,48 @@ +<script> +import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +export default { + components: { + TimeagoTooltip, + }, + props: { + erasedByUser: { + type: Boolean, + required: true, + }, + username: { + type: String, + required: false, + default: null, + }, + linkToUser: { + type: String, + required: false, + default: null, + }, + erasedAt: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <div class="prepend-top-default js-build-erased"> + <div class="erased alert alert-warning"> + <template v-if="erasedByUser"> + {{ s__("Job|Job has been erased by") }} + <a :href="linkToUser"> + {{ username }} + </a> + </template> + <template v-else> + {{ s__("Job|Job has been erased") }} + </template> + + <timeago-tooltip + :time="erasedAt" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/job_log.vue b/app/assets/javascripts/jobs/components/job_log.vue new file mode 100644 index 00000000000..3c4749d996b --- /dev/null +++ b/app/assets/javascripts/jobs/components/job_log.vue @@ -0,0 +1,33 @@ +<script> + export default { + name: 'JobLog', + props: { + trace: { + type: String, + required: true, + }, + isReceivingBuildTrace: { + type: Boolean, + required: true, + }, + }, + }; +</script> +<template> + <pre class="build-trace"> + <code + class="bash" + v-html="trace" + > + </code> + + <div + v-if="isReceivingBuildTrace" + class="js-log-animation build-loader-animation" + > + <div class="dot"></div> + <div class="dot"></div> + <div class="dot"></div> + </div> + </pre> +</template> diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue new file mode 100644 index 00000000000..513851e376f --- /dev/null +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -0,0 +1,139 @@ +<script> + import Icon from '~/vue_shared/components/icon.vue'; + import tooltip from '~/vue_shared/directives/tooltip'; + import { numberToHumanSize } from '~/lib/utils/number_utils'; + import { s__, sprintf } from '~/locale'; + + export default { + components: { + Icon, + }, + directives: { + tooltip, + }, + props: { + canEraseJob: { + type: Boolean, + required: true, + }, + size: { + type: Number, + required: true, + }, + rawTracePath: { + type: String, + required: false, + default: null, + }, + canScrollToTop: { + type: Boolean, + required: true, + }, + canScrollToBottom: { + type: Boolean, + required: true, + }, + }, + computed: { + jobLogSize() { + return sprintf('Showing last %{startSpanTag} %{size} %{endSpanTag} of log -', { + startSpanTag: '<span class="s-truncated-info-size truncated-info-size">', + endSpanTag: '</span>', + size: numberToHumanSize(this.size), + }); + }, + }, + methods: { + handleEraseJobClick() { + // eslint-disable-next-line no-alert + if (window.confirm(s__('Job|Are you sure you want to erase this job?'))) { + this.$emit('eraseJob'); + } + }, + handleScrollToTop() { + this.$emit('scrollJobLogTop'); + }, + handleScrollToBottom() { + this.$emit('scrollJobLogBottom'); + }, + }, + }; +</script> +<template> + <div class="top-bar"> + <!-- truncate information --> + <div class="js-truncated-info truncated-info d-none d-sm-block float-left"> + <p v-html="jobLogSize"></p> + + <a + v-if="rawTracePath" + :href="rawTracePath" + class="js-raw-link raw-link" + > + {{ s__("Job|Complete Raw") }} + </a> + </div> + <!-- eo truncate information --> + + <div class="controllers float-right"> + <!-- links --> + <a + v-tooltip + v-if="rawTracePath" + :title="s__('Job|Show complete raw')" + :href="rawTracePath" + class="js-raw-link-controller controllers-buttons" + data-container="body" + > + <icon name="doc-text" /> + </a> + + <button + v-tooltip + v-if="canEraseJob" + :title="s__('Job|Erase job log')" + type="button" + class="js-erase-link controllers-buttons" + data-container="body" + @click="handleEraseJobClick" + > + <icon name="remove" /> + </button> + <!-- eo links --> + + <!-- scroll buttons --> + <div + v-tooltip + :title="s__('Job|Scroll to top')" + class="controllers-buttons" + data-container="body" + > + <button + :disabled="!canScrollToTop" + type="button" + class="js-scroll-top btn-scroll btn-transparent btn-blank" + @click="handleScrollToTop" + > + <icon name="scroll_up"/> + </button> + </div> + + <div + v-tooltip + :title="s__('Job|Scroll to bottom')" + class="controllers-buttons" + data-container="body" + > + <button + :disabled="!canScrollToBottom" + type="button" + class="js-scroll-bottom btn-scroll btn-transparent btn-blank" + @click="handleScrollToBottom" + > + <icon name="scroll_down"/> + </button> + </div> + <!-- eo scroll buttons --> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/jobs_container.vue b/app/assets/javascripts/jobs/components/jobs_container.vue new file mode 100644 index 00000000000..b81109bdd06 --- /dev/null +++ b/app/assets/javascripts/jobs/components/jobs_container.vue @@ -0,0 +1,60 @@ +<script> + import CiIcon from '~/vue_shared/components/ci_icon.vue'; + import Icon from '~/vue_shared/components/icon.vue'; + import tooltip from '~/vue_shared/directives/tooltip'; + + export default { + components: { + CiIcon, + Icon, + }, + directives: { + tooltip, + }, + props: { + jobs: { + type: Array, + required: true, + }, + }, + }; +</script> +<template> + <div class="builds-container"> + <div + class="build-job" + > + <a + v-tooltip + v-for="job in jobs" + :key="job.id" + :href="job.path" + :title="job.tooltip" + :class="{ active: job.active, retried: job.retried }" + > + <icon + v-if="job.active" + name="arrow-right" + class="js-arrow-right" + /> + + <ci-icon :status="job.status" /> + + <span> + <template v-if="job.name"> + {{ job.name }} + </template> + <template v-else> + {{ job.id }} + </template> + </span> + + <icon + v-if="job.retried" + name="retry" + class="js-retry-icon" + /> + </a> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue index d2adf628050..36d4a3e2bc9 100644 --- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue +++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue @@ -1,14 +1,16 @@ <script> -import detailRow from './sidebar_detail_row.vue'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import timeagoMixin from '../../vue_shared/mixins/timeago'; -import { timeIntervalInWords } from '../../lib/utils/datetime_utility'; +import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; +import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; +import Icon from '~/vue_shared/components/icon.vue'; +import DetailRow from './sidebar_detail_row.vue'; export default { name: 'SidebarDetailsBlock', components: { - detailRow, - loadingIcon, + DetailRow, + LoadingIcon, + Icon, }, mixins: [timeagoMixin], props: { @@ -20,16 +22,16 @@ export default { type: Boolean, required: true, }, - canUserRetry: { - type: Boolean, - required: false, - default: false, - }, runnerHelpUrl: { type: String, required: false, default: '', }, + terminalPath: { + type: String, + required: false, + default: null, + }, }, computed: { shouldRenderContent() { @@ -92,7 +94,7 @@ export default { {{ job.name }} </strong> <a - v-if="canUserRetry" + v-if="job.retry_path" :class="retryButtonClass" :href="job.retry_path" data-method="post" @@ -100,6 +102,16 @@ export default { > {{ __('Retry') }} </a> + <a + v-if="terminalPath" + :href="terminalPath" + class="js-terminal-link pull-right btn btn-primary + btn-inverted visible-md-block visible-lg-block" + target="_blank" + > + {{ __('Debug') }} + <icon name="external-link" /> + </a> <button :aria-label="__('Toggle Sidebar')" type="button" @@ -125,7 +137,7 @@ export default { {{ __('New issue') }} </a> <a - v-if="canUserRetry" + v-if="job.retry_path" :href="job.retry_path" class="js-retry-job btn btn-inverted-secondary" data-method="post" diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue new file mode 100644 index 00000000000..d6d64fa32f7 --- /dev/null +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -0,0 +1,97 @@ +<script> + import CiIcon from '~/vue_shared/components/ci_icon.vue'; + import Icon from '~/vue_shared/components/icon.vue'; + + import { sprintf, __ } from '~/locale'; + + export default { + components: { + CiIcon, + Icon, + }, + props: { + pipelineId: { + type: Number, + required: true, + }, + pipelinePath: { + type: String, + required: true, + }, + pipelineRef: { + type: String, + required: true, + }, + pipelineRefPath: { + type: String, + required: true, + }, + stages: { + type: Array, + required: true, + }, + pipelineStatus: { + type: Object, + required: true, + }, + }, + data() { + return { + selectedStage: this.stages.length > 0 ? this.stages[0].name : __('More'), + }; + }, + computed: { + pipelineLink() { + return sprintf(__('Pipeline %{pipelineLinkStart} #%{pipelineId} %{pipelineLinkEnd} from %{pipelineLinkRefStart} %{pipelineRef} %{pipelineLinkRefEnd}'), { + pipelineLinkStart: `<a href=${this.pipelinePath} class="js-pipeline-path link-commit">`, + pipelineId: this.pipelineId, + pipelineLinkEnd: '</a>', + pipelineLinkRefStart: `<a href=${this.pipelineRefPath} class="link-commit ref-name">`, + pipelineRef: this.pipelineRef, + pipelineLinkRefEnd: '</a>', + }, false); + }, + }, + methods: { + onStageClick(stage) { + // todo: consider moving into store + this.selectedStage = stage.name; + + // update dropdown with jobs + // jobs container is a new component. + this.$emit('requestSidebarStageDropdown', stage); + }, + }, + }; +</script> +<template> + <div class="block-last"> + <ci-icon :status="pipelineStatus" /> + + <p v-html="pipelineLink"></p> + + <div class="dropdown"> + <button + type="button" + data-toggle="dropdown" + > + {{ selectedStage }} + <icon name="chevron-down" /> + </button> + <ul class="dropdown-menu"> + <li + v-for="(stage, index) in stages" + :key="index" + > + <button + type="button" + class="stage-item" + @click="onStageClick(stage)" + > + {{ stage.name }} + </button> + </li> + </ul> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue new file mode 100644 index 00000000000..18883fea950 --- /dev/null +++ b/app/assets/javascripts/jobs/components/stuck_block.vue @@ -0,0 +1,63 @@ +<script> +/** + * Renders Stuck Runners block for job's view. + */ +export default { + props: { + hasNoRunnersForProject: { + type: Boolean, + required: true, + }, + tags: { + type: Array, + required: false, + default: () => [], + }, + runnersPath: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <div class="bs-callout bs-callout-warning"> + <p + v-if="hasNoRunnersForProject" + class="js-stuck-no-runners" + > + {{ s__(`Job|This job is stuck, because the project + doesn't have any runners online assigned to it.`) }} + </p> + <p + v-else-if="tags.length" + class="js-stuck-with-tags" + > + {{ s__(`This job is stuck, because you don't have + any active runners online with any of these tags assigned to them:`) }} + <span + v-for="(tag, index) in tags" + :key="index" + class="badge badge-primary" + > + {{ tag }} + </span> + </p> + <p + v-else + class="js-stuck-no-active-runner" + > + {{ s__(`This job is stuck, because you don't + have any active runners that can run this job.`) }} + </p> + + {{ __("Go to") }} + <a + v-if="runnersPath" + :href="runnersPath" + class="js-runners-path" + > + {{ __("Runners page") }} + </a> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue new file mode 100644 index 00000000000..8a88e5da6aa --- /dev/null +++ b/app/assets/javascripts/jobs/components/trigger_block.vue @@ -0,0 +1,84 @@ +<script> + export default { + props: { + shortToken: { + type: String, + required: false, + default: null, + }, + + variables: { + type: Object, + required: false, + default: () => ({}), + }, + }, + data() { + return { + areVariablesVisible: false, + }; + }, + computed: { + hasVariables() { + return Object.keys(this.variables).length > 0; + }, + }, + methods: { + revealVariables() { + this.areVariablesVisible = true; + }, + }, + }; +</script> + +<template> + <div class="build-widget block"> + <h4 class="title"> + {{ __('Trigger') }} + </h4> + + <p + v-if="shortToken" + class="js-short-token" + > + <span class="build-light-text"> + {{ __('Token') }} + </span> + {{ shortToken }} + </p> + + <p v-if="hasVariables"> + <button + type="button" + class="btn btn-default group js-reveal-variables" + @click="revealVariables" + > + {{ __('Reveal Variables') }} + </button> + + </p> + + <dl + v-if="areVariablesVisible" + class="js-build-variables trigger-build-variables" + > + <template + v-for="(value, key) in variables" + > + <dt + :key="`${key}-variable`" + class="js-build-variable trigger-build-variable" + > + {{ key }} + </dt> + + <dd + :key="`${key}-value`" + class="js-build-value trigger-build-value" + > + {{ value }} + </dd> + </template> + </dl> + </div> +</template> diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js index 0db7b95636c..a84324f14b2 100644 --- a/app/assets/javascripts/jobs/job_details_bundle.js +++ b/app/assets/javascripts/jobs/job_details_bundle.js @@ -52,9 +52,9 @@ export default () => { return createElement('details-block', { props: { isLoading: this.mediator.state.isLoading, - canUserRetry: !!('canUserRetry' in detailsBlockDataset), job: this.mediator.store.state.job, runnerHelpUrl: dataset.runnerHelpUrl, + terminalPath: detailsBlockDataset.terminalPath, }, }); }, diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js new file mode 100644 index 00000000000..7f5406d6f43 --- /dev/null +++ b/app/assets/javascripts/jobs/store/actions.js @@ -0,0 +1,175 @@ +import Visibility from 'visibilityjs'; +import * as types from './mutation_types'; +import axios from '../../lib/utils/axios_utils'; +import Poll from '../../lib/utils/poll'; +import { setCiStatusFavicon } from '../../lib/utils/common_utils'; +import flash from '../../flash'; +import { __ } from '../../locale'; + +export const setJobEndpoint = ({ commit }, endpoint) => commit(types.SET_JOB_ENDPOINT, endpoint); +export const setTraceEndpoint = ({ commit }, endpoint) => + commit(types.SET_TRACE_ENDPOINT, endpoint); +export const setStagesEndpoint = ({ commit }, endpoint) => + commit(types.SET_STAGES_ENDPOINT, endpoint); +export const setJobsEndpoint = ({ commit }, endpoint) => commit(types.SET_JOBS_ENDPOINT, endpoint); + +let eTagPoll; + +export const clearEtagPoll = () => { + eTagPoll = null; +}; + +export const stopPolling = () => { + if (eTagPoll) eTagPoll.stop(); +}; + +export const restartPolling = () => { + if (eTagPoll) eTagPoll.restart(); +}; + +export const requestJob = ({ commit }) => commit(types.REQUEST_JOB); + +export const fetchJob = ({ state, dispatch }) => { + dispatch('requestJob'); + + eTagPoll = new Poll({ + resource: { + getJob(endpoint) { + return axios.get(endpoint); + }, + }, + data: state.jobEndpoint, + method: 'getJob', + successCallback: ({ data }) => dispatch('receiveJobSuccess', data), + errorCallback: () => dispatch('receiveJobError'), + }); + + if (!Visibility.hidden()) { + eTagPoll.makeRequest(); + } else { + axios + .get(state.jobEndpoint) + .then(({ data }) => dispatch('receiveJobSuccess', data)) + .catch(() => dispatch('receiveJobError')); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + dispatch('restartPolling'); + } else { + dispatch('stopPolling'); + } + }); +}; + +export const receiveJobSuccess = ({ commit }, data) => commit(types.RECEIVE_JOB_SUCCESS, data); +export const receiveJobError = ({ commit }) => { + commit(types.RECEIVE_JOB_ERROR); + flash(__('An error occurred while fetching the job.')); +}; + +/** + * Job's Trace + */ +export const scrollTop = ({ commit }) => { + commit(types.SCROLL_TO_TOP); + window.scrollTo({ top: 0 }); +}; + +export const scrollBottom = ({ commit }) => { + commit(types.SCROLL_TO_BOTTOM); + window.scrollTo({ top: document.height }); +}; + +export const requestTrace = ({ commit }) => commit(types.REQUEST_TRACE); + +let traceTimeout; +export const fetchTrace = ({ dispatch, state }) => { + dispatch('requestTrace'); + + axios + .get(`${state.traceEndpoint}/trace.json`, { + params: { state: state.traceState }, + }) + .then(({ data }) => { + if (!state.fetchingStatusFavicon) { + dispatch('fetchFavicon'); + } + dispatch('receiveTraceSuccess', data); + + if (!data.complete) { + traceTimeout = setTimeout(() => { + dispatch('fetchTrace'); + }, 4000); + } else { + dispatch('stopPollingTrace'); + } + }) + .catch(() => dispatch('receiveTraceError')); +}; +export const stopPollingTrace = ({ commit }) => { + commit(types.STOP_POLLING_TRACE); + clearTimeout(traceTimeout); +}; +export const receiveTraceSuccess = ({ commit }, log) => commit(types.RECEIVE_TRACE_SUCCESS, log); +export const receiveTraceError = ({ commit }) => { + commit(types.RECEIVE_TRACE_ERROR); + clearTimeout(traceTimeout); + flash(__('An error occurred while fetching the job log.')); +}; + +export const fetchFavicon = ({ state, dispatch }) => { + dispatch('requestStatusFavicon'); + setCiStatusFavicon(`${state.pagePath}/status.json`) + .then(() => dispatch('receiveStatusFaviconSuccess')) + .catch(() => dispatch('requestStatusFaviconError')); +}; +export const requestStatusFavicon = ({ commit }) => commit(types.REQUEST_STATUS_FAVICON); +export const receiveStatusFaviconSuccess = ({ commit }) => + commit(types.RECEIVE_STATUS_FAVICON_SUCCESS); +export const requestStatusFaviconError = ({ commit }) => commit(types.RECEIVE_STATUS_FAVICON_ERROR); + +/** + * Stages dropdown on sidebar + */ +export const requestStages = ({ commit }) => commit(types.REQUEST_STAGES); +export const fetchStages = ({ state, dispatch }) => { + dispatch('requestStages'); + + axios + .get(state.stagesEndpoint) + .then(({ data }) => dispatch('receiveStagesSuccess', data)) + .catch(() => dispatch('receiveStagesError')); +}; +export const receiveStagesSuccess = ({ commit }, data) => + commit(types.RECEIVE_STAGES_SUCCESS, data); +export const receiveStagesError = ({ commit }) => { + commit(types.RECEIVE_STAGES_ERROR); + flash(__('An error occurred while fetching stages.')); +}; + +/** + * Jobs list on sidebar - depend on stages dropdown + */ +export const requestJobsForStage = ({ commit }) => commit(types.REQUEST_JOBS_FOR_STAGE); +export const setSelectedStage = ({ commit }, stage) => commit(types.SET_SELECTED_STAGE, stage); + +// On stage click, set selected stage + fetch job +export const fetchJobsForStage = ({ state, dispatch }, stage) => { + dispatch('setSelectedStage', stage); + dispatch('requestJobsForStage'); + + axios + .get(state.stageJobsEndpoint) + .then(({ data }) => dispatch('receiveJobsForStageSuccess', data)) + .catch(() => dispatch('receiveJobsForStageError')); +}; +export const receiveJobsForStageSuccess = ({ commit }, data) => + commit(types.RECEIVE_JOBS_FOR_STAGE_SUCCESS, data); +export const receiveJobsForStageError = ({ commit }) => { + commit(types.RECEIVE_JOBS_FOR_STAGE_ERROR); + flash(__('An error occurred while fetching the jobs.')); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/jobs/store/index.js b/app/assets/javascripts/jobs/store/index.js new file mode 100644 index 00000000000..d8f6f56ce61 --- /dev/null +++ b/app/assets/javascripts/jobs/store/index.js @@ -0,0 +1,13 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import state from './state'; +import * as actions from './actions'; +import mutations from './mutations'; + +Vue.use(Vuex); + +export default () => new Vuex.Store({ + actions, + mutations, + state: state(), +}); diff --git a/app/assets/javascripts/jobs/store/mutation_types.js b/app/assets/javascripts/jobs/store/mutation_types.js new file mode 100644 index 00000000000..e66e1d4f116 --- /dev/null +++ b/app/assets/javascripts/jobs/store/mutation_types.js @@ -0,0 +1,29 @@ +export const SET_JOB_ENDPOINT = 'SET_JOB_ENDPOINT'; +export const SET_TRACE_ENDPOINT = 'SET_TRACE_ENDPOINT'; +export const SET_STAGES_ENDPOINT = 'SET_STAGES_ENDPOINT'; +export const SET_JOBS_ENDPOINT = 'SET_JOBS_ENDPOINT'; + +export const SCROLL_TO_TOP = 'SCROLL_TO_TOP'; +export const SCROLL_TO_BOTTOM = 'SCROLL_TO_BOTTOM'; + +export const REQUEST_JOB = 'REQUEST_JOB'; +export const RECEIVE_JOB_SUCCESS = 'RECEIVE_JOB_SUCCESS'; +export const RECEIVE_JOB_ERROR = 'RECEIVE_JOB_ERROR'; + +export const REQUEST_TRACE = 'REQUEST_TRACE'; +export const STOP_POLLING_TRACE = 'STOP_POLLING_TRACE'; +export const RECEIVE_TRACE_SUCCESS = 'RECEIVE_TRACE_SUCCESS'; +export const RECEIVE_TRACE_ERROR = 'RECEIVE_TRACE_ERROR'; + +export const REQUEST_STATUS_FAVICON = 'REQUEST_STATUS_FAVICON'; +export const RECEIVE_STATUS_FAVICON_SUCCESS = 'RECEIVE_STATUS_FAVICON_SUCCESS'; +export const RECEIVE_STATUS_FAVICON_ERROR = 'RECEIVE_STATUS_FAVICON_ERROR'; + +export const REQUEST_STAGES = 'REQUEST_STAGES'; +export const RECEIVE_STAGES_SUCCESS = 'RECEIVE_STAGES_SUCCESS'; +export const RECEIVE_STAGES_ERROR = 'RECEIVE_STAGES_ERROR'; + +export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE'; +export const REQUEST_JOBS_FOR_STAGE = 'REQUEST_JOBS_FOR_STAGE'; +export const RECEIVE_JOBS_FOR_STAGE_SUCCESS = 'RECEIVE_JOBS_FOR_STAGE_SUCCESS'; +export const RECEIVE_JOBS_FOR_STAGE_ERROR = 'RECEIVE_JOBS_FOR_STAGE_ERROR'; diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js new file mode 100644 index 00000000000..2a451ef0cd1 --- /dev/null +++ b/app/assets/javascripts/jobs/store/mutations.js @@ -0,0 +1,94 @@ +/* eslint-disable no-param-reassign */ + +import * as types from './mutation_types'; + +export default { + [types.REQUEST_STATUS_FAVICON](state) { + state.fetchingStatusFavicon = true; + }, + [types.RECEIVE_STATUS_FAVICON_SUCCESS](state) { + state.fetchingStatusFavicon = false; + }, + [types.RECEIVE_STATUS_FAVICON_ERROR](state) { + state.fetchingStatusFavicon = false; + }, + + [types.RECEIVE_TRACE_SUCCESS](state, log) { + if (log.state) { + state.traceState = log.state; + } + + if (log.append) { + state.trace += log.html; + state.traceSize += log.size; + } else { + state.trace = log.html; + state.traceSize = log.size; + } + + if (state.traceSize < log.total) { + state.isTraceSizeVisible = true; + } else { + state.isTraceSizeVisible = false; + } + + state.isTraceComplete = log.complete; + state.hasTraceError = false; + }, + [types.STOP_POLLING_TRACE](state) { + state.isTraceComplete = true; + }, + // todo_fl: check this. + [types.RECEIVE_TRACE_ERROR](state) { + state.isLoadingTrace = false; + state.isTraceComplete = true; + state.hasTraceError = true; + }, + + [types.REQUEST_JOB](state) { + state.isLoading = true; + }, + [types.RECEIVE_JOB_SUCCESS](state, job) { + state.isLoading = false; + state.hasError = false; + state.job = job; + }, + [types.RECEIVE_JOB_ERROR](state) { + state.isLoading = false; + state.hasError = true; + state.job = {}; + }, + + [types.SCROLL_TO_TOP](state) { + state.isTraceScrolledToBottom = false; + state.hasBeenScrolled = true; + }, + [types.SCROLL_TO_BOTTOM](state) { + state.isTraceScrolledToBottom = true; + state.hasBeenScrolled = true; + }, + + [types.REQUEST_STAGES](state) { + state.isLoadingStages = true; + }, + [types.RECEIVE_STAGES_SUCCESS](state, stages) { + state.isLoadingStages = false; + state.stages = stages; + }, + [types.RECEIVE_STAGES_ERROR](state) { + state.isLoadingStages = false; + state.stages = []; + }, + + [types.REQUEST_JOBS_FOR_STAGE](state) { + state.isLoadingJobs = true; + }, + [types.RECEIVE_JOBS_FOR_STAGE_SUCCESS](state, jobs) { + state.isLoadingJobs = false; + state.jobs = jobs; + }, + [types.RECEIVE_JOBS_FOR_STAGE_ERROR](state) { + state.isLoadingJobs = false; + state.jobs = []; + }, +}; diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js new file mode 100644 index 00000000000..509cb69a5d3 --- /dev/null +++ b/app/assets/javascripts/jobs/store/state.js @@ -0,0 +1,40 @@ +export default () => ({ + jobEndpoint: null, + traceEndpoint: null, + + // dropdown options + stagesEndpoint: null, + // list of jobs on sidebard + stageJobsEndpoint: null, + + // job log + isLoading: false, + hasError: false, + job: {}, + + // trace + isLoadingTrace: false, + hasTraceError: false, + + trace: '', + + isTraceScrolledToBottom: false, + hasBeenScrolled: false, + + isTraceComplete: false, + traceSize: 0, // todo_fl: needs to be converted into human readable format in components + isTraceSizeVisible: false, + + fetchingStatusFavicon: false, + // used as a query parameter + traceState: null, + // used to check if we need to redirect the user - todo_fl: check if actually needed + traceStatus: null, + + // sidebar dropdown + isLoadingStages: false, + isLoadingJobs: false, + selectedStage: null, + stages: [], + jobs: [], +}); diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index cb851ff6745..6499b919787 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -4,7 +4,7 @@ import $ from 'jquery'; import _ from 'underscore'; -import { __ } from './locale'; +import { sprintf, __ } from './locale'; import axios from './lib/utils/axios_utils'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import DropdownUtils from './filtered_search/dropdown_utils'; @@ -39,7 +39,7 @@ export default class LabelsSelect { showNo = $dropdown.data('showNo'); showAny = $dropdown.data('showAny'); showMenuAbove = $dropdown.data('showMenuAbove'); - defaultLabel = $dropdown.data('defaultLabel') || 'Label'; + defaultLabel = $dropdown.data('defaultLabel') || __('Label'); abilityName = $dropdown.data('abilityName'); $selectbox = $dropdown.closest('.selectbox'); $block = $selectbox.closest('.block'); @@ -267,7 +267,10 @@ export default class LabelsSelect { return selectedLabels; } else if (selectedLabels.length) { - return selectedLabels[0] + " +" + (selectedLabels.length - 1) + " more"; + return sprintf(__('%{firstLabel} +%{labelCount} more'), { + firstLabel: selectedLabels[0], + labelCount: selectedLabels.length - 1 + }); } else { return defaultLabel; diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 2f3dd6f6cbc..3e208764b3e 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -491,7 +491,10 @@ export const setCiStatusFavicon = pageUrl => } return resetFavicon(); }) - .catch(resetFavicon); + .catch((error) => { + resetFavicon(); + throw error; + }); export const spriteIcon = (icon, className = '') => { const classAttribute = className.length > 0 ? `class="${className}"` : ''; diff --git a/app/assets/javascripts/locale/ensure_single_line.js b/app/assets/javascripts/locale/ensure_single_line.js new file mode 100644 index 00000000000..47c52fe6c50 --- /dev/null +++ b/app/assets/javascripts/locale/ensure_single_line.js @@ -0,0 +1,25 @@ +/* eslint-disable import/no-commonjs */ + +const SPLIT_REGEX = /\s*[\r\n]+\s*/; + +/** + * + * strips newlines from strings and replaces them with a single space + * + * @example + * + * ensureSingleLine('foo \n bar') === 'foo bar' + * + * @param {String} str + * @returns {String} + */ +module.exports = function ensureSingleLine(str) { + // This guard makes the function significantly faster + if (str.includes('\n') || str.includes('\r')) { + return str + .split(SPLIT_REGEX) + .filter(s => s !== '') + .join(' '); + } + return str; +}; diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js index 2cc5fb10027..1ae3362c4bc 100644 --- a/app/assets/javascripts/locale/index.js +++ b/app/assets/javascripts/locale/index.js @@ -1,4 +1,5 @@ import Jed from 'jed'; +import ensureSingleLine from './ensure_single_line'; import sprintf from './sprintf'; const languageCode = () => document.querySelector('html').getAttribute('lang') || 'en'; @@ -10,7 +11,7 @@ delete window.translations; @param text The text to be translated @returns {String} The translated text */ -const gettext = locale.gettext.bind(locale); +const gettext = text => locale.gettext.bind(locale)(ensureSingleLine(text)); /** Translate the text with a number @@ -23,7 +24,10 @@ const gettext = locale.gettext.bind(locale); @returns {String} Translated text with the number replaced (eg. '2 days') */ const ngettext = (text, pluralText, count) => { - const translated = locale.ngettext(text, pluralText, count).replace(/%d/g, count).split('|'); + const translated = locale + .ngettext(ensureSingleLine(text), ensureSingleLine(pluralText), count) + .replace(/%d/g, count) + .split('|'); return translated[translated.length - 1]; }; @@ -40,7 +44,7 @@ const ngettext = (text, pluralText, count) => { @returns {String} Translated context based text */ const pgettext = (keyOrContext, key) => { - const normalizedKey = key ? `${keyOrContext}|${key}` : keyOrContext; + const normalizedKey = ensureSingleLine(key ? `${keyOrContext}|${key}` : keyOrContext); const translated = gettext(normalizedKey).split('|'); return translated[translated.length - 1]; @@ -52,8 +56,7 @@ const pgettext = (keyOrContext, key) => { @param formatOptions for available options, please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat @returns {Intl.DateTimeFormat} */ -const createDateTimeFormat = - formatOptions => Intl.DateTimeFormat(languageCode(), formatOptions); +const createDateTimeFormat = formatOptions => Intl.DateTimeFormat(languageCode(), formatOptions); export { languageCode }; export { gettext as __ }; diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 6afaefc56f8..ae96ac3b80c 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -172,7 +172,7 @@ export default { <template> <div v-if="!showEmptyState" - class="prometheus-graphs prepend-top-10" + class="prometheus-graphs prepend-top-default" > <div class="environments d-flex align-items-center"> {{ s__('Metrics|Environment') }} diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index 225d9f18612..e111d3b9ac2 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -82,29 +82,17 @@ export default { getAwardHTML(name) { return glEmojiTag(name); }, - getAwardClassBindings(awardList, awardName) { + getAwardClassBindings(awardList) { return { active: this.hasReactionByCurrentUser(awardList), - disabled: !this.canInteractWithEmoji(awardList, awardName), + disabled: !this.canInteractWithEmoji(), }; }, - canInteractWithEmoji(awardList, awardName) { - let isAllowed = true; - const restrictedEmojis = ['thumbsup', 'thumbsdown']; - - // Users can not add :+1: and :-1: to their own notes - if ( - this.getUserData.id === this.noteAuthorId && - restrictedEmojis.indexOf(awardName) > -1 - ) { - isAllowed = false; - } - - return this.getUserData.id && isAllowed; + canInteractWithEmoji() { + return this.getUserData.id; }, hasReactionByCurrentUser(awardList) { - return awardList.filter(award => award.user.id === this.getUserData.id) - .length; + return awardList.filter(award => award.user.id === this.getUserData.id).length; }, awardTitle(awardsList) { const hasReactionByCurrentUser = this.hasReactionByCurrentUser( @@ -197,7 +185,7 @@ export default { v-tooltip v-for="(awardList, awardName, index) in groupedAwards" :key="index" - :class="getAwardClassBindings(awardList, awardName)" + :class="getAwardClassBindings(awardList)" :title="awardTitle(awardList)" class="btn award-control" data-boundary="viewport" diff --git a/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js b/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js new file mode 100644 index 00000000000..7281f907ec7 --- /dev/null +++ b/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js @@ -0,0 +1,25 @@ +import { __ } from '~/locale'; + +export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE = __('Regex pattern'); +export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE = __('To define internal users, first enable new users set to external'); + +function setUserInternalRegexPlaceholder(checkbox) { + const userInternalRegex = document.getElementById('application_setting_user_default_internal_regex'); + if (checkbox && userInternalRegex) { + if (checkbox.checked) { + userInternalRegex.readOnly = false; + userInternalRegex.placeholder = PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE; + } else { + userInternalRegex.readOnly = true; + userInternalRegex.placeholder = PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE; + } + } +} + +export default function initUserInternalRegexPlaceholder() { + const checkbox = document.getElementById('application_setting_user_default_external'); + setUserInternalRegexPlaceholder(checkbox); + checkbox.addEventListener('change', () => { + setUserInternalRegexPlaceholder(checkbox); + }); +} diff --git a/app/assets/javascripts/pages/admin/application_settings/index.js b/app/assets/javascripts/pages/admin/application_settings/index.js index 48d75f5443b..47bd70537f1 100644 --- a/app/assets/javascripts/pages/admin/application_settings/index.js +++ b/app/assets/javascripts/pages/admin/application_settings/index.js @@ -1,6 +1,8 @@ import initSettingsPanels from '~/settings_panels'; +import projectSelect from '~/project_select'; document.addEventListener('DOMContentLoaded', () => { // Initialize expandable settings panels initSettingsPanels(); + projectSelect(); }); diff --git a/app/assets/javascripts/pages/admin/index.js b/app/assets/javascripts/pages/admin/index.js index e50b61f09e2..3aa793e47b9 100644 --- a/app/assets/javascripts/pages/admin/index.js +++ b/app/assets/javascripts/pages/admin/index.js @@ -1,3 +1,7 @@ import initAdmin from './admin'; +import initUserInternalRegexPlaceholder from './application_settings/account_and_limits'; -document.addEventListener('DOMContentLoaded', initAdmin); +document.addEventListener('DOMContentLoaded', () => { + initAdmin(); + initUserInternalRegexPlaceholder(); +}); diff --git a/app/assets/javascripts/pages/admin/users/new/index.js b/app/assets/javascripts/pages/admin/users/new/index.js new file mode 100644 index 00000000000..58bfa8d64e7 --- /dev/null +++ b/app/assets/javascripts/pages/admin/users/new/index.js @@ -0,0 +1,49 @@ +import $ from 'jquery'; + +export default class UserInternalRegexHandler { + constructor() { + this.regexPattern = $('[data-user-internal-regex-pattern]').data('user-internal-regex-pattern'); + if (this.regexPattern && this.regexPattern !== '') { + this.regexOptions = $('[data-user-internal-regex-options]').data('user-internal-regex-options'); + this.external = $('#user_external'); + this.warningMessage = $('#warning_external_automatically_set'); + this.addListenerToEmailField(); + this.addListenerToUserExternalCheckbox(); + } + } + + addListenerToEmailField() { + $('#user_email').on('input', (event) => { + this.setExternalCheckbox(event.currentTarget.value); + }); + } + + addListenerToUserExternalCheckbox() { + this.external.on('click', () => { + this.warningMessage.addClass('hidden'); + }); + } + + isEmailInternal(email) { + const regex = new RegExp(this.regexPattern, this.regexOptions); + return regex.test(email); + } + + setExternalCheckbox(email) { + const isChecked = this.external.prop('checked'); + if (this.isEmailInternal(email)) { + if (isChecked) { + this.external.prop('checked', false); + this.warningMessage.removeClass('hidden'); + } + } else if (!isChecked) { + this.external.prop('checked', true); + this.warningMessage.addClass('hidden'); + } + } +} + +document.addEventListener('DOMContentLoaded', () => { + // eslint-disable-next-line + new UserInternalRegexHandler(); +}); diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js index 8737f537296..002b2279fcc 100644 --- a/app/assets/javascripts/pages/groups/edit/index.js +++ b/app/assets/javascripts/pages/groups/edit/index.js @@ -2,14 +2,13 @@ import groupAvatar from '~/group_avatar'; import TransferDropdown from '~/groups/transfer_dropdown'; import initConfirmDangerModal from '~/confirm_danger_modal'; import initSettingsPanels from '~/settings_panels'; +import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; +import { GROUP_BADGE } from '~/badges/constants'; document.addEventListener('DOMContentLoaded', () => { groupAvatar(); new TransferDropdown(); // eslint-disable-line no-new initConfirmDangerModal(); -}); - -document.addEventListener('DOMContentLoaded', () => { - // Initialize expandable settings panels initSettingsPanels(); + mountBadgeSettings(GROUP_BADGE); }); diff --git a/app/assets/javascripts/pages/groups/milestones/show/index.js b/app/assets/javascripts/pages/groups/milestones/show/index.js index 74cc4ba42c1..ebaea5ef3dc 100644 --- a/app/assets/javascripts/pages/groups/milestones/show/index.js +++ b/app/assets/javascripts/pages/groups/milestones/show/index.js @@ -1,8 +1,10 @@ import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; +import initDeleteMilestoneModal from '~/pages/milestones/shared/delete_milestone_modal_init'; + import Milestone from '~/milestone'; document.addEventListener('DOMContentLoaded', () => { initMilestonesShow(); - + initDeleteMilestoneModal(); Milestone.initDeprecationMessage(); }); diff --git a/app/assets/javascripts/pages/admin/cohorts/index.js b/app/assets/javascripts/pages/instance_statistics/cohorts/index.js index 2d5020dbef4..2d5020dbef4 100644 --- a/app/assets/javascripts/pages/admin/cohorts/index.js +++ b/app/assets/javascripts/pages/instance_statistics/cohorts/index.js diff --git a/app/assets/javascripts/pages/admin/cohorts/usage_ping.js b/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js index 914a9661c27..914a9661c27 100644 --- a/app/assets/javascripts/pages/admin/cohorts/usage_ping.js +++ b/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js diff --git a/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js b/app/assets/javascripts/pages/instance_statistics/conversational_development_index/index.js index c1056537f90..c1056537f90 100644 --- a/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js +++ b/app/assets/javascripts/pages/instance_statistics/conversational_development_index/index.js diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue index 4061c11ba8f..48668562f09 100644 --- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue +++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue @@ -40,8 +40,8 @@ if (this.issueCount === 0 && this.mergeRequestCount === 0) { return sprintf( s__(`Milestones| -You’re about to permanently delete the milestone %{milestoneTitle} from this project. -%{milestoneTitle} is not currently used in any issues or merge requests.`), +You’re about to permanently delete the milestone %{milestoneTitle}. +This milestone is not currently used in any issues or merge requests.`), { milestoneTitle, }, @@ -51,7 +51,7 @@ You’re about to permanently delete the milestone %{milestoneTitle} from this p return sprintf( s__(`Milestones| -You’re about to permanently delete the milestone %{milestoneTitle} from this project and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. +You’re about to permanently delete the milestone %{milestoneTitle} and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. Once deleted, it cannot be undone or recovered.`), { milestoneTitle, diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js index 949219a0837..aea7b649c20 100644 --- a/app/assets/javascripts/pages/profiles/show/index.js +++ b/app/assets/javascripts/pages/profiles/show/index.js @@ -3,15 +3,22 @@ import createFlash from '~/flash'; import GfmAutoComplete from '~/gfm_auto_complete'; import EmojiMenu from './emoji_menu'; +const defaultStatusEmoji = 'speech_balloon'; + document.addEventListener('DOMContentLoaded', () => { const toggleEmojiMenuButtonSelector = '.js-toggle-emoji-menu'; const toggleEmojiMenuButton = document.querySelector(toggleEmojiMenuButtonSelector); const statusEmojiField = document.getElementById('js-status-emoji-field'); const statusMessageField = document.getElementById('js-status-message-field'); - const findNoEmojiPlaceholder = () => document.getElementById('js-no-emoji-placeholder'); + const toggleNoEmojiPlaceholder = (isVisible) => { + const placeholderElement = document.getElementById('js-no-emoji-placeholder'); + placeholderElement.classList.toggle('hidden', !isVisible); + }; + + const findStatusEmoji = () => toggleEmojiMenuButton.querySelector('gl-emoji'); const removeStatusEmoji = () => { - const statusEmoji = toggleEmojiMenuButton.querySelector('gl-emoji'); + const statusEmoji = findStatusEmoji(); if (statusEmoji) { statusEmoji.remove(); } @@ -19,7 +26,7 @@ document.addEventListener('DOMContentLoaded', () => { const selectEmojiCallback = (emoji, emojiTag) => { statusEmojiField.value = emoji; - findNoEmojiPlaceholder().classList.add('hidden'); + toggleNoEmojiPlaceholder(false); removeStatusEmoji(); toggleEmojiMenuButton.innerHTML += emojiTag; }; @@ -29,7 +36,7 @@ document.addEventListener('DOMContentLoaded', () => { statusEmojiField.value = ''; statusMessageField.value = ''; removeStatusEmoji(); - findNoEmojiPlaceholder().classList.remove('hidden'); + toggleNoEmojiPlaceholder(true); }); const emojiAutocomplete = new GfmAutoComplete(); @@ -44,6 +51,23 @@ document.addEventListener('DOMContentLoaded', () => { selectEmojiCallback, ); emojiMenu.bindEvents(); + + const defaultEmojiTag = Emoji.glEmojiTag(defaultStatusEmoji); + statusMessageField.addEventListener('input', () => { + const hasStatusMessage = statusMessageField.value.trim() !== ''; + const statusEmoji = findStatusEmoji(); + if (hasStatusMessage && statusEmoji) { + return; + } + + if (hasStatusMessage) { + toggleNoEmojiPlaceholder(false); + toggleEmojiMenuButton.innerHTML += defaultEmojiTag; + } else if (statusEmoji.dataset.name === defaultStatusEmoji) { + toggleNoEmojiPlaceholder(true); + removeStatusEmoji(); + } + }); }) .catch(() => createFlash('Failed to load emoji list!')); }); diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js index 628913483c6..f5b1cf85e68 100644 --- a/app/assets/javascripts/pages/projects/edit/index.js +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -1,6 +1,8 @@ +import { PROJECT_BADGE } from '~/badges/constants'; import initSettingsPanels from '~/settings_panels'; import setupProjectEdit from '~/project_edit'; import initConfirmDangerModal from '~/confirm_danger_modal'; +import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; import initProjectLoadingSpinner from '../shared/save_project_loader'; import projectAvatar from '../shared/project_avatar'; import initProjectPermissionsSettings from '../shared/permissions'; @@ -13,4 +15,5 @@ document.addEventListener('DOMContentLoaded', () => { projectAvatar(); initProjectPermissionsSettings(); initConfirmDangerModal(); + mountBadgeSettings(PROJECT_BADGE); }); diff --git a/app/assets/javascripts/pages/projects/settings/badges/index/index.js b/app/assets/javascripts/pages/projects/settings/badges/index/index.js deleted file mode 100644 index 30469550866..00000000000 --- a/app/assets/javascripts/pages/projects/settings/badges/index/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import Vue from 'vue'; -import Translate from '~/vue_shared/translate'; -import { PROJECT_BADGE } from '~/badges/constants'; -import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; - -Vue.use(Translate); - -document.addEventListener('DOMContentLoaded', () => { - mountBadgeSettings(PROJECT_BADGE); -}); diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js index ffc84dc106b..78cf5406e43 100644 --- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js @@ -1,3 +1,9 @@ import initForm from '../form'; +import MirrorRepos from './mirror_repos'; -document.addEventListener('DOMContentLoaded', initForm); +document.addEventListener('DOMContentLoaded', () => { + initForm(); + + const mirrorReposContainer = document.querySelector('.js-mirror-settings'); + if (mirrorReposContainer) new MirrorRepos(mirrorReposContainer).init(); +}); diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/mirror_repos.js b/app/assets/javascripts/pages/projects/settings/repository/show/mirror_repos.js new file mode 100644 index 00000000000..4c56af20cc3 --- /dev/null +++ b/app/assets/javascripts/pages/projects/settings/repository/show/mirror_repos.js @@ -0,0 +1,94 @@ +import $ from 'jquery'; +import _ from 'underscore'; +import { __ } from '~/locale'; +import Flash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; + +export default class MirrorRepos { + constructor(container) { + this.$container = $(container); + this.$form = $('.js-mirror-form', this.$container); + this.$urlInput = $('.js-mirror-url', this.$form); + this.$protectedBranchesInput = $('.js-mirror-protected', this.$form); + this.$table = $('.js-mirrors-table-body', this.$container); + this.mirrorEndpoint = this.$form.data('projectMirrorEndpoint'); + } + + init() { + this.initMirrorPush(); + this.registerUpdateListeners(); + } + + initMirrorPush() { + this.$passwordGroup = $('.js-password-group', this.$container); + this.$password = $('.js-password', this.$passwordGroup); + this.$authMethod = $('.js-auth-method', this.$form); + + this.$authMethod.on('change', () => this.togglePassword()); + this.$password.on('input.updateUrl', () => this.debouncedUpdateUrl()); + } + + updateUrl() { + let val = this.$urlInput.val(); + + if (this.$password) { + const password = this.$password.val(); + if (password) val = val.replace('@', `:${password}@`); + } + + $('.js-mirror-url-hidden', this.$form).val(val); + } + + updateProtectedBranches() { + const val = this.$protectedBranchesInput.get(0).checked + ? this.$protectedBranchesInput.val() + : '0'; + $('.js-mirror-protected-hidden', this.$form).val(val); + } + + registerUpdateListeners() { + this.debouncedUpdateUrl = _.debounce(() => this.updateUrl(), 200); + this.$urlInput.on('input', () => this.debouncedUpdateUrl()); + this.$protectedBranchesInput.on('change', () => this.updateProtectedBranches()); + this.$table.on('click', '.js-delete-mirror', event => this.deleteMirror(event)); + } + + togglePassword() { + const isPassword = this.$authMethod.val() === 'password'; + + if (!isPassword) { + this.$password.val(''); + this.updateUrl(); + } + this.$passwordGroup.collapse(isPassword ? 'show' : 'hide'); + } + + deleteMirror(event, existingPayload) { + const $target = $(event.currentTarget); + let payload = existingPayload; + + if (!payload) { + payload = { + project: { + remote_mirrors_attributes: { + id: $target.data('mirrorId'), + enabled: 0, + }, + }, + }; + } + + return axios + .put(this.mirrorEndpoint, payload) + .then(() => this.removeRow($target)) + .catch(() => Flash(__('Failed to remove mirror.'))); + } + + /* eslint-disable class-methods-use-this */ + removeRow($target) { + const row = $target.closest('tr'); + $('.js-delete-mirror', row).tooltip('hide'); + row.remove(); + } + /* eslint-enable class-methods-use-this */ +} diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js index 41880d27516..6e5ef0ac0b2 100644 --- a/app/assets/javascripts/performance_bar/index.js +++ b/app/assets/javascripts/performance_bar/index.js @@ -1,5 +1,4 @@ import Vue from 'vue'; -import Flash from '../flash'; import PerformanceBarService from './services/performance_bar_service'; import PerformanceBarStore from './stores/performance_bar_store'; @@ -46,7 +45,8 @@ export default ({ container }) => this.store.addRequestDetails(requestId, res.data.data); }) .catch(() => - Flash(`Error getting performance bar results for ${requestId}`), + // eslint-disable-next-line no-console + console.warn(`Error getting performance bar results for ${requestId}`), ); }, }, diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js index bc71911ae35..60d9ba62570 100644 --- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js +++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js @@ -11,13 +11,10 @@ export default class PerformanceBarService { static registerInterceptor(peekUrl, callback) { const interceptor = response => { - const requestId = response.headers['x-request-id']; - // Get the request URL from response.config for Axios, and response for - // Vue Resource. - const requestUrl = (response.config || response).url; - const cachedResponse = response.headers['x-gitlab-from-cache'] === 'true'; + const [fireCallback, requestId, requestUrl] = + PerformanceBarService.callbackParams(response, peekUrl); - if (requestUrl !== peekUrl && requestId && !cachedResponse) { + if (fireCallback) { callback(requestId, requestUrl); } @@ -38,4 +35,16 @@ export default class PerformanceBarService { vueResourceInterceptor, ); } + + static callbackParams(response, peekUrl) { + const requestId = response.headers && response.headers['x-request-id']; + // Get the request URL from response.config for Axios, and response for + // Vue Resource. + const requestUrl = (response.config || response).url; + const apiRequest = requestUrl && requestUrl.match(/^\/api\//); + const cachedResponse = response.headers && response.headers['x-gitlab-from-cache'] === 'true'; + const fireCallback = requestUrl !== peekUrl && requestId && !apiRequest && !cachedResponse; + + return [fireCallback, requestId, requestUrl]; + } } diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue index 8487c8036ee..2ad66f4fe86 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -1,6 +1,5 @@ <script> import $ from 'jquery'; -import _ from 'underscore'; import JobNameComponent from './job_name_component.vue'; import JobComponent from './job_component.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; @@ -47,7 +46,7 @@ export default { computed: { tooltipText() { - return _.escape(`${this.job.name} - ${this.job.status.label}`); + return `${this.job.name} - ${this.job.status.label}`; }, }, diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index 66f95147193..9ac16b7e541 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -1,5 +1,4 @@ <script> -import _ from 'underscore'; import ActionComponent from './action_component.vue'; import JobNameComponent from './job_name_component.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; @@ -62,7 +61,7 @@ export default { const textBuilder = []; if (this.job.name) { - textBuilder.push(_.escape(this.job.name)); + textBuilder.push(this.job.name); } if (this.job.name && this.status.tooltip) { @@ -106,7 +105,6 @@ export default { :class="cssClassJobName" :data-boundary="tooltipBoundary" data-container="body" - data-html="true" class="js-pipeline-graph-job-link" > @@ -122,7 +120,6 @@ export default { :title="tooltipText" :class="cssClassJobName" class="js-job-component-tooltip non-details-job-component" - data-html="true" data-container="body" > diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 804822a3ea8..29b347824de 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -330,7 +330,7 @@ export default { <pipelines-artifacts-component v-if="pipeline.details.artifacts.length" :artifacts="pipeline.details.artifacts" - class="d-none d-sm-none d-md-block" + class="d-md-block" /> <loading-button diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index bce7556bd40..6f3b32f8eea 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -14,6 +14,7 @@ export default function projectSelect() { this.orderBy = $(select).data('orderBy') || 'id'; this.withIssuesEnabled = $(select).data('withIssuesEnabled'); this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled'); + this.allowClear = $(select).data('allowClear') || false; placeholder = "Search for project"; if (this.includeGroups) { @@ -71,6 +72,13 @@ export default function projectSelect() { text: function (project) { return project.name_with_namespace || project.name; }, + + initSelection: function(el, callback) { + return Api.project(el.val()).then(({ data }) => callback(data)); + }, + + allowClear: this.allowClear, + dropdownCssClass: "ajax-project-dropdown" }); if (simpleFilter) return select; diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue index 140475b4dfa..7b37f4e9a97 100644 --- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue +++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue @@ -1,10 +1,10 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; import { s__ } from '~/locale'; - import { componentNames } from '~/vue_shared/components/reports/issue_body'; - import ReportSection from '~/vue_shared/components/reports/report_section.vue'; - import SummaryRow from '~/vue_shared/components/reports/summary_row.vue'; - import IssuesList from '~/vue_shared/components/reports/issues_list.vue'; + import { componentNames } from './issue_body'; + import ReportSection from './report_section.vue'; + import SummaryRow from './summary_row.vue'; + import IssuesList from './issues_list.vue'; import Modal from './modal.vue'; import createStore from '../store'; import { summaryTextBuilder, reportTextBuilder, statusIcon } from '../store/utils'; diff --git a/app/assets/javascripts/vue_shared/components/reports/issue_body.js b/app/assets/javascripts/reports/components/issue_body.js index 54dfb7b16bf..8b5af263d50 100644 --- a/app/assets/javascripts/vue_shared/components/reports/issue_body.js +++ b/app/assets/javascripts/reports/components/issue_body.js @@ -1,4 +1,4 @@ -import TestIssueBody from '~/reports/components/test_issue_body.vue'; +import TestIssueBody from './test_issue_body.vue'; export const components = { TestIssueBody, diff --git a/app/assets/javascripts/vue_shared/components/reports/issue_status_icon.vue b/app/assets/javascripts/reports/components/issue_status_icon.vue index f8189117ac3..85811698a37 100644 --- a/app/assets/javascripts/vue_shared/components/reports/issue_status_icon.vue +++ b/app/assets/javascripts/reports/components/issue_status_icon.vue @@ -1,11 +1,10 @@ <script> import Icon from '~/vue_shared/components/icon.vue'; - import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS, -} from '~/vue_shared/components/reports/constants'; +} from '../constants'; export default { name: 'IssueStatusIcon', diff --git a/app/assets/javascripts/vue_shared/components/reports/issues_list.vue b/app/assets/javascripts/reports/components/issues_list.vue index 2545e84f932..df42201b5de 100644 --- a/app/assets/javascripts/vue_shared/components/reports/issues_list.vue +++ b/app/assets/javascripts/reports/components/issues_list.vue @@ -1,10 +1,10 @@ <script> -import IssuesBlock from '~/vue_shared/components/reports/report_issues.vue'; +import IssuesBlock from '~/reports/components/report_issues.vue'; import { STATUS_SUCCESS, STATUS_FAILED, STATUS_NEUTRAL, -} from '~/vue_shared/components/reports/constants'; +} from '~/reports/constants'; /** * Renders block of issues diff --git a/app/assets/javascripts/vue_shared/components/reports/modal_open_name.vue b/app/assets/javascripts/reports/components/modal_open_name.vue index 4f81cee2a38..4f81cee2a38 100644 --- a/app/assets/javascripts/vue_shared/components/reports/modal_open_name.vue +++ b/app/assets/javascripts/reports/components/modal_open_name.vue diff --git a/app/assets/javascripts/vue_shared/components/reports/report_issues.vue b/app/assets/javascripts/reports/components/report_issues.vue index 1f13e555b31..c553a374f66 100644 --- a/app/assets/javascripts/vue_shared/components/reports/report_issues.vue +++ b/app/assets/javascripts/reports/components/report_issues.vue @@ -1,6 +1,6 @@ <script> -import IssueStatusIcon from '~/vue_shared/components/reports/issue_status_icon.vue'; -import { components, componentNames } from '~/vue_shared/components/reports/issue_body'; +import IssueStatusIcon from '~/reports/components/issue_status_icon.vue'; +import { components, componentNames } from '~/reports/components/issue_body'; export default { name: 'ReportIssues', diff --git a/app/assets/javascripts/vue_shared/components/reports/report_link.vue b/app/assets/javascripts/reports/components/report_link.vue index 74d68f9f439..74d68f9f439 100644 --- a/app/assets/javascripts/vue_shared/components/reports/report_link.vue +++ b/app/assets/javascripts/reports/components/report_link.vue diff --git a/app/assets/javascripts/vue_shared/components/reports/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue index a6dbf21092b..dc609d6f90e 100644 --- a/app/assets/javascripts/vue_shared/components/reports/report_section.vue +++ b/app/assets/javascripts/reports/components/report_section.vue @@ -1,8 +1,8 @@ <script> import { __ } from '~/locale'; import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; +import Popover from '~/vue_shared/components/help_popover.vue'; import IssuesList from './issues_list.vue'; -import Popover from '../help_popover.vue'; const LOADING = 'LOADING'; const ERROR = 'ERROR'; diff --git a/app/assets/javascripts/vue_shared/components/reports/summary_row.vue b/app/assets/javascripts/reports/components/summary_row.vue index 063beab58fc..4456d84c968 100644 --- a/app/assets/javascripts/vue_shared/components/reports/summary_row.vue +++ b/app/assets/javascripts/reports/components/summary_row.vue @@ -1,7 +1,7 @@ <script> import CiIcon from '~/vue_shared/components/ci_icon.vue'; import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; -import Popover from '../help_popover.vue'; +import Popover from '~/vue_shared/components/help_popover.vue'; /** * Renders the summary row for each report diff --git a/app/assets/javascripts/reports/constants.js b/app/assets/javascripts/reports/constants.js index 807ecb1039e..c323dc543f3 100644 --- a/app/assets/javascripts/reports/constants.js +++ b/app/assets/javascripts/reports/constants.js @@ -11,6 +11,8 @@ export const SUCCESS = 'SUCCESS'; export const STATUS_FAILED = 'failed'; export const STATUS_SUCCESS = 'success'; +export const STATUS_NEUTRAL = 'neutral'; + export const ICON_WARNING = 'warning'; export const ICON_SUCCESS = 'success'; export const ICON_NOTFOUND = 'notfound'; diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue index 5e7b8f9698f..63082654101 100644 --- a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue +++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue @@ -1,4 +1,5 @@ <script> +import { __ } from '~/locale'; import $ from 'jquery'; import eventHub from '../../event_hub'; @@ -17,7 +18,7 @@ export default { computed: { buttonText() { - return this.isLocked ? this.__('Unlock') : this.__('Lock'); + return this.isLocked ? __('Unlock') : __('Lock'); }, toggleLock() { diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue index 8bbc59f623a..ab7fab7e5ca 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -1,5 +1,5 @@ <script> -import { __ } from '~/locale'; +import { __, sprintf } from '~/locale'; import Flash from '~/flash'; import tooltip from '~/vue_shared/directives/tooltip'; import issuableMixin from '~/vue_shared/mixins/issuable'; @@ -79,11 +79,9 @@ export default { .then(() => window.location.reload()) .catch(() => Flash( - this.__( - `Something went wrong trying to change the locked state of this ${ - this.issuableDisplayName - }`, - ), + sprintf(__('Something went wrong trying to change the locked state of this %{issuableDisplayName}'), { + issuableDisplayName: this.issuableDisplayName, + }), ), ); }, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index 21f21232596..d530ab2767b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -1,5 +1,6 @@ <script> import Icon from '~/vue_shared/components/icon.vue'; +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import timeagoMixin from '../../vue_shared/mixins/timeago'; import tooltip from '../../vue_shared/directives/tooltip'; import LoadingButton from '../../vue_shared/components/loading_button.vue'; @@ -16,6 +17,7 @@ export default { MemoryUsage, StatusIcon, Icon, + TooltipOnTruncate, }, directives: { tooltip, @@ -88,14 +90,20 @@ export default { <span> Deployed to </span> - <a - :href="deployment.url" - target="_blank" - rel="noopener noreferrer nofollow" - class="deploy-link js-deploy-meta" + <tooltip-on-truncate + :title="deployment.name" + truncate-target="child" + class="deploy-link label-truncate" > - {{ deployment.name }} - </a> + <a + :href="deployment.url" + target="_blank" + rel="noopener noreferrer nofollow" + class="js-deploy-meta" + > + {{ deployment.name }} + </a> + </tooltip-on-truncate> </template> <span v-tooltip diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue index a4c2289c590..4c3f8dff3c4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue @@ -1,18 +1,21 @@ <script> -import tooltip from '~/vue_shared/directives/tooltip'; -import { n__ } from '~/locale'; +import _ from 'underscore'; +import { n__, s__, sprintf } from '~/locale'; import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility'; import Icon from '~/vue_shared/components/icon.vue'; import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; export default { name: 'MRWidgetHeader', - directives: { - tooltip, - }, components: { Icon, clipboardButton, + TooltipOnTruncate, + }, + directives: { + tooltip, }, props: { mr: { @@ -24,8 +27,12 @@ export default { shouldShowCommitsBehindText() { return this.mr.divergedCommitsCount > 0; }, - commitsText() { - return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount); + commitsBehindText() { + return sprintf(s__('mrWidget|The source branch is %{commitsBehindLinkStart}%{commitsBehind}%{commitsBehindLinkEnd} the target branch'), { + commitsBehindLinkStart: `<a href="${_.escape(this.mr.targetBranchPath)}">`, + commitsBehind: n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount), + commitsBehindLinkEnd: '</a>', + }, false); }, branchNameClipboardData() { // This supports code in app/assets/javascripts/copy_to_clipboard.js that @@ -36,22 +43,20 @@ export default { gfm: `\`${this.mr.sourceBranch}\``, }); }, - isSourceBranchLong() { - return this.isBranchTitleLong(this.mr.sourceBranch); - }, - isTargetBranchLong() { - return this.isBranchTitleLong(this.mr.targetBranch); - }, webIdePath() { - return mergeUrlParams({ - target_project: this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath ? - this.mr.targetProjectFullPath : '', - }, webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`)); + if (this.mr.canPushToSourceBranch) { + return mergeUrlParams({ + target_project: this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath ? + this.mr.targetProjectFullPath : '', + }, webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`)); + } + + return null; }, - }, - methods: { - isBranchTitleLong(branchTitle) { - return branchTitle.length > 32; + ideButtonTitle() { + return !this.mr.canPushToSourceBranch + ? s__('mrWidget|You are not allowed to edit this project directly. Please fork to make changes.') + : ''; }, }, }; @@ -65,30 +70,21 @@ export default { <div class="normal"> <strong> {{ s__("mrWidget|Request to merge") }} - <span - :class="{ 'label-truncated': isSourceBranchLong }" - :title="isSourceBranchLong ? mr.sourceBranch : ''" - :v-tooltip="isSourceBranchLong" - class="label-branch js-source-branch" - data-placement="bottom" + <tooltip-on-truncate + :title="mr.sourceBranch" + truncate-target="child" + class="label-branch label-truncate js-source-branch" v-html="mr.sourceBranchLink" - > - </span> - - <clipboard-button + /><clipboard-button :text="branchNameClipboardData" :title="__('Copy branch name to clipboard')" css-class="btn-default btn-transparent btn-clipboard" /> - {{ s__("mrWidget|into") }} - - <span - :v-tooltip="isTargetBranchLong" - :class="{ 'label-truncatedtooltip': isTargetBranchLong }" - :title="isTargetBranchLong ? mr.targetBranch : ''" - class="label-branch" - data-placement="bottom" + <tooltip-on-truncate + :title="mr.targetBranch" + truncate-target="child" + class="label-branch label-truncate" > <a :href="mr.targetBranchTreePath" @@ -96,15 +92,13 @@ export default { > {{ mr.targetBranch }} </a> - </span> + </tooltip-on-truncate> </strong> <div v-if="shouldShowCommitsBehindText" class="diverged-commits-count" + v-html="commitsBehindText" > - <span class="monospace">{{ mr.sourceBranch }}</span> - is {{ commitsText }} - <span class="monospace">{{ mr.targetBranch }}</span> </div> </div> @@ -112,13 +106,22 @@ export default { v-if="mr.isOpen" class="branch-actions" > - <a - v-if="!mr.sourceBranchRemoved" - :href="webIdePath" - class="btn btn-default inline js-web-ide d-none d-md-inline-block" + <span + v-tooltip + :title="ideButtonTitle" + data-placement="bottom" + tabindex="0" > - {{ s__("mrWidget|Open in Web IDE") }} - </a> + <a + v-if="!mr.sourceBranchRemoved" + :href="webIdePath" + :class="{ disabled: !mr.canPushToSourceBranch }" + class="btn btn-default inline js-web-ide d-none d-md-inline-block" + role="button" + > + {{ s__("mrWidget|Open in Web IDE") }} + </a> + </span> <button :disabled="mr.sourceBranchRemoved" data-target="#modal_merge_info" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index 4a3fd01fa39..fee41b239e8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -3,6 +3,7 @@ import PipelineStage from '~/pipelines/components/stage.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import Icon from '~/vue_shared/components/icon.vue'; +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; export default { name: 'MRWidgetPipeline', @@ -10,6 +11,7 @@ export default { PipelineStage, CiIcon, Icon, + TooltipOnTruncate, }, props: { pipeline: { @@ -30,6 +32,10 @@ export default { type: String, required: false, }, + sourceBranch: { + type: String, + required: false, + }, }, computed: { hasPipeline() { @@ -107,11 +113,12 @@ export default { > {{ pipeline.commit.short_id }}</a> on - <span - class="label-branch" + <tooltip-on-truncate + :title="sourceBranch" + truncate-target="child" + class="label-branch label-truncate" v-html="sourceBranchLink" - > - </span> + /> </template> </div> <div diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 80593d1f34a..dc6be025f11 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -254,6 +254,7 @@ export default { :pipeline="mr.pipeline" :ci-status="mr.ciStatus" :has-ci="mr.hasCI" + :source-branch="mr.sourceBranch" :source-branch-link="mr.sourceBranchLink" /> <deployment diff --git a/app/assets/javascripts/vue_shared/components/reports/constants.js b/app/assets/javascripts/vue_shared/components/reports/constants.js deleted file mode 100644 index dbde648bfdb..00000000000 --- a/app/assets/javascripts/vue_shared/components/reports/constants.js +++ /dev/null @@ -1,3 +0,0 @@ -export const STATUS_FAILED = 'failed'; -export const STATUS_SUCCESS = 'success'; -export const STATUS_NEUTRAL = 'neutral'; diff --git a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue index f44d361c47e..78fde463507 100644 --- a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue +++ b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue @@ -71,7 +71,11 @@ export default { }, methods: { getPercent(count) { - return roundOffFloat((count / this.totalCount) * 100, 1); + const percent = roundOffFloat((count / this.totalCount) * 100, 1); + if (percent > 0 && percent < 1) { + return '< 1'; + } + return percent; }, barStyle(percent) { return `width: ${percent}%;`; diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue new file mode 100644 index 00000000000..125826da6c3 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue @@ -0,0 +1,67 @@ +<script> +import _ from 'underscore'; +import tooltip from '../directives/tooltip'; + +export default { + directives: { + tooltip, + }, + props: { + title: { + type: String, + required: false, + default: '', + }, + placement: { + type: String, + required: false, + default: 'top', + }, + truncateTarget: { + type: [String, Function], + required: false, + default: '', + }, + }, + data() { + return { + showTooltip: false, + }; + }, + mounted() { + const target = this.selectTarget(); + + if (target && target.scrollWidth > target.offsetWidth) { + this.showTooltip = true; + } + }, + methods: { + selectTarget() { + if (_.isFunction(this.truncateTarget)) { + return this.truncateTarget(this.$el); + } else if (this.truncateTarget === 'child') { + return this.$el.childNodes[0]; + } + + return this.$el; + }, + }, +}; +</script> + +<template> + <span + v-tooltip + v-if="showTooltip" + :title="title" + :data-placement="placement" + class="js-show-tooltip" + > + <slot></slot> + </span> + <span + v-else + > + <slot></slot> + </span> +</template> diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index c20738a20c3..c91f5e279ea 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -4,17 +4,16 @@ $text-color: $gl-text-color; -$brand-primary: $gl-primary; -$brand-success: $gl-success; -$brand-info: $gl-info; -$brand-warning: $gl-warning; -$brand-danger: $gl-danger; +$brand-primary: $blue-500; +$brand-success: $green-500; +$brand-info: $blue-500; +$brand-warning: $orange-500; +$brand-danger: $red-500; $border-radius-base: 3px !default; $modal-body-bg: $white-light; $input-border: $border-color; -$input-border-focus: $focus-border-color; $padding-base-vertical: $gl-vert-padding; $padding-base-horizontal: $gl-padding; @@ -86,7 +85,7 @@ strong { } a { - color: $gl-link-color; + color: $blue-600; } hr { diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index c46b0b5db09..b1a20c06910 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -1,4 +1,5 @@ @import 'framework/variables'; +@import 'framework/variables_overrides'; @import 'framework/mixins'; @import 'bootstrap'; diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 369556dc24e..9dd0384a228 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -8,7 +8,7 @@ float: left; margin-right: 15px; border-radius: $avatar-radius; - border: 1px solid $avatar-border; + border: 1px solid $gray-normal; &.s16 { @include avatar-size(16px, 6px); } &.s18 { @include avatar-size(18px, 6px); } &.s19 { @include avatar-size(19px, 6px); } @@ -36,7 +36,7 @@ width: 40px; height: 40px; padding: 0; - background: $avatar-background; + background: $gray-lightest; overflow: hidden; &.avatar-inline { @@ -62,7 +62,7 @@ } &:not([href]):hover { - border-color: darken($avatar-border, 10%); + border-color: darken($gray-normal, 10%); } } @@ -70,7 +70,7 @@ text-align: center; vertical-align: top; color: $identicon-fg-color; - background-color: $identicon-gray; + background-color: $gray-darker; // Sizes &.s16 { font-size: 12px; line-height: 1.33; } @@ -94,7 +94,7 @@ &.bg4 { background-color: $identicon-blue; } &.bg5 { background-color: $identicon-teal; } &.bg6 { background-color: $identicon-orange; } - &.bg7 { background-color: $identicon-gray; } + &.bg7 { background-color: $gray-darker; } } .avatar-container { @@ -103,6 +103,7 @@ display: flex; a { + width: 100%; display: flex; } @@ -121,7 +122,7 @@ .avatar-counter { background-color: $gray-darkest; color: $white-light; - border: 1px solid $avatar-border; + border: 1px solid $gray-normal; border-radius: 1em; font-family: $regular-font; font-size: 9px; diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index 8d11b92cf88..a265e4206f1 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -141,8 +141,8 @@ &:hover, &:active, &.is-active { - background-color: $row-hover; - border-color: $row-hover-border; + background-color: $blue-50; + border-color: $blue-200; box-shadow: none; outline: 0; diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss index 57df9b969c3..c6060161dec 100644 --- a/app/assets/stylesheets/framework/badges.scss +++ b/app/assets/stylesheets/framework/badges.scss @@ -1,6 +1,6 @@ .badge.badge-pill { font-weight: $gl-font-weight-normal; background-color: $badge-bg; - color: $badge-color; + color: $gl-text-color-secondary; vertical-align: baseline; } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 646cedd79ed..72b4ed0ac33 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -350,7 +350,7 @@ &:focus { cursor: text; box-shadow: none; - border-color: lighten($dropdown-input-focus-border, 20%); + border-color: lighten($blue-300, 20%); color: $gray-darkest; background-color: $gray-light; } @@ -434,7 +434,7 @@ &:hover, &:active, &:focus { - color: $gl-link-color; + color: $blue-600; text-decoration: none; } } @@ -445,21 +445,21 @@ &:hover, &:active, &:focus { - color: $gl-link-color; + color: $blue-600; text-decoration: none; } } } .btn-missing { - color: $notes-light-color; + color: $gl-text-color-secondary; border: 1px dashed $border-gray-normal-dashed; border-radius: $border-radius-default; &:hover, &:active, &:focus { - color: $notes-light-color; + color: $gl-text-color-secondary; background-color: $white-normal; } } diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index 1bd94c0acba..bdd7f09d926 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -25,25 +25,25 @@ /* Variations */ .bs-callout-danger { - background-color: $callout-danger-bg; - border-color: $callout-danger-border; - color: $callout-danger-color; + background-color: $red-100; + border-color: $red-200; + color: $red-700; } .bs-callout-warning { - background-color: $callout-warning-bg; - border-color: $callout-warning-border; - color: $callout-warning-color; + background-color: $orange-100; + border-color: $orange-200; + color: $orange-700; } .bs-callout-info { - background-color: $callout-info-bg; - border-color: $callout-info-border; - color: $callout-info-color; + background-color: $blue-100; + border-color: $blue-200; + color: $blue-700; } .bs-callout-success { - background-color: $callout-success-bg; - border-color: $callout-success-border; - color: $callout-success-color; + background-color: $green-100; + border-color: $green-200; + color: $green-700; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index af17210f341..72e27f9ad16 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -1,8 +1,8 @@ /** COLORS **/ -.cgray { color: $common-gray; } +.cgray { color: $gl-text-color; } .clgray { color: $common-gray-light; } -.cred { color: $common-red; } -.cgreen { color: $common-green; } +.cred { color: $red-500; } +.cgreen { color: $green-600; } .cdark { color: $common-gray-dark; } .text-plain, @@ -44,10 +44,10 @@ } .hint { font-style: italic; color: $hint-color; } -.light { color: $common-gray; } +.light { color: $gl-text-color; } .slead { - color: $common-gray; + color: $gl-text-color; font-size: 14px; margin-bottom: 12px; font-weight: $gl-font-weight-normal; @@ -71,7 +71,7 @@ pre { } &.card.card-body-pre { - border: 1px solid $well-pre-bg; + border: 1px solid $gray-darker; background: $gray-light; border-radius: 0; color: $well-pre-color; @@ -114,7 +114,11 @@ hr { .item-title { font-weight: $gl-font-weight-bold; } .author-link { - color: $gl-link-color; + color: $blue-600; +} + +.author-link:hover { + text-decoration: none; } .back-link { @@ -229,7 +233,7 @@ li.note { .error-message { padding: 10px; - background: $error-bg; + background: $red-400; margin: 0; color: $white-light; @@ -240,11 +244,11 @@ li.note { } .warning_message { - border-left: 4px solid $warning-message-border; - color: $warning-message-color; + border-left: 4px solid $orange-200; + color: $orange-700; padding: 10px; margin-bottom: 10px; - background: $warning-message-bg; + background: $orange-100; padding-left: 20px; &.centered { @@ -344,20 +348,11 @@ img.emoji { } } -.profiler-results { - top: 73px !important; - - .profiler-button, - .profiler-controls { - border-color: $profiler-border !important; - } -} - .dropzone .dz-preview .dz-progress { border-color: $border-color !important; .dz-upload { - background: $gl-success !important; + background: $green-500 !important; } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index eebce8b9011..8a224dc517e 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -147,7 +147,7 @@ } @mixin dropdown-item-hover { - background-color: $dropdown-item-hover-bg; + background-color: $gray-darker; color: $gl-text-color; outline: 0; @@ -195,7 +195,7 @@ text-decoration: none; .badge.badge-pill { - background-color: darken($dropdown-link-hover-bg, 5%); + background-color: darken($blue-50, 5%); } } @@ -233,7 +233,7 @@ font-weight: $gl-font-weight-normal; padding: 8px 0; background-color: $white-light; - border: 1px solid $dropdown-border-color; + border: 1px solid $border-color; border-radius: $border-radius-base; box-shadow: 0 2px 4px $dropdown-shadow-color; @@ -615,7 +615,7 @@ &:focus { color: $dropdown-link-color; - border-color: $dropdown-input-focus-border; + border-color: $blue-300; box-shadow: 0 0 4px $dropdown-input-focus-shadow; ~ .fa { @@ -874,7 +874,7 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu { overflow-y: auto; li.section-empty.section-failure { - color: $callout-danger-color; + color: $red-700; } .frequent-items-list-item-container a { diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss index 3cde0490371..6c50ea719d3 100644 --- a/app/assets/stylesheets/framework/emojis.scss +++ b/app/assets/stylesheets/framework/emojis.scss @@ -2,7 +2,7 @@ gl-emoji { font-style: normal; display: inline-flex; vertical-align: middle; - font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; - font-size: 1.5em; - line-height: 0.9; + font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: 1.4em; + line-height: 1em; } diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss index cad915bc86f..85cabf43e9e 100644 --- a/app/assets/stylesheets/framework/feature_highlight.scss +++ b/app/assets/stylesheets/framework/feature_highlight.scss @@ -72,11 +72,11 @@ .feature-highlight-popover { width: 240px; padding: 0; - border: 1px solid $dropdown-border-color; + border: 1px solid $border-color; box-shadow: 0 2px 4px $dropdown-shadow-color; &.right > .arrow { - border-right-color: $dropdown-border-color; + border-right-color: $border-color; } .popover-body { diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 00eac1688f2..1d3512bbb4c 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -112,7 +112,7 @@ &.image_file, &.video { - background: $file-image-bg; + background: $gray-darker; text-align: center; padding: 30px; @@ -131,7 +131,7 @@ } &.blob-no-preview { - background: $blob-bg; + background: $gray-darker; text-shadow: 0 1px 2px $white-light; padding: 100px 0; } @@ -146,7 +146,7 @@ } tr { - border-bottom: 1px solid $blame-border; + border-bottom: 1px solid $gray-darker; &:last-child { border-bottom: 0; @@ -211,7 +211,7 @@ } &.logs { - background: $logs-bg; + background: $gray-darker; max-height: 700px; overflow-y: auto; @@ -233,7 +233,7 @@ } &:hover { - background: $row-hover; + background: $blue-50; } } } @@ -286,19 +286,19 @@ span.idiff { .new-file { a { - color: $gl-text-green; + color: $green-600; } } .renamed-file { a { - color: $gl-text-orange; + color: $orange-600; } } .deleted-file { a { - color: $gl-text-red; + color: $red-500; } } @@ -312,11 +312,11 @@ span.idiff { text-decoration: none; .new-file { - color: $notify-new-file; + color: $green-600; } .deleted-file { - color: $notify-deleted-file; + color: $red-700; } } } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 5d79610b21e..abfe350677e 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -205,8 +205,8 @@ &.focus, &.focus:hover { - border-color: $dropdown-input-focus-border; - box-shadow: 0 0 4px $search-input-focus-shadow-color; + border-color: $blue-300; + box-shadow: 0 0 4px $dropdown-input-focus-shadow; } gl-emoji { @@ -294,7 +294,7 @@ &:hover, &:focus { color: $gl-text-color; - border-color: $dropdown-input-focus-border; + border-color: $blue-300; outline: none; } diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index e4bcb92876d..7a4c3914fb0 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -16,10 +16,10 @@ color: $gl-text-color; a { - color: $gl-link-color; + color: $blue-600; &:hover { - color: $gl-link-hover-color; + color: $blue-800; text-decoration: none; } } diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index d7149d93622..afd888af672 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -8,7 +8,7 @@ input { input[type='text'].danger { background: $input-danger-bg !important; - border-color: $input-danger-border; + border-color: $red-400; text-shadow: 0 1px 1px $white-light; } @@ -170,7 +170,7 @@ label { } .form-control::-webkit-input-placeholder { - color: $placeholder-text-color; + color: $gl-text-color-tertiary; } .input-group { @@ -201,7 +201,7 @@ label { } .gl-show-field-errors { - .form-control { + .form-control:not(textarea) { height: 34px; } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index e7e13d35d8e..11a30d83f03 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -554,7 +554,7 @@ float: left; margin-right: 5px; border-radius: 50%; - border: 1px solid $avatar-border; + border: 1px solid $gray-normal; } .with-performance-bar .navbar-gitlab { diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index d1f7ff4438b..f002edced8a 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -11,7 +11,7 @@ .ci-status-icon-failed { svg { - fill: $gl-danger; + fill: $red-500; } &.add-border { diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index f878ec1ca91..1e93bf2b751 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -25,7 +25,7 @@ &.svg-#{$width} { img, svg { - width: #{$width + 'px'}; + max-width: #{$width + 'px'}; } } } diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 86de88729ee..2d672e62e08 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -26,12 +26,12 @@ &.status-box-closed, &.status-box-mr-closed { - background-color: $gl-danger; + background-color: $red-500; } &.status-box-issue-closed, &.status-box-mr-merged { - background-color: $gl-primary; + background-color: $blue-500; } &.status-box-open { @@ -39,7 +39,7 @@ } &.status-box-expired { - background-color: $issue-status-expired; + background-color: $orange-500; } &.status-box-upcoming { diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 52b5f059f20..d4bae4cb137 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -111,3 +111,42 @@ body { .with-performance-bar .layout-page { margin-top: $header-height + $performance-bar-height; } + +.fullscreen-layout { + padding-top: 0; + height: 100vh; + width: 100%; + display: flex; + flex-direction: column; + align-items: stretch; + overflow: hidden; + + > #js-peek, + > .navbar-gitlab { + position: static; + top: auto; + } + + .flash-container { + margin-top: 0; + margin-bottom: 0; + } + + .alert-wrapper .flash-container .flash-alert:last-child, + .alert-wrapper .flash-container .flash-notice:last-child { + margin-bottom: 0; + } + + .content-wrapper { + margin-top: 0; + padding-bottom: 0; + flex: 1; + min-height: 0; + } + + &.flash-shown { + .content-wrapper { + margin-top: 0; + } + } +} diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 4b67eab05b3..fdc0454d837 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -21,11 +21,11 @@ } &.disabled { - color: $list-text-disabled-color; + color: $gl-text-color-tertiary; } &:not(.ui-sort-disabled):hover { - background: $row-hover; + background: $blue-50; } &.unstyled { @@ -35,12 +35,12 @@ } &.warning-row { - background-color: $list-warning-row-bg; - border-color: $list-warning-row-border; - color: $list-warning-row-color; + background-color: $orange-100; + border-color: $orange-200; + color: $orange-700; &:hover { - background: $list-warning-row-bg; + background: $orange-100; } } @@ -73,7 +73,7 @@ } .card.card-body-title { - font-size: $list-font-size; + font-size: $gl-font-size; line-height: 18px; } } @@ -109,8 +109,8 @@ ul.content-list { li { border-color: $white-normal; - font-size: $list-font-size; - color: $list-text-color; + font-size: $gl-font-size; + color: $gl-text-color; &.no-description { .title { diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 7290a174668..d8391b59a8c 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -179,7 +179,7 @@ &:hover, &:focus { svg { - fill: $gl-link-color; + fill: $blue-600; } } } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 98bf26a5222..7edb89ce6f3 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -50,7 +50,7 @@ @include clearfix; padding: 10px 0; - border-bottom: 1px solid $list-border-light; + border-bottom: 1px solid $gray-darker; display: block; margin: 0; diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 6244fb86fea..033e5e57177 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -4,11 +4,6 @@ margin-top: 20px; } - .container-fluid { - padding-left: 5px; - padding-right: 5px; - } - .nav-links > li > a { padding: 10px; font-size: 12px; diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index ffb40166c15..7d53a631cdf 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -79,7 +79,6 @@ body.modal-open { .modal { background-color: $black-transparent; - z-index: 2100; @include media-breakpoint-up(md) { .modal-dialog { diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index b40dcf93969..3ae2c7078d6 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -58,7 +58,7 @@ .select2-drop.select2-drop-above { box-shadow: 0 2px 4px $dropdown-shadow-color; border-radius: $border-radius-base; - border: 1px solid $dropdown-border-color; + border: 1px solid $border-color; min-width: 175px; color: $gl-text-color; z-index: 999; @@ -69,7 +69,7 @@ } .select2-drop.select2-drop-above.select2-drop-active { - border-top: 1px solid $dropdown-border-color; + border-top: 1px solid $border-color; margin-top: -6px; } @@ -153,7 +153,7 @@ transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; &:focus { - border-color: $input-border-focus; + border-color: $blue-300; } &.select2-active { @@ -193,7 +193,7 @@ color: $gl-text-color; .select2-result-label { - background: $dropdown-item-hover-bg; + background: $gray-darker; } } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 473ca408c04..9929f1bdebf 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -7,7 +7,7 @@ } a { - color: $md-link-color; + color: $blue-600; } img:not(.emoji) { @@ -180,7 +180,7 @@ } a > code { - color: $gl-link-color; + color: $blue-600; } dd { @@ -327,7 +327,7 @@ h6 { pre { font-family: $monospace-font; display: block; - padding: $gl-padding-8; + padding: $gl-padding-8 $input-horizontal-padding; margin: 0 0 $gl-padding-8; font-size: 13px; word-break: break-all; @@ -423,25 +423,25 @@ h4 { input, textarea { &::-webkit-input-placeholder { - color: $placeholder-text-color; + color: $gl-text-color-tertiary; } // support firefox 19+ vendor prefix &::-moz-placeholder { - color: $placeholder-text-color; + color: $gl-text-color-tertiary; opacity: 1; // FF defaults to 0.54 } // scss-lint:disable PseudoElement // support Edge vendor prefix &::-ms-input-placeholder { - color: $placeholder-text-color; + color: $gl-text-color-tertiary; } // scss-lint:disable PseudoElement // support IE vendor prefix &:-ms-input-placeholder { - color: $placeholder-text-color; + color: $gl-text-color-tertiary; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 4db9efff6ee..d76f5cbd9ff 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -177,7 +177,6 @@ $border-gray-dark: darken($white-normal, $darken-border-factor); * UI elements */ $border-color: #e5e5e5; -$focus-border-color: $blue-300; $well-expand-item: #e8f2f7; $well-inner-border: #eef0f2; $well-light-border: #f1f1f1; @@ -196,38 +195,20 @@ $gl-text-color-quaternary: #d6d6d6; $gl-text-color-inverted: rgba(255, 255, 255, 1); $gl-text-color-secondary-inverted: rgba(255, 255, 255, 0.85); $gl-text-color-disabled: #919191; -$gl-text-green: $green-600; -$gl-text-green-hover: $green-700; -$gl-text-red: $red-500; -$gl-text-orange: $orange-600; -$gl-link-color: $blue-600; -$gl-link-hover-color: $blue-800; $gl-grayish-blue: #7f8fa4; -$gl-gray: $gl-text-color; $gl-gray-dark: #313236; $gl-gray-light: #5c5c5c; $gl-header-color: #4c4e54; -$gl-header-nav-hover-color: #434343; -$placeholder-text-color: $gl-text-color-tertiary; /* * Lists */ -$list-font-size: $gl-font-size; -$list-title-color: $gl-text-color; -$list-text-color: $gl-text-color; -$list-text-disabled-color: $gl-text-color-tertiary; -$list-border-light: #eee; $list-border: rgba(0, 0, 0, 0.05); $list-text-height: 42px; -$list-warning-row-bg: $orange-100; -$list-warning-row-border: $orange-200; -$list-warning-row-color: $orange-700; /* * Markdown */ -$md-link-color: $gl-link-color; $md-area-border: #ddd; /* @@ -255,12 +236,11 @@ $gl-vert-padding: 6px; $gl-padding-top: 10px; $gl-sidebar-padding: 22px; $gl-bar-padding: 3px; +$input-horizontal-padding: 12px; /* * Misc */ -$row-hover: $blue-50; -$row-hover-border: $blue-200; $progress-color: #c0392b; $header-height: 40px; $ide-statusbar-height: 25px; @@ -268,19 +248,13 @@ $fixed-layout-width: 1280px; $limited-layout-width: 990px; $container-text-max-width: 540px; $gl-avatar-size: 40px; -$error-exclamation-point: $red-500; $border-radius-default: 4px; $border-radius-small: 2px; $settings-icon-size: 18px; -$provider-btn-not-active-color: $blue-500; -$link-underline-blue: $blue-500; -$active-item-blue: $blue-500; $layout-link-gray: #7e7c7c; $btn-side-margin: 10px; $btn-sm-side-margin: 7px; $btn-margin-5: 5px; -$issue-status-expired: $orange-500; -$issuable-sidebar-color: $gl-text-color-secondary; $sidebar-block-hover-color: #ebebeb; $group-path-color: #999; $namespace-kind-color: #aaa; @@ -302,7 +276,6 @@ $breadcrumb-min-height: 48px; * Common component specific colors */ $hint-color: #999; -$well-pre-bg: #eee; $well-pre-color: #555; $loading-color: #555; $update-author-color: #999; @@ -311,10 +284,6 @@ $user-mention-bg-hover: rgba($blue-500, 0.15); $time-color: #999; $project-member-show-color: #aaa; $gl-promo-color: #aaa; -$error-bg: $red-400; -$warning-message-bg: $orange-100; -$warning-message-border: $orange-200; -$warning-message-color: $orange-700; $control-group-descr-color: #666; $table-permission-x-bg: #d9edf7; $username-color: #666; @@ -329,19 +298,9 @@ $tanuki-yellow: #fca326; /* * State colors: */ -$gl-primary: $blue-500; -$gl-success: $green-500; -$gl-success-focus: rgba($gl-success, 0.4); -$gl-info: $blue-500; -$gl-warning: $orange-500; -$gl-danger: $red-500; +$green-500-focus: rgba($green-500, 0.4); $gl-btn-active-background: rgba(0, 0, 0, 0.16); $gl-btn-active-gradient: inset 0 2px 3px $gl-btn-active-background; -// Bootstrap override states -$success: $gl-success; -$info: $gl-info; -$warning: $gl-warning; -$danger: $gl-danger; /* * Commit Diff Colors @@ -361,10 +320,9 @@ $line-select-yellow-dark: #f0e2bd; $dark-diff-match-bg: rgba(255, 255, 255, 0.3); $dark-diff-match-color: rgba(255, 255, 255, 0.1); $file-mode-changed: #777; -$file-mode-changed: #777; -$diff-image-info-color: grey; +$diff-image-info-color: gray; $diff-swipe-border: #999; -$diff-view-modes-color: grey; +$diff-view-modes-color: gray; $diff-view-modes-border: #c1c1c1; $diff-jagged-border-gradient-color: darken($white-normal, 8%); @@ -374,7 +332,7 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%); $monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; $regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, - 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; /* * Dropdowns @@ -384,20 +342,16 @@ $dropdown-min-height: 40px; $dropdown-max-height: 312px; $dropdown-vertical-offset: 4px; $dropdown-link-color: #555; -$dropdown-link-hover-bg: $row-hover; $dropdown-empty-row-bg: rgba(#000, 0.04); -$dropdown-border-color: $border-color; $dropdown-shadow-color: rgba(#000, 0.1); $dropdown-divider-color: rgba(#000, 0.1); $dropdown-title-btn-color: #bfbfbf; $dropdown-input-color: #555; $dropdown-input-fa-color: #c7c7c7; -$dropdown-input-focus-border: $focus-border-color; -$dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, 0.4); +$dropdown-input-focus-shadow: rgba($blue-300, 0.4); $dropdown-loading-bg: rgba(#fff, 0.6); $dropdown-chevron-size: 10px; $dropdown-toggle-active-border-color: darken($border-color, 14%); -$dropdown-item-hover-bg: $gray-darker; $dropdown-fade-mask-height: 32px; $dropdown-member-form-control-width: 163px; @@ -405,7 +359,6 @@ $dropdown-member-form-control-width: 163px; * Filtered Search */ $filtered-search-term-shadow-color: rgba(0, 0, 0, 0.09); -$dropdown-hover-color: $blue-400; /* * Contextual Sidebar @@ -420,7 +373,7 @@ $sidebar-milestone-toggle-bottom-margin: 10px; * Buttons */ $btn-active-gray: #ececec; -$btn-active-gray-light: e4e7ed; +$btn-active-gray-light: #e4e7ed; $btn-white-active: #848484; $gl-btn-padding: 10px; $gl-btn-line-height: 16px; @@ -431,7 +384,6 @@ $gl-btn-horz-padding: 12px; * Badges */ $badge-bg: rgba(0, 0, 0, 0.07); -$badge-color: $gl-text-color-secondary; /* * Pagination @@ -439,21 +391,12 @@ $badge-color: $gl-text-color-secondary; $pagination-padding-y: 6px; $pagination-padding-x: 16px; $pagination-line-height: 20px; -$pagination-border-color: $border-color; -$pagination-active-bg: $blue-600; -$pagination-active-border-color: $blue-600; -$pagination-hover-bg: $blue-50; -$pagination-hover-border-color: $border-color; -$pagination-hover-color: $gl-text-color; $pagination-disabled-color: #cdcdcd; -$pagination-disabled-bg: $gray-light; -$pagination-disabled-border-color: $border-color; /* * Status icons */ $status-icon-size: 22px; -$status-icon-margin: $gl-btn-padding; /* * Award emoji @@ -466,16 +409,13 @@ $award-emoji-positive-add-lines: #bb9c13; * Search Box */ $search-input-border-color: rgba($blue-400, 0.8); -$search-input-focus-shadow-color: $dropdown-input-focus-shadow; $search-input-width: 240px; $search-input-active-width: 320px; -$location-badge-active-bg: $blue-500; $location-icon-color: #e7e9ed; /* * Notes */ -$notes-light-color: $gl-text-color-secondary; $note-disabled-comment-color: #b2b2b2; $note-targe3-outside: #fffff0; $note-targe3-inside: #ffffd3; @@ -496,7 +436,6 @@ $identicon-indigo: #e8eaf6; $identicon-blue: #e3f2fd; $identicon-teal: #e0f2f1; $identicon-orange: #fbe9e7; -$identicon-gray: $gray-darker; $identicon-fg-color: #555555; /* @@ -512,7 +451,6 @@ $calendar-user-contrib-text: #959494; $cycle-analytics-box-padding: 30px; $cycle-analytics-box-text-color: #8c8c8c; $cycle-analytics-big-font: 19px; -$cycle-analytics-dark-text: $gl-text-color; $cycle-analytics-light-gray: #bfbfbf; $cycle-analytics-dismiss-icon-color: #b2b2b2; @@ -540,9 +478,6 @@ $issue-board-list-difference-md: $issue-board-list-difference-sm + $issue-boards * Avatar */ $avatar-radius: 50%; -$avatar-border: $gray-normal; -$avatar-border-hover: $gray-darker; -$avatar-background: $gray-lightest; $gl-avatar-size: 40px; /* @@ -558,22 +493,6 @@ $blame-blue: #254e77; $builds-trace-bg: #111; /* -* Callout -*/ -$callout-danger-bg: $red-100; -$callout-danger-border: $red-200; -$callout-danger-color: $red-700; -$callout-warning-bg: $orange-100; -$callout-warning-border: $orange-200; -$callout-warning-color: $orange-700; -$callout-info-bg: $blue-100; -$callout-info-border: $blue-200; -$callout-info-color: $blue-700; -$callout-success-bg: $green-100; -$callout-success-border: $green-200; -$callout-success-color: $green-700; - -/* * Commit Page */ $commit-max-width-marker-color: rgba(0, 0, 0, 0); @@ -582,16 +501,8 @@ $commit-message-text-area-bg: rgba(0, 0, 0, 0); /* * Common */ -$common-gray: $gl-text-color; $common-gray-light: #bbb; $common-gray-dark: #444; -$common-red: $gl-text-red; -$common-green: $gl-text-green; - -/* -* Editor -*/ -$editor-cancel-color: $red-600; /* * Events @@ -603,11 +514,7 @@ $events-body-border: #ddd; /* * Files */ -$file-image-bg: #eee; -$blob-bg: #eee; -$blame-border: #eee; $blame-line-numbers-border: #ddd; -$logs-bg: #eee; $logs-li-color: #888; $logs-p-color: #333; @@ -616,7 +523,6 @@ $logs-p-color: #333; */ $input-height: 34px; $input-danger-bg: #f2dede; -$input-danger-border: $red-400; $input-group-addon-bg: #f7f8fa; $gl-field-focus-shadow: rgba(0, 0, 0, 0.075); $gl-field-focus-shadow-error: rgba($red-500, 0.6); @@ -663,22 +569,14 @@ $fade-mask-transition-duration: 0.1s; $fade-mask-transition-curve: ease-in-out; /* -* Lint -*/ -$lint-incorrect-color: $red-500; -$lint-correct-color: $green-500; - -/* * Login */ $login-brand-holder-color: #888; -$login-devise-error-color: $red-700; /* * Nav */ $nav-link-gray: #959494; -$nav-badge-bg: #eee; $nav-toggle-gray: #666; /* @@ -686,15 +584,12 @@ $nav-toggle-gray: #666; */ $notify-details: #777; $notify-footer: #777; -$notify-new-file: $green-600; -$notify-deleted-file: $red-700; /* * Projects */ $project-option-descr-color: #54565b; $project-breadcrumb-color: #999; -$project-private-forks-notice-odd: $green-600; $project-network-controls-color: #888; $feature-toggle-color: #fff; @@ -703,21 +598,10 @@ $feature-toggle-color-disabled: #999; $feature-toggle-color-enabled: #4a8bee; /* -* Runners -*/ -$runner-state-shared-bg: $green-400; -$runner-state-specific-bg: $blue-400; -$runner-status-online-color: $green-600; -$runner-status-offline-color: $gray-darkest; -$runner-status-paused-color: $red-500; - -/* Stat Graph */ $stat-graph-common-bg: #f3f3f3; -$stat-graph-area-fill: $green-500; $stat-graph-axis-fill: #aaa; -$stat-graph-orange-fill: $orange-500; $stat-graph-selection-fill: #333; $stat-graph-selection-stroke: #333; @@ -730,7 +614,6 @@ $select2-drop-shadow2: rgba(31, 37, 50, 0.317647); /* * Todo */ -$todo-alert-blue: $blue-500; $todo-body-pre-color: #777; $todo-body-border: #ddd; @@ -753,7 +636,6 @@ $ui-dev-kit-example-border: #ddd; /* Pipeline Graph */ -$stage-hover-bg: $gray-darker; $ci-action-icon-size: 22px; $ci-action-icon-size-lg: 24px; $pipeline-dropdown-line-height: 20px; @@ -781,13 +663,6 @@ Animation Functions $dropdown-animation-timing: cubic-bezier(0.23, 1, 0.32, 1); /* -Convdev Index -*/ -$color-high-score: $green-400; -$color-average-score: $orange-400; -$color-low-score: $red-400; - -/* Performance Bar */ $perf-bar-text: #999; @@ -828,25 +703,5 @@ Modals */ $modal-body-height: 134px; -/* -Prometheus -*/ -$prometheus-table-row-highlight-color: $theme-gray-100; $priority-label-empty-state-width: 114px; - -/* - * Override Bootstrap 4 variables - */ - -$secondary: $gray-light; -$input-disabled-bg: $gray-light; -$input-border-color: $theme-gray-200; -$input-color: $gl-text-color; -$font-family-sans-serif: $regular-font; -$font-family-monospace: $monospace-font; -$input-line-height: 20px; -$btn-line-height: 20px; -$table-accent-bg: $gray-light; -$card-border-color: $border-color; -$card-cap-bg: $gray-light; diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss new file mode 100644 index 00000000000..7d90452e1f4 --- /dev/null +++ b/app/assets/stylesheets/framework/variables_overrides.scss @@ -0,0 +1,20 @@ +/* + * This file is only for overriding Bootstrap 4 variables. + * Please add any new variables to variables.scss + */ + +$secondary: $gray-light; +$input-disabled-bg: $gray-light; +$input-border-color: $theme-gray-200; +$input-color: $gl-text-color; +$font-family-sans-serif: $regular-font; +$font-family-monospace: $monospace-font; +$input-line-height: 20px; +$btn-line-height: 20px; +$table-accent-bg: $gray-light; +$card-border-color: $border-color; +$card-cap-bg: $gray-light; +$success: $green-500; +$info: $blue-500; +$warning: $orange-500; +$danger: $red-500; diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index dbd3144b9b4..f2d296fb875 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -44,7 +44,7 @@ color: $gl-text-color-secondary; &:hover { - color: $gl-link-color; + color: $blue-600; text-decoration: none; } } diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 2b8163b8c68..5ff4e487d04 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -28,11 +28,10 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; .ide-view { position: relative; - display: flex; - height: calc(100vh - #{$header-height}); margin-top: 0; padding-bottom: $ide-statusbar-height; color: $gl-text-color; + min-height: 0; // firefox fix &.is-collapsed { .ide-file-list { @@ -50,7 +49,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; display: flex; flex-direction: column; flex: 1; - min-height: 0; + min-height: 0; // firefox fix .file { height: 32px; @@ -357,7 +356,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; .multi-file-editor-holder { height: 100%; - min-height: 0; + min-height: 0; // firefox fix &.is-readonly, .editor.original { @@ -546,7 +545,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; border-left: 1px solid $white-dark; border-top: 1px solid $white-dark; border-top-left-radius: $border-radius-small; - min-height: 0; + min-height: 0; // firefox fix } } @@ -758,7 +757,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; .ide-loading { display: flex; - height: 100vh; + height: 100%; align-items: center; justify-content: center; } @@ -772,60 +771,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; .ide { overflow: hidden; - - &.nav-only { - padding-top: $header-height; - - .with-performance-bar & { - padding-top: $header-height + $performance-bar-height; - } - - .flash-container { - margin-top: 0; - margin-bottom: 0; - } - - .alert-wrapper .flash-container .flash-alert:last-child, - .alert-wrapper .flash-container .flash-notice:last-child { - margin-bottom: 0; - } - - .content-wrapper { - margin-top: 0; - padding-bottom: 0; - } - - &.flash-shown { - .content-wrapper { - margin-top: 0; - } - - .ide-view { - height: calc(100vh - #{$header-height + $flash-height}); - } - } - } -} - -.with-performance-bar .ide.nav-only { - .flash-container { - margin-top: 0; - } - - .content-wrapper { - margin-top: 0; - padding-bottom: 0; - } - - .ide-view { - height: calc(100vh - #{$header-height + $performance-bar-height}); - } - - &.flash-shown { - .ide-view { - height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height}); - } - } + flex: 1; } .drag-handle { @@ -1158,7 +1104,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; } a { - color: $gl-link-color; + color: $blue-600; } } @@ -1199,7 +1145,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; } .avatar-container { - flex: initial; + flex: 0 0 auto; margin-right: 0; } @@ -1209,7 +1155,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; } .ide-context-body { - min-height: 0; + min-height: 0; // firefox fix } .ide-sidebar-project-title { @@ -1293,6 +1239,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; &.build-page .top-bar { top: 0; + height: auto; font-size: 12px; border-top-right-radius: $border-radius-default; } diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index a68b47b1d02..69d7de886b4 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -225,7 +225,7 @@ outline: 0; &:hover { - color: $gl-link-color; + color: $blue-600; } } @@ -288,7 +288,7 @@ &.is-active, &.is-active .board-card-assignee:hover a { - background-color: $row-hover; + background-color: $blue-50; } .badge { diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index e8158cd7f6b..14ba8b1df83 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -221,7 +221,7 @@ color: $gl-text-color; &:hover { - color: $gl-link-color; + color: $blue-600; text-decoration: none; } } @@ -397,7 +397,7 @@ } &:hover { - background-color: $dropdown-item-hover-bg; + background-color: $gray-darker; } .icon-retry { diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index bce83bf0dd0..10764e0f3df 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -279,7 +279,7 @@ } &.autodevops-link { - color: $gl-link-color; + color: $blue-600; } } @@ -321,7 +321,7 @@ } .commit-sha { - color: $gl-link-color; + color: $blue-600; } .commit-row-message { diff --git a/app/assets/stylesheets/pages/convdev_index.scss b/app/assets/stylesheets/pages/convdev_index.scss index bd338326154..52fcdf4a405 100644 --- a/app/assets/stylesheets/pages/convdev_index.scss +++ b/app/assets/stylesheets/pages/convdev_index.scss @@ -80,7 +80,7 @@ $space-between-cards: 8px; } .convdev-card-low { - border-top-color: $color-low-score; + border-top-color: $red-400; .board-card-score-big { background-color: $red-50; @@ -88,7 +88,7 @@ $space-between-cards: 8px; } .convdev-card-average { - border-top-color: $color-average-score; + border-top-color: $orange-400; .board-card-score-big { background-color: $orange-50; @@ -96,7 +96,7 @@ $space-between-cards: 8px; } .convdev-card-high { - border-top-color: $color-high-score; + border-top-color: $green-400; .board-card-score-big { background-color: $green-50; @@ -243,13 +243,13 @@ $space-between-cards: 8px; } .convdev-high-score { - color: $color-high-score; + color: $green-400; } .convdev-average-score { - color: $color-average-score; + color: $orange-400; } .convdev-low-score { - color: $color-low-score; + color: $red-400; } diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index e2c0a7a6225..f0228768b5a 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -165,7 +165,7 @@ border-right-color: transparent; border-top-color: $border-color; border-bottom-color: $border-color; - box-shadow: inset 2px 0 0 0 $active-item-blue; + box-shadow: inset 2px 0 0 0 $blue-500; .stage-name { font-weight: $gl-font-weight-bold; @@ -285,7 +285,7 @@ .total-time { font-size: $cycle-analytics-big-font; - color: $cycle-analytics-dark-text; + color: $gl-text-color; span { color: $gl-text-color; @@ -360,7 +360,7 @@ } .commit-sha { - color: $gl-link-color; + color: $blue-600; line-height: 1.3; vertical-align: top; font-weight: $gl-font-weight-normal; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 591e21243ed..7d7143631f2 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -25,10 +25,6 @@ color: $gl-text-color; border-radius: 0 0 3px 3px; - .code { - padding: 0; - } - .unfold { cursor: pointer; } @@ -186,7 +182,7 @@ } .image { - background: $file-image-bg; + background: $gray-darker; text-align: center; padding: 30px; @@ -511,13 +507,13 @@ padding: 0; background-color: transparent; border: 0; - color: $gl-link-color; + color: $blue-600; font-weight: $gl-font-weight-bold; &:hover, &:focus { outline: none; - color: $gl-link-hover-color; + color: $blue-800; } .caret-icon { diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index ddd1f8cc98a..04570c057d1 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -23,10 +23,10 @@ } .cancel-btn { - color: $editor-cancel-color; + color: $red-600; &:hover { - color: $editor-cancel-color; + color: $red-600; } } @@ -36,10 +36,7 @@ line-height: 35px; padding-top: 7px; padding-bottom: 7px; - - .float-right { - height: 20px; - } + display: flex; } .editor-ref { @@ -60,20 +57,18 @@ .new-file-name { display: inline-block; - max-width: 450px; + max-width: 420px; float: left; @media(max-width: map-get($grid-breakpoints, lg)-1) { - width: 280px; - } - - @media(max-width: map-get($grid-breakpoints, md)-1) { width: 180px; } } .file-buttons { - font-size: 0; + display: flex; + flex: 1; + justify-content: flex-end; } .select2 { @@ -111,12 +106,10 @@ } -@include media-breakpoint-down(xs) { +@include media-breakpoint-down(sm) { .file-editor { .file-title { - .float-right { - height: auto; - } + display: block; } .new-file-name { @@ -144,6 +137,10 @@ } } } + + .editor-ref { + max-width: 250px; + } } } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 8a074017344..196f6ae6d8c 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -479,10 +479,10 @@ .deploy-info-text-link { font-family: $monospace-font; - fill: $gl-link-color; + fill: $blue-600; &:hover { - fill: $gl-link-hover-color; + fill: $blue-800; } } @@ -501,5 +501,5 @@ } .prometheus-table-row-highlight { - background-color: $prometheus-table-row-highlight-color; + background-color: $theme-gray-100; } diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index f79586b68b9..da0c9b44498 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -6,7 +6,7 @@ font-size: $gl-font-size; padding: $gl-padding-top 0 $gl-padding-top 40px; border-bottom: 1px solid $white-normal; - color: $list-text-color; + color: $gl-text-color; position: relative; &.event-inline { @@ -58,7 +58,7 @@ .event-title { @include str-truncated(calc(100% - 174px)); font-weight: $gl-font-weight-bold; - color: $list-text-color; + color: $gl-text-color; } .event-body { diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss index 49d8a5d959b..22fce893fd7 100644 --- a/app/assets/stylesheets/pages/graph.scss +++ b/app/assets/stylesheets/pages/graph.scss @@ -24,11 +24,11 @@ } .graph-additions { - color: $gl-text-green; + color: $green-600; } .graph-deletions { - color: $gl-text-red; + color: $red-500; } } diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 1587aebfe1d..60b4d39bb1a 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -385,8 +385,8 @@ padding: $gl-padding-top; &:hover { - border-color: $row-hover-border; - background-color: $row-hover; + border-color: $blue-200; + background-color: $blue-50; cursor: pointer; } @@ -419,6 +419,7 @@ .stats { position: relative; line-height: normal; + text-align: right; flex-shrink: 0; > span { @@ -464,7 +465,7 @@ } .last-updated { - position: absolute; + position: relative; right: 12px; min-width: 250px; text-align: right; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 8e78d9f65eb..9ac47a771a5 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -141,10 +141,10 @@ color: inherit; &:hover { - color: $gl-link-hover-color; + color: $blue-800; .avatar { - border-color: rgba($avatar-border, .2); + border-color: rgba($gray-normal, .2); } } @@ -231,7 +231,7 @@ } a.edit-link:not([href]):hover { - color: rgba($avatar-border, .2); + color: rgba($gray-normal, .2); } .lock-edit, // uses same style, different js behaviour @@ -241,7 +241,7 @@ &:hover { text-decoration: underline; - color: $gl-link-hover-color; + color: $blue-800; } } } @@ -329,7 +329,7 @@ } .btn-secondary-hover-link:hover { - color: $gl-link-color; + color: $blue-600; } .sidebar-collapsed-icon { @@ -423,10 +423,10 @@ width: 100%; text-align: center; margin-bottom: 10px; - color: $issuable-sidebar-color; + color: $gl-text-color-secondary; svg { - fill: $issuable-sidebar-color; + fill: $gl-text-color-secondary; } &:hover:not(.disabled), @@ -448,8 +448,8 @@ } .todo-undone { - color: $gl-link-color; - fill: $gl-link-color; + color: $blue-600; + fill: $blue-600; } .author { @@ -457,14 +457,14 @@ } .avatar-counter:hover { - color: $issuable-sidebar-color; - border-color: $issuable-sidebar-color; + color: $gl-text-color-secondary; + border-color: $gl-text-color-secondary; } .btn-clipboard { border: 0; background: transparent; - color: $issuable-sidebar-color; + color: $gl-text-color-secondary; &:hover { color: $gl-text-color; @@ -821,7 +821,7 @@ svg { width: 16px; height: 16px; - fill: $issuable-sidebar-color; + fill: $gl-text-color-secondary; } &:hover svg { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 212e5979273..0f95fb911e1 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -157,7 +157,7 @@ ul.related-merge-requests > li { .issuable-email-modal-btn { padding: 0; - color: $gl-link-color; + color: $blue-600; background-color: transparent; border: 0; outline: 0; @@ -190,7 +190,7 @@ ul.related-merge-requests > li { .create-mr-dropdown-wrap { .ref::selection { - color: $placeholder-text-color; + color: $gl-text-color-tertiary; } .dropdown { diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 2b40404971c..d2b9470be69 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -67,7 +67,7 @@ .dropdown-labels-error { padding: 5px 10px; margin-bottom: 10px; - background-color: $gl-danger; + background-color: $red-500; color: $white-light; } @@ -114,10 +114,10 @@ } &:hover { - color: $gl-link-color; + color: $blue-600; &.remove-row { - color: $gl-danger; + color: $red-500; } } } @@ -253,7 +253,7 @@ text-align: right; padding: 0; position: relative; - top: -3px; + margin: 0; } .label-badge { @@ -274,6 +274,7 @@ .label-links { list-style: none; + margin: 0; padding: 0; white-space: nowrap; } @@ -342,10 +343,10 @@ &.remove-row { &:hover { - color: $gl-text-red; + color: $red-500; svg { - fill: $gl-text-red; + fill: $red-500; } } } diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 8a4a2caa6c9..c9e5fb9c579 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -186,7 +186,7 @@ h2 { margin-top: 0; font-size: 14px; - color: $login-devise-error-color; + color: $red-700; } } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 621321101cd..7b8cad254c7 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -195,12 +195,13 @@ .ci-widget-content { display: flex; align-items: center; + flex: 1; } } .mr-widget-icon { font-size: 22px; - margin-right: $status-icon-margin; + margin-right: $gl-btn-padding; } .ci-status-icon svg { @@ -222,6 +223,7 @@ .normal { flex: 1; + flex-basis: auto; } .capitalize { @@ -235,22 +237,23 @@ font-weight: normal; overflow: hidden; word-break: break-all; + } - &.label-truncated { - position: relative; - display: inline-block; - width: 250px; - margin-bottom: -3px; - white-space: nowrap; - text-overflow: clip; - line-height: 14px; - - &::after { - position: absolute; - content: '...'; - right: 0; - font-family: $regular-font; - background-color: $gray-light; + .deploy-link, + .label-branch { + &.label-truncate { + // NOTE: This selector targets its children because some of the HTML comes from + // 'source_branch_link'. Once this external HTML is no longer used, we could + // simplify this. + > a, + > span { + display: inline-block; + max-width: 12.5em; + margin-bottom: -3px; + white-space: nowrap; + text-overflow: ellipsis; + line-height: 14px; + overflow: hidden; } } } @@ -281,7 +284,7 @@ margin-bottom: 0; &.has-conflicts .fa-exclamation-triangle { - color: $gl-warning; + color: $orange-500; } time { @@ -313,7 +316,7 @@ } .danger { - color: $gl-danger; + color: $red-500; } .spacing, @@ -514,7 +517,7 @@ } .mr-links { - padding-left: $status-icon-size + $status-icon-margin; + padding-left: $status-icon-size + $gl-btn-padding; } .mr-info-list { @@ -582,7 +585,7 @@ @include media-breakpoint-down(md) { flex-direction: column; - align-items: flex-start; + align-items: stretch; .branch-actions { margin-top: 16px; @@ -593,13 +596,13 @@ .branch-actions { align-self: center; margin-left: $gl-padding; + white-space: nowrap; } } } .diverged-commits-count { color: $gl-text-color-secondary; - font-size: 12px; } } @@ -918,7 +921,7 @@ flex: 1; flex-direction: row; - @include media-breakpoint-down(md) { + @include media-breakpoint-down(sm) { flex-direction: column; .stage-cell .stage-container { diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 46437ce5841..1e92582d6d9 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -30,7 +30,7 @@ .milestone-progress { a { - color: $gl-link-color; + color: $blue-600; } } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index dcf590e7331..ac7b701c2e2 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -69,18 +69,18 @@ .comment-toolbar, .nav-links { - border-color: $focus-border-color; + border-color: $blue-300; } } &.is-dropzone-hover { - border-color: $gl-success; + border-color: $green-500; box-shadow: 0 0 2px $black-transparent, - 0 0 4px $gl-success-focus; + 0 0 4px $green-500-focus; .comment-toolbar, .nav-links { - border-color: $gl-success; + border-color: $green-500; } } } @@ -306,7 +306,7 @@ &:hover, &:focus { - color: $gl-link-color; + color: $blue-600; outline: 0; } @@ -424,7 +424,7 @@ .uploading-error-icon, .uploading-error-message { - color: $gl-text-red; + color: $red-500; } .uploading-error-message { @@ -443,7 +443,7 @@ .attach-new-file, .button-attach-file, .retry-uploading-link { - color: $gl-link-color; + color: $blue-600; padding: 0; background: none; border: 0; @@ -452,5 +452,5 @@ } .markdown-selector { - color: $gl-link-color; + color: $blue-600; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index c369d89d63c..dbe9f0c03fb 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -210,7 +210,7 @@ ul.notes { } a { - color: $gl-link-color; + color: $blue-600; } p { @@ -253,14 +253,14 @@ ul.notes { overflow: hidden; .system-note-commit-list-toggler { - color: $gl-link-color; + color: $blue-600; padding: 10px 0 0; cursor: pointer; position: relative; z-index: 2; &:hover { - color: $gl-link-color; + color: $blue-600; text-decoration: underline; } } @@ -390,7 +390,7 @@ ul.notes { color: inherit; &:hover { - color: $gl-link-color; + color: $blue-600; } &:focus, @@ -446,12 +446,12 @@ ul.notes { .note-headline-light, .discussion-headline-light { - color: $notes-light-color; + color: $gl-text-color-secondary; } .discussion-headline-light { a { - color: $gl-link-color; + color: $blue-600; } } @@ -560,12 +560,12 @@ ul.notes { &:hover, &.is-active { .danger-highlight { - color: $gl-text-red; + color: $red-500; } .link-highlight { - color: $gl-link-color; - fill: $gl-link-color; + color: $blue-600; + fill: $blue-600; } .award-control-icon-neutral { @@ -597,13 +597,13 @@ ul.notes { transition: color 0.1s linear; &:hover { - color: $gl-link-color; + color: $blue-600; } &:focus { text-decoration: underline; outline: none; - color: $gl-link-color; + color: $blue-600; } .fa { @@ -673,7 +673,7 @@ ul.notes { } a { - color: $gl-link-color; + color: $blue-600; } } @@ -759,16 +759,16 @@ ul.notes { &:not(.is-disabled) { &:hover, &:focus { - color: $gl-text-green; + color: $green-600; } } &.is-active { - color: $gl-text-green; + color: $green-600; &:hover, &:focus { - color: $gl-text-green-hover; + color: $green-700; } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index b68c89c25d8..8bb8b83dc5e 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -19,7 +19,7 @@ background-color: $white-light; &:hover { - background-color: $stage-hover-bg; + background-color: $gray-darker; border: 1px solid $dropdown-toggle-active-border-color; color: $gl-text-color; } @@ -175,7 +175,7 @@ } .commit-sha { - color: $gl-link-color; + color: $blue-600; } .badge { @@ -595,7 +595,7 @@ a.build-content:hover, button.build-content:hover { - background-color: $stage-hover-bg; + background-color: $gray-darker; border: 1px solid $dropdown-toggle-active-border-color; } @@ -668,7 +668,7 @@ display: block; &:hover { - background-color: $stage-hover-bg; + background-color: $gray-darker; border: 1px solid $dropdown-toggle-active-border-color; svg { @@ -835,7 +835,7 @@ button.mini-pipeline-graph-dropdown-toggle { display: block; &:hover { - background-color: $stage-hover-bg; + background-color: $gray-darker; border: 1px solid $dropdown-toggle-active-border-color; svg { @@ -934,7 +934,7 @@ button.mini-pipeline-graph-dropdown-toggle { &:focus { outline: none; text-decoration: none; - background-color: $stage-hover-bg; + background-color: $gray-darker; } } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index b45e305897c..17f34319050 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -20,7 +20,7 @@ .account-btn-link, .profile-settings-sidebar a, .settings-sidebar a { - color: $md-link-color; + color: $blue-600; } .private-tokens-reset div.reset-action:not(:first-child) { @@ -137,7 +137,7 @@ .profile-settings-content { a { - color: $md-link-color; + color: $blue-600; } } @@ -170,7 +170,7 @@ background-color: $gray-light; &.not-active { - color: $provider-btn-not-active-color; + color: $blue-500; } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 944421604fe..a95e78931b1 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -388,29 +388,29 @@ line-height: $gl-btn-line-height; &:hover { - color: $gl-link-color; + color: $blue-600; } } } .vs-public { - color: $gl-primary; + color: $blue-500; } .vs-internal { - color: $gl-warning; + color: $orange-500; } .vs-private { - color: $gl-success; + color: $green-500; } .lfs-enabled { - color: $gl-success; + color: $green-500; } .lfs-disabled { - color: $gl-warning; + color: $orange-500; } .breadcrumb.repo-breadcrumb { @@ -449,8 +449,8 @@ &:hover:not(.disabled), &.forked { - background-color: $row-hover; - border-color: $row-hover-border; + background-color: $blue-50; + border-color: $blue-200; } .avatar-container, @@ -731,7 +731,7 @@ background-color: transparent; font-size: $gl-font-size; line-height: $gl-btn-line-height; - color: $notes-light-color; + color: $gl-text-color-secondary; } .stat-link { @@ -754,8 +754,19 @@ } .repository-languages-bar { - height: 6px; - margin-bottom: 8px; + height: 8px; + margin-bottom: $gl-padding-8; + background-color: $white-light; + border-radius: $border-radius-default; + + .progress-bar { + margin-right: 2px; + padding: 0 $gl-padding-4; + + &:last-child { + margin-right: 0; + } + } } pre.light-well { @@ -823,10 +834,6 @@ pre.light-well { .avatar-container { align-self: flex-start; - - > a { - width: 100%; - } } .project-details { @@ -887,13 +894,13 @@ pre.light-well { .cannot-be-merged, .cannot-be-merged:hover { - color: $error-exclamation-point; + color: $red-500; margin-top: 2px; } .private-forks-notice .private-fork-icon { i:nth-child(1) { - color: $project-private-forks-notice-odd; + color: $green-600; } i:nth-child(2) { @@ -954,7 +961,7 @@ pre.light-well { margin-left: 5px; &.is-done { - color: $gl-text-green; + color: $green-600; } } @@ -1121,12 +1128,12 @@ pre.light-well { .project-ci-body { .incorrect-syntax { font-size: 18px; - color: $lint-incorrect-color; + color: $red-500; } .correct-syntax { font-size: 18px; - color: $lint-correct-color; + color: $green-500; } } diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss index 2734faec558..59f01f3e958 100644 --- a/app/assets/stylesheets/pages/runners.scss +++ b/app/assets/stylesheets/pages/runners.scss @@ -4,24 +4,24 @@ color: $white-light; &.runner-state-shared { - background: $runner-state-shared-bg; + background: $green-400; } &.runner-state-specific { - background: $runner-state-specific-bg; + background: $blue-400; } } .runner-status-online { - color: $runner-status-online-color; + color: $green-600; } .runner-status-offline { - color: $runner-status-offline-color; + color: $gray-darkest; } .runner-status-paused { - color: $runner-status-paused-color; + color: $red-500; } .runner { diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 60b280fd12e..77119aea9e2 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -23,13 +23,13 @@ $search-avatar-size: 16px; .search-text-input:hover, .form-control:hover, :not[readonly] { - border-color: lighten($dropdown-input-focus-border, 20%); - box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%); + border-color: lighten($blue-300, 20%); + box-shadow: 0 0 4px lighten($dropdown-input-focus-shadow, 20%); } input[type='checkbox']:hover { - box-shadow: 0 0 2px 2px lighten($search-input-focus-shadow-color, 20%), - 0 0 0 1px lighten($search-input-focus-shadow-color, 20%); + box-shadow: 0 0 2px 2px lighten($dropdown-input-focus-shadow, 20%), + 0 0 0 1px lighten($dropdown-input-focus-shadow, 20%); } .search { @@ -127,7 +127,7 @@ input[type='checkbox']:hover { &.search-active { form { @extend .form-control:focus; - border-color: $dropdown-input-focus-border; + border-color: $blue-300; box-shadow: none; @include media-breakpoint-up(xl) { @@ -181,7 +181,7 @@ input[type='checkbox']:hover { width: $search-avatar-size; height: $search-avatar-size; border-radius: 50%; - border: 1px solid $avatar-border; + border: 1px solid $gray-normal; } } @@ -259,6 +259,6 @@ input[type='checkbox']:hover { &:hover, &:focus { - color: $gl-link-color; + color: $blue-600; } } diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 839ac5ba59b..e351dd7c0bb 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -113,18 +113,18 @@ .settings-message { padding: 5px; line-height: 1.3; - color: $warning-message-color; - background-color: $warning-message-bg; - border: 1px solid $warning-message-border; + color: $orange-700; + background-color: $orange-100; + border: 1px solid $orange-200; border-radius: $border-radius-base; } .warning-title { - color: $gl-warning; + color: $orange-500; } .danger-title { - color: $gl-danger; + color: $red-500; } .integration-settings-form { @@ -301,3 +301,17 @@ margin-bottom: 0; } } + +.mirror-error-badge { + background-color: $red-400; + border-radius: $border-radius-default; + color: $white-light; +} + +.push-pull-table { + margin-top: 1em; + + .mirror-action-buttons { + padding-right: 0; + } +} diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss index 3f6f5f06075..d331edaa302 100644 --- a/app/assets/stylesheets/pages/stat_graph.scss +++ b/app/assets/stylesheets/pages/stat_graph.scss @@ -5,7 +5,7 @@ } .area { - fill: $stat-graph-area-fill; + fill: $green-500; fill-opacity: 0.5; } @@ -54,7 +54,7 @@ } .area-contributor { - fill: $stat-graph-orange-fill; + fill: $orange-500; } } } diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 010a2c05a1c..5d3b7b21ce4 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -12,8 +12,8 @@ flex-direction: row; &:hover { - background-color: $row-hover; - border-color: $row-hover-border; + background-color: $blue-50; + border-color: $blue-200; cursor: pointer; } @@ -22,7 +22,7 @@ border-bottom: 1px solid transparent; &:hover { - border-color: $row-hover-border; + border-color: $blue-200; } } diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 1cc26d40ba9..dc5ca78ff58 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -111,9 +111,9 @@ &:hover:not(.tree-truncated-warning) { td { - background-color: $row-hover; - border-top: 1px solid $row-hover-border; - border-bottom: 1px solid $row-hover-border; + background-color: $blue-50; + border-top: 1px solid $blue-200; + border-bottom: 1px solid $blue-200; cursor: pointer; } } @@ -229,7 +229,7 @@ .upload-link { font-weight: $gl-font-weight-normal; - color: $md-link-color; + color: $blue-600; } .repo-charts { diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss index 7a93c4dfa28..57d43beaf21 100644 --- a/app/assets/stylesheets/performance_bar.scss +++ b/app/assets/stylesheets/performance_bar.scss @@ -6,7 +6,7 @@ left: 0; top: 0; width: 100%; - z-index: 2000; + z-index: 1039; height: $performance-bar-height; background: $black; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 05ed3669a41..e5b38898a67 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -11,7 +11,6 @@ class ApplicationController < ActionController::Base include EnforcesTwoFactorAuthentication include WithPerformanceBar - before_action :limit_unauthenticated_session_times before_action :authenticate_sessionless_user! before_action :authenticate_user! before_action :enforce_terms!, if: :should_enforce_terms? @@ -27,6 +26,7 @@ class ApplicationController < ActionController::Base around_action :set_locale after_action :set_page_title_header, if: :json_request? + after_action :limit_unauthenticated_session_times protect_from_forgery with: :exception, prepend: true diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 86bade49ec9..9e30b982b06 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -1,67 +1,39 @@ class AutocompleteController < ApplicationController - AWARD_EMOJI_MAX = 100 - skip_before_action :authenticate_user!, only: [:users, :award_emojis] - before_action :load_project, only: [:users] - before_action :load_group, only: [:users] def users - @users = AutocompleteUsersFinder.new(params: params, current_user: current_user, project: @project, group: @group).execute - - render json: UserSerializer.new.represent(@users) - end - - def user - @user = User.find(params[:id]) - render json: UserSerializer.new.represent(@user) - end - - def projects - project = Project.find_by_id(params[:project_id]) - projects = projects_finder.execute(project, search: params[:search], offset_id: params[:offset_id]) + project = Autocomplete::ProjectFinder + .new(current_user, params) + .execute - render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace) - end + group = Autocomplete::GroupFinder + .new(current_user, project, params) + .execute - def award_emojis - emoji_with_count = AwardEmoji - .limit(AWARD_EMOJI_MAX) - .where(user: current_user) - .group(:name) - .order('count_all DESC, name ASC') - .count + users = Autocomplete::UsersFinder + .new(params: params, current_user: current_user, project: project, group: group) + .execute - # Transform from hash to array to guarantee json order - # e.g. { 'thumbsup' => 2, 'thumbsdown' = 1 } - # => [{ name: 'thumbsup' }, { name: 'thumbsdown' }] - render json: emoji_with_count.map { |k, v| { name: k } } + render json: UserSerializer.new.represent(users) end - private - - def load_group - @group ||= begin - if @project.blank? && params[:group_id].present? - group = Group.find(params[:group_id]) - return render_404 unless can?(current_user, :read_group, group) + def user + user = UserFinder.new(params).execute! - group - end - end + render json: UserSerializer.new.represent(user) end - def load_project - @project ||= begin - if params[:project_id].present? - project = Project.find(params[:project_id]) - return render_404 unless can?(current_user, :read_project, project) + # Displays projects to use for the dropdown when moving a resource from one + # project to another. + def projects + projects = Autocomplete::MoveToProjectFinder + .new(current_user, params) + .execute - project - end - end + render json: MoveToProjectSerializer.new.represent(projects) end - def projects_finder - MoveToProjectFinder.new(current_user) + def award_emojis + render json: AwardedEmojiFinder.new(current_user).execute end end diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 2ef2ee76855..a2c96f5d635 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -1,5 +1,6 @@ module IssuableCollections extend ActiveSupport::Concern + include CookiesHelper include SortingHelper include Gitlab::IssuableMetadata include Gitlab::Utils::StrongMemoize @@ -107,11 +108,18 @@ module IssuableCollections end def set_sort_order_from_cookie - key = 'issuable_sort' + sort_param = params[:sort] if params[:sort].present? + # fallback to legacy cookie value for backward compatibility + sort_param ||= cookies['issuable_sort'] + sort_param ||= cookies[remember_sorting_key] - cookies[key] = params[:sort] if params[:sort].present? - cookies[key] = update_cookie_value(cookies[key]) - params[:sort] = cookies[key] + sort_value = update_cookie_value(sort_param) + set_secure_cookie(remember_sorting_key, sort_value) + params[:sort] = sort_value + end + + def remember_sorting_key + @remember_sorting_key ||= "#{collection_type.downcase}_sort" end def default_sort_order @@ -140,16 +148,14 @@ module IssuableCollections end def finder - strong_memoize(:finder) do - issuable_finder_for(finder_type) - end + @finder ||= issuable_finder_for(finder_type) end def collection_type - @collection_type ||= case finder - when IssuesFinder + @collection_type ||= case finder_type.name + when 'IssuesFinder' 'Issue' - when MergeRequestsFinder + when 'MergeRequestsFinder' 'MergeRequest' end end diff --git a/app/controllers/concerns/renders_commits.rb b/app/controllers/concerns/renders_commits.rb index fb41dc1e8a8..b1c9b1e532f 100644 --- a/app/controllers/concerns/renders_commits.rb +++ b/app/controllers/concerns/renders_commits.rb @@ -1,4 +1,24 @@ module RendersCommits + def limited_commits(commits) + if commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE + [ + commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE), + commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE + ] + else + [commits, 0] + end + end + + # This is used as a helper method in a controller. + # rubocop: disable Gitlab/ModuleWithInstanceVariables + def set_commits_for_rendering(commits) + @total_commit_count = commits.size + limited, @hidden_commit_count = limited_commits(commits) + prepare_commits_for_rendering(limited) + end + # rubocop: enable Gitlab/ModuleWithInstanceVariables + def prepare_commits_for_rendering(commits) Banzai::CommitRenderer.render(commits, @project, current_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables diff --git a/app/controllers/concerns/sends_blob.rb b/app/controllers/concerns/sends_blob.rb new file mode 100644 index 00000000000..971390d9118 --- /dev/null +++ b/app/controllers/concerns/sends_blob.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module SendsBlob + extend ActiveSupport::Concern + + included do + include BlobHelper + include SendFileUpload + end + + def send_blob(blob, params = {}) + if blob + headers['X-Content-Type-Options'] = 'nosniff' + + return if cached_blob?(blob) + + if blob.stored_externally? + send_lfs_object(blob) + else + send_git_blob(repository, blob, params) + end + else + render_404 + end + end + + private + + def cached_blob?(blob) + stale = stale?(etag: blob.id) # The #stale? method sets cache headers. + + # Because we are opinionated we set the cache headers ourselves. + response.cache_control[:public] = project.public? + + response.cache_control[:max_age] = + if @ref && @commit && @ref == @commit.id # rubocop:disable Gitlab/ModuleWithInstanceVariables + # This is a link to a commit by its commit SHA. That means that the blob + # is immutable. The only reason to invalidate the cache is if the commit + # was deleted or if the user lost access to the repository. + Blob::CACHE_TIME_IMMUTABLE + else + # A branch or tag points at this blob. That means that the expected blob + # value may change over time. + Blob::CACHE_TIME + end + + response.etag = blob.id + !stale + end + + def send_lfs_object(blob) + lfs_object = find_lfs_object(blob) + + if lfs_object && lfs_object.project_allowed_access?(project) + send_upload(lfs_object.file, attachment: blob.name) + else + render_404 + end + end + + def find_lfs_object(blob) + lfs_object = LfsObject.find_by_oid(blob.lfs_oid) + if lfs_object && lfs_object.file.exists? + lfs_object + else + nil + end + end +end diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb index ba5b7d33f87..ae0b815f85e 100644 --- a/app/controllers/concerns/toggle_award_emoji.rb +++ b/app/controllers/concerns/toggle_award_emoji.rb @@ -5,7 +5,7 @@ module ToggleAwardEmoji authenticate_user! name = params.require(:name) - if awardable.user_can_award?(current_user, name) + if awardable.user_can_award?(current_user) awardable.toggle_award_emoji(name, current_user) todoable = to_todoable(awardable) diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 9bd51de7e97..6bdc0f79ef2 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -2,8 +2,8 @@ class Groups::MilestonesController < Groups::ApplicationController include MilestoneActions before_action :group_projects - before_action :milestone, only: [:edit, :show, :update, :merge_requests, :participants, :labels] - before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update] + before_action :milestone, only: [:edit, :show, :update, :merge_requests, :participants, :labels, :destroy] + before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update, :destroy] def index respond_to do |format| @@ -56,10 +56,21 @@ class Groups::MilestonesController < Groups::ApplicationController redirect_to milestone_path end + def destroy + return render_404 if @milestone.legacy_group_milestone? + + Milestones::DestroyService.new(group, current_user).execute(@milestone) + + respond_to do |format| + format.html { redirect_to group_milestones_path(group), status: :see_other } + format.js { head :ok } + end + end + private def authorize_admin_milestones! - return render_404 unless can?(current_user, :admin_milestones, group) + return render_404 unless can?(current_user, :admin_milestone, group) end def milestone_params diff --git a/app/controllers/groups/settings/badges_controller.rb b/app/controllers/groups/settings/badges_controller.rb deleted file mode 100644 index ccbd0a3bc02..00000000000 --- a/app/controllers/groups/settings/badges_controller.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Groups - module Settings - class BadgesController < Groups::ApplicationController - include API::Helpers::RelatedResourcesHelpers - - before_action :authorize_admin_group! - - def index - @badge_api_endpoint = expose_url(api_v4_groups_badges_path(id: @group.id)) - end - end - end -end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 83169636ccf..e57b9ff23a7 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,4 +1,5 @@ class GroupsController < Groups::ApplicationController + include API::Helpers::RelatedResourcesHelpers include IssuesAction include MergeRequestsAction include ParamsBackwardCompatibility @@ -77,6 +78,7 @@ class GroupsController < Groups::ApplicationController end def edit + @badge_api_endpoint = expose_url(api_v4_groups_badges_path(id: @group.id)) end def projects diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb index 1ff25a45398..96bb2237d90 100644 --- a/app/controllers/ide_controller.rb +++ b/app/controllers/ide_controller.rb @@ -1,5 +1,5 @@ class IdeController < ApplicationController - layout 'nav_only' + layout 'fullscreen' def index end diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb index ed20302487c..461f26561f1 100644 --- a/app/controllers/notification_settings_controller.rb +++ b/app/controllers/notification_settings_controller.rb @@ -5,14 +5,14 @@ class NotificationSettingsController < ApplicationController return render_404 unless can_read?(resource) @notification_setting = current_user.notification_settings_for(resource) - @saved = @notification_setting.update(notification_setting_params) + @saved = @notification_setting.update(notification_setting_params_for(resource)) render_response end def update @notification_setting = current_user.notification_settings.find(params[:id]) - @saved = @notification_setting.update(notification_setting_params) + @saved = @notification_setting.update(notification_setting_params_for(@notification_setting.source)) render_response end @@ -42,8 +42,8 @@ class NotificationSettingsController < ApplicationController } end - def notification_setting_params - allowed_fields = NotificationSetting::EMAIL_EVENTS.dup + def notification_setting_params_for(source) + allowed_fields = NotificationSetting.email_events(source).dup allowed_fields << :level params.require(:notification_setting).permit(allowed_fields) end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index b4f814fd3a4..695ffd90a85 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -1,4 +1,5 @@ class Projects::ApplicationController < ApplicationController + include CookiesHelper include RoutableActions include ChecksCollaboration @@ -74,7 +75,7 @@ class Projects::ApplicationController < ApplicationController end def apply_diff_view_cookie! - cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present? + set_secure_cookie(:diff_view, params.delete(:view), permanent: true) if params[:view].present? end def require_pages_enabled! diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb index 07627ffb69f..a8f73ed5cb0 100644 --- a/app/controllers/projects/autocomplete_sources_controller.rb +++ b/app/controllers/projects/autocomplete_sources_controller.rb @@ -32,13 +32,8 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController end def target - case params[:type]&.downcase - when 'issue' - IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:type_id]) - when 'mergerequest' - MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: params[:type_id]) - when 'commit' - @project.commit(params[:type_id]) - end + QuickActions::TargetService + .new(project, current_user) + .execute(params[:type], params[:type_id]) end end diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb index 53fdc5843b5..878c82cd183 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -1,24 +1,16 @@ class Projects::AvatarsController < Projects::ApplicationController - include BlobHelper + include SendsBlob before_action :authorize_admin_project!, only: [:destroy] def show @blob = @repository.blob_at_branch(@repository.root_ref, @project.avatar_in_git) - if @blob - headers['X-Content-Type-Options'] = 'nosniff' - return if cached_blob? - - send_git_blob @repository, @blob - else - render_404 - end + send_blob(@blob) end def destroy @project.remove_avatar! - @project.save redirect_to edit_project_path(@project, anchor: 'js-general-project-settings'), status: :found diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index ebc61264b39..56dafa31332 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -127,7 +127,7 @@ class Projects::BlobController < Projects::ApplicationController add_match_line - render json: @lines + render json: DiffLineSerializer.new.represent(@lines) end def add_match_line diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 44b176d304e..53637780a07 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -101,7 +101,7 @@ class Projects::CommitController < Projects::ApplicationController @branch_name = create_new_branch? ? @commit.cherry_pick_branch_name : @start_branch - create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked.", + create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked into #{@branch_name}.", success_path: -> { successful_change_path }, failure_path: failed_change_path) end diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 36faea8056e..5546bef850b 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -63,7 +63,7 @@ class Projects::CommitsController < Projects::ApplicationController end @commits = @commits.with_pipeline_status - @commits = prepare_commits_for_rendering(@commits) + @commits = set_commits_for_rendering(@commits) end # Rails 5 sets request.format from the extension. diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index f93e500a07a..a1e12821caf 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -78,7 +78,7 @@ class Projects::CompareController < Projects::ApplicationController end def define_commits - @commits = compare.present? ? prepare_commits_for_rendering(compare.commits) : [] + @commits = compare.present? ? set_commits_for_rendering(@compare.commits) : [] end def define_diffs diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index ef8159aa553..c3ac8e107fb 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -113,7 +113,7 @@ class Projects::IssuesController < Projects::ApplicationController end def referenced_merge_requests - @merge_requests, @closed_by_merge_requests = ::Issues::FetchReferencedMergeRequestsService.new(project, current_user).execute(issue) + @merge_requests, @closed_by_merge_requests = ::Issues::ReferencedMergeRequestsService.new(project, current_user).execute(issue) respond_to do |format| format.json do diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index 81129456ad8..03d0290ac1d 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -101,7 +101,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap @target_project = @merge_request.target_project @source_project = @merge_request.source_project - @commits = prepare_commits_for_rendering(@merge_request.commits) + @commits = set_commits_for_rendering(@merge_request.commits) @commit = @merge_request.diff_head_commit @labels = LabelsFinder.new(current_user, project_id: @project.id).execute diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 1b069fe507b..d31b58972ca 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -79,7 +79,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo # Get commits from repository # or from cache if already merged @commits = - prepare_commits_for_rendering(@merge_request.commits.with_pipeline_status) + set_commits_for_rendering(@merge_request.commits.with_pipeline_status) render json: { html: view_to_html_string('projects/merge_requests/_commits') } end diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb index cae6e2c40b8..ff49911d892 100644 --- a/app/controllers/projects/pages_controller.rb +++ b/app/controllers/projects/pages_controller.rb @@ -11,7 +11,7 @@ class Projects::PagesController < Projects::ApplicationController def destroy project.remove_pages - project.pages_domains.destroy_all + project.pages_domains.destroy_all # rubocop: disable DestroyAll respond_to do |format| format.html do diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index 1cba0011304..91cf35bc70b 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -1,8 +1,7 @@ # Controller for viewing a file's raw class Projects::RawController < Projects::ApplicationController include ExtractsPath - include BlobHelper - include SendFileUpload + include SendsBlob before_action :require_non_empty_project before_action :assign_ref_vars @@ -10,39 +9,7 @@ class Projects::RawController < Projects::ApplicationController def show @blob = @repository.blob_at(@commit.id, @path) - if @blob - headers['X-Content-Type-Options'] = 'nosniff' - return if cached_blob? - - if @blob.stored_externally? - send_lfs_object - else - send_git_blob @repository, @blob, inline: (params[:inline] != 'false') - end - else - render_404 - end - end - - private - - def send_lfs_object - lfs_object = find_lfs_object - - if lfs_object && lfs_object.project_allowed_access?(@project) - send_upload(lfs_object.file, attachment: @blob.name) - else - render_404 - end - end - - def find_lfs_object - lfs_object = LfsObject.find_by_oid(@blob.lfs_oid) - if lfs_object && lfs_object.file.exists? - lfs_object - else - nil - end + send_blob(@blob, inline: (params[:inline] != 'false')) end end diff --git a/app/controllers/projects/settings/badges_controller.rb b/app/controllers/projects/settings/badges_controller.rb deleted file mode 100644 index 7887bee49c5..00000000000 --- a/app/controllers/projects/settings/badges_controller.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Projects - module Settings - class BadgesController < Projects::ApplicationController - include API::Helpers::RelatedResourcesHelpers - - before_action :authorize_admin_project! - - def index - @badge_api_endpoint = expose_url(api_v4_projects_badges_path(id: @project.id)) - end - end - end -end diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index b17753222a0..7f2c3ca38ad 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -17,6 +17,11 @@ class Projects::TagsController < Projects::ApplicationController tag_names = @tags.map(&:name) @tags_pipelines = @project.pipelines.latest_successful_for_refs(tag_names) @releases = project.releases.where(tag: tag_names) + + respond_to do |format| + format.html + format.atom { render layout: 'xml.atom' } + end end def show diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index e9ae8c13142..0eaf9f94e37 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,4 +1,5 @@ class ProjectsController < Projects::ApplicationController + include API::Helpers::RelatedResourcesHelpers include IssuableCollections include ExtractsPath include PreviewMarkdown @@ -32,6 +33,7 @@ class ProjectsController < Projects::ApplicationController end def edit + @badge_api_endpoint = expose_url(api_v4_projects_badges_path(id: @project.id)) render 'edit' end diff --git a/app/finders/autocomplete/group_finder.rb b/app/finders/autocomplete/group_finder.rb new file mode 100644 index 00000000000..dd97ac4c817 --- /dev/null +++ b/app/finders/autocomplete/group_finder.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Autocomplete + # Finder for retrieving a group to use for autocomplete data sources. + class GroupFinder + attr_reader :current_user, :project, :group_id + + # current_user - The currently logged in user, if any. + # project - The Project (if any) to use for the autocomplete data sources. + # params - A Hash containing parameters to use for finding the project. + # + # The following parameters are supported: + # + # * group_id: The ID of the group to find. + def initialize(current_user = nil, project = nil, params = {}) + @current_user = current_user + @project = project + @group_id = params[:group_id] + end + + # Attempts to find a Group based on the current group ID. + def execute + return unless project.blank? && group_id.present? + + group = Group.find(group_id) + + # This removes the need for using `return render_404` and similar patterns + # in controllers that use this finder. + unless Ability.allowed?(current_user, :read_group, group) + raise ActiveRecord::RecordNotFound + .new("Could not find a Group with ID #{group_id}") + end + + group + end + end +end diff --git a/app/finders/autocomplete/move_to_project_finder.rb b/app/finders/autocomplete/move_to_project_finder.rb new file mode 100644 index 00000000000..edaf74c5f92 --- /dev/null +++ b/app/finders/autocomplete/move_to_project_finder.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Autocomplete + # Finder that retrieves a list of projects that an issue can be moved to. + class MoveToProjectFinder + attr_reader :current_user, :search, :project_id, :offset_id + + # current_user - The User object of the user that wants to view the list of + # projects. + # + # params - A Hash containing additional parameters to set. + # + # The following parameters can be set (as Symbols): + # + # * search: An optional search query to apply to the list of projects. + # * project_id: The ID of a project to exclude from the returned relation. + # * offset_id: The ID of a project to use for pagination. When given, only + # projects with a lower ID are included in the list. + def initialize(current_user, params = {}) + @current_user = current_user + @search = params[:search] + @project_id = params[:project_id] + @offset_id = params[:offset_id] + end + + def execute + current_user + .projects_where_can_admin_issues + .optionally_search(search) + .excluding_project(project_id) + .paginate_in_descending_order_using_id(before: offset_id) + .eager_load_namespace_and_owner + end + end +end diff --git a/app/finders/autocomplete/project_finder.rb b/app/finders/autocomplete/project_finder.rb new file mode 100644 index 00000000000..3a4696f4c2e --- /dev/null +++ b/app/finders/autocomplete/project_finder.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Autocomplete + # Finder for retrieving a project to use for autocomplete data sources. + class ProjectFinder + attr_reader :current_user, :project_id + + # current_user - The currently logged in user, if any. + # params - A Hash containing parameters to use for finding the project. + # + # The following parameters are supported: + # + # * project_id: The ID of the project to find. + def initialize(current_user = nil, params = {}) + @current_user = current_user + @project_id = params[:project_id] + end + + # Attempts to find a Project based on the current project ID. + def execute + return if project_id.blank? + + project = Project.find(project_id) + + # This removes the need for using `return render_404` and similar patterns + # in controllers that use this finder. + unless Ability.allowed?(current_user, :read_project, project) + raise ActiveRecord::RecordNotFound + .new("Could not find a Project with ID #{project_id}") + end + + project + end + end +end diff --git a/app/finders/autocomplete/users_finder.rb b/app/finders/autocomplete/users_finder.rb new file mode 100644 index 00000000000..b2557469079 --- /dev/null +++ b/app/finders/autocomplete/users_finder.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +module Autocomplete + class UsersFinder + # The number of users to display in the results is hardcoded to 20, and + # pagination is not supported. This ensures that performance remains + # consistent and removes the need for implementing keyset pagination to + # ensure good performance. + LIMIT = 20 + + attr_reader :current_user, :project, :group, :search, :skip_users, + :author_id, :todo_filter, :todo_state_filter, + :filter_by_current_user + + def initialize(params:, current_user:, project:, group:) + @current_user = current_user + @project = project + @group = group + @search = params[:search] + @skip_users = params[:skip_users] + @author_id = params[:author_id] + @todo_filter = params[:todo_filter] + @todo_state_filter = params[:todo_state_filter] + @filter_by_current_user = params[:current_user] + end + + def execute + items = limited_users + + if search.blank? + # Include current user if available to filter by "Me" + items.unshift(current_user) if prepend_current_user? + + if prepend_author? && (author = User.find_by_id(author_id)) + items.unshift(author) + end + end + + items.uniq + end + + private + + # Returns the users based on the input parameters, as an Array. + # + # This method is separate so it is easier to extend in EE. + def limited_users + # When changing the order of these method calls, make sure that + # reorder_by_name() is called _before_ optionally_search(), otherwise + # reorder_by_name will break the ORDER BY applied in optionally_search(). + find_users + .active + .reorder_by_name + .optionally_search(search) + .where_not_in(skip_users) + .limit_to_todo_authors( + user: current_user, + with_todos: todo_filter, + todo_state: todo_state_filter + ) + .limit(LIMIT) + .to_a + end + + def prepend_current_user? + filter_by_current_user.present? && current_user + end + + def prepend_author? + author_id.present? && current_user + end + + def find_users + if project + project.authorized_users.union_with_user(author_id) + elsif group + group.users_with_parents + elsif current_user + User.all + else + User.none + end + end + end +end diff --git a/app/finders/autocomplete_users_finder.rb b/app/finders/autocomplete_users_finder.rb deleted file mode 100644 index e8a03947f59..00000000000 --- a/app/finders/autocomplete_users_finder.rb +++ /dev/null @@ -1,68 +0,0 @@ -class AutocompleteUsersFinder - # The number of users to display in the results is hardcoded to 20, and - # pagination is not supported. This ensures that performance remains - # consistent and removes the need for implementing keyset pagination to ensure - # good performance. - LIMIT = 20 - - attr_reader :current_user, :project, :group, :search, :skip_users, - :author_id, :params - - def initialize(params:, current_user:, project:, group:) - @current_user = current_user - @project = project - @group = group - @search = params[:search] - @skip_users = params[:skip_users] - @author_id = params[:author_id] - @params = params - end - - def execute - items = find_users - items = items.active - items = items.reorder(:name) - items = items.search(search) if search.present? - items = items.where.not(id: skip_users) if skip_users.present? - items = items.limit(LIMIT) - - if params[:todo_filter].present? && current_user - items = items.todo_authors(current_user.id, params[:todo_state_filter]) - end - - if search.blank? - # Include current user if available to filter by "Me" - if params[:current_user].present? && current_user - items = [current_user, *items].uniq - end - - if author_id.present? && current_user - author = User.find_by_id(author_id) - items = [author, *items].uniq if author - end - end - - items - end - - private - - def find_users - return users_from_project if project - return group.users_with_parents if group - return User.all if current_user - - User.none - end - - def users_from_project - if author_id.present? - union = Gitlab::SQL::Union - .new([project.authorized_users, User.where(id: author_id)]) - - User.from("(#{union.to_sql}) #{User.table_name}") - else - project.authorized_users - end - end -end diff --git a/app/finders/awarded_emoji_finder.rb b/app/finders/awarded_emoji_finder.rb new file mode 100644 index 00000000000..f0cc17f3b26 --- /dev/null +++ b/app/finders/awarded_emoji_finder.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Class for retrieving information about emoji awarded _by_ a particular user. +class AwardedEmojiFinder + attr_reader :current_user + + # current_user - The User to generate the data for. + def initialize(current_user = nil) + @current_user = current_user + end + + def execute + return [] unless current_user + + # We want the resulting data set to be an Array containing the emoji names + # in descending order, based on how often they were awarded. + AwardEmoji + .award_counts_for_user(current_user) + .map { |name, _| { name: name } } + end +end diff --git a/app/finders/license_template_finder.rb b/app/finders/license_template_finder.rb new file mode 100644 index 00000000000..fad33f0eca2 --- /dev/null +++ b/app/finders/license_template_finder.rb @@ -0,0 +1,36 @@ +# LicenseTemplateFinder +# +# Used to find license templates, which may come from a variety of external +# sources +# +# Arguments: +# popular: boolean. When set to true, only "popular" licenses are shown. When +# false, all licenses except popular ones are shown. When nil (the +# default), *all* licenses will be shown. +class LicenseTemplateFinder + attr_reader :params + + def initialize(params = {}) + @params = params + end + + def execute + Licensee::License.all(featured: popular_only?).map do |license| + LicenseTemplate.new( + id: license.key, + name: license.name, + nickname: license.nickname, + category: (license.featured? ? :Popular : :Other), + content: license.content, + url: license.url, + meta: license.meta + ) + end + end + + private + + def popular_only? + params.fetch(:popular, nil) + end +end diff --git a/app/finders/move_to_project_finder.rb b/app/finders/move_to_project_finder.rb deleted file mode 100644 index 038d5565a1e..00000000000 --- a/app/finders/move_to_project_finder.rb +++ /dev/null @@ -1,21 +0,0 @@ -class MoveToProjectFinder - PAGE_SIZE = 50 - - def initialize(user) - @user = user - end - - def execute(from_project, search: nil, offset_id: nil) - projects = @user.projects_where_can_admin_issues - projects = projects.search(search) if search.present? - projects = projects.excluding_project(from_project) - projects = projects.order_id_desc - - # infinite scroll using offset - projects = projects.where('projects.id < ?', offset_id) if offset_id.present? - projects = projects.limit(PAGE_SIZE) - - # to ask for Project#name_with_namespace - projects.includes(namespace: :owner) - end -end diff --git a/app/finders/user_finder.rb b/app/finders/user_finder.rb new file mode 100644 index 00000000000..484a93c9873 --- /dev/null +++ b/app/finders/user_finder.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# A simple finding for obtaining a single User. +# +# While using `User.find_by` directly is straightforward, it can lead to a lot +# of code duplication. Sometimes we just want to find a user by an ID, other +# times we may want to exclude blocked user. By using this finder (and extending +# it whenever necessary) we can keep this logic in one place. +class UserFinder + attr_reader :params + + def initialize(params) + @params = params + end + + # Tries to find a User, returning nil if none could be found. + def execute + User.find_by(id: params[:id]) + end + + # Tries to find a User, raising a `ActiveRecord::RecordNotFound` if it could + # not be found. + def execute! + User.find(params[:id]) + end +end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 2bdf2c2c120..684c84c3006 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -254,6 +254,8 @@ module ApplicationSettingsHelper :usage_ping_enabled, :instance_statistics_visibility_private, :user_default_external, + :user_show_add_ssh_key_message, + :user_default_internal_regex, :user_oauth_applications, :version_check_enabled, :web_ide_clientside_preview_enabled diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index d48dae8f06d..494f785e305 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -1,28 +1,10 @@ module AvatarsHelper def project_icon(project_id, options = {}) - project = - if project_id.respond_to?(:avatar_url) - project_id - else - Project.find_by_full_path(project_id) - end - - if project.avatar_url - image_tag project.avatar_url, options - else # generated icon - project_identicon(project, options) - end + source_icon(Project, project_id, options) end - def project_identicon(project, options = {}) - bg_key = (project.id % 7) + 1 - options[:class] ||= '' - options[:class] << ' identicon' - options[:class] << " bg#{bg_key}" - - content_tag(:div, class: options[:class]) do - project.name[0, 1].upcase - end + def group_icon(group_id, options = {}) + source_icon(Group, group_id, options) end # Takes both user and email and returns the avatar_icon by @@ -123,4 +105,32 @@ module AvatarsHelper mail_to(options[:user_email], avatar) end end + + private + + def source_icon(klass, source_id, options = {}) + source = + if source_id.respond_to?(:avatar_url) + source_id + else + klass.find_by_full_path(source_id) + end + + if source.avatar_url + image_tag source.avatar_url, options + else + source_identicon(source, options) + end + end + + def source_identicon(source, options = {}) + bg_key = (source.id % 7) + 1 + options[:class] ||= '' + options[:class] << ' identicon' + options[:class] << " bg#{bg_key}" + + content_tag(:div, class: options[:class].strip) do + source.name[0, 1].upcase + end + end end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 7eb45ddd117..00ebafd177b 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -157,37 +157,17 @@ module BlobHelper end end - def cached_blob? - stale = stale?(etag: @blob.id) # The #stale? method sets cache headers. - - # Because we are opionated we set the cache headers ourselves. - response.cache_control[:public] = @project.public? - - response.cache_control[:max_age] = - if @ref && @commit && @ref == @commit.id - # This is a link to a commit by its commit SHA. That means that the blob - # is immutable. The only reason to invalidate the cache is if the commit - # was deleted or if the user lost access to the repository. - Blob::CACHE_TIME_IMMUTABLE - else - # A branch or tag points at this blob. That means that the expected blob - # value may change over time. - Blob::CACHE_TIME - end - - response.etag = @blob.id - !stale - end - def licenses_for_select return @licenses_for_select if defined?(@licenses_for_select) - licenses = Licensee::License.all + grouped_licenses = LicenseTemplateFinder.new.execute.group_by(&:category) + categories = grouped_licenses.keys - @licenses_for_select = { - Popular: licenses.select(&:featured).map { |license| { name: license.name, id: license.key } }, - Other: licenses.reject(&:featured).map { |license| { name: license.name, id: license.key } } - } + @licenses_for_select = categories.each_with_object({}) do |category, hash| + hash[category] = grouped_licenses[category].map do |license| + { name: license.name, id: license.id } + end + end end def ref_project diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 0171a880164..26e3850a540 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -67,13 +67,17 @@ module ButtonHelper def http_dropdown_description(protocol) if current_user.try(:require_password_creation_for_git?) _("Set a password on your account to pull or push via %{protocol}.") % { protocol: protocol } - else + elsif current_user.try(:require_personal_access_token_creation_for_git_auth?) _("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol } end end def ssh_clone_button(project, append_link: true) - dropdown_description = _("You won't be able to pull or push project code via SSH until you add an SSH key to your profile") if current_user.try(:require_ssh_key?) + if Gitlab::CurrentSettings.user_show_add_ssh_key_message? && + current_user.try(:require_ssh_key?) + dropdown_description = _("You won't be able to pull or push project code via SSH until you add an SSH key to your profile") + end + append_url = project.ssh_url_to_repo if append_link dropdown_item_with_description('SSH', dropdown_description, href: append_url) diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 89fe90fd801..7a942c44ac4 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -210,17 +210,6 @@ module CommitsHelper Sanitize.clean(string, remove_contents: true) end - def limited_commits(commits) - if commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE - [ - commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE), - commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE - ] - else - [commits, 0] - end - end - def commit_path(project, commit, merge_request: nil) if merge_request&.persisted? diffs_project_merge_request_path(project, merge_request, commit_id: commit.id) diff --git a/app/helpers/cookies_helper.rb b/app/helpers/cookies_helper.rb new file mode 100644 index 00000000000..3a7e9987190 --- /dev/null +++ b/app/helpers/cookies_helper.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module CookiesHelper + def set_secure_cookie(key, value, httponly: false, permanent: false) + cookie_jar = permanent ? cookies.permanent : cookies + + cookie_jar[key] = { value: value, secure: Gitlab.config.gitlab.https, httponly: httponly } + end +end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 3c5c8bbd71b..5b51d2f2425 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -33,11 +33,6 @@ module GroupsHelper .count end - def group_icon(group, options = {}) - img_path = group_icon_url(group, options) - image_tag img_path, options - end - def group_icon_url(group, options = {}) if group.is_a?(String) group = Group.find_by_full_path(group) diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 41084ec686f..a8a10c98d69 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -62,6 +62,8 @@ module IconsHelper names = "key" when "two-factor" names = "key" + when "google_oauth2" + names = "google" end options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options) diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index 4664b1728c4..c65f1565425 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -5,6 +5,10 @@ module ImportHelper false end + def sanitize_project_name(name) + name.gsub(/[^\w\-]/, '-') + end + def import_project_target(owner, name) namespace = current_user.can_create_group? ? owner : current_user.namespace_path "#{namespace}/#{name}" diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 96dc7ae1185..5b27d1d9404 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -92,14 +92,6 @@ module IssuesHelper end end - def award_user_authored_class(award) - if award == 'thumbsdown' || award == 'thumbsup' - 'user-authored js-user-authored' - else - '' - end - end - def awards_sort(awards) awards.sort_by do |award, award_emojis| if award == "thumbsup" diff --git a/app/helpers/mirror_helper.rb b/app/helpers/mirror_helper.rb new file mode 100644 index 00000000000..93ed22513ac --- /dev/null +++ b/app/helpers/mirror_helper.rb @@ -0,0 +1,5 @@ +module MirrorHelper + def mirrors_form_data_attributes + { project_mirror_endpoint: project_mirror_path(@project) } + end +end diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index 30585cb403d..6535afb6425 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -55,7 +55,7 @@ module NamespacesHelper # group if one exists by that name to prevent duplicates. def dedup_extra_group(extra_group) unless extra_group.persisted? - existing_group = Group.find_by(name: extra_group.name) + existing_group = Group.find_by(path: extra_group.path) extra_group = existing_group if existing_group&.persisted? end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 3e42063224e..a185f2916d4 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -83,21 +83,11 @@ module NotificationsHelper end def notification_event_name(event) - # All values from NotificationSetting::EMAIL_EVENTS + # All values from NotificationSetting.email_events case event when :success_pipeline s_('NotificationEvent|Successful pipeline') else - N_('NotificationEvent|New note') - N_('NotificationEvent|New issue') - N_('NotificationEvent|Reopen issue') - N_('NotificationEvent|Close issue') - N_('NotificationEvent|Reassign issue') - N_('NotificationEvent|New merge request') - N_('NotificationEvent|Close merge request') - N_('NotificationEvent|Reassign merge request') - N_('NotificationEvent|Merge merge request') - N_('NotificationEvent|Failed pipeline') s_(event.to_s.humanize) end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index aaf9dff43ee..18b3badda8d 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -192,7 +192,10 @@ module ProjectsHelper end def show_no_ssh_key_message? - cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key? + Gitlab::CurrentSettings.user_show_add_ssh_key_message? && + cookies[:hide_no_ssh_message].blank? && + !current_user.hide_no_ssh_key && + current_user.require_ssh_key? end def show_no_password_message? @@ -444,7 +447,7 @@ module ProjectsHelper end def project_permissions_panel_data(project) - data = { + { currentSettings: project_permissions_settings(project), canChangeVisibilityLevel: can_change_visibility_level?(project, current_user), allowedVisibilityOptions: project_allowed_visibility_levels(project), @@ -454,8 +457,10 @@ module ProjectsHelper lfsAvailable: Gitlab.config.lfs.enabled, lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') } + end - data.to_json.html_safe + def project_permissions_panel_data_json(project) + project_permissions_panel_data(project).to_json.html_safe end def project_allowed_visibility_levels(project) diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb index ebfde993456..ec2cf2b16c0 100644 --- a/app/helpers/submodule_helper.rb +++ b/app/helpers/submodule_helper.rb @@ -64,8 +64,7 @@ module SubmoduleHelper end def relative_self_url?(url) - # (./)?(../repo.git) || (./)?(../../project/repo.git) ) - url =~ %r{\A((\./)?(\.\./))(?!(\.\.)|(.*/)).*(\.git)?\z} || url =~ %r{\A((\./)?(\.\./){2})(?!(\.\.))([^/]*)/(?!(\.\.)|(.*/)).*(\.git)?\z} + url.start_with?('../', './') end def standard_links(host, namespace, project, commit) @@ -73,25 +72,29 @@ module SubmoduleHelper [base, [base, '/tree/', commit].join('')] end - def relative_self_links(url, commit, project) - url.rstrip! - # Map relative links to a namespace and project - # For example: - # ../bar.git -> same namespace, repo bar - # ../foo/bar.git -> namespace foo, repo bar - # ../../foo/bar/baz.git -> namespace bar, repo baz - components = url.split('/') - base = components.pop.gsub(/.git$/, '') - namespace = components.pop.gsub(/^\.\.$/, '') - - if namespace.empty? - namespace = project.namespace.full_path + def relative_self_links(relative_path, commit, project) + relative_path.rstrip! + absolute_project_path = "/" + project.full_path + + # Resolve `relative_path` to target path + # Assuming `absolute_project_path` is `/g1/p1`: + # ../p2.git -> /g1/p2 + # ../g2/p3.git -> /g1/g2/p3 + # ../../g3/g4/p4.git -> /g3/g4/p4 + submodule_project_path = File.absolute_path(relative_path, absolute_project_path) + target_namespace_path = File.dirname(submodule_project_path) + + if target_namespace_path == '/' || target_namespace_path.start_with?(absolute_project_path) + return [nil, nil] end + target_namespace_path.sub!(%r{^/}, '') + submodule_base = File.basename(submodule_project_path, '.git') + begin [ - namespace_project_path(namespace, base), - namespace_project_tree_path(namespace, base, commit) + namespace_project_path(target_namespace_path, submodule_base), + namespace_project_tree_path(target_namespace_path, submodule_base, commit) ] rescue ActionController::UrlGenerationError [nil, nil] diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index ceea4384f91..2c0c4254a0c 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -23,6 +23,17 @@ module UsersHelper profile_tabs.include?(tab) end + def user_internal_regex_data + settings = Gitlab::CurrentSettings.current_application_settings + + pattern, options = if settings.user_default_internal_regex_enabled? + regex = settings.user_default_internal_regex_instance + JsRegex.new(regex).to_h.slice(:source, :options).values + end + + { user_internal_regex_pattern: pattern, user_internal_regex_options: options } + end + def current_user_menu_items @current_user_menu_items ||= get_current_user_menu_items end diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb index 41f9eedd4bd..17940aeb900 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -1,4 +1,6 @@ module WikiHelper + include API::Helpers::RelatedResourcesHelpers + # Produces a pure text breadcrumb for a given page. # # page_slug - The slug of a WikiPage object. @@ -39,4 +41,8 @@ module WikiHelper end end end + + def wiki_attachment_upload_url + expose_url(api_v4_projects_wikis_attachments_path(id: @project.id)) + end end diff --git a/app/mailers/abuse_report_mailer.rb b/app/mailers/abuse_report_mailer.rb index fe5f68ba3d5..e032f568913 100644 --- a/app/mailers/abuse_report_mailer.rb +++ b/app/mailers/abuse_report_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AbuseReportMailer < BaseMailer def notify(abuse_report_id) return unless deliverable? diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb index 654468bc7fe..5fd209c4761 100644 --- a/app/mailers/base_mailer.rb +++ b/app/mailers/base_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class BaseMailer < ActionMailer::Base around_action :render_with_default_locale diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb index 962570a0efd..7aa75ee30e6 100644 --- a/app/mailers/devise_mailer.rb +++ b/app/mailers/devise_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DeviseMailer < Devise::Mailer default from: "#{Gitlab.config.gitlab.email_display_name} <#{Gitlab.config.gitlab.email_from}>" default reply_to: Gitlab.config.gitlab.email_reply_to @@ -9,8 +11,9 @@ class DeviseMailer < Devise::Mailer protected def subject_for(key) - subject = super - subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present? - subject + subject = [super] + subject << Gitlab.config.gitlab.email_subject_suffix if Gitlab.config.gitlab.email_subject_suffix.present? + + subject.join(' | ') end end diff --git a/app/mailers/email_rejection_mailer.rb b/app/mailers/email_rejection_mailer.rb index 76db31a4c45..45fc5a6c383 100644 --- a/app/mailers/email_rejection_mailer.rb +++ b/app/mailers/email_rejection_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class EmailRejectionMailer < BaseMailer def rejection(reason, original_raw, can_retry = false) @reason = reason diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index 392cc0bee03..c8b1ab5033a 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Issues def new_issue_email(recipient_id, issue_id, reason = nil) diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb index 75cf56a51f2..91dfdf58982 100644 --- a/app/mailers/emails/members.rb +++ b/app/mailers/emails/members.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Members extend ActiveSupport::Concern diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 70509e9066d..70f65d4e58d 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module MergeRequests def new_merge_request_email(recipient_id, merge_request_id, reason = nil) diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index d9a6fe2a41e..d3284e90568 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Notes def note_commit_email(recipient_id, note_id) diff --git a/app/mailers/emails/pages_domains.rb b/app/mailers/emails/pages_domains.rb index 0027dfdc36b..ce449237ef6 100644 --- a/app/mailers/emails/pages_domains.rb +++ b/app/mailers/emails/pages_domains.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module PagesDomains def pages_domain_enabled_email(domain, recipient) diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb index f9f45ab987b..31e183640ad 100644 --- a/app/mailers/emails/pipelines.rb +++ b/app/mailers/emails/pipelines.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Pipelines def pipeline_success_email(pipeline, recipients) @@ -39,10 +41,10 @@ module Emails end def pipeline_subject(status) - commit = @pipeline.short_sha - commit << " in #{@merge_request.to_reference}" if @merge_request + commit = [@pipeline.short_sha] + commit << "in #{@merge_request.to_reference}" if @merge_request - subject("Pipeline ##{@pipeline.id} has #{status} for #{@pipeline.ref}", commit) + subject("Pipeline ##{@pipeline.id} has #{status} for #{@pipeline.ref}", commit.join(' ')) end end end diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb index 4f5edeb9bda..40d7b9ccd7a 100644 --- a/app/mailers/emails/profile.rb +++ b/app/mailers/emails/profile.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Profile def new_user_email(user_id, token = nil) diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 761d873c01c..d7e6c2ba7b2 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Projects def project_was_moved_email(project_id, user_id, old_path_with_namespace) diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 0e1e39501f5..f4eeb85270e 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Notify < BaseMailer include ActionDispatch::Routing::PolymorphicRoutes include GitlabRoutingHelper @@ -92,12 +94,14 @@ class Notify < BaseMailer # >> subject('Lorem ipsum', 'Dolor sit amet') # => "Lorem ipsum | Dolor sit amet" def subject(*extra) - subject = "" - subject << "#{@project.name} | " if @project - subject << "#{@group.name} | " if @group - subject << extra.join(' | ') if extra.present? - subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present? - subject + subject = [] + + subject << @project.name if @project + subject << @group.name if @group + subject.concat(extra) if extra.present? + subject << Gitlab.config.gitlab.email_subject_suffix if Gitlab.config.gitlab.email_subject_suffix.present? + + subject.join(' | ') end # Return a string suitable for inclusion in the 'Message-Id' mail header. diff --git a/app/mailers/previews/devise_mailer_preview.rb b/app/mailers/previews/devise_mailer_preview.rb index d6588efc486..3b9ef0d3ac0 100644 --- a/app/mailers/previews/devise_mailer_preview.rb +++ b/app/mailers/previews/devise_mailer_preview.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DeviseMailerPreview < ActionMailer::Preview def confirmation_instructions_for_signup DeviseMailer.confirmation_instructions(unsaved_user, 'faketoken', {}) diff --git a/app/mailers/previews/email_rejection_mailer_preview.rb b/app/mailers/previews/email_rejection_mailer_preview.rb index 639e8471232..402066151ef 100644 --- a/app/mailers/previews/email_rejection_mailer_preview.rb +++ b/app/mailers/previews/email_rejection_mailer_preview.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class EmailRejectionMailerPreview < ActionMailer::Preview def rejection EmailRejectionMailer.rejection("some rejection reason", "From: someone@example.com\nraw email here").message diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb index 3615cde8026..df470930e9e 100644 --- a/app/mailers/previews/notify_preview.rb +++ b/app/mailers/previews/notify_preview.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class NotifyPreview < ActionMailer::Preview def note_merge_request_email_for_individual_note note_email(:note_merge_request_email) do diff --git a/app/mailers/previews/repository_check_mailer_preview.rb b/app/mailers/previews/repository_check_mailer_preview.rb index 19d4eab1805..834d7594719 100644 --- a/app/mailers/previews/repository_check_mailer_preview.rb +++ b/app/mailers/previews/repository_check_mailer_preview.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RepositoryCheckMailerPreview < ActionMailer::Preview def notify RepositoryCheckMailer.notify(3).message diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb index 22a9f5da646..4bcf371cfc0 100644 --- a/app/mailers/repository_check_mailer.rb +++ b/app/mailers/repository_check_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RepositoryCheckMailer < BaseMailer def notify(failed_count) @message = diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index bbe7811841a..03bd7fa016e 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -192,6 +192,8 @@ class ApplicationSetting < ActiveRecord::Base numericality: { less_than_or_equal_to: :gitaly_timeout_default }, if: :gitaly_timeout_default + validates :user_default_internal_regex, js_regex: true, allow_nil: true + SUPPORTED_KEY_TYPES.each do |type| validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } end @@ -298,7 +300,9 @@ class ApplicationSetting < ActiveRecord::Base unique_ips_limit_time_window: 3600, usage_ping_enabled: Settings.gitlab['usage_ping_enabled'], instance_statistics_visibility_private: false, - user_default_external: false + user_default_external: false, + user_default_internal_regex: nil, + user_show_add_ssh_key_message: true } end @@ -434,6 +438,14 @@ class ApplicationSetting < ActiveRecord::Base password_authentication_enabled_for_web? || password_authentication_enabled_for_git? end + def user_default_internal_regex_enabled? + user_default_external? && user_default_internal_regex.present? + end + + def user_default_internal_regex_instance + Regexp.new(user_default_internal_regex, Regexp::IGNORECASE) + end + delegate :terms, to: :latest_terms, allow_nil: true def latest_terms @latest_terms ||= Term.latest diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index 99c7866d636..ddc516ccb60 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -28,6 +28,23 @@ class AwardEmoji < ActiveRecord::Base .where('name IN (?) AND awardable_type = ? AND awardable_id IN (?)', [DOWNVOTE_NAME, UPVOTE_NAME], type, ids) .group('name', 'awardable_id') end + + # Returns the top 100 emoji awarded by the given user. + # + # The returned value is a Hash mapping emoji names to the number of times + # they were awarded: + # + # { 'thumbsup' => 2, 'thumbsdown' => 1 } + # + # user - The User to get the awards for. + # limt - The maximum number of emoji to return. + def award_counts_for_user(user, limit = 100) + limit(limit) + .where(user: user) + .group(:name) + .order('count_all DESC, name ASC') + .count + end end def downvote? diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 9292929be98..faa160ad6ba 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -67,6 +67,10 @@ module Ci '', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive) end + scope :with_archived_trace, ->() do + where('EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace) + end + scope :without_archived_trace, ->() do where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace) end @@ -77,6 +81,7 @@ module Ci end scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) } + scope :with_archived_trace_stored_locally, -> { with_archived_trace.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) } scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } @@ -226,7 +231,7 @@ module Ci end def cancelable? - active? + active? || created? end def retryable? diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index d7c5f29be96..17b7ee4f07e 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -33,7 +33,7 @@ module Ci where(file_type: types) end - delegate :exists?, :open, to: :file + delegate :filename, :exists?, :open, to: :file enum file_type: { archive: 1, diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index e4aed76f611..526bf7af99b 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -649,8 +649,7 @@ module Ci def keep_around_commits return unless project - project.repository.keep_around(self.sha) - project.repository.keep_around(self.before_sha) + project.repository.keep_around(self.sha, self.before_sha) end def valid_source diff --git a/app/models/commit.rb b/app/models/commit.rb index 8b9f4490ffa..594972ad344 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -193,6 +193,7 @@ class Commit # otherwise returns commit message without first line def description return safe_message if full_title.length >= 100 + return no_commit_message if safe_message.blank? safe_message.split("\n", 2)[1].try(:chomp) end @@ -448,6 +449,10 @@ class Commit true end + def to_ability_name + model_name.singular + end + def touch # no-op but needs to be defined since #persisted? is defined end diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb index 7f6d48d972c..4e15b60ccd1 100644 --- a/app/models/concerns/atomic_internal_id.rb +++ b/app/models/concerns/atomic_internal_id.rb @@ -26,7 +26,7 @@ module AtomicInternalId extend ActiveSupport::Concern - module ClassMethods + class_methods do def has_internal_id(column, scope:, init:, presence: true) # rubocop:disable Naming/PredicateName # We require init here to retain the ability to recalculate in the absence of a # InternaLId record (we may delete records in `internal_ids` for example). diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index dd07f389fa5..6f29c92d176 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -12,7 +12,7 @@ module Awardable end end - module ClassMethods + class_methods do def awarded(user, name) sql = <<~EOL EXISTS ( @@ -76,12 +76,8 @@ module Awardable true end - def awardable_votes?(name) - AwardEmoji::UPVOTE_NAME == name || AwardEmoji::DOWNVOTE_NAME == name - end - - def user_can_award?(current_user, name) - awardable_by_user?(current_user, name) && Ability.allowed?(current_user, :award_emoji, self) + def user_can_award?(current_user) + Ability.allowed?(current_user, :award_emoji, self) end def user_authored?(current_user) @@ -101,7 +97,7 @@ module Awardable end def remove_award_emoji(name, current_user) - award_emoji.where(name: name, user: current_user).destroy_all + award_emoji.where(name: name, user: current_user).destroy_all # rubocop: disable DestroyAll end def toggle_award_emoji(emoji_name, current_user) @@ -117,12 +113,4 @@ 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/concerns/case_sensitivity.rb b/app/models/concerns/case_sensitivity.rb index 0ba542b75ab..6e80365ee5b 100644 --- a/app/models/concerns/case_sensitivity.rb +++ b/app/models/concerns/case_sensitivity.rb @@ -4,7 +4,7 @@ module CaseSensitivity extend ActiveSupport::Concern - module ClassMethods + class_methods do # Queries the given columns regardless of the casing used. # # Unlike other ActiveRecord methods this method only operates on a Hash. diff --git a/app/models/concerns/each_batch.rb b/app/models/concerns/each_batch.rb index a9e14cb55eb..8cf0b8b154d 100644 --- a/app/models/concerns/each_batch.rb +++ b/app/models/concerns/each_batch.rb @@ -3,7 +3,7 @@ module EachBatch extend ActiveSupport::Concern - module ClassMethods + class_methods do # Iterates over the rows in a relation in batches, similar to Rails' # `in_batches` but in a more efficient way. # diff --git a/app/models/concerns/fast_destroy_all.rb b/app/models/concerns/fast_destroy_all.rb index 65ed46ea202..c342d01243e 100644 --- a/app/models/concerns/fast_destroy_all.rb +++ b/app/models/concerns/fast_destroy_all.rb @@ -34,7 +34,7 @@ module FastDestroyAll included do before_destroy do - raise ForbiddenActionError, '`destroy` and `destroy_all` are forbbiden. Please use `fast_destroy_all`' + raise ForbiddenActionError, '`destroy` and `destroy_all` are forbidden. Please use `fast_destroy_all`' end end diff --git a/app/models/concerns/ignorable_column.rb b/app/models/concerns/ignorable_column.rb index 2b074c1921c..5c1f7dfcd2a 100644 --- a/app/models/concerns/ignorable_column.rb +++ b/app/models/concerns/ignorable_column.rb @@ -14,7 +14,7 @@ module IgnorableColumn extend ActiveSupport::Concern - module ClassMethods + class_methods do def columns super.reject { |column| ignored_columns.include?(column.name) } end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index e8072145551..f881ce2321c 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -118,7 +118,7 @@ module Issuable end end - module ClassMethods + class_methods do # Searches for records with a matching title. # # This method uses ILIKE on PostgreSQL and LIKE on MySQL. diff --git a/app/models/concerns/loaded_in_group_list.rb b/app/models/concerns/loaded_in_group_list.rb index a2233eb2997..fc15c6d55ed 100644 --- a/app/models/concerns/loaded_in_group_list.rb +++ b/app/models/concerns/loaded_in_group_list.rb @@ -3,7 +3,7 @@ module LoadedInGroupList extend ActiveSupport::Concern - module ClassMethods + class_methods do def with_counts(archived:) selects_including_counts = [ 'namespaces.*', diff --git a/app/models/concerns/manual_inverse_association.rb b/app/models/concerns/manual_inverse_association.rb index d0d781dc15f..e18edd33ba7 100644 --- a/app/models/concerns/manual_inverse_association.rb +++ b/app/models/concerns/manual_inverse_association.rb @@ -3,7 +3,7 @@ module ManualInverseAssociation extend ActiveSupport::Concern - module ClassMethods + class_methods do def manual_inverse_association(association, inverse) define_method(association) do |*args| super(*args).tap do |value| diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 7e7eccb1c27..393607e82c4 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -10,7 +10,7 @@ module Mentionable extend ActiveSupport::Concern - module ClassMethods + class_methods do # Indicate which attributes of the Mentionable to search for GFM references. def attr_mentionable(attr, options = {}) attr = attr.to_s diff --git a/app/models/concerns/optionally_search.rb b/app/models/concerns/optionally_search.rb new file mode 100644 index 00000000000..4093429e372 --- /dev/null +++ b/app/models/concerns/optionally_search.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module OptionallySearch + extend ActiveSupport::Concern + + class_methods do + def search(*) + raise( + NotImplementedError, + 'Your model must implement the "search" class method' + ) + end + + # Optionally limits a result set to those matching the given search query. + def optionally_search(query = nil) + query.present? ? search(query) : all + end + end +end diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index 1f6c42f3b3a..614c3242874 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -26,7 +26,7 @@ module Participable extend ActiveSupport::Concern - module ClassMethods + class_methods do # Adds a list of participant attributes. Attributes can either be symbols or # Procs. # diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb index 468eaf68883..58143a32fdc 100644 --- a/app/models/concerns/referable.rb +++ b/app/models/concerns/referable.rb @@ -40,7 +40,7 @@ module Referable end end - module ClassMethods + class_methods do # The character that prefixes the actual reference identifier # # This should be overridden by the including class. diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb index f47e20229f1..16ea330701d 100644 --- a/app/models/concerns/resolvable_note.rb +++ b/app/models/concerns/resolvable_note.rb @@ -20,7 +20,7 @@ module ResolvableNote scope :unresolved, -> { resolvable.where(resolved_at: nil) } end - module ClassMethods + class_methods do # This method must be kept in sync with `#resolve!` def resolve!(current_user) unresolved.update_all(resolved_at: Time.now, resolved_by_id: current_user.id) diff --git a/app/models/concerns/select_for_project_authorization.rb b/app/models/concerns/select_for_project_authorization.rb index 39306179eb8..333c9118aa5 100644 --- a/app/models/concerns/select_for_project_authorization.rb +++ b/app/models/concerns/select_for_project_authorization.rb @@ -3,7 +3,7 @@ module SelectForProjectAuthorization extend ActiveSupport::Concern - module ClassMethods + class_methods do def select_for_project_authorization select("projects.id AS project_id, members.access_level") end diff --git a/app/models/concerns/sha_attribute.rb b/app/models/concerns/sha_attribute.rb index c322c356db2..e51b4e22c96 100644 --- a/app/models/concerns/sha_attribute.rb +++ b/app/models/concerns/sha_attribute.rb @@ -3,7 +3,7 @@ module ShaAttribute extend ActiveSupport::Concern - module ClassMethods + class_methods do def sha_attribute(name) return if ENV['STATIC_VERIFICATION'] diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index 501bd1bb83c..29e48f0c5f7 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -19,7 +19,7 @@ module Sortable scope :order_name_desc, -> { reorder(Arel::Nodes::Descending.new(arel_table[:name].lower)) } end - module ClassMethods + class_methods do def order_by(method) case method.to_s when 'created_asc' then order_created_asc diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb index c6e3dc385fe..3ff4b4046d3 100644 --- a/app/models/concerns/spammable.rb +++ b/app/models/concerns/spammable.rb @@ -3,7 +3,7 @@ module Spammable extend ActiveSupport::Concern - module ClassMethods + class_methods do def attr_spammable(attr, options = {}) spammable_attrs << [attr.to_s, options] end diff --git a/app/models/concerns/strip_attribute.rb b/app/models/concerns/strip_attribute.rb index 344f677a3f3..c9f5ba7793d 100644 --- a/app/models/concerns/strip_attribute.rb +++ b/app/models/concerns/strip_attribute.rb @@ -14,7 +14,7 @@ module StripAttribute extend ActiveSupport::Concern - module ClassMethods + class_methods do def strip_attributes(*attrs) strip_attrs.concat(attrs) end diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb index f55ab2fcaf3..223a61119e5 100644 --- a/app/models/concerns/triggerable_hooks.rb +++ b/app/models/concerns/triggerable_hooks.rb @@ -6,6 +6,7 @@ module TriggerableHooks push_hooks: :push_events, tag_push_hooks: :tag_push_events, issue_hooks: :issues_events, + confidential_note_hooks: :confidential_note_events, confidential_issue_hooks: :confidential_issues_events, note_hooks: :note_events, merge_request_hooks: :merge_requests_events, diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 58d949315e0..716cf6574d3 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -191,14 +191,18 @@ class DiffNote < Note end def keep_around_commits - project.repository.keep_around(self.original_position.base_sha) - project.repository.keep_around(self.original_position.start_sha) - project.repository.keep_around(self.original_position.head_sha) + shas = [ + self.original_position.base_sha, + self.original_position.start_sha, + self.original_position.head_sha + ] if self.position != self.original_position - project.repository.keep_around(self.position.base_sha) - project.repository.keep_around(self.position.start_sha) - project.repository.keep_around(self.position.head_sha) + shas << self.position.base_sha + shas << self.position.start_sha + shas << self.position.head_sha end + + project.repository.keep_around(*shas) end end diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb index 4eb211eff61..e7168d49db9 100644 --- a/app/models/internal_id.rb +++ b/app/models/internal_id.rb @@ -111,7 +111,7 @@ class InternalId < ActiveRecord::Base # Generates next internal id and returns it def generate - subject.transaction do + InternalId.transaction do # Create a record in internal_ids if one does not yet exist # and increment its last value # @@ -125,7 +125,7 @@ class InternalId < ActiveRecord::Base # # Note this will acquire a ROW SHARE lock on the InternalId record def track_greatest(new_value) - subject.transaction do + InternalId.transaction do (lookup || create_record).track_greatest_and_save!(new_value) end end @@ -148,7 +148,7 @@ class InternalId < ActiveRecord::Base # violation. We can safely roll-back the nested transaction and perform # a lookup instead to retrieve the record. def create_record - subject.transaction(requires_new: true) do + InternalId.transaction(requires_new: true) do InternalId.create!( **scope, usage: usage_value, diff --git a/app/models/issue.rb b/app/models/issue.rb index 94cf12f3c2b..d0cd7461daa 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -170,27 +170,6 @@ class Issue < ActiveRecord::Base "#{project.to_reference(from, full: full)}#{reference}" end - def referenced_merge_requests(current_user = nil) - ext = all_references(current_user) - - notes_with_associations.each do |object| - object.all_references(current_user, extractor: ext) - end - - merge_requests = ext.merge_requests.sort_by(&:iid) - - cross_project_filter = -> (merge_requests) do - merge_requests.select { |mr| mr.target_project == project } - end - - Ability.merge_requests_readable_by_user( - merge_requests, current_user, - filters: { - read_cross_project: cross_project_filter - } - ) - end - # All branches containing the current issue's ID, except for # those with a merge request open referencing the current issue. def related_branches(current_user) @@ -198,7 +177,11 @@ class Issue < ActiveRecord::Base branch =~ /\A#{iid}-(?!\d+-stable)/i end - branches_with_merge_request = self.referenced_merge_requests(current_user).map(&:source_branch) + branches_with_merge_request = + Issues::ReferencedMergeRequestsService + .new(project, current_user) + .referenced_merge_requests(self) + .map(&:source_branch) branches_with_iid - branches_with_merge_request end @@ -225,26 +208,6 @@ class Issue < ActiveRecord::Base project end - # From all notes on this issue, we'll select the system notes about linked - # merge requests. Of those, the MRs closing `self` are returned. - def closed_by_merge_requests(current_user = nil) - return [] unless open? - - ext = all_references(current_user) - - notes.system.each do |note| - note.all_references(current_user, extractor: ext) - end - - merge_requests = ext.merge_requests.select(&:open?) - if merge_requests.any? - ids = MergeRequestsClosingIssues.where(merge_request_id: merge_requests.map(&:id), issue_id: id).pluck(:merge_request_id) - merge_requests.select { |mr| mr.id.in?(ids) } - else - [] - end - end - def moved? !moved_to.nil? end diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index 2a1a4ef48b7..97bf5d611c2 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -29,11 +29,13 @@ class LfsObject < ActiveRecord::Base [nil, LfsObjectUploader::Store::LOCAL].include?(self.file_store) end + # rubocop: disable DestroyAll def self.destroy_unreferenced joins("LEFT JOIN lfs_objects_projects ON lfs_objects_projects.lfs_object_id = #{table_name}.id") .where(lfs_objects_projects: { id: nil }) .destroy_all end + # rubocop: enable DestroyAll def self.calculate_oid(path) Digest::SHA256.file(path).hexdigest diff --git a/app/models/license_template.rb b/app/models/license_template.rb new file mode 100644 index 00000000000..0ad75b27827 --- /dev/null +++ b/app/models/license_template.rb @@ -0,0 +1,53 @@ +class LicenseTemplate + PROJECT_TEMPLATE_REGEX = + %r{[\<\{\[] + (project|description| + one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here + [\>\}\]]}xi.freeze + YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze + FULLNAME_TEMPLATE_REGEX = + %r{[\<\{\[] + (fullname|name\sof\s(author|copyright\sowner)) + [\>\}\]]}xi.freeze + + attr_reader :id, :name, :category, :nickname, :url, :meta + + alias_method :key, :id + + def initialize(id:, name:, category:, content:, nickname: nil, url: nil, meta: {}) + @id = id + @name = name + @category = category + @content = content + @nickname = nickname + @url = url + @meta = meta + end + + def popular? + category == :Popular + end + alias_method :featured?, :popular? + + # Returns the text of the license + def content + if @content.respond_to?(:call) + @content = @content.call + else + @content + end + end + + # Populate placeholders in the LicenseTemplate content + def resolve!(project_name: nil, fullname: nil, year: Time.now.year.to_s) + # Ensure the string isn't shared with any other instance of LicenseTemplate + new_content = content.dup + new_content.gsub!(YEAR_TEMPLATE_REGEX, year) if year.present? + new_content.gsub!(PROJECT_TEMPLATE_REGEX, project_name) if project_name.present? + new_content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname.present? + + @content = new_content + + self + end +end diff --git a/app/models/member.rb b/app/models/member.rb index 05c0bc8cb97..d9b4e8d2ac6 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -103,7 +103,7 @@ class Member < ActiveRecord::Base def filter_by_2fa(value) case value when 'enabled' - left_join_users.merge(User.with_two_factor_indistinct) + left_join_users.merge(User.with_two_factor) when 'disabled' left_join_users.merge(User.without_two_factor) else diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index d9393b4e545..bbe4f6f7969 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -314,9 +314,7 @@ class MergeRequestDiff < ActiveRecord::Base def keep_around_commits [repository, merge_request.source_project.repository].uniq.each do |repo| - repo.keep_around(start_commit_sha) - repo.keep_around(head_commit_sha) - repo.keep_around(base_commit_sha) + repo.keep_around(start_commit_sha, head_commit_sha, base_commit_sha) end end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index b974309aeb6..0deb44d7916 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -10,6 +10,7 @@ class Namespace < ActiveRecord::Base include Storage::LegacyNamespace include Gitlab::SQL::Pattern include IgnorableColumn + include FeatureGate ignore_column :deleted_at @@ -124,7 +125,6 @@ class Namespace < ActiveRecord::Base def to_param full_path end - alias_method :flipper_id, :to_param def human_name owner_name diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 1df3a51a7fc..1600acfc575 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -45,6 +45,15 @@ class NotificationSetting < ActiveRecord::Base :success_pipeline ].freeze + # Update unfound_translations.rb when events are changed + def self.email_events(source = nil) + EMAIL_EVENTS + end + + def email_events + self.class.email_events(source) + end + EXCLUDED_PARTICIPATING_EVENTS = [ :success_pipeline ].freeze diff --git a/app/models/programming_language.rb b/app/models/programming_language.rb index 400d6c407a7..0e667dac21e 100644 --- a/app/models/programming_language.rb +++ b/app/models/programming_language.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ProgrammingLanguage < ActiveRecord::Base validates :name, presence: true validates :color, allow_blank: false, color: true diff --git a/app/models/project.rb b/app/models/project.rb index 36089995ed3..67593c9b2fe 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -27,6 +27,8 @@ class Project < ActiveRecord::Base include FastDestroyAll::Helpers include WithUploads include BatchDestroyDependentAssociations + include FeatureGate + include OptionallySearch extend Gitlab::Cache::RequestCache extend Gitlab::ConfigHelper @@ -83,8 +85,7 @@ class Project < ActiveRecord::Base after_create :create_project_feature, unless: :project_feature after_create -> { SiteStatistic.track(STATISTICS_ATTRIBUTE) } - before_destroy ->(project) { project.project_feature.untrack_statistics_for_deletion! } - after_destroy -> { SiteStatistic.untrack(STATISTICS_ATTRIBUTE) } + before_destroy :untrack_site_statistics after_create :create_ci_cd_settings, unless: :ci_cd_settings, @@ -139,7 +140,6 @@ class Project < ActiveRecord::Base has_one :flowdock_service has_one :assembla_service has_one :asana_service - has_one :gemnasium_service has_one :mattermost_slash_commands_service has_one :mattermost_service has_one :slack_slash_commands_service @@ -383,6 +383,26 @@ class Project < ActiveRecord::Base only_integer: true, message: 'needs to be beetween 10 minutes and 1 month' } + # Paginates a collection using a `WHERE id < ?` condition. + # + # before - A project ID to use for filtering out projects with an equal or + # greater ID. If no ID is given, all projects are included. + # + # limit - The maximum number of rows to include. + def self.paginate_in_descending_order_using_id( + before: nil, + limit: Kaminari.config.default_per_page + ) + relation = order_id_desc.limit(limit) + relation = relation.where('projects.id < ?', before) if before + + relation + end + + def self.eager_load_namespace_and_owner + includes(namespace: :owner) + end + # Returns a collection of projects that is either public or visible to the # logged in user. def self.public_or_visible_to_user(user = nil) @@ -470,6 +490,24 @@ class Project < ActiveRecord::Base }x end + def reference_postfix + '>' + end + + def reference_postfix_escaped + '>' + end + + # Pattern used to extract `namespace/project>` project references from text. + # '>' or its escaped form ('>') are checked for because '>' is sometimes escaped + # when the reference comes from an external source. + def markdown_reference_pattern + %r{ + #{reference_pattern} + (#{reference_postfix}|#{reference_postfix_escaped}) + }x + end + def trending joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id') .reorder('trending_projects.id ASC') @@ -501,18 +539,19 @@ class Project < ActiveRecord::Base def auto_devops_enabled? if auto_devops&.enabled.nil? - Gitlab::CurrentSettings.auto_devops_enabled? + has_auto_devops_implicitly_enabled? else auto_devops.enabled? end end def has_auto_devops_implicitly_enabled? - auto_devops&.enabled.nil? && Gitlab::CurrentSettings.auto_devops_enabled? + auto_devops&.enabled.nil? && + (Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self)) end def has_auto_devops_implicitly_disabled? - auto_devops&.enabled.nil? && !Gitlab::CurrentSettings.auto_devops_enabled? + auto_devops&.enabled.nil? && !(Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self)) end def empty_repo? @@ -908,6 +947,10 @@ class Project < ActiveRecord::Base end end + def to_reference_with_postfix + "#{to_reference(full: true)}#{self.class.reference_postfix}" + end + # `from` argument can be a Namespace or Project. def to_reference(from = nil, full: false) if full || cross_namespace_reference?(from) @@ -2028,13 +2071,19 @@ class Project < ActiveRecord::Base private def rename_or_migrate_repository! - if Gitlab::CurrentSettings.hashed_storage_enabled? && storage_version != LATEST_STORAGE_VERSION + if Gitlab::CurrentSettings.hashed_storage_enabled? && + storage_upgradable? && + Feature.disabled?(:skip_hashed_storage_upgrade) # kill switch in case we need to disable upgrade behavior ::Projects::HashedStorageMigrationService.new(self, full_path_was).execute else storage.rename_repo end end + def storage_upgradable? + storage_version != LATEST_STORAGE_VERSION + end + def after_rename_repository(full_path_before, path_before) execute_rename_repository_hooks!(full_path_before) @@ -2049,6 +2098,11 @@ class Project < ActiveRecord::Base Gitlab::PagesTransfer.new.rename_project(path_before, self.path, namespace.full_path) end + def untrack_site_statistics + SiteStatistic.untrack(STATISTICS_ATTRIBUTE) + self.project_feature.untrack_statistics_for_deletion! + end + def execute_rename_repository_hooks!(full_path_before) # When we import a project overwriting the original project, there # is a move operation. In that case we don't want to send the instructions. diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb index 155400d1a43..dc6736dd9cd 100644 --- a/app/models/project_auto_devops.rb +++ b/app/models/project_auto_devops.rb @@ -47,12 +47,8 @@ class ProjectAutoDevops < ActiveRecord::Base end def needs_to_create_deploy_token? - auto_devops_enabled? && + project.auto_devops_enabled? && !project.public? && !project.deploy_tokens.find_by(name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME).present? end - - def auto_devops_enabled? - Gitlab::CurrentSettings.auto_devops_enabled? || enabled? - end end diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 4f289e6e215..35c19049c04 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'asana' class AsanaService < Service diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb index 4234b8044e5..60575e45a90 100644 --- a/app/models/project_services/assembla_service.rb +++ b/app/models/project_services/assembla_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AssemblaService < Service prop_accessor :token, :subdomain validates :token, presence: true, if: :activated? diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index edc5c00d9c4..d502423726c 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class BambooService < CiService include ReactiveService diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb index e4e3a80976b..1a2bb6a171b 100644 --- a/app/models/project_services/bugzilla_service.rb +++ b/app/models/project_services/bugzilla_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class BugzillaService < IssueTrackerService validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index 35884c4560c..43edfde851c 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "addressable/uri" class BuildkiteService < CiService diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index 0c526b53d72..f2295a95b60 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This class is to be removed with 9.1 # We should also by then remove BuildsEmailService from database class BuildsEmailService < Service diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index cb4af73807b..1d7877a1fb5 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CampfireService < Service prop_accessor :token, :subdomain, :room validates :token, presence: true, if: :activated? @@ -82,7 +84,7 @@ class CampfireService < Service before = push[:before] after = push[:after] - message = "" + message = [] message << "[#{project.full_name}] " message << "#{push[:user_name]} " @@ -95,6 +97,6 @@ class CampfireService < Service message << "#{project.web_url}/compare/#{before}...#{after}" end - message + message.join end end diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb index f710fa85b5d..8c68ddc40f2 100644 --- a/app/models/project_services/chat_message/base_message.rb +++ b/app/models/project_services/chat_message/base_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'slack-notifier' module ChatMessage diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb index 3273f41dbd2..0cdcfcf0237 100644 --- a/app/models/project_services/chat_message/issue_message.rb +++ b/app/models/project_services/chat_message/issue_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ChatMessage class IssueMessage < BaseMessage attr_reader :title diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb index f412b6833d9..58631e09538 100644 --- a/app/models/project_services/chat_message/merge_message.rb +++ b/app/models/project_services/chat_message/merge_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ChatMessage class MergeMessage < BaseMessage attr_reader :merge_request_iid diff --git a/app/models/project_services/chat_message/note_message.rb b/app/models/project_services/chat_message/note_message.rb index 7f9486132e6..741474fb27b 100644 --- a/app/models/project_services/chat_message/note_message.rb +++ b/app/models/project_services/chat_message/note_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ChatMessage class NoteMessage < BaseMessage attr_reader :note diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb index 96fd23aede3..62aec4351db 100644 --- a/app/models/project_services/chat_message/pipeline_message.rb +++ b/app/models/project_services/chat_message/pipeline_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ChatMessage class PipelineMessage < BaseMessage attr_reader :ref_type diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb index 8d599c5f116..82be33a12a1 100644 --- a/app/models/project_services/chat_message/push_message.rb +++ b/app/models/project_services/chat_message/push_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ChatMessage class PushMessage < BaseMessage attr_reader :after diff --git a/app/models/project_services/chat_message/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb index d84b80f2de2..b605d289278 100644 --- a/app/models/project_services/chat_message/wiki_page_message.rb +++ b/app/models/project_services/chat_message/wiki_page_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ChatMessage class WikiPageMessage < BaseMessage attr_reader :title diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index a60b4c7fd0d..c10ee07ccf4 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Base class for Chat notifications services # This class is not meant to be used directly, but only to inherit from. class ChatNotificationService < Service diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index 82979c8bd34..f0ef2d925ab 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Base class for CI services # List methods you need to implement to get your CI service # working with GitLab Merge Requests diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb index 456c7f5cee2..b8f8072869c 100644 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CustomIssueTrackerService < IssueTrackerService validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb index 5b8320158fc..6dae4f3a4a6 100644 --- a/app/models/project_services/deployment_service.rb +++ b/app/models/project_services/deployment_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Base class for deployment services # # These services integrate with a deployment solution like Kubernetes/OpenShift, diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index ab4e46da89f..158ae0bf255 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DroneCiService < CiService include ReactiveService diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index b604d860a87..fb73d430fb1 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class EmailsOnPushService < Service boolean_accessor :send_from_committer_email boolean_accessor :disable_diffs diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb index a4b1ef09e93..d2835c6ac82 100644 --- a/app/models/project_services/external_wiki_service.rb +++ b/app/models/project_services/external_wiki_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ExternalWikiService < Service prop_accessor :external_wiki_url diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index da01ac1b7cf..2545df06f6b 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "flowdock-git-hook" # Flow dock depends on Grit to compute the number of commits between two given diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb deleted file mode 100644 index 8a6b0ed1a5f..00000000000 --- a/app/models/project_services/gemnasium_service.rb +++ /dev/null @@ -1,60 +0,0 @@ -require "gemnasium/gitlab_service" - -class GemnasiumService < Service - prop_accessor :token, :api_key - validates :token, :api_key, presence: true, if: :activated? - validate :deprecation_validation - - def title - 'Gemnasium' - end - - def description - 'Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities.' - end - - def self.to_param - 'gemnasium' - end - - def fields - [ - { type: 'text', name: 'api_key', placeholder: 'Your personal API KEY on gemnasium.com ', required: true }, - { type: 'text', name: 'token', placeholder: 'The project\'s slug on gemnasium.com', required: true } - ] - end - - def self.supported_events - %w(push) - end - - def deprecated? - true - end - - def deprecation_message - "Gemnasium has been acquired by GitLab in January 2018. Since May 15, 2018, the service provided by Gemnasium is no longer available." - end - - def deprecation_validation - errors[:base] << deprecation_message - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - # Gitaly: this class will be removed https://gitlab.com/gitlab-org/gitlab-ee/issues/6010 - repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do - project.repository.path_to_repo - end - - Gemnasium::GitlabService.execute( - ref: data[:ref], - before: data[:before], - after: data[:after], - token: token, - api_key: api_key, - repo: repo_path - ) - end -end diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 16e32a4139e..fa9abf58e62 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class GitlabIssueTrackerService < IssueTrackerService include Gitlab::Routing diff --git a/app/models/project_services/hangouts_chat_service.rb b/app/models/project_services/hangouts_chat_service.rb index a8512c5f57c..272cd0f4e47 100644 --- a/app/models/project_services/hangouts_chat_service.rb +++ b/app/models/project_services/hangouts_chat_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'hangouts_chat' class HangoutsChatService < ChatNotificationService diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index dce878e485f..66012f0da99 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class HipchatService < Service include ActionView::Helpers::SanitizeHelper @@ -108,7 +110,7 @@ class HipchatService < Service before = push[:before] after = push[:after] - message = "" + message = [] message << "#{push[:user_name]} " if Gitlab::Git.blank_ref?(before) @@ -132,7 +134,7 @@ class HipchatService < Service end end - message + message.join end def markdown(text, options = {}) @@ -165,11 +167,11 @@ class HipchatService < Service description = obj_attr[:description] issue_link = "<a href=\"#{issue_url}\">issue ##{issue_iid}</a>" - message = "#{user_name} #{state} #{issue_link} in #{project_link}: <b>#{title}</b>" + message = ["#{user_name} #{state} #{issue_link} in #{project_link}: <b>#{title}</b>"] message << "<pre>#{markdown(description)}</pre>" - message + message.join end def create_merge_request_message(data) @@ -184,12 +186,11 @@ class HipchatService < Service merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}" merge_request_link = "<a href=\"#{merge_request_url}\">merge request !#{merge_request_id}</a>" - message = "#{user_name} #{state} #{merge_request_link} in " \ - "#{project_link}: <b>#{title}</b>" + message = ["#{user_name} #{state} #{merge_request_link} in " \ + "#{project_link}: <b>#{title}</b>"] message << "<pre>#{markdown(description)}</pre>" - - message + message.join end def format_title(title) @@ -235,12 +236,11 @@ class HipchatService < Service end subject_html = "<a href=\"#{note_url}\">#{subject_type} #{subject_desc}</a>" - message = "#{user_name} commented on #{subject_html} in #{project_link}: " + message = ["#{user_name} commented on #{subject_html} in #{project_link}: "] message << title message << "<pre>#{markdown(note, ref: commit_id)}</pre>" - - message + message.join end def create_pipeline_message(data) diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index 27bdf708c80..a783a314071 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'uri' class IrkerService < Service diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index df6dcd90985..c7520d766a8 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class IssueTrackerService < Service validate :one_issue_tracker, if: :activated?, on: :manual_change diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 82d438d5378..cc98b3f5a41 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class JiraService < IssueTrackerService include Gitlab::Routing include ApplicationHelper diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 722642f6da7..bda1f67b8ff 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ## # NOTE: # We'll move this class to Clusters::Platforms::Kubernetes, which contains exactly the same logic. diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb index 0362ed172c7..b8bc83b870e 100644 --- a/app/models/project_services/mattermost_service.rb +++ b/app/models/project_services/mattermost_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MattermostService < ChatNotificationService def title 'Mattermost notifications' diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 227d430083d..ca324f68d2d 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MattermostSlashCommandsService < SlashCommandsService include TriggersHelper diff --git a/app/models/project_services/microsoft_teams_service.rb b/app/models/project_services/microsoft_teams_service.rb index 99500caec0e..5b0e5fed092 100644 --- a/app/models/project_services/microsoft_teams_service.rb +++ b/app/models/project_services/microsoft_teams_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MicrosoftTeamsService < ChatNotificationService def title 'Microsoft Teams Notification' diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb index b89dc07a73e..6883976f0c8 100644 --- a/app/models/project_services/mock_ci_service.rb +++ b/app/models/project_services/mock_ci_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # For an example companion mocking service, see https://gitlab.com/gitlab-org/gitlab-mock-ci-service class MockCiService < CiService ALLOWED_STATES = %w[failed canceled running pending success success_with_warnings skipped not_found].freeze diff --git a/app/models/project_services/mock_deployment_service.rb b/app/models/project_services/mock_deployment_service.rb index 59a3811ce5d..7ab1687f8ba 100644 --- a/app/models/project_services/mock_deployment_service.rb +++ b/app/models/project_services/mock_deployment_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MockDeploymentService < DeploymentService def title 'Mock deployment' diff --git a/app/models/project_services/mock_monitoring_service.rb b/app/models/project_services/mock_monitoring_service.rb index ed0318c6b27..bcf8f1df5da 100644 --- a/app/models/project_services/mock_monitoring_service.rb +++ b/app/models/project_services/mock_monitoring_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MockMonitoringService < MonitoringService def title 'Mock monitoring' diff --git a/app/models/project_services/monitoring_service.rb b/app/models/project_services/monitoring_service.rb index 9af68b4e821..1b530a8247b 100644 --- a/app/models/project_services/monitoring_service.rb +++ b/app/models/project_services/monitoring_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Base class for monitoring services # # These services integrate with a deployment solution like Prometheus diff --git a/app/models/project_services/packagist_service.rb b/app/models/project_services/packagist_service.rb index ba62a5b7ac0..003884bb7ac 100644 --- a/app/models/project_services/packagist_service.rb +++ b/app/models/project_services/packagist_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PackagistService < Service prop_accessor :username, :token, :server diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb index 4cf149ac044..6f39a5e6e83 100644 --- a/app/models/project_services/pipelines_email_service.rb +++ b/app/models/project_services/pipelines_email_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PipelinesEmailService < Service prop_accessor :recipients boolean_accessor :notify_only_broken_pipelines diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index 3476e7d2283..617e502b639 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PivotaltrackerService < Service API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'.freeze diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index df4254e0523..509e5b6089b 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PrometheusService < MonitoringService include PrometheusAdapter diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index 8777a44b72f..4e48c348b45 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PushoverService < Service BASE_URI = 'https://api.pushover.net/1'.freeze @@ -79,7 +81,7 @@ class PushoverService < Service end if data[:total_commits_count] > 0 - message << "\nTotal commits count: #{data[:total_commits_count]}" + message = [message, "Total commits count: #{data[:total_commits_count]}"].join("\n") end pushover_data = { diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index 3721093a6d1..a80be4b06da 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RedmineService < IssueTrackerService validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 71da0af75f6..482808255f9 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class SlackService < ChatNotificationService def title 'Slack notifications' diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb index 1c3892a3f75..6c82e088231 100644 --- a/app/models/project_services/slack_slash_commands_service.rb +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class SlackSlashCommandsService < SlashCommandsService include TriggersHelper diff --git a/app/models/project_services/slash_commands_service.rb b/app/models/project_services/slash_commands_service.rb index 37ea45109ae..e3ab60adefd 100644 --- a/app/models/project_services/slash_commands_service.rb +++ b/app/models/project_services/slash_commands_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Base class for Chat services # This class is not meant to be used directly, but only to inherrit from. class SlashCommandsService < Service diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index 802678147cf..eeeff5e802a 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TeamcityService < CiService include ReactiveService diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb index e8d35ac326f..b0d5c64e931 100644 --- a/app/models/protected_branch/merge_access_level.rb +++ b/app/models/protected_branch/merge_access_level.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base include ProtectedBranchAccess end diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index 7a2e9e5ec5d..b2a88229853 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ProtectedBranch::PushAccessLevel < ActiveRecord::Base include ProtectedBranchAccess end diff --git a/app/models/protected_tag.rb b/app/models/protected_tag.rb index a36f0d36262..94746141945 100644 --- a/app/models/protected_tag.rb +++ b/app/models/protected_tag.rb @@ -4,6 +4,8 @@ class ProtectedTag < ActiveRecord::Base include Gitlab::ShellAdapter include ProtectedRef + validates :name, uniqueness: { scope: :project_id } + protected_ref_access_levels :create def self.protected?(project, ref_name) diff --git a/app/models/protected_tag/create_access_level.rb b/app/models/protected_tag/create_access_level.rb index 6b6ab3d8279..b06e55fb5dd 100644 --- a/app/models/protected_tag/create_access_level.rb +++ b/app/models/protected_tag/create_access_level.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ProtectedTag::CreateAccessLevel < ActiveRecord::Base include ProtectedTagAccess diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index 833faf3bc82..c1f53b5da4f 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -150,6 +150,15 @@ class RemoteMirror < ActiveRecord::Base result.to_s end + def ensure_remote! + return unless project + return unless remote_name && url + + # If this fails or the remote already exists, we won't know due to + # https://gitlab.com/gitlab-org/gitaly/issues/1317 + project.repository.add_remote(remote_name, url) + end + private def raw diff --git a/app/models/repository.rb b/app/models/repository.rb index 69f375dc6f3..cf255c8951f 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -247,15 +247,22 @@ class Repository # Git GC will delete commits from the repository that are no longer in any # branches or tags, but we want to keep some of these commits around, for # example if they have comments or CI builds. - def keep_around(sha) - return unless sha.present? && commit_by(oid: sha) + # + # For Geo's sake, pass in multiple shas rather than calling it multiple times, + # to avoid unnecessary syncing. + def keep_around(*shas) + shas.each do |sha| + begin + next unless sha.present? && commit_by(oid: sha) - return if kept_around?(sha) + next if kept_around?(sha) - # This will still fail if the file is corrupted (e.g. 0 bytes) - raw_repository.write_ref(keep_around_ref_name(sha), sha, shell: false) - rescue Gitlab::Git::CommandError => ex - Rails.logger.error "Unable to create keep-around reference for repository #{disk_path}: #{ex}" + # This will still fail if the file is corrupted (e.g. 0 bytes) + raw_repository.write_ref(keep_around_ref_name(sha), sha, shell: false) + rescue Gitlab::Git::CommandError => ex + Rails.logger.error "Unable to create keep-around reference for repository #{disk_path}: #{ex}" + end + end end def kept_around?(sha) diff --git a/app/models/repository_language.rb b/app/models/repository_language.rb index f467d4eafa3..b18142a2ac4 100644 --- a/app/models/repository_language.rb +++ b/app/models/repository_language.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RepositoryLanguage < ActiveRecord::Base belongs_to :project belongs_to :programming_language diff --git a/app/models/site_statistic.rb b/app/models/site_statistic.rb index 9c9c3172fe6..48324570f0b 100644 --- a/app/models/site_statistic.rb +++ b/app/models/site_statistic.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class SiteStatistic < ActiveRecord::Base # prevents the creation of multiple rows default_value_for :id, 1 @@ -47,7 +49,7 @@ class SiteStatistic < ActiveRecord::Base # # @return [SiteStatistic] record with tracked information def self.fetch - SiteStatistic.transaction(requires_new: true) do + transaction(requires_new: true) do SiteStatistic.first_or_create! end rescue ActiveRecord::RecordNotUnique diff --git a/app/models/storage/hashed_project.rb b/app/models/storage/hashed_project.rb index 26b4b78ac64..90710f73fd3 100644 --- a/app/models/storage/hashed_project.rb +++ b/app/models/storage/hashed_project.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Storage class HashedProject attr_accessor :project diff --git a/app/models/storage/legacy_project.rb b/app/models/storage/legacy_project.rb index 27cb388c702..9f6f19acb41 100644 --- a/app/models/storage/legacy_project.rb +++ b/app/models/storage/legacy_project.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Storage class LegacyProject attr_accessor :project diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index c5c77bc8333..376ef673ca8 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -15,7 +15,7 @@ class SystemNoteMetadata < ActiveRecord::Base commit description merge confidential visible label assignee cross_reference title time_tracking branch milestone discussion task moved opened closed merged duplicate locked unlocked - outdated + outdated tag ].freeze validates :note, presence: true diff --git a/app/models/user.rb b/app/models/user.rb index 37f2e8b680e..f21ca1c569f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -19,6 +19,7 @@ class User < ActiveRecord::Base include BulkMemberAccessLoad include BlocksJsonSerialization include WithUploads + include OptionallySearch DEFAULT_NOTIFICATION_LEVEL = :participating @@ -101,6 +102,10 @@ class User < ActiveRecord::Base has_many :groups, through: :group_members has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :group has_many :maintainers_groups, -> { where(members: { access_level: Gitlab::Access::MAINTAINER }) }, through: :group_members, source: :group + has_many :owned_or_maintainers_groups, + -> { where(members: { access_level: [Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) }, + through: :group_members, + source: :group alias_attribute :masters_groups, :maintainers_groups # Projects @@ -249,18 +254,51 @@ class User < ActiveRecord::Base scope :external, -> { where(external: true) } scope :active, -> { with_state(:active).non_internal } scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) } - scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) } scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) } scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) } scope :confirmed, -> { where.not(confirmed_at: nil) } - def self.with_two_factor_indistinct - joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id") - .where("u2f.id IS NOT NULL OR users.otp_required_for_login = ?", true) + # Limits the users to those that have TODOs, optionally in the given state. + # + # user - The user to get the todos for. + # + # with_todos - If we should limit the result set to users that are the + # authors of todos. + # + # todo_state - An optional state to require the todos to be in. + def self.limit_to_todo_authors(user: nil, with_todos: false, todo_state: nil) + if user && with_todos + where(id: Todo.where(user: user, state: todo_state).select(:author_id)) + else + all + end + end + + # Returns a relation that optionally includes the given user. + # + # user_id - The ID of the user to include. + def self.union_with_user(user_id = nil) + if user_id.present? + union = Gitlab::SQL::Union.new([all, User.unscoped.where(id: user_id)]) + + # We use "unscoped" here so that any inner conditions are not repeated for + # the outer query, which would be redundant. + User.unscoped.from("(#{union.to_sql}) #{User.table_name}") + else + all + end end def self.with_two_factor - with_two_factor_indistinct.distinct(arel_table[:id]) + with_u2f_registrations = <<-SQL + EXISTS ( + SELECT * + FROM u2f_registrations AS u2f + WHERE u2f.user_id = users.id + ) OR users.otp_required_for_login = ? + SQL + + where(with_u2f_registrations, true) end def self.without_two_factor @@ -361,6 +399,18 @@ class User < ActiveRecord::Base ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name) end + # Limits the result set to users _not_ in the given query/list of IDs. + # + # users - The list of users to ignore. This can be an + # `ActiveRecord::Relation`, or an Array. + def where_not_in(users = nil) + users ? where.not(id: users) : all + end + + def reorder_by_name + reorder(:name) + end + # searches user by given pattern # it compares name, email, username fields and user's secondary emails with given pattern # This method uses ILIKE on PostgreSQL and LIKE on MySQL. @@ -512,7 +562,7 @@ class User < ActiveRecord::Base otp_grace_period_started_at: nil, otp_backup_codes: nil ) - self.u2f_registrations.destroy_all + self.u2f_registrations.destroy_all # rubocop: disable DestroyAll end end @@ -982,15 +1032,7 @@ class User < ActiveRecord::Base end def manageable_groups - union_sql = Gitlab::SQL::Union.new([owned_groups.select(:id), maintainers_groups.select(:id)]).to_sql - - # Update this line to not use raw SQL when migrated to Rails 5.2. - # Either ActiveRecord or Arel constructions are fine. - # This was replaced with the raw SQL construction because of bugs in the arel gem. - # Bugs were fixed in arel 9.0.0 (Rails 5.2). - owned_and_maintainer_groups = Group.where("namespaces.id IN (#{union_sql})") # rubocop:disable GitlabSecurity/SqlInjection - - Gitlab::GroupHierarchy.new(owned_and_maintainer_groups).base_and_descendants + Gitlab::GroupHierarchy.new(owned_or_maintainers_groups).base_and_descendants end def namespaces @@ -1244,11 +1286,6 @@ class User < ActiveRecord::Base !terms_accepted? end - def owned_or_maintainers_groups - union = Gitlab::SQL::Union.new([owned_groups, maintainers_groups]) - Group.from("(#{union.to_sql}) namespaces") - end - # @deprecated alias_method :owned_or_masters_groups, :owned_or_maintainers_groups diff --git a/app/policies/commit_policy.rb b/app/policies/commit_policy.rb new file mode 100644 index 00000000000..67e9bc12804 --- /dev/null +++ b/app/policies/commit_policy.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class CommitPolicy < BasePolicy + delegate { @subject.project } +end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index a8d7a05f509..73c93b22c95 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -53,7 +53,7 @@ class GroupPolicy < BasePolicy rule { has_access }.enable :read_namespace - rule { developer }.enable :admin_milestones + rule { developer }.enable :admin_milestone rule { reporter }.policy do enable :admin_label @@ -72,6 +72,8 @@ class GroupPolicy < BasePolicy enable :admin_namespace enable :admin_group_member enable :change_visibility_level + + enable :set_note_created_at end rule { can?(:read_nested_project_resources) }.policy do diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 50d39d826a2..273a93a1423 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -143,6 +143,10 @@ class ProjectPolicy < BasePolicy enable :destroy_merge_request enable :destroy_issue enable :remove_pages + + enable :set_issue_iid + enable :set_issue_created_at + enable :set_note_created_at end rule { can?(:guest_access) }.policy do @@ -252,6 +256,7 @@ class ProjectPolicy < BasePolicy enable :update_pages enable :read_cluster enable :create_cluster + enable :create_environment_terminal end rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb index 02f6c5bdf81..880218e2727 100644 --- a/app/presenters/ci/build_runner_presenter.rb +++ b/app/presenters/ci/build_runner_presenter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Ci class BuildRunnerPresenter < SimpleDelegator def artifacts diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index b887b99d31c..271ff668eda 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -9,6 +9,28 @@ class BuildDetailsEntity < JobEntity expose :metadata, using: BuildMetadataEntity + expose :artifact, if: -> (*) { can?(current_user, :read_build, build) } do + expose :download_path, if: -> (*) { build.artifacts? } do |build| + download_project_job_artifacts_path(project, build) + end + + expose :browse_path, if: -> (*) { build.browsable_artifacts? } do |build| + browse_project_job_artifacts_path(project, build) + end + + expose :keep_path, if: -> (*) { build.has_expiring_artifacts? && can?(current_user, :update_build, build) } do |build| + keep_project_job_artifacts_path(project, build) + end + + expose :expire_at, if: -> (*) { build.artifacts_expire_at.present? } do |build| + build.artifacts_expire_at + end + + expose :expired, if: -> (*) { build.artifacts_expire_at.present? } do |build| + build.artifacts_expired? + end + end + expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build| erase_project_job_path(project, build) diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb index 79844c9210a..cbe6f200b86 100644 --- a/app/serializers/diff_file_entity.rb +++ b/app/serializers/diff_file_entity.rb @@ -2,7 +2,6 @@ class DiffFileEntity < Grape::Entity include RequestAwareEntity - include BlobHelper include CommitsHelper include DiffHelper include SubmoduleHelper @@ -136,12 +135,12 @@ class DiffFileEntity < Grape::Entity end # Used for inline diffs - expose :highlighted_diff_lines, if: -> (diff_file, _) { diff_file.text? } do |diff_file| + expose :highlighted_diff_lines, using: DiffLineEntity, if: -> (diff_file, _) { diff_file.text? } do |diff_file| diff_file.diff_lines_for_serializer end # Used for parallel diffs - expose :parallel_diff_lines, if: -> (diff_file, _) { diff_file.text? } + expose :parallel_diff_lines, using: DiffLineParallelEntity, if: -> (diff_file, _) { diff_file.text? } def current_user request.current_user diff --git a/app/serializers/diff_line_entity.rb b/app/serializers/diff_line_entity.rb new file mode 100644 index 00000000000..2119a1017d3 --- /dev/null +++ b/app/serializers/diff_line_entity.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class DiffLineEntity < Grape::Entity + expose :line_code + expose :type + expose :old_line + expose :new_line + expose :text + expose :meta_positions, as: :meta_data + + expose :rich_text do |line| + line.rich_text || CGI.escapeHTML(line.text) + end +end diff --git a/app/serializers/diff_line_parallel_entity.rb b/app/serializers/diff_line_parallel_entity.rb new file mode 100644 index 00000000000..0438a67d51b --- /dev/null +++ b/app/serializers/diff_line_parallel_entity.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class DiffLineParallelEntity < Grape::Entity + expose :left, using: DiffLineEntity + expose :right, using: DiffLineEntity +end diff --git a/app/serializers/diff_line_serializer.rb b/app/serializers/diff_line_serializer.rb new file mode 100644 index 00000000000..7f1f2d9aa7c --- /dev/null +++ b/app/serializers/diff_line_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class DiffLineSerializer < BaseSerializer + entity DiffLineEntity +end diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb index b8321037fa5..ed09db0f3f4 100644 --- a/app/serializers/discussion_entity.rb +++ b/app/serializers/discussion_entity.rb @@ -43,7 +43,7 @@ class DiscussionEntity < Grape::Entity project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion) end - expose :truncated_diff_lines, if: -> (d, _) { d.diff_discussion? && d.on_text? && (d.expanded? || render_truncated_diff_lines?) } + expose :truncated_diff_lines, using: DiffLineEntity, if: -> (d, _) { d.diff_discussion? && d.on_text? && (d.expanded? || render_truncated_diff_lines?) } expose :image_diff_html, if: -> (d, _) { d.diff_discussion? && d.on_image? } do |discussion| diff_file = discussion.diff_file diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index b18e9706db6..07a13c33b89 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -23,9 +23,8 @@ class EnvironmentEntity < Grape::Entity stop_project_environment_path(environment.project, environment) end - expose :terminal_path, if: ->(environment, _) { environment.has_terminals? } do |environment| - can?(request.current_user, :admin_environment, environment.project) && - terminal_project_environment_path(environment.project, environment) + expose :terminal_path, if: ->(*) { environment.has_terminals? && can_access_terminal? } do |environment| + terminal_project_environment_path(environment.project, environment) end expose :folder_path do |environment| @@ -40,7 +39,13 @@ class EnvironmentEntity < Grape::Entity private + alias_method :environment, :object + def current_user request.current_user end + + def can_access_terminal? + can?(request.current_user, :create_environment_terminal, environment) + end end diff --git a/app/serializers/move_to_project_entity.rb b/app/serializers/move_to_project_entity.rb new file mode 100644 index 00000000000..dac1124b0b3 --- /dev/null +++ b/app/serializers/move_to_project_entity.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class MoveToProjectEntity < Grape::Entity + expose :id + expose :name_with_namespace +end diff --git a/app/serializers/move_to_project_serializer.rb b/app/serializers/move_to_project_serializer.rb new file mode 100644 index 00000000000..6a59317505c --- /dev/null +++ b/app/serializers/move_to_project_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class MoveToProjectSerializer < BaseSerializer + entity MoveToProjectEntity +end diff --git a/app/serializers/project_mirror_serializer.rb b/app/serializers/project_mirror_serializer.rb new file mode 100644 index 00000000000..6a9462aa7cb --- /dev/null +++ b/app/serializers/project_mirror_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ProjectMirrorSerializer < BaseSerializer + entity ProjectMirrorEntity +end diff --git a/app/serializers/test_case_entity.rb b/app/serializers/test_case_entity.rb index 5c1cbf37182..ec60055ba5b 100644 --- a/app/serializers/test_case_entity.rb +++ b/app/serializers/test_case_entity.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TestCaseEntity < Grape::Entity expose :status expose :name diff --git a/app/serializers/test_reports_comparer_entity.rb b/app/serializers/test_reports_comparer_entity.rb index b95d820d093..d7a3dd34fdc 100644 --- a/app/serializers/test_reports_comparer_entity.rb +++ b/app/serializers/test_reports_comparer_entity.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TestReportsComparerEntity < Grape::Entity expose :total_status, as: :status diff --git a/app/serializers/test_reports_comparer_serializer.rb b/app/serializers/test_reports_comparer_serializer.rb index a739858efb2..7fb8d28b09a 100644 --- a/app/serializers/test_reports_comparer_serializer.rb +++ b/app/serializers/test_reports_comparer_serializer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TestReportsComparerSerializer < BaseSerializer entity TestReportsComparerEntity end diff --git a/app/serializers/test_suite_comparer_entity.rb b/app/serializers/test_suite_comparer_entity.rb index a3965ba3930..9fa3a897ebe 100644 --- a/app/serializers/test_suite_comparer_entity.rb +++ b/app/serializers/test_suite_comparer_entity.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TestSuiteComparerEntity < Grape::Entity expose :name expose :total_status, as: :status diff --git a/app/services/ci/enqueue_build_service.rb b/app/services/ci/enqueue_build_service.rb new file mode 100644 index 00000000000..8140651d980 --- /dev/null +++ b/app/services/ci/enqueue_build_service.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +module Ci + class EnqueueBuildService < BaseService + def execute(build) + build.enqueue + end + end +end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index cda9bbff3b4..cafee76a33c 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -37,7 +37,7 @@ module Ci def process_build(build, current_status) if valid_statuses_for_when(build.when).include?(current_status) - build.action? ? build.actionize : build.enqueue + build.action? ? build.actionize : enqueue_build(build) true else build.skip @@ -93,5 +93,9 @@ module Ci .where.not(id: latest_statuses.map(&:first)) .update_all(retried: true) if latest_statuses.any? end + + def enqueue_build(build) + Ci::EnqueueBuildService.new(project, @user).execute(build) + end end end diff --git a/app/services/commits/tag_service.rb b/app/services/commits/tag_service.rb new file mode 100644 index 00000000000..7961ba4d3c4 --- /dev/null +++ b/app/services/commits/tag_service.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Commits + class TagService < BaseService + def execute(commit) + unless params[:tag_name] + return error('Missing parameter tag_name') + end + + tag_name = params[:tag_name] + message = params[:tag_message] + release_description = nil + + result = Tags::CreateService + .new(commit.project, current_user) + .execute(tag_name, commit.sha, message, release_description) + + if result[:status] == :success + tag = result[:tag] + SystemNoteService.tag_commit(commit, commit.project, current_user, tag.name) + end + + result + end + end +end diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 025f093a428..fc7b236f7da 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -7,8 +7,8 @@ module Files def initialize(*args) super - @author_email = params[:author_email] - @author_name = params[:author_name] + @author_email = params[:author_email] || current_user&.email + @author_name = params[:author_name] || current_user&.name @commit_message = params[:commit_message] @last_commit_sha = params[:last_commit_sha] diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 637c1df4ad9..26e90e8cf8c 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -140,7 +140,6 @@ class GitPushService < BaseService EventCreateService.new.push(project, current_user, build_push_data) Ci::CreatePipelineService.new(project, current_user, build_push_data).execute(:push) - SystemHookPushWorker.perform_async(build_push_data.dup, :push_hooks) project.execute_hooks(build_push_data.dup, :push_hooks) project.execute_services(build_push_data.dup, :push_hooks) @@ -159,7 +158,7 @@ class GitPushService < BaseService end def process_default_branch - offset = [push_commits_count - PROCESS_COMMIT_LIMIT, 0].max + offset = [push_commits_count_for_ref - PROCESS_COMMIT_LIMIT, 0].max @push_commits = project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT) project.after_create_default_branch @@ -173,7 +172,7 @@ class GitPushService < BaseService params[:newrev], params[:ref], @push_commits, - commits_count: push_commits_count) + commits_count: commits_count) end def push_to_existing_branch? @@ -214,8 +213,14 @@ class GitPushService < BaseService end end - def push_commits_count - strong_memoize(:push_commits_count) do + def commits_count + return push_commits_count_for_ref if default_branch? && push_to_new_branch? + + Array(@push_commits).size + end + + def push_commits_count_for_ref + strong_memoize(:push_commits_count_for_ref) do project.repository.commit_count_for_ref(params[:ref]) end end diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb index c4554ce45fb..93d84bd8a9c 100644 --- a/app/services/groups/destroy_service.rb +++ b/app/services/groups/destroy_service.rb @@ -2,6 +2,8 @@ module Groups class DestroyService < Groups::BaseService + DestroyError = Class.new(StandardError) + def async_execute job_id = GroupDestroyWorker.perform_async(group.id, current_user.id) Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}") @@ -10,13 +12,15 @@ module Groups def execute group.prepare_for_destroy - group.projects.each do |project| + group.projects.includes(:project_feature).each do |project| # Execute the destruction of the models immediately to ensure atomic cleanup. - # Skip repository removal because we remove directory with namespace - # that contain all these repositories - ::Projects::DestroyService.new(project, current_user, skip_repo: project.legacy_storage?).execute + success = ::Projects::DestroyService.new(project, current_user).execute + raise DestroyError, "Project #{project.id} can't be deleted" unless success end + # reload the relation to prevent triggering destroy hooks on the projects again + group.projects.reload + group.children.each do |group| # This needs to be synchronous since the namespace gets destroyed below DestroyService.new(group, current_user).execute diff --git a/app/services/issues/fetch_referenced_merge_requests_service.rb b/app/services/issues/fetch_referenced_merge_requests_service.rb deleted file mode 100644 index 5e84f3c81c9..00000000000 --- a/app/services/issues/fetch_referenced_merge_requests_service.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -module Issues - class FetchReferencedMergeRequestsService < Issues::BaseService - def execute(issue) - referenced_merge_requests = issue.referenced_merge_requests(current_user) - referenced_merge_requests = Gitlab::IssuableSorter.sort(project, referenced_merge_requests) { |i| i.iid.to_s } - closed_by_merge_requests = issue.closed_by_merge_requests(current_user) - closed_by_merge_requests = Gitlab::IssuableSorter.sort(project, closed_by_merge_requests) { |i| i.iid.to_s } - - [referenced_merge_requests, closed_by_merge_requests] - end - end -end diff --git a/app/services/issues/referenced_merge_requests_service.rb b/app/services/issues/referenced_merge_requests_service.rb new file mode 100644 index 00000000000..40d78502697 --- /dev/null +++ b/app/services/issues/referenced_merge_requests_service.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module Issues + class ReferencedMergeRequestsService < Issues::BaseService + def execute(issue) + referenced = referenced_merge_requests(issue) + closed_by = closed_by_merge_requests(issue) + preloader = ActiveRecord::Associations::Preloader.new + + preloader.preload(referenced + closed_by, + head_pipeline: { project: [:route, { namespace: :route }] }) + + [sort_by_iid(referenced), sort_by_iid(closed_by)] + end + + def referenced_merge_requests(issue) + merge_requests = extract_merge_requests(issue) + + cross_project_filter = -> (merge_requests) do + merge_requests.select { |mr| mr.target_project == project } + end + + Ability.merge_requests_readable_by_user( + merge_requests, + current_user, + filters: { + read_cross_project: cross_project_filter + } + ) + end + + def closed_by_merge_requests(issue) + return [] unless issue.open? + + merge_requests = extract_merge_requests(issue, filter: :system).select(&:open?) + + return [] if merge_requests.empty? + + ids = MergeRequestsClosingIssues.where(merge_request_id: merge_requests.map(&:id), issue_id: issue.id).pluck(:merge_request_id) + merge_requests.select { |mr| mr.id.in?(ids) } + end + + private + + def extract_merge_requests(issue, filter: nil) + ext = issue.all_references(current_user) + notes = issue_notes(issue) + notes = notes.select(&filter) if filter + + notes.each do |note| + note.all_references(current_user, extractor: ext) + end + + ext.merge_requests + end + + def issue_notes(issue) + @issue_notes ||= {} + @issue_notes[issue] ||= issue.notes.includes(:author) + end + + def sort_by_iid(merge_requests) + Gitlab::IssuableSorter.sort(project, merge_requests) { |mr| mr.iid.to_s } + end + end +end diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb index c0463052821..623a5f0950e 100644 --- a/app/services/labels/promote_service.rb +++ b/app/services/labels/promote_service.rb @@ -65,7 +65,7 @@ module Labels end def update_project_labels(label_ids) - Label.where(id: label_ids).destroy_all + Label.where(id: label_ids).destroy_all # rubocop: disable DestroyAll end def clone_label_to_group_label(label) diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index bc988eb2a26..55750269bb4 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -128,8 +128,7 @@ module MergeRequests # def assign_title_and_description assign_title_and_description_from_single_commit - assign_title_from_issue if target_project.issues_enabled? || target_project.external_issue_tracker - + merge_request.title ||= title_from_issue if target_project.issues_enabled? || target_project.external_issue_tracker merge_request.title ||= source_branch.titleize.humanize merge_request.title = wip_title if compare_commits.empty? @@ -159,20 +158,18 @@ module MergeRequests merge_request.description ||= commit.description.try(:strip) end - def assign_title_from_issue + def title_from_issue return unless issue - merge_request.title = "Resolve \"#{issue.title}\"" if issue.is_a?(Issue) + return "Resolve \"#{issue.title}\"" if issue.is_a?(Issue) - return if merge_request.title.present? + return if issue_iid.blank? - if issue_iid.present? - title_parts = ["Resolve #{issue.to_reference}"] - branch_title = source_branch.downcase.remove(issue_iid.downcase).titleize.humanize + title_parts = ["Resolve #{issue.to_reference}"] + branch_title = source_branch.downcase.remove(issue_iid.downcase).titleize.humanize - title_parts << "\"#{branch_title}\"" if branch_title.present? - merge_request.title = title_parts.join(' ') - end + title_parts << "\"#{branch_title}\"" if branch_title.present? + title_parts.join(' ') end def issue_iid diff --git a/app/services/milestones/destroy_service.rb b/app/services/milestones/destroy_service.rb index 15c04525075..7cda802c120 100644 --- a/app/services/milestones/destroy_service.rb +++ b/app/services/milestones/destroy_service.rb @@ -3,8 +3,6 @@ module Milestones class DestroyService < Milestones::BaseService def execute(milestone) - return unless milestone.project_milestone? - Milestone.transaction do update_params = { milestone: nil } @@ -16,15 +14,21 @@ module Milestones MergeRequests::UpdateService.new(parent, current_user, update_params).execute(merge_request) end - event_service.destroy_milestone(milestone, current_user) - - Event.for_milestone_id(milestone.id).each do |event| - event.target_id = nil - event.save - end + log_destroy_event_for(milestone) milestone.destroy end end + + def log_destroy_event_for(milestone) + return if milestone.group_milestone? + + event_service.destroy_milestone(milestone, current_user) + + Event.for_milestone_id(milestone.id).each do |event| + event.target_id = nil + event.save + end + end end end diff --git a/app/services/milestones/promote_service.rb b/app/services/milestones/promote_service.rb index 37aa6d3a9bc..660b4faaec0 100644 --- a/app/services/milestones/promote_service.rb +++ b/app/services/milestones/promote_service.rb @@ -73,7 +73,7 @@ module Milestones end def destroy_old_milestones(milestone) - Milestone.where(id: milestone_ids_for_merge(milestone)).destroy_all + Milestone.where(id: milestone_ids_for_merge(milestone)).destroy_all # rubocop: disable DestroyAll end def group_project_ids diff --git a/app/services/notes/quick_actions_service.rb b/app/services/notes/quick_actions_service.rb index 7280449bb1c..4c14d834949 100644 --- a/app/services/notes/quick_actions_service.rb +++ b/app/services/notes/quick_actions_service.rb @@ -4,7 +4,8 @@ module Notes class QuickActionsService < BaseService UPDATE_SERVICES = { 'Issue' => Issues::UpdateService, - 'MergeRequest' => MergeRequests::UpdateService + 'MergeRequest' => MergeRequests::UpdateService, + 'Commit' => Commits::TagService }.freeze def self.noteable_update_service(note) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 4389fd89538..5c0e8a35cb0 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -130,7 +130,7 @@ module NotificationRecipientService end def add_project_watchers - add_recipients(project_watchers, :watch, nil) + add_recipients(project_watchers, :watch, nil) if project end def add_group_watchers @@ -220,6 +220,8 @@ module NotificationRecipientService end class Default < Base + MENTION_TYPE_ACTIONS = [:new_issue, :new_merge_request].freeze + attr_reader :target attr_reader :current_user attr_reader :action @@ -252,7 +254,7 @@ module NotificationRecipientService add_subscribed_users - if [:new_issue, :new_merge_request].include?(custom_action) + if self.class.mention_type_actions.include?(custom_action) # These will all be participants as well, but adding with the :mention # type ensures that users with the mention notification level will # receive them, too. @@ -279,10 +281,14 @@ module NotificationRecipientService end # Build event key to search on custom notification level - # Check NotificationSetting::EMAIL_EVENTS + # Check NotificationSetting.email_events def custom_action @custom_action ||= "#{action}_#{target.class.model_name.name.underscore}".to_sym end + + def self.mention_type_actions + MENTION_TYPE_ACTIONS.dup + end end class NewNote < Base diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb index a15ee4911ef..11b996ed4b6 100644 --- a/app/services/preview_markdown_service.rb +++ b/app/services/preview_markdown_service.rb @@ -16,7 +16,7 @@ class PreviewMarkdownService < BaseService private def explain_quick_actions(text) - return text, [] unless %w(Issue MergeRequest).include?(commands_target_type) + return text, [] unless %w(Issue MergeRequest Commit).include?(commands_target_type) quick_actions_service = QuickActions::InterpretService.new(project, current_user) quick_actions_service.explain(text, find_commands_target) @@ -29,13 +29,9 @@ class PreviewMarkdownService < BaseService end def find_commands_target - if commands_target_id.present? - finder = commands_target_type == 'Issue' ? IssuesFinder : MergeRequestsFinder - finder.new(current_user, project_id: project.id).find(commands_target_id) - else - collection = commands_target_type == 'Issue' ? project.issues : project.merge_requests - collection.build - end + QuickActions::TargetService + .new(project, current_user) + .execute(commands_target_type, commands_target_id) end def commands_target_type diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index 10eb2cea4a2..5286b92ab6b 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -47,15 +47,7 @@ module Projects end def commands(noteable, type) - noteable ||= - case type - when 'Issue' - @project.issues.build - when 'MergeRequest' - @project.merge_requests.build - end - - return [] unless noteable&.is_a?(Issuable) + return [] unless noteable QuickActions::InterpretService.new(project, current_user).available_commands(noteable) end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 46a8a5e4d98..76e22507698 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -83,9 +83,6 @@ module Projects end def remove_repository(path) - # Skip repository removal. We use this flag when remove user or group - return true if params[:skip_repo] == true - # There is a possibility project does not have repository or wiki return true unless repo_exists?(path) diff --git a/app/services/projects/detect_repository_languages_service.rb b/app/services/projects/detect_repository_languages_service.rb index 4b4108de231..3488b9ce47e 100644 --- a/app/services/projects/detect_repository_languages_service.rb +++ b/app/services/projects/detect_repository_languages_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Projects class DetectRepositoryLanguagesService < BaseService attr_reader :detected_repository_languages, :programming_languages diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 33ad2120a75..cbbb88a9410 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -17,6 +17,14 @@ module Projects link_fork_network(fork_to_project) + # A forked project stores its LFS objects in the `forked_from_project`. + # So the LFS objects become inaccessible, and therefore delete them from + # the database so they'll get cleaned up. + # + # TODO: refactor this to get the correct lfs objects when implementing + # https://gitlab.com/gitlab-org/gitlab-ce/issues/39769 + fork_to_project.lfs_objects_projects.delete_all + fork_to_project end diff --git a/app/services/projects/move_deploy_keys_projects_service.rb b/app/services/projects/move_deploy_keys_projects_service.rb index 40a22837eaf..9f3f44f30ea 100644 --- a/app/services/projects/move_deploy_keys_projects_service.rb +++ b/app/services/projects/move_deploy_keys_projects_service.rb @@ -27,7 +27,7 @@ module Projects end def remove_remaining_deploy_keys_projects - source_project.deploy_keys_projects.destroy_all + source_project.deploy_keys_projects.destroy_all # rubocop: disable DestroyAll end end end diff --git a/app/services/projects/move_lfs_objects_projects_service.rb b/app/services/projects/move_lfs_objects_projects_service.rb index a5099519594..f78546a1e9c 100644 --- a/app/services/projects/move_lfs_objects_projects_service.rb +++ b/app/services/projects/move_lfs_objects_projects_service.rb @@ -21,7 +21,7 @@ module Projects end def remove_remaining_lfs_objects_project - source_project.lfs_objects_projects.destroy_all + source_project.lfs_objects_projects.destroy_all # rubocop: disable DestroyAll end def non_existent_lfs_objects_projects diff --git a/app/services/projects/move_notification_settings_service.rb b/app/services/projects/move_notification_settings_service.rb index 746605d56f1..109a00dd6d9 100644 --- a/app/services/projects/move_notification_settings_service.rb +++ b/app/services/projects/move_notification_settings_service.rb @@ -22,7 +22,7 @@ module Projects # Remove remaining notification settings from source_project def remove_remaining_notification_settings - source_project.notification_settings.destroy_all + source_project.notification_settings.destroy_all # rubocop: disable DestroyAll end # Get users of current notification_settings diff --git a/app/services/projects/move_project_group_links_service.rb b/app/services/projects/move_project_group_links_service.rb index d9038030f7e..1efafdce36d 100644 --- a/app/services/projects/move_project_group_links_service.rb +++ b/app/services/projects/move_project_group_links_service.rb @@ -26,7 +26,7 @@ module Projects # Remove remaining project group links from source_project def remove_remaining_project_group_links - source_project.reload.project_group_links.destroy_all + source_project.reload.project_group_links.destroy_all # rubocop: disable DestroyAll end def group_links_in_target_project diff --git a/app/services/projects/move_project_members_service.rb b/app/services/projects/move_project_members_service.rb index bb0c0d10242..ec983582d94 100644 --- a/app/services/projects/move_project_members_service.rb +++ b/app/services/projects/move_project_members_service.rb @@ -25,7 +25,7 @@ module Projects def remove_remaining_members # Remove remaining members and authorizations from source_project - source_project.project_members.destroy_all + source_project.project_members.destroy_all # rubocop: disable DestroyAll end def project_members_in_target_project diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index c2a0c5fa7f3..3746cfef702 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -43,8 +43,8 @@ module Projects @new_path = File.join(@new_namespace.try(:full_path) || '', project.path) @old_namespace = project.namespace - if Project.where(path: project.path, namespace_id: @new_namespace.try(:id)).exists? - raise TransferError.new("Project with same path in target namespace already exists") + if Project.where(namespace_id: @new_namespace.try(:id)).where('path = ? or name = ?', project.path, project.name).exists? + raise TransferError.new("Project with same name or path in target namespace already exists") end if project.has_container_registry_tags? @@ -118,6 +118,7 @@ module Projects def rollback_side_effects rollback_folder_move + project.reload update_namespace_and_visibility(@old_namespace) write_repository_config(@old_path) end diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb index 4651f7c4f8f..591b38b8151 100644 --- a/app/services/projects/update_remote_mirror_service.rb +++ b/app/services/projects/update_remote_mirror_service.rb @@ -10,6 +10,7 @@ module Projects return success unless remote_mirror.enabled? begin + remote_mirror.ensure_remote! repository.fetch_remote(remote_mirror.remote_name, no_tags: true) opts = {} diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 97f181ccea8..e390d7a04c3 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -30,7 +30,7 @@ module Projects def run_auto_devops_pipeline? return false if project.repository.gitlab_ci_yml || !project.auto_devops&.previous_changes&.include?('enabled') - project.auto_devops.enabled? || (project.auto_devops.enabled.nil? && Gitlab::CurrentSettings.auto_devops_enabled?) + project.auto_devops_enabled? end private diff --git a/app/services/protected_branches/legacy_api_update_service.rb b/app/services/protected_branches/legacy_api_update_service.rb index 1f6bbe72f85..da8bf2ce02a 100644 --- a/app/services/protected_branches/legacy_api_update_service.rb +++ b/app/services/protected_branches/legacy_api_update_service.rb @@ -38,11 +38,11 @@ module ProtectedBranches def delete_redundant_access_levels unless @developers_can_merge.nil? - @protected_branch.merge_access_levels.destroy_all + @protected_branch.merge_access_levels.destroy_all # rubocop: disable DestroyAll end unless @developers_can_push.nil? - @protected_branch.push_access_levels.destroy_all + @protected_branch.push_access_levels.destroy_all # rubocop: disable DestroyAll end end end diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index cdc8514c47c..a4c4c9e4812 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -60,7 +60,8 @@ module QuickActions "Closes this #{issuable.to_ability_name.humanize(capitalize: false)}." end condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.open? && current_user.can?(:"update_#{issuable.to_ability_name}", issuable) end @@ -75,7 +76,8 @@ module QuickActions "Reopens this #{issuable.to_ability_name.humanize(capitalize: false)}." end condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.closed? && current_user.can?(:"update_#{issuable.to_ability_name}", issuable) end @@ -149,7 +151,8 @@ module QuickActions issuable.allows_multiple_assignees? ? '@user1 @user2' : '' end condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.assignees.any? && current_user.can?(:"admin_#{issuable.to_ability_name}", project) end @@ -188,7 +191,8 @@ module QuickActions "Removes #{issuable.milestone.to_reference(format: :name)} milestone." end condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.milestone_id? && current_user.can?(:"admin_#{issuable.to_ability_name}", project) end @@ -231,7 +235,8 @@ module QuickActions end params '~label1 ~"label 2"' condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.labels.any? && current_user.can?(:"admin_#{issuable.to_ability_name}", project) end @@ -257,7 +262,8 @@ module QuickActions end params '~label1 ~"label 2"' condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.labels.any? && current_user.can?(:"admin_#{issuable.to_ability_name}", project) end @@ -295,7 +301,8 @@ module QuickActions desc 'Add a todo' explanation 'Adds a todo.' condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && !TodoService.new.todo_exist?(issuable, current_user) end command :todo do @@ -317,7 +324,8 @@ module QuickActions "Subscribes to this #{issuable.to_ability_name.humanize(capitalize: false)}." end condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && !issuable.subscribed?(current_user, project) end command :subscribe do @@ -329,7 +337,8 @@ module QuickActions "Unsubscribes from this #{issuable.to_ability_name.humanize(capitalize: false)}." end condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.subscribed?(current_user, project) end command :unsubscribe do @@ -385,14 +394,15 @@ module QuickActions end params ':emoji:' condition do - issuable.persisted? + issuable.is_a?(Issuable) && + issuable.persisted? end parse_params do |emoji_param| match = emoji_param.match(Banzai::Filter::EmojiFilter.emoji_pattern) match[1] if match end command :award do |name| - if name && issuable.user_can_award?(current_user, name) + if name && issuable.user_can_award?(current_user) @updates[:emoji_award] = name end end @@ -574,6 +584,23 @@ module QuickActions @updates[:confidential] = true end + desc 'Tag this commit.' + explanation do |tag_name, message| + with_message = %{ with "#{message}"} if message.present? + "Tags this commit to #{tag_name}#{with_message}." + end + params 'v1.2.3 <message>' + parse_params do |tag_name_and_message| + tag_name_and_message.split(' ', 2) + end + condition do + issuable.is_a?(Commit) && current_user.can?(:push_code, project) + end + command :tag do |tag_name, message| + @updates[:tag_name] = tag_name + @updates[:tag_message] = message + end + def extract_users(params) return [] if params.nil? diff --git a/app/services/quick_actions/target_service.rb b/app/services/quick_actions/target_service.rb new file mode 100644 index 00000000000..d8ba52c6e50 --- /dev/null +++ b/app/services/quick_actions/target_service.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module QuickActions + class TargetService < BaseService + def execute(type, type_id) + case type&.downcase + when 'issue' + issue(type_id) + when 'mergerequest' + merge_request(type_id) + when 'commit' + commit(type_id) + end + end + + private + + def issue(type_id) + IssuesFinder.new(current_user, project_id: project.id).find_by(iid: type_id) || project.issues.build + end + + def merge_request(type_id) + MergeRequestsFinder.new(current_user, project_id: project.id).find_by(iid: type_id) || project.merge_requests.build + end + + def commit(type_id) + project.commit(type_id) + end + end +end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 77494295f14..dda89830179 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -32,6 +32,21 @@ module SystemNoteService create_note(NoteSummary.new(noteable, project, author, body, action: 'commit', commit_count: total_count)) end + # Called when a commit was tagged + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the tag + # tag_name - The created tag name + # + # Returns the created Note object + def tag_commit(noteable, project, author, tag_name) + link = url_helpers.project_tag_url(project, id: tag_name) + body = "tagged commit #{noteable.sha} to [`#{tag_name}`](#{link})" + + create_note(NoteSummary.new(noteable, project, author, body, action: 'tag')) + end + # Called when the assignee of a Noteable is changed or removed # # noteable - Noteable object diff --git a/app/services/tags/create_service.rb b/app/services/tags/create_service.rb index 329722df747..35390f5082c 100644 --- a/app/services/tags/create_service.rb +++ b/app/services/tags/create_service.rb @@ -7,7 +7,7 @@ module Tags return error('Tag name invalid') unless valid_tag repository = project.repository - message&.strip! + message = message&.strip new_tag = nil diff --git a/app/services/todos/destroy/base_service.rb b/app/services/todos/destroy/base_service.rb index dff5e1f30e5..aeb60e50c64 100644 --- a/app/services/todos/destroy/base_service.rb +++ b/app/services/todos/destroy/base_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Todos module Destroy class BaseService diff --git a/app/services/todos/destroy/confidential_issue_service.rb b/app/services/todos/destroy/confidential_issue_service.rb index c5b66df057a..efec0f22da5 100644 --- a/app/services/todos/destroy/confidential_issue_service.rb +++ b/app/services/todos/destroy/confidential_issue_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Todos module Destroy class ConfidentialIssueService < ::Todos::Destroy::BaseService diff --git a/app/services/todos/destroy/entity_leave_service.rb b/app/services/todos/destroy/entity_leave_service.rb index 045f5ecaae7..4cb9d08713d 100644 --- a/app/services/todos/destroy/entity_leave_service.rb +++ b/app/services/todos/destroy/entity_leave_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Todos module Destroy class EntityLeaveService < ::Todos::Destroy::BaseService diff --git a/app/services/todos/destroy/group_private_service.rb b/app/services/todos/destroy/group_private_service.rb index d13fa7a6516..f67f1d40597 100644 --- a/app/services/todos/destroy/group_private_service.rb +++ b/app/services/todos/destroy/group_private_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Todos module Destroy class GroupPrivateService < ::Todos::Destroy::BaseService diff --git a/app/services/todos/destroy/private_features_service.rb b/app/services/todos/destroy/private_features_service.rb index 4d8e2877bfb..7e204885b31 100644 --- a/app/services/todos/destroy/private_features_service.rb +++ b/app/services/todos/destroy/private_features_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Todos module Destroy class PrivateFeaturesService < ::Todos::Destroy::BaseService diff --git a/app/services/todos/destroy/project_private_service.rb b/app/services/todos/destroy/project_private_service.rb index 315a0c33398..ae8fab3ffca 100644 --- a/app/services/todos/destroy/project_private_service.rb +++ b/app/services/todos/destroy/project_private_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Todos module Destroy class ProjectPrivateService < ::Todos::Destroy::BaseService diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb index acc2fa153ae..9417c63c43a 100644 --- a/app/services/users/build_service.rb +++ b/app/services/users/build_service.rb @@ -2,6 +2,10 @@ module Users class BuildService < BaseService + delegate :user_default_internal_regex_enabled?, + :user_default_internal_regex_instance, + to: :'Gitlab::CurrentSettings.current_application_settings' + def initialize(current_user, params = {}) @current_user = current_user @params = params.dup @@ -89,6 +93,10 @@ module Users if params[:reset_password] user_params.merge!(force_random_password: true, password_expires_at: nil) end + + if user_default_internal_regex_enabled? && !user_params.key?(:external) + user_params[:external] = user_external? + end else allowed_signup_params = signup_params allowed_signup_params << :skip_confirmation if skip_authorization @@ -105,5 +113,9 @@ module Users def skip_user_confirmation_email_from_setting !Gitlab::CurrentSettings.send_user_confirmation_email end + + def user_external? + user_default_internal_regex_instance.match(params[:email]).nil? + end end end diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb index 4bc78b5b64e..73fa6089945 100644 --- a/app/services/users/destroy_service.rb +++ b/app/services/users/destroy_service.rb @@ -2,6 +2,8 @@ module Users class DestroyService + DestroyError = Class.new(StandardError) + attr_accessor :current_user def initialize(current_user) @@ -46,9 +48,8 @@ module Users namespace.prepare_for_destroy user.personal_projects.each do |project| - # Skip repository removal because we remove directory with namespace - # that contain all this repositories - ::Projects::DestroyService.new(project, current_user, skip_repo: project.legacy_storage?).execute + success = ::Projects::DestroyService.new(project, current_user).execute + raise DestroyError, "Project #{project.id} can't be deleted" unless success end yield(user) if block_given? diff --git a/app/services/wikis/create_attachment_service.rb b/app/services/wikis/create_attachment_service.rb new file mode 100644 index 00000000000..30fe0e371a6 --- /dev/null +++ b/app/services/wikis/create_attachment_service.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module Wikis + class CreateAttachmentService < Files::CreateService + ATTACHMENT_PATH = 'uploads'.freeze + MAX_FILENAME_LENGTH = 255 + + delegate :wiki, to: :project + delegate :repository, to: :wiki + + def initialize(*args) + super + + @file_name = truncate_file_name(params[:file_name]) + @file_path = File.join(ATTACHMENT_PATH, SecureRandom.hex, @file_name) if @file_name + @commit_message ||= "Upload attachment #{@file_name}" + @branch_name ||= wiki.default_branch + end + + def create_commit! + commit_result(create_transformed_commit(@file_content)) + end + + private + + def truncate_file_name(file_name) + return unless file_name.present? + return file_name if file_name.length <= MAX_FILENAME_LENGTH + + extension = File.extname(file_name) + truncate_at = MAX_FILENAME_LENGTH - extension.length - 1 + base_name = File.basename(file_name, extension)[0..truncate_at] + base_name + extension + end + + def validate! + validate_file_name! + validate_permissions! + end + + def validate_file_name! + raise_error('The file name cannot be empty') unless @file_name + end + + def validate_permissions! + unless can?(current_user, :create_wiki, project) + raise_error('You are not allowed to push to the wiki') + end + end + + def create_transformed_commit(content) + repository.create_file( + current_user, + @file_path, + content, + message: @commit_message, + branch_name: @branch_name, + author_email: @author_email, + author_name: @author_name) + end + + def commit_result(commit_id) + { + file_name: @file_name, + file_path: @file_path, + branch: @branch_name, + commit: commit_id + } + end + end +end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index b1365659834..ffc1e5f75ca 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -122,12 +122,6 @@ class FileUploader < GitlabUploader } end - def markdown_link - markdown = +"[#{markdown_name}](#{secure_url})" - markdown.prepend("!") if image_or_video? || dangerous? - markdown - end - def to_h { alt: markdown_name, @@ -192,10 +186,6 @@ class FileUploader < GitlabUploader storage.delete_dir!(store_dir) # only remove when empty end - def markdown_name - (image_or_video? ? File.basename(filename, File.extname(filename)) : filename).gsub("]", "\\]") - end - def identifier @identifier ||= filename end diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb index 2a2b54a9270..e8a2dce7755 100644 --- a/app/uploaders/uploader_helper.rb +++ b/app/uploaders/uploader_helper.rb @@ -2,32 +2,7 @@ # Extra methods for uploader module UploaderHelper - IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze - # We recommend using the .mp4 format over .mov. Videos in .mov format can - # still be used but you really need to make sure they are served with the - # proper MIME type video/mp4 and not video/quicktime or your videos won't play - # on IE >= 9. - # http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html - VIDEO_EXT = %w[mp4 m4v mov webm ogv].freeze - # These extension types can contain dangerous code and should only be embedded inline with - # proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline". - DANGEROUS_EXT = %w[svg].freeze - - def image? - extension_match?(IMAGE_EXT) - end - - def video? - extension_match?(VIDEO_EXT) - end - - def image_or_video? - image? || video? - end - - def dangerous? - extension_match?(DANGEROUS_EXT) - end + include Gitlab::FileMarkdownLinkBuilder private diff --git a/app/validators/js_regex_validator.rb b/app/validators/js_regex_validator.rb new file mode 100644 index 00000000000..a515af7b919 --- /dev/null +++ b/app/validators/js_regex_validator.rb @@ -0,0 +1,15 @@ +class JsRegexValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + return true if value.blank? + + parsed_regex = JsRegex.new(Regexp.new(value, Regexp::IGNORECASE)) + + if parsed_regex.source.empty? + record.errors.add(attribute, "Regex Pattern #{value} can not be expressed in Javascript") + else + parsed_regex.warnings.each { |warning| record.errors.add(attribute, warning) } + end + rescue RegexpError => regex_error + record.errors.add(attribute, regex_error.to_s) + end +end diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index 7c8243a7a90..9121e44d31b 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -29,5 +29,18 @@ = f.check_box :user_default_external, class: 'form-check-input' = f.label :user_default_external, class: 'form-check-label' do Newly registered users will by default be external + .prepend-top-10 + = _('Internal users') + = f.text_field :user_default_internal_regex, placeholder: _('Regex pattern'), class: 'form-control prepend-top-5' + .help-block + = _('Specify an e-mail address regex pattern to identify default internal users.') + = link_to _('More information'), help_page_path('user/permissions', anchor: 'external-users-permissions'), + target: '_blank' + .form-group + = f.label :user_show_add_ssh_key_message, 'Prompt users to upload SSH keys', class: 'label-bold' + .form-check + = f.check_box :user_show_add_ssh_key_message, class: 'form-check-input' + = f.label :user_show_add_ssh_key_message, class: 'form-check-label' do + Inform users without uploaded SSH keys that they can't push over SSH until one is added = f.submit 'Save changes', class: 'btn btn-success' diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml index 5037017e38a..97be658cd34 100644 --- a/app/views/admin/application_settings/_ci_cd.html.haml +++ b/app/views/admin/application_settings/_ci_cd.html.haml @@ -38,6 +38,8 @@ .form-text.text-muted Set the default expiration time for each job's artifacts. 0 for unlimited. + The default unit is in seconds, but you can define an alternative. For example: + <code>4 mins 2 sec</code>, <code>2h42min</code>. = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml index 258d50ad676..6133a7646f4 100644 --- a/app/views/admin/application_settings/show.html.haml +++ b/app/views/admin/application_settings/show.html.haml @@ -325,6 +325,8 @@ .settings-content = render partial: 'repository_mirrors_form' += render_if_exists 'admin/application_settings/templates', expanded: expanded + %section.settings.as-third-party-offers.no-animate#js-third-party-offers-settings{ class: ('expanded' if expanded) } .settings-header %h4 diff --git a/app/views/admin/hook_logs/show.html.haml b/app/views/admin/hook_logs/show.html.haml index 2eb3ac85722..86729dbe7bc 100644 --- a/app/views/admin/hook_logs/show.html.haml +++ b/app/views/admin/hook_logs/show.html.haml @@ -4,7 +4,6 @@ %hr -= link_to 'Resend Request', retry_admin_hook_hook_log_path(@hook, @hook_log), class: "btn btn-default float-right prepend-left-10" += link_to 'Resend Request', retry_admin_hook_hook_log_path(@hook, @hook_log), method: :post, class: "btn btn-default float-right prepend-left-10" = render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log } - diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 8dfd176f1b7..9280ff4d478 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -49,7 +49,7 @@ = submit_tag 'Search', class: 'btn' .float-right.light - Runners with last contact more than a minute ago: #{@active_runners_cnt} + Runners currently online: #{@active_runners_cnt} %br diff --git a/app/views/admin/spam_logs/index.html.haml b/app/views/admin/spam_logs/index.html.haml index 8aaa6379730..b45d3e4823b 100644 --- a/app/views/admin/spam_logs/index.html.haml +++ b/app/views/admin/spam_logs/index.html.haml @@ -17,6 +17,6 @@ %th Primary Action %th = render @spam_logs - = paginate @spam_logs + = paginate @spam_logs, theme: 'gitlab' - else %h4 There are no Spam Logs diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml index 04acc5f8423..12e24ddef02 100644 --- a/app/views/admin/users/_access_levels.html.haml +++ b/app/views/admin/users/_access_levels.html.haml @@ -1,15 +1,18 @@ %fieldset %legend Access .form-group.row - = f.label :projects_limit, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :projects_limit, class: 'col-form-label' .col-sm-10= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control' .form-group.row - = f.label :can_create_group, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :can_create_group, class: 'col-form-label' .col-sm-10= f.check_box :can_create_group .form-group.row - = f.label :access_level, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :access_level, class: 'col-form-label' .col-sm-10 - editing_current_user = (current_user == @user) @@ -29,9 +32,14 @@ You cannot remove your own admin rights. .form-group.row - = f.label :external, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :external, class: 'col-form-label' + .hidden{ data: user_internal_regex_data } .col-sm-10 = f.check_box :external do External %p.light External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups. + %row.hidden#warning_external_automatically_set.hidden + .badge.badge-warning.text-white + = _('Automatically marked as default internal user') diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index 58be07fc83e..7f21bdb91c8 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -5,17 +5,20 @@ %fieldset %legend Account .form-group.row - = f.label :name, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :name, class: 'col-form-label' .col-sm-10 = f.text_field :name, required: true, autocomplete: 'off', class: 'form-control' %span.help-inline * required .form-group.row - = f.label :username, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :username, class: 'col-form-label' .col-sm-10 = f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control' %span.help-inline * required .form-group.row - = f.label :email, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :email, class: 'col-form-label' .col-sm-10 = f.text_field :email, required: true, autocomplete: 'off', class: 'form-control' %span.help-inline * required @@ -24,7 +27,8 @@ %fieldset %legend Password .form-group.row - = f.label :password, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :password, class: 'col-form-label' .col-sm-10 %strong Reset link will be generated and sent to the user. @@ -34,10 +38,12 @@ %fieldset %legend Password .form-group.row - = f.label :password, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :password, class: 'col-form-label' .col-sm-10= f.password_field :password, disabled: f.object.force_random_password, class: 'form-control' .form-group.row - = f.label :password_confirmation, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :password_confirmation, class: 'col-form-label' .col-sm-10= f.password_field :password_confirmation, disabled: f.object.force_random_password, class: 'form-control' = render partial: 'access_levels', locals: { f: f } @@ -45,21 +51,26 @@ %fieldset %legend Profile .form-group.row - = f.label :avatar, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :avatar, class: 'col-form-label' .col-sm-10 = f.file_field :avatar .form-group.row - = f.label :skype, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :skype, class: 'col-form-label' .col-sm-10= f.text_field :skype, class: 'form-control' .form-group.row - = f.label :linkedin, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :linkedin, class: 'col-form-label' .col-sm-10= f.text_field :linkedin, class: 'form-control' .form-group.row - = f.label :twitter, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :twitter, class: 'col-form-label' .col-sm-10= f.text_field :twitter, class: 'form-control' .form-group.row - = f.label :website_url, 'Website', class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :website_url, 'Website', class: 'col-form-label' .col-sm-10= f.text_field :website_url, class: 'form-control' .form-actions diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index f730fd05176..a74e052707f 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -39,6 +39,10 @@ %strong= email.email = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-sm btn btn-remove float-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do %i.fa.fa-times + %li + %span.light ID: + %strong + = @user.id %li.two-factor-status %span.light Two-factor Authentication: @@ -128,8 +132,8 @@ .col-md-6 - unless @user == current_user - unless @user.confirmed? - .card.bg-info - .card-header + .card.border-info + .card-header.bg-info.text-white Confirm user .card-body - if @user.unconfirmed_email.present? @@ -138,8 +142,8 @@ %br = link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } - if @user.blocked? - .card.bg-info - .card-header + .card.border-info + .card-header.bg-info.text-white This user is blocked .card-body %p A blocked user cannot: @@ -162,8 +166,8 @@ %br = link_to 'Block user', block_admin_user_path(@user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put, class: "btn btn-warning" - if @user.access_locked? - .card.bg-info - .card-header + .card.border-info + .card-header.bg-info.text-white This account has been locked .card-body %p This user has been temporarily locked due to excessive number of failed logins. You may manually unlock the account. diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index 8ca9fb4512e..a758a63dfb3 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -1,9 +1,8 @@ - grouped_emojis = awardable.grouped_awards(with_thumbs: inline) -- user_authored = awardable.user_authored?(current_user) .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(awardable, awards, current_user)), (award_user_authored_class(emoji) if user_authored)], + class: [(award_state_class(awardable, awards, current_user))], data: { placement: "bottom", title: award_user_list(awards, current_user) } } = emoji_icon(emoji) %span.award-control-text.js-counter @@ -13,7 +12,6 @@ .award-menu-holder.js-award-holder %button.btn.award-control.has-tooltip.js-add-award{ type: 'button', 'aria-label': _('Add reaction'), - class: ("js-user-authored" if user_authored), data: { title: _('Add reaction'), placement: "bottom" } } %span{ class: "award-control-icon award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face') %span{ class: "award-control-icon award-control-icon-positive" }= custom_icon('emoji_smiley') diff --git a/app/views/ci/runner/_how_to_setup_runner.html.haml b/app/views/ci/runner/_how_to_setup_runner.html.haml index 13f96b9747c..c26eb873718 100644 --- a/app/views/ci/runner/_how_to_setup_runner.html.haml +++ b/app/views/ci/runner/_how_to_setup_runner.html.haml @@ -1,6 +1,6 @@ - link = link_to _("Install GitLab Runner"), 'https://docs.gitlab.com/runner/install/', target: '_blank' .append-bottom-10 - %h4= _("Setup a #{type} Runner manually") + %h4= _("Setup a %{type} Runner manually") % { type: type } %ol %li diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml index 8b0463db000..9de9143e8b1 100644 --- a/app/views/ci/status/_dropdown_graph_badge.html.haml +++ b/app/views/ci/status/_dropdown_graph_badge.html.haml @@ -6,12 +6,12 @@ - tooltip = "#{subject.name} - #{status.status_tooltip}" - if status.has_details? - = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, html: 'true', container: 'body' } do + = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do %span{ class: klass }= sprite_icon(status.icon) %span.ci-build-text= subject.name - else - .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', html: 'true', title: tooltip, container: 'body' } } + .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } } %span{ class: klass }= sprite_icon(status.icon) %span.ci-build-text= subject.name diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml index 646e89e9bd1..44c898e0fac 100644 --- a/app/views/discussions/_diff_with_notes.html.haml +++ b/app/views/discussions/_diff_with_notes.html.haml @@ -1,6 +1,4 @@ - diff_file = discussion.diff_file -- blob = discussion.blob -- discussions = { discussion.original_line_code => [discussion] } - diff_file_class = diff_file.text? ? 'text-file' : 'js-image-file' - diff_data = {} - expanded = discussion.expanded? || local_assigns.fetch(:expanded, nil) diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml index 08f2442f025..69cc510e9c1 100644 --- a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml +++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml @@ -1,4 +1,3 @@ -- submit_btn_css ||= 'btn btn-link btn-remove' - if defined?(token) - path = oauth_authorized_application_path(0, token_id: token) - else diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index cae2df4699e..fc17dd2d310 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -25,6 +25,18 @@ .settings-content = render 'groups/settings/permissions' +%section.settings.no-animate{ class: ('expanded' if expanded) } + .settings-header + %h4 + = s_('GroupSettings|Badges') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = s_('GroupSettings|Customize your group badges.') + = link_to s_('GroupSettings|Learn more about badges.'), help_page_path('user/project/badges') + .settings-content + = render 'shared/badges/badge_settings' + %section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded) } .settings-header %h4 diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index db7eaff6658..e1e38a7e82f 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -1,7 +1,6 @@ - @no_container = true - page_title "Labels" - can_admin_label = can?(current_user, :admin_label, @group) -- hide_class = '' - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') - issuables = ['issues', 'merge requests'] diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index f5f621507b8..b6424df55cd 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -5,7 +5,7 @@ .nav-controls = render 'shared/milestones_sort_dropdown' - - if can?(current_user, :admin_milestones, @group) + - if can?(current_user, :admin_milestone, @group) = link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new" .milestones diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml index 4cae9c51acc..d8bd37fe986 100644 --- a/app/views/ide/index.html.haml +++ b/app/views/ide/index.html.haml @@ -1,4 +1,4 @@ -- @body_class = 'ide' +- @body_class = 'ide-layout' - page_title 'IDE' - content_for :page_specific_javascripts do diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml index f0d1e837317..f4a29ed18dc 100644 --- a/app/views/import/_githubish_status.html.haml +++ b/app/views/import/_githubish_status.html.haml @@ -45,7 +45,7 @@ = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true %span.input-group-prepend .input-group-text / - = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true + = text_field_tag :path, sanitize_project_name(repo.name), class: "input-mini form-control", tabindex: 2, autofocus: true, required: true %td.import-actions.job-status = button_tag class: "btn btn-import js-add-to-import" do = has_ci_cd_only_params? ? _('Connect') : _('Import') diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index a75b7aa9dd2..3b1b5e55302 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -63,7 +63,7 @@ = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true %span.input-group-prepend .input-group-text / - = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true + = text_field_tag :path, sanitize_project_name(repo.slug), class: "input-mini form-control", tabindex: 2, autofocus: true, required: true %td.import-actions.job-status = button_tag class: 'btn btn-import js-add-to-import' do = _('Import') diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml index 3d05a5e696f..ae09e0dfa18 100644 --- a/app/views/import/bitbucket_server/status.html.haml +++ b/app/views/import/bitbucket_server/status.html.haml @@ -61,7 +61,7 @@ = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true %span.input-group-prepend .input-group-text / - = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true + = text_field_tag :path, sanitize_project_name(repo.slug), class: "input-mini form-control", tabindex: 2, autofocus: true, required: true %td.import-actions.job-status = button_tag class: 'btn btn-import js-add-to-import' do Import diff --git a/app/views/instance_statistics/cohorts/_cohorts_table.html.haml b/app/views/instance_statistics/cohorts/_cohorts_table.html.haml index 701a4e62b39..6a7c999bff3 100644 --- a/app/views/instance_statistics/cohorts/_cohorts_table.html.haml +++ b/app/views/instance_statistics/cohorts/_cohorts_table.html.haml @@ -3,7 +3,7 @@ User cohorts are shown for the last #{@cohorts[:months_included]} months. Only users with activity are counted in the cohort total; inactive users are counted separately. - = link_to icon('question-circle'), help_page_path('user/admin_area/user_cohorts', anchor: 'cohorts'), title: 'About this feature', target: '_blank' + = link_to icon('question-circle'), help_page_path('user/instance_statistics/user_cohorts', anchor: 'cohorts'), title: 'About this feature', target: '_blank' .table-holder %table.table diff --git a/app/views/instance_statistics/conversational_development_index/_no_data.html.haml b/app/views/instance_statistics/conversational_development_index/_no_data.html.haml index d69c46194b4..dd795aee135 100644 --- a/app/views/instance_statistics/conversational_development_index/_no_data.html.haml +++ b/app/views/instance_statistics/conversational_development_index/_no_data.html.haml @@ -4,4 +4,4 @@ %h4 Data is still calculating... %p In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index. - = link_to 'Learn more', help_page_path('user/admin_area/monitoring/convdev'), target: '_blank' + = link_to 'Learn more', help_page_path('user/instance_statistics/convdev'), target: '_blank' diff --git a/app/views/instance_statistics/conversational_development_index/index.html.haml b/app/views/instance_statistics/conversational_development_index/index.html.haml index e3d1aa31dc2..dd63b98376f 100644 --- a/app/views/instance_statistics/conversational_development_index/index.html.haml +++ b/app/views/instance_statistics/conversational_development_index/index.html.haml @@ -19,7 +19,7 @@ index %br score - = link_to icon('question-circle', 'aria-hidden' => 'true'), help_page_path('user/admin_area/monitoring/convdev') + = link_to icon('question-circle', 'aria-hidden' => 'true'), help_page_path('user/instance_statistics/convdev') .convdev-cards.board-card-container - @metric.cards.each do |card| diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 9a7a67cfa83..a86972d8cf3 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -1,7 +1,3 @@ -- if controller.controller_path =~ /^groups/ && @group.persisted? - - label = _('This group') -- if controller.controller_path =~ /^projects/ && @project.persisted? - - label = _('This project') - if @group && @group.persisted? && @group.path - group_data_attrs = { group_path: j(@group.path), name: @group.name, issues_path: issues_group_path(j(@group.path)), mr_path: merge_requests_group_path(j(@group.path)) } - if @project && @project.persisted? diff --git a/app/views/layouts/explore.html.haml b/app/views/layouts/explore.html.haml index 2ab9e55441b..80bda34a3f5 100644 --- a/app/views/layouts/explore.html.haml +++ b/app/views/layouts/explore.html.haml @@ -1,5 +1,5 @@ -- page_title = _("Explore") +- page_title _("Explore") - unless current_user - - header_title = _("Explore GitLab"), explore_root_path + - header_title _("Explore GitLab"), explore_root_path = render template: "layouts/application" diff --git a/app/views/layouts/nav_only.html.haml b/app/views/layouts/fullscreen.html.haml index 0811211f7b2..95db8313821 100644 --- a/app/views/layouts/nav_only.html.haml +++ b/app/views/layouts/fullscreen.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: I18n.locale, class: page_class } = render "layouts/head" - %body{ class: "#{user_application_theme} #{@body_class} nav-only", data: { page: body_data_page } } + %body{ class: "#{user_application_theme} #{@body_class} fullscreen-layout", data: { page: body_data_page } } = render 'peek/bar' = render "layouts/header/default" = render 'shared/outdated_browser' @@ -10,5 +10,5 @@ = render "layouts/broadcast" = yield :flash_message = render "layouts/flash" - .content{ id: "content-body" } + .content-wrapper{ id: "content-body", class: "d-flex flex-column align-items-stretch" } = yield diff --git a/app/views/layouts/group_settings.html.haml b/app/views/layouts/group_settings.html.haml index 14c5f0ce04c..9db78ec58e4 100644 --- a/app/views/layouts/group_settings.html.haml +++ b/app/views/layouts/group_settings.html.haml @@ -1,4 +1,4 @@ -- page_title = _("Settings") -- nav "group" +- page_title _("Settings") +- nav "group" = render template: "layouts/group" diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index d471dd84550..4158bb69452 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -122,12 +122,6 @@ %span = _('General') - = nav_link(controller: :badges) do - = link_to group_settings_badges_path(@group), title: _('Project Badges') do - %span - = _('Project Badges') - - = nav_link(path: 'groups#projects') do = link_to projects_group_path(@group), title: _('Projects') do %span diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 34f47806205..30e0e9fca27 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -313,11 +313,6 @@ %span = _('Members') - if can_edit - = nav_link(controller: :badges) do - = link_to project_settings_badges_path(@project), title: _('Badges') do - %span - = _('Badges') - - if can_edit = nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do = link_to project_settings_integrations_path(@project), title: _('Integrations') do %span diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index fbe88ec9618..1b6c4193c4d 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,5 +1,4 @@ - empty_repo = @project.empty_repo? -- fork_network = @project.fork_network .project-home-panel.text-center{ class: ("empty-project" if empty_repo) } .limit-container-width{ class: container_class } .avatar-container.s70.project-avatar diff --git a/app/views/projects/_issuable_by_email.html.haml b/app/views/projects/_issuable_by_email.html.haml index 22adf5b4008..d59191a6f87 100644 --- a/app/views/projects/_issuable_by_email.html.haml +++ b/app/views/projects/_issuable_by_email.html.haml @@ -19,9 +19,16 @@ = text_field_tag :issuable_email, email, class: "monospace js-select-on-focus form-control", readonly: true .input-group-append = clipboard_button(target: '#issuable_email', class: 'btn btn-clipboard input-group-text btn-transparent d-none d-sm-block') + + - if issuable_type == 'issue' + - enter_title_text = _('Enter the issue title') + - enter_description_text = _('Enter the issue description') + - else + - enter_title_text = _('Enter the merge request title') + - enter_description_text = _('Enter the merge request description') = mail_to email, class: 'btn btn-clipboard btn-transparent', - subject: _("Enter the #{name} title"), - body: _("Enter the #{name} description"), + subject: enter_title_text, + body: enter_description_text, title: _('Send email'), data: { toggle: 'tooltip', placement: 'bottom' } do = sprite_icon('mail') diff --git a/app/views/projects/_merge_request_merge_method_settings.html.haml b/app/views/projects/_merge_request_merge_method_settings.html.haml index 540e996e4d8..935581643cd 100644 --- a/app/views/projects/_merge_request_merge_method_settings.html.haml +++ b/app/views/projects/_merge_request_merge_method_settings.html.haml @@ -1,5 +1,4 @@ - form = local_assigns.fetch(:form) -- project = local_assigns.fetch(:project) .form-group = label_tag :merge_method_merge, class: 'label-bold' do diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 8560b72fe85..24f256d083b 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -2,11 +2,11 @@ .file-holder-bottom-radius.file-holder.file.append-bottom-default .js-file-title.file-title.clearfix{ data: { current_action: action } } - .editor-ref + .editor-ref.block-truncated = sprite_icon('fork', size: 12) = ref - %span.editor-file-name - - if current_action?(:edit) || current_action?(:update) + - if current_action?(:edit) || current_action?(:update) + %span.editor-file-name = text_field_tag 'file_path', (params[:file_path] || @path), class: 'form-control new-file-path js-file-path-name-input' @@ -16,7 +16,7 @@ = text_field_tag 'file_name', params[:file_name], placeholder: "File name", required: true, class: 'form-control new-file-name js-file-path-name-input' - .float-right.file-buttons + .file-buttons = button_tag class: 'soft-wrap-toggle btn', type: 'button', tabindex: '-1' do %span.no-wrap = custom_icon('icon_no_wrap') diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml index 8f8eb2c3d5a..6ed65d07202 100644 --- a/app/views/projects/commits/_commit_list.html.haml +++ b/app/views/projects/commits/_commit_list.html.haml @@ -1,9 +1,10 @@ -- commits, hidden = limited_commits(@commits) +- commits = @commits +- hidden = @hidden_commit_count - commits = Commit.decorate(commits, @project) .card .card-header - Commits (#{@commits.count}) + Commits (#{@total_commit_count}) - if hidden > 0 %ul.content-list - commits.each do |commit| diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index ac6852751be..ec05ff50f25 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -2,7 +2,8 @@ - project = local_assigns.fetch(:project) { merge_request&.project } - ref = local_assigns.fetch(:ref) { merge_request&.source_branch } -- commits, hidden = limited_commits(@commits) +- commits = @commits +- hidden = @hidden_commit_count - commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits| %li.commit-header.js-commit-header{ data: { day: day } } diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index cd0fb21f8a7..ffdca500abe 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -32,9 +32,9 @@ %a{ href: "##{line_code}", data: { linenumber: link_text } } %td.line_content.noteable_line{ class: type }< - if email - %pre= line.text + %pre= line.rich_text - else - = diff_line_content(line.text) + = diff_line_content(line.rich_text) - if line_discussions&.any? - discussion_expanded = local_assigns.fetch(:discussion_expanded, line_discussions.any?(&:expanded?)) diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 1f0ca211074..4b1d4b3ea17 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -5,7 +5,6 @@ - diff_file.parallel_diff_lines.each do |line| - left = line[:left] - right = line[:right] - - last_line = right.new_pos if right - discussions_left, discussions_right = parallel_diff_discussions(left, right, diff_file) %tr.line_holder.parallel - if left @@ -24,7 +23,7 @@ - discussion_left = discussions_left.try(:first) - if discussion_left && discussion_left.resolvable? %diff-note-avatars{ "discussion-id" => discussion_left.id } - %td.line_content.parallel.noteable_line.left-side{ id: left_line_code, class: left.type }= diff_line_content(left.text) + %td.line_content.parallel.noteable_line.left-side{ id: left_line_code, class: left.type }= diff_line_content(left.rich_text) - else %td.old_line.diff-line-num.empty-cell %td.line_content.parallel.left-side @@ -45,7 +44,7 @@ - discussion_right = discussions_right.try(:first) - if discussion_right && discussion_right.resolvable? %diff-note-avatars{ "discussion-id" => discussion_right.id } - %td.line_content.parallel.noteable_line.right-side{ id: right_line_code, class: right.type }= diff_line_content(right.text) + %td.line_content.parallel.noteable_line.right-side{ id: right_line_code, class: right.type }= diff_line_content(right.rich_text) - else %td.old_line.diff-line-num.empty-cell %td.line_content.parallel.right-side diff --git a/app/views/projects/diffs/_single_image_diff.html.haml b/app/views/projects/diffs/_single_image_diff.html.haml index 12be8beab39..454f814795a 100644 --- a/app/views/projects/diffs/_single_image_diff.html.haml +++ b/app/views/projects/diffs/_single_image_diff.html.haml @@ -1,7 +1,5 @@ - blob = diff_file.blob -- old_blob = diff_file.old_blob - blob_raw_url = diff_file_blob_raw_url(diff_file) -- old_blob_raw_url = diff_file_old_blob_raw_url(diff_file) - click_to_comment = local_assigns.fetch(:click_to_comment, true) - diff_view_data = local_assigns.fetch(:diff_view_data, '') - class_name = '' diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 30544dde451..fb837b27207 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -78,7 +78,7 @@ = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| %input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' } -# haml-lint:disable InlineJavaScript - %script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project) + %script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data_json(@project) .js-project-permissions-form = f.submit 'Save changes', class: "btn btn-save" @@ -102,6 +102,18 @@ = render_if_exists 'projects/service_desk_settings' + %section.settings.no-animate{ class: ('expanded' if expanded) } + .settings-header + %h4 + = s_('ProjectSettings|Badges') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = s_('ProjectSettings|Customize your project badges.') + = link_to s_('ProjectSettings|Learn more about badges.'), help_page_path('user/project/badges') + .settings-content + = render 'shared/badges/badge_settings' + = render 'export', project: @project %section.qa-advanced-settings.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded) } diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index af86b8e8e67..4222963a754 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -2,11 +2,4 @@ - page_title "Metrics for environment", @environment.name .prometheus-container{ class: container_class } - .top-area - .row - .col-sm-6 - %h3 - Environment: - = link_to @environment.name, environment_path(@environment) - #prometheus-graphs{ data: metrics_data(@project, @environment) } diff --git a/app/views/projects/forks/_fork_button.html.haml b/app/views/projects/forks/_fork_button.html.haml index 8a549d431ee..12cf40bb65f 100644 --- a/app/views/projects/forks/_fork_button.html.haml +++ b/app/views/projects/forks/_fork_button.html.haml @@ -5,7 +5,7 @@ .bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default.forked = link_to project_path(forked_project) do - if /no_((\w*)_)*avatar/.match(avatar) - = project_identicon(namespace, class: "avatar s100 identicon") + = project_icon(namespace, class: "avatar s100 identicon") - else .avatar-container.s100 = image_tag(avatar, class: "avatar s100") @@ -18,7 +18,7 @@ class: ("disabled has-tooltip" unless can_create_project), title: (_('You have reached your project limit') unless can_create_project) do - if /no_((\w*)_)*avatar/.match(avatar) - = project_identicon(namespace, class: "avatar s100 identicon") + = project_icon(namespace, class: "avatar s100 identicon") - else .avatar-container.s100 = image_tag(avatar, class: "avatar s100") diff --git a/app/views/projects/hook_logs/show.html.haml b/app/views/projects/hook_logs/show.html.haml index e51efa85df0..bd8ca5e7d70 100644 --- a/app/views/projects/hook_logs/show.html.haml +++ b/app/views/projects/hook_logs/show.html.haml @@ -4,6 +4,6 @@ Request details .col-lg-9 - = link_to 'Resend Request', retry_project_hook_hook_log_path(@project, @hook, @hook_log), class: "btn btn-default float-right prepend-left-10" + = link_to 'Resend Request', retry_project_hook_hook_log_path(@project, @hook, @hook_log), method: :post, class: "btn btn-default float-right prepend-left-10" = render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log } diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml index 759efd4e9d4..acc1e17b811 100644 --- a/app/views/projects/jobs/_sidebar.html.haml +++ b/app/views/projects/jobs/_sidebar.html.haml @@ -1,12 +1,7 @@ %aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } } .sidebar-container .blocks-container - - if can?(current_user, :create_build_terminal, @build) - .block - = link_to terminal_project_job_path(@project, @build), class: 'terminal-button pull-right btn visible-md-block visible-lg-block', title: 'Terminal' do - Terminal - - #js-details-block-vue{ data: { can_user_retry: can?(current_user, :update_build, @build) && @build.retryable? } } + #js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } } - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?) .block @@ -87,7 +82,7 @@ - builds.select{|build| build.status == build_status}.each do |build| .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } } - tooltip = sanitize(build.tooltip_message.dup) - = link_to(project_job_path(@project, build), data: { toggle: 'tooltip', html: 'true', title: tooltip, container: 'body' }) do + = link_to(project_job_path(@project, build), data: { toggle: 'tooltip', title: tooltip, container: 'body' }) do = sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right') %span{ class: "ci-status-icon-#{build.status}" } = ci_icon_for_status(build.status) diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 768ce9bd103..dfac62e7985 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -1,7 +1,6 @@ - @no_container = true - page_title "Labels" - can_admin_label = can?(current_user, :admin_label, @project) -- hide_class = '' - search = params[:search] - if can_admin_label diff --git a/app/views/projects/merge_requests/_how_to_merge.html.haml b/app/views/projects/merge_requests/_how_to_merge.html.haml index 62dd21ef6e0..15499c89ffb 100644 --- a/app/views/projects/merge_requests/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/_how_to_merge.html.haml @@ -1,5 +1,5 @@ #modal_merge_info.modal{ tabindex: '-1' } - .modal-dialog + .modal-dialog.modal-lg .modal-content .modal-header %h3.modal-title Check out, review, and merge locally @@ -30,11 +30,13 @@ %pre.dark#merge-info-3 - if @merge_request.for_fork? :preserve - git checkout #{h @merge_request.target_branch} + git fetch origin + git checkout origin/#{h @merge_request.target_branch} git merge --no-ff #{h @merge_request.source_project_path}-#{h @merge_request.source_branch} - else :preserve - git checkout #{h @merge_request.target_branch} + git fetch origin + git checkout origin/#{h @merge_request.target_branch} git merge --no-ff #{h @merge_request.source_branch} %p %strong Step 4. diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml index f7a5d85500f..d5c4134dee2 100644 --- a/app/views/projects/merge_requests/creations/_new_submit.html.haml +++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml @@ -33,7 +33,7 @@ %li.commits-tab.new-tab = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tabvue'} do Commits - %span.badge.badge-pill= @commits.size + %span.badge.badge-pill= @total_commit_count - if @pipelines.any? %li.builds-tab = link_to url_for(safe_params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tabvue'} do diff --git a/app/views/projects/milestones/_deprecation_message.html.haml b/app/views/projects/milestones/_deprecation_message.html.haml new file mode 100644 index 00000000000..b2cca3690d6 --- /dev/null +++ b/app/views/projects/milestones/_deprecation_message.html.haml @@ -0,0 +1,7 @@ +.banner-callout.compact.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 + = _('The tabs below will be removed in a future version') + %p.milestone-banner-text + = _('Learn more about %{issue_boards_url}, to keep track of issues in multiple lists, using labels, assignees, and milestones. If you’re missing something from issue boards, please create an issue on %{gitlab_issues_url}.').html_safe % { issue_boards_url: link_to(_('issue boards'), help_page_url('user/project/issue_board'), target: '_blank', rel: 'noopener noreferrer'), gitlab_issues_url: link_to(_('GitLab’s issue tracker'), 'https://gitlab.com/gitlab-org/gitlab-ce/issues', target: '_blank', rel: 'noopener noreferrer') } diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 2a9e20c2caa..5859de61d71 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -43,18 +43,7 @@ - else = link_to 'Reopen milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped" - %button.js-delete-milestone-button.btn.btn-grouped.btn-danger{ data: { toggle: 'modal', - target: '#delete-milestone-modal', - milestone_id: @milestone.id, - milestone_title: markdown_field(@milestone, :title), - milestone_url: project_milestone_path(@project, @milestone), - milestone_issue_count: @milestone.issues.count, - milestone_merge_request_count: @milestone.merge_requests.count }, - disabled: true } - = _('Delete') - = icon('spin spinner', class: 'js-loading-icon hidden' ) - - #delete-milestone-modal + = render 'shared/milestones/delete_button' %a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: "#" } = icon('angle-double-left') @@ -78,5 +67,6 @@ .alert.alert-success.prepend-top-default %span All issues for this milestone are closed. You may close this milestone now. + = render 'deprecation_message' = render 'shared/milestones/tabs', milestone: @milestone = render 'shared/milestones/sidebar', milestone: @milestone, project: @project, affix_offset: 153 diff --git a/app/views/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml index 64f0fde30cf..35a6885318a 100644 --- a/app/views/projects/mirrors/_instructions.html.haml +++ b/app/views/projects/mirrors/_instructions.html.haml @@ -1,10 +1,13 @@ .account-well.prepend-top-default.append-bottom-default %ul %li - The repository must be accessible over <code>http://</code>, <code>https://</code>, <code>ssh://</code> or <code>git://</code>. + = _('The repository must be accessible over <code>http://</code>, + <code>https://</code>, <code>ssh://</code> and <code>git://</code>.').html_safe + %li= _('Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>.').html_safe %li - Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>. + - minutes = Gitlab.config.gitlab_shell.git_timeout / 60 + = _("The update action will time out after %{number_of_minutes} minutes. For big repositories, use a clone/push combination.") % { number_of_minutes: minutes } + %li= _('The Git LFS objects will <strong>not</strong> be synced.').html_safe %li - The update action will time out after 10 minutes. For big repositories, use a clone/push combination. - %li - The Git LFS objects will <strong>not</strong> be synced. + = _('This user will be the author of all events in the activity feed that are the result of an update, + like new branches being created or new commits being pushed to existing branches.') diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml new file mode 100644 index 00000000000..c6764c7607a --- /dev/null +++ b/app/views/projects/mirrors/_mirror_repos.html.haml @@ -0,0 +1,63 @@ +- expanded = Rails.env.test? +- protocols = Gitlab::UrlSanitizer::ALLOWED_SCHEMES.join('|') + +%section.settings.project-mirror-settings.js-mirror-settings.no-animate#js-push-remote-settings{ class: ('expanded' if expanded) } + .settings-header + %h4= _('Mirroring repositories') + %button.btn.js-settings-toggle + = expanded ? _('Collapse') : _('Expand') + %p + = _('Set up your project to automatically push and/or pull changes to/from another repository. Branches, tags, and commits will be synced automatically.') + = link_to _('Read more'), help_page_path('workflow/repository_mirroring'), target: '_blank' + + .settings-content + = form_for @project, url: project_mirror_path(@project), html: { class: 'gl-show-field-errors js-mirror-form', autocomplete: 'false', data: mirrors_form_data_attributes } do |f| + .panel.panel-default + .panel-heading + %h3.panel-title= _('Mirror a repository') + .panel-body + %div= form_errors(@project) + + .form-group.has-feedback + = label_tag :url, _('Git repository URL'), class: 'label-light' + = text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+" + + = render 'projects/mirrors/instructions' + + = render 'projects/mirrors/mirror_repos_form', f: f + + .form-check.append-bottom-10 + = check_box_tag :only_protected_branches, '1', false, class: 'js-mirror-protected form-check-input' + = label_tag :only_protected_branches, _('Only mirror protected branches'), class: 'form-check-label' + = link_to icon('question-circle'), help_page_path('user/project/protected_branches') + + .panel-footer + = f.submit _('Mirror repository'), class: 'btn btn-create', name: :update_remote_mirror + + .panel.panel-default + .table-responsive + %table.table.push-pull-table + %thead + %tr + %th + = _('Mirrored repositories') + = render_if_exists 'projects/mirrors/mirrored_repositories_count' + %th= _('Direction') + %th= _('Last update') + %th + %th + %tbody.js-mirrors-table-body + = render_if_exists 'projects/mirrors/table_pull_row' + - @project.remote_mirrors.each_with_index do |mirror, index| + - if mirror.enabled + %tr + %td= mirror.safe_url + %td= _('Push') + %td= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never') + %td + - if mirror.last_error.present? + .badge.mirror-error-badge{ data: { toggle: 'tooltip', html: 'true' }, title: html_escape(mirror.last_error.try(:strip)) }= _('Error') + %td.mirror-action-buttons + .btn-group.mirror-actions-group.pull-right{ role: 'group' } + = render 'shared/remote_mirror_update_button', remote_mirror: mirror + %button.js-delete-mirror.btn.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= icon('trash-o') diff --git a/app/views/projects/mirrors/_mirror_repos_form.html.haml b/app/views/projects/mirrors/_mirror_repos_form.html.haml new file mode 100644 index 00000000000..93994cb30ac --- /dev/null +++ b/app/views/projects/mirrors/_mirror_repos_form.html.haml @@ -0,0 +1,18 @@ +- protocols = Gitlab::UrlSanitizer::ALLOWED_SCHEMES.join('|') + +.form-group + = label_tag :mirror_direction, _('Mirror direction'), class: 'label-light' + = select_tag :mirror_direction, options_for_select([[_('Push'), 'push']]), class: 'form-control js-mirror-direction', disabled: true + += f.fields_for :remote_mirrors, @project.remote_mirrors.build do |rm_f| + = rm_f.hidden_field :enabled, value: '1' + = rm_f.hidden_field :url, class: 'js-mirror-url-hidden', required: true, pattern: "(#{protocols}):\/\/.+" + = rm_f.hidden_field :only_protected_branches, class: 'js-mirror-protected-hidden' + +.form-group + = label_tag :auth_method, _('Authentication method'), class: 'label-bold' + = select_tag :auth_method, options_for_select([[_('None'), 'none'], [_('Password'), 'password']], 'none'), { class: "form-control js-auth-method" } + +.form-group.js-password-group.collapse + = label_tag :password, _('Password'), class: 'label-bold' + = text_field_tag :password, '', class: 'form-control js-password' diff --git a/app/views/projects/mirrors/_push.html.haml b/app/views/projects/mirrors/_push.html.haml deleted file mode 100644 index 08375e09816..00000000000 --- a/app/views/projects/mirrors/_push.html.haml +++ /dev/null @@ -1,50 +0,0 @@ -- expanded = Rails.env.test? -%section.settings.no-animate#js-push-remote-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - Push to a remote repository - %button.btn.js-settings-toggle - = expanded ? 'Collapse' : 'Expand' - %p - Set up the remote repository that you want to update with the content of the current repository - every time someone pushes to it. - = link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pushing-to-a-remote-repository'), target: '_blank' - .settings-content - = form_for @project, url: project_mirror_path(@project) do |f| - %div - = form_errors(@project) - = render "shared/remote_mirror_update_button", remote_mirror: @remote_mirror - - if @remote_mirror.last_error.present? - .panel.panel-danger - .panel-heading - - if @remote_mirror.last_update_at - The remote repository failed to update #{time_ago_with_tooltip(@remote_mirror.last_update_at)}. - - else - The remote repository failed to update. - - - if @remote_mirror.last_successful_update_at - Last successful update #{time_ago_with_tooltip(@remote_mirror.last_successful_update_at)}. - .panel-body - %pre - :preserve - #{h(@remote_mirror.last_error.strip)} - = f.fields_for :remote_mirrors, @remote_mirror do |rm_form| - .form-group - = rm_form.check_box :enabled, class: "float-left" - .prepend-left-20 - = rm_form.label :enabled, "Remote mirror repository", class: "label-bold append-bottom-0" - %p.light.append-bottom-0 - Automatically update the remote mirror's branches, tags, and commits from this repository every time someone pushes to it. - .form-group.has-feedback - = rm_form.label :url, "Git repository URL", class: "label-bold" - = rm_form.text_field :url, class: "form-control", placeholder: 'https://username:password@gitlab.company.com/group/project.git' - - = render "projects/mirrors/instructions" - - .form-group - = rm_form.check_box :only_protected_branches, class: 'float-left' - .prepend-left-20 - = rm_form.label :only_protected_branches, class: 'label-bold' - = link_to icon('question-circle'), help_page_path('user/project/protected_branches') - - = f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror' diff --git a/app/views/projects/mirrors/_show.html.haml b/app/views/projects/mirrors/_show.html.haml index de77701a373..8318d5898a1 100644 --- a/app/views/projects/mirrors/_show.html.haml +++ b/app/views/projects/mirrors/_show.html.haml @@ -1,3 +1 @@ -- if can?(current_user, :admin_remote_mirror, @project) - = render 'projects/mirrors/push' - += render 'projects/mirrors/mirror_repos' diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 6c363345e38..f9b4cddf9b2 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -3,7 +3,6 @@ - @hide_top_links = true - page_title 'New Project' - header_title "Projects", dashboard_projects_path -- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility - active_tab = local_assigns.fetch(:active_tab, 'blank') .project-edit-container diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml index b4fe1cabdfd..eb6838cec8d 100644 --- a/app/views/projects/notes/_actions.html.haml +++ b/app/views/projects/notes/_actions.html.haml @@ -38,9 +38,8 @@ - if can?(current_user, :award_emoji, note) - if note.emoji_awardable? - - user_authored = note.user_authored?(current_user) .note-actions-item - = button_tag title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip btn btn-transparent", data: { position: 'right', container: 'body' } do + = button_tag title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji} has-tooltip btn btn-transparent", data: { position: 'right', container: 'body' } do = icon('spinner spin') %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml index cd9177c0f9e..988dabef3a0 100644 --- a/app/views/projects/pages/_use.html.haml +++ b/app/views/projects/pages/_use.html.haml @@ -1,6 +1,6 @@ - unless @project.pages_deployed? - .card.bg-info - .card-header + .card.border-info + .card-header.bg-info.text-white Configure pages .card-body %p diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml index c13e3194340..5b6823da1f6 100644 --- a/app/views/projects/pipelines/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -1,5 +1,5 @@ - breadcrumb_title "Pipelines" -- page_title = s_("Pipeline|Run Pipeline") +- page_title s_("Pipeline|Run Pipeline") - settings_link = link_to _('CI/CD settings'), project_settings_ci_cd_path(@project) %h3.page-title diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index 6c11ce3b394..314af44490e 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -13,4 +13,4 @@ %h4.underlined-title Available specific runners %ul.bordered-list.available-specific-runners = render partial: 'projects/runners/runner', collection: @assignable_runners, as: :runner - = paginate @assignable_runners, theme: "gitlab" + = paginate @assignable_runners, theme: "gitlab", :params => { :anchor => '#js-runners-settings' } diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml index ab9ba5c7569..ab92b757836 100644 --- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml +++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml @@ -5,7 +5,6 @@ %fieldset.builds-feature.js-auto-devops-settings .form-group - message = auto_devops_warning_message(@project) - - ci_file_formatted = '<code>.gitlab-ci.yml</code>'.html_safe - if message %p.auto-devops-warning-message.settings-message.text-center = message.html_safe diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index 434aed2f603..9134257b631 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -17,7 +17,7 @@ %h5.prepend-top-0 = _("Git strategy for pipelines") %p - = _("Choose between <code>clone</code> or <code>fetch</code> to get the recent application code") + = _("Choose between <code>clone</code> or <code>fetch</code> to get the recent application code").html_safe = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy'), target: '_blank' .form-check = f.radio_button :build_allow_git_fetch, 'false', { class: 'form-check-input' } @@ -47,7 +47,7 @@ = f.label :ci_config_path, _('Custom CI config path'), class: 'label-bold' = f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml' %p.form-text.text-muted - = _("The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>") + = _("The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>").html_safe = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank' %hr diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 8a5abb64515..df8a5742450 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -21,8 +21,7 @@ %nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] } = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout) = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout) - - if Feature.enabled?(:repository_languages, @project.namespace.becomes(Namespace)) - = repository_languages_bar(@project.repository_languages) + = repository_languages_bar(@project.repository_languages) %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } - if @project.archived? diff --git a/app/views/projects/tags/_tag.atom.builder b/app/views/projects/tags/_tag.atom.builder new file mode 100644 index 00000000000..60d4b21b9d1 --- /dev/null +++ b/app/views/projects/tags/_tag.atom.builder @@ -0,0 +1,19 @@ +commit = @repository.commit(tag.dereferenced_target) +release = @releases.find { |r| r.tag == tag.name } +tag_url = project_tag_url(@project, tag.name) + +if commit + xml.entry do + xml.id tag_url + xml.link href: tag_url + xml.title truncate(tag.name, length: 80) + xml.summary strip_gpg_signature(tag.message) + xml.content markdown_field(release, :description), type: 'html' + xml.updated release.updated_at.xmlschema if release + xml.media :thumbnail, width: '40', height: '40', url: image_url(avatar_icon_for_email(commit.author_email)) + xml.author do |author| + xml.name commit.author_name + xml.email commit.author_email + end + end +end diff --git a/app/views/projects/tags/index.atom.builder b/app/views/projects/tags/index.atom.builder new file mode 100644 index 00000000000..b9b58b7beaa --- /dev/null +++ b/app/views/projects/tags/index.atom.builder @@ -0,0 +1,7 @@ +xml.title "#{@project.name} tags" +xml.link href: project_tags_url(@project, @ref, rss_url_options), rel: 'self', type: 'application/atom+xml' +xml.link href: project_tags_url(@project, @ref), rel: 'alternate', type: 'text/html' +xml.id project_tags_url(@project, @ref) +xml.updated @releases.first.updated_at.xmlschema if @releases.any? + +xml << render(partial: 'tag', collection: @tags) if @tags.any? diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index dab95ba09f2..20b4705521c 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -1,6 +1,8 @@ - @no_container = true - @sort ||= sort_value_recently_updated - page_title s_('TagsPage|Tags') += content_for :meta_tags do + = auto_discovery_link_tag(:atom, project_tags_url(@project, rss_url_options), title: "#{@project.name} tags") .flex-list{ class: container_class } .top-area.adjust @@ -25,6 +27,8 @@ - if can?(current_user, :push_code, @project) = link_to new_project_tag_path(@project), class: 'btn btn-create new-tag-btn' do = s_('TagsPage|New tag') + = link_to project_tags_path(@project, rss_url_options), title: _("Tags feed"), class: 'btn rss-btn has-tooltip' do + = icon("rss") = render_if_exists 'projects/commits/mirror_status' diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index d80d2957466..71359708022 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -41,3 +41,8 @@ = render 'sidebar' #delete-wiki-modal.modal.fade + +- content_for :scripts_body do + -# haml-lint:disable InlineJavaScript + :javascript + window.uploads_path = "#{wiki_attachment_upload_url}"; diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml index fdcd126e7a3..a8d4d4af93a 100644 --- a/app/views/search/results/_blob.html.haml +++ b/app/views/search/results/_blob.html.haml @@ -1,4 +1,6 @@ - project = find_project_for_result_blob(blob) +- return unless project + - file_name, blob = parse_search_result(blob) - blob_link = project_blob_path(project, tree_join(blob.ref, file_name)) diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index 2c3cbd0b986..71f34c0d85b 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -4,8 +4,6 @@ - use_label_priority = local_assigns.fetch(:use_label_priority, false) - force_priority = local_assigns.fetch(:force_priority, use_label_priority ? label.priority.present? : false) - toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user -- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project) -- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project) - tooltip_title = label_status_tooltip(label, status) if status %li.label-list-item{ id: label_css_id, data: { id: label.id } } diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml index ac2164a4a71..28b34e38b15 100644 --- a/app/views/shared/_mini_pipeline_graph.html.haml +++ b/app/views/shared/_mini_pipeline_graph.html.haml @@ -3,7 +3,6 @@ - if stage.status - detailed_status = stage.detailed_status(current_user) - icon_status = "#{detailed_status.icon}_borderless" - - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}" .stage-container.dropdown{ class: klass } %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_ajax_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } } diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index 4e7061eef1c..7cbc5810c10 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -1,5 +1,3 @@ -- show_create = local_assigns.fetch(:show_create, false) - - dropdown_toggle_text = @ref || @project.default_branch = form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do = hidden_field_tag :destination, destination diff --git a/app/views/shared/_remote_mirror_update_button.html.haml b/app/views/shared/_remote_mirror_update_button.html.haml index 34de1c0695f..f32cff18fa8 100644 --- a/app/views/shared/_remote_mirror_update_button.html.haml +++ b/app/views/shared/_remote_mirror_update_button.html.haml @@ -1,13 +1,6 @@ -- if @project.has_remote_mirror? - .append-bottom-default - - if remote_mirror.update_in_progress? - %span.btn.disabled - = icon("refresh spin") - Updating… - - else - = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn" do - = icon("refresh") - Update Now - - if @remote_mirror.last_successful_update_at - %p.inline.prepend-left-10 - Successfully updated #{time_ago_with_tooltip(@remote_mirror.last_successful_update_at)}. +- if remote_mirror.update_in_progress? + %button.btn.disabled{ type: 'button', data: { toggle: 'tooltip', container: 'body' }, title: _('Updating') } + = icon("refresh spin") +- else + = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do + = icon("refresh") diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 1cd8ce0826c..c7037335866 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -1,5 +1,3 @@ -- boards_page = controller.controller_name == 'boards' - .issues-filters .issues-details-filters.row-content-block.second-block = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 34911fd2712..0b42b33581a 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -7,7 +7,6 @@ - data_options = local_assigns.fetch(:data_options, {}) - classes = local_assigns.fetch(:classes, []) - selected = local_assigns.fetch(:selected, nil) -- selected_toggle = local_assigns.fetch(:selected_toggle, nil) - dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label") - dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"} - dropdown_data.merge!(data_options) diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml index 3b017c62a80..d8580ad8ab4 100644 --- a/app/views/shared/issuable/form/_metadata.html.haml +++ b/app/views/shared/issuable/form/_metadata.html.haml @@ -3,7 +3,6 @@ - return unless can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) - has_due_date = issuable.has_attribute?(:due_date) -- has_labels = @labels && @labels.any? - form = local_assigns.fetch(:form) %hr diff --git a/app/views/shared/milestones/_delete_button.html.haml b/app/views/shared/milestones/_delete_button.html.haml new file mode 100644 index 00000000000..e236c24b088 --- /dev/null +++ b/app/views/shared/milestones/_delete_button.html.haml @@ -0,0 +1,14 @@ +- milestone_url = @milestone.project_milestone? ? project_milestone_path(@project, @milestone) : group_milestone_path(@group, @milestone) + +%button.js-delete-milestone-button.btn.btn-grouped.btn-danger{ data: { toggle: 'modal', + target: '#delete-milestone-modal', + milestone_id: @milestone.id, + milestone_title: markdown_field(@milestone, :title), + milestone_url: milestone_url, + milestone_issue_count: @milestone.issues.count, + milestone_merge_request_count: @milestone.merge_requests.count }, + disabled: true } + = _('Delete') + = icon('spin spinner', class: 'js-loading-icon hidden' ) + +#delete-milestone-modal diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml index ee6354b1c28..ee97f0172da 100644 --- a/app/views/shared/milestones/_issuables.html.haml +++ b/app/views/shared/milestones/_issuables.html.haml @@ -2,8 +2,8 @@ - primary = local_assigns.fetch(:primary, false) - panel_class = primary ? 'bg-primary text-white' : '' -.card{ class: panel_class } - .card-header +.card + .card-header{ class: panel_class } .title = title - if show_counter diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index c559945a9c9..3dd2842be4f 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -16,6 +16,9 @@ = milestone_date_range(milestone) %div = render('shared/milestone_expired', milestone: milestone) + - if milestone.group_milestone? + .label-badge.label-badge-blue.d-inline-block + = milestone.group.full_name - if milestone.legacy_group_milestone? .projects - milestone.milestones.each do |milestone| @@ -49,7 +52,7 @@ - unless milestone.active? = link_to 'Reopen Milestone', project_milestone_path(@project, milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" - if @group - - if can?(current_user, :admin_milestones, @group) + - if can?(current_user, :admin_milestone, @group) - if milestone.closed? = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen" - else diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index 320e3788a0f..0499b04a482 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -23,7 +23,7 @@ = milestone_date_range(milestone) - if group .float-right - - if can?(current_user, :admin_milestones, group) + - if can?(current_user, :admin_milestone, group) - if milestone.group_milestone? = link_to edit_group_milestone_path(group, milestone), class: "btn btn btn-grouped" do Edit @@ -32,6 +32,9 @@ - else = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" + - unless is_dynamic_milestone + = render 'shared/milestones/delete_button' + = render 'shared/milestones/deprecation_message' if is_dynamic_milestone .detail-page-description.milestone-detail diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml index 1f6e8f98bbb..43a87fd8397 100644 --- a/app/views/shared/notifications/_custom_notifications.html.haml +++ b/app/views/shared/notifications/_custom_notifications.html.haml @@ -19,7 +19,7 @@ - paragraph = _('Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}.') % { notification_link: notification_link.html_safe } #{ paragraph.html_safe } .col-lg-8 - - NotificationSetting::EMAIL_EVENTS.each_with_index do |event, index| + - notification_setting.email_events.each_with_index do |event, index| - field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]" .form-group .form-check{ class: ("prepend-top-0" if index == 0) } diff --git a/app/views/shared/plugins/_index.html.haml b/app/views/shared/plugins/_index.html.haml index 7bcc54e7459..9d230d12be2 100644 --- a/app/views/shared/plugins/_index.html.haml +++ b/app/views/shared/plugins/_index.html.haml @@ -19,5 +19,5 @@ .monospace = File.basename(file) - else - %p.card.bg-light.text-center - No plugins found. + .card.bg-light.text-center + .nothing-here-block No plugins found. diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index be053d481e4..aba790e1217 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -1,9 +1,7 @@ - avatar = true unless local_assigns[:avatar] == false - stars = true unless local_assigns[:stars] == false - forks = false unless local_assigns[:forks] == true -- ci = false unless local_assigns[:ci] == true - skip_namespace = false unless local_assigns[:skip_namespace] == true -- user = local_assigns[:user] - access = max_project_member_access(project) - css_class = '' unless local_assigns[:css_class] - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && can_show_last_commit_in_list?(project) diff --git a/app/views/shared/runners/_form.html.haml b/app/views/shared/runners/_form.html.haml index 0337680d79b..fa93307be31 100644 --- a/app/views/shared/runners/_form.html.haml +++ b/app/views/shared/runners/_form.html.haml @@ -1,56 +1,56 @@ = form_for runner, url: runner_form_url do |f| = form_errors(runner) .form-group.row - = label :active, "Active", class: 'col-form-label col-sm-2' + = label :active, _("Active"), class: 'col-form-label col-sm-2' .col-sm-10 .form-check = f.check_box :active, { class: 'form-check-input' } - %span.light Paused Runners don't accept new jobs + %label.light{ for: :runner_active }= _("Paused Runners don't accept new jobs") .form-group.row - = label :protected, "Protected", class: 'col-form-label col-sm-2' + = label :protected, _("Protected"), class: 'col-form-label col-sm-2' .col-sm-10 .form-check = f.check_box :access_level, { class: 'form-check-input' }, 'ref_protected', 'not_protected' - %span.light This runner will only run on pipelines triggered on protected branches + %label.light{ for: :runner_access_level }= _('This runner will only run on pipelines triggered on protected branches') .form-group.row - = label :run_untagged, 'Run untagged jobs', class: 'col-form-label col-sm-2' + = label :run_untagged, _('Run untagged jobs'), class: 'col-form-label col-sm-2' .col-sm-10 .form-check = f.check_box :run_untagged, { class: 'form-check-input' } - %span.light Indicates whether this runner can pick jobs without tags + %label.light{ for: :runner_run_untagged }= _('Indicates whether this runner can pick jobs without tags') - unless runner.group_type? .form-group.row = label :locked, _('Lock to current projects'), class: 'col-form-label col-sm-2' .col-sm-10 .form-check = f.check_box :locked, { class: 'form-check-input' } - %span.light= _('When a runner is locked, it cannot be assigned to other projects') + %label.light{ for: :runner_locked }= _('When a runner is locked, it cannot be assigned to other projects') .form-group.row = label_tag :token, class: 'col-form-label col-sm-2' do - Token + = _('Token') .col-sm-10 = f.text_field :token, class: 'form-control', readonly: true .form-group.row = label_tag :ip_address, class: 'col-form-label col-sm-2' do - IP Address + = _('IP Address') .col-sm-10 = f.text_field :ip_address, class: 'form-control', readonly: true .form-group.row = label_tag :description, class: 'col-form-label col-sm-2' do - Description + = _('Description') .col-sm-10 = f.text_field :description, class: 'form-control' .form-group.row = label_tag :maximum_timeout_human_readable, class: 'col-form-label col-sm-2' do - Maximum job timeout + = _('Maximum job timeout') .col-sm-10 = f.text_field :maximum_timeout_human_readable, class: 'form-control' - .form-text.text-muted This timeout will take precedence when lower than Project-defined timeout + .form-text.text-muted= _('This timeout will take precedence when lower than Project-defined timeout') .form-group.row = label_tag :tag_list, class: 'col-form-label col-sm-2' do - Tags + = _('Tags') .col-sm-10 = f.text_field :tag_list, value: runner.tag_list.sort.join(', '), class: 'form-control' - .form-text.text-muted You can setup jobs to only use Runners with specific tags. Separate tags with commas. + .form-text.text-muted= _('You can setup jobs to only use Runners with specific tags. Separate tags with commas.') .form-actions - = f.submit 'Save changes', class: 'btn btn-save' + = f.submit _('Save changes'), class: 'btn btn-success' diff --git a/app/views/snippets/notes/_actions.html.haml b/app/views/snippets/notes/_actions.html.haml index 3a50324770d..220ba2b49e6 100644 --- a/app/views/snippets/notes/_actions.html.haml +++ b/app/views/snippets/notes/_actions.html.haml @@ -1,8 +1,7 @@ - if current_user - if note.emoji_awardable? - - user_authored = note.user_authored?(current_user) .note-actions-item - = link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do + = link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji has-tooltip", data: { position: 'right' } do = icon('spinner spin') %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') diff --git a/app/workers/background_migration_worker.rb b/app/workers/background_migration_worker.rb index 7d006cc348e..688b600649a 100644 --- a/app/workers/background_migration_worker.rb +++ b/app/workers/background_migration_worker.rb @@ -10,17 +10,7 @@ class BackgroundMigrationWorker # maintenance related tasks have plenty of time to clean up after a migration # has been performed. def self.minimum_interval - if enable_health_check? - 2.minutes.to_i - else - 5.minutes.to_i - end - end - - def self.enable_health_check? - Rails.env.development? || - Rails.env.test? || - Feature.enabled?('background_migration_health_check') + 2.minutes.to_i end # Performs the background migration. @@ -86,8 +76,6 @@ class BackgroundMigrationWorker # class_name - The name of the background migration that we might want to # run. def healthy_database? - return true unless self.class.enable_health_check? - return true unless Gitlab::Database.postgresql? !Postgresql::ReplicationSlot.lag_too_great? diff --git a/app/workers/concerns/application_worker.rb b/app/workers/concerns/application_worker.rb index bb06e31641d..d64c2f82a09 100644 --- a/app/workers/concerns/application_worker.rb +++ b/app/workers/concerns/application_worker.rb @@ -11,7 +11,7 @@ module ApplicationWorker set_queue end - module ClassMethods + class_methods do def inherited(subclass) subclass.set_queue end diff --git a/app/workers/concerns/waitable_worker.rb b/app/workers/concerns/waitable_worker.rb index d85bc7d1660..27b94a82444 100644 --- a/app/workers/concerns/waitable_worker.rb +++ b/app/workers/concerns/waitable_worker.rb @@ -3,7 +3,7 @@ module WaitableWorker extend ActiveSupport::Concern - module ClassMethods + class_methods do # Schedules multiple jobs and waits for them to be completed. def bulk_perform_and_wait(args_list, timeout: 10) # Short-circuit: it's more efficient to do small numbers of jobs inline diff --git a/app/workers/detect_repository_languages_worker.rb b/app/workers/detect_repository_languages_worker.rb index 537b8fd5963..854b74b884a 100644 --- a/app/workers/detect_repository_languages_worker.rb +++ b/app/workers/detect_repository_languages_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DetectRepositoryLanguagesWorker include ApplicationWorker include ExceptionBacktrace @@ -14,8 +16,6 @@ class DetectRepositoryLanguagesWorker user = User.find_by(id: user_id) return unless project && user - return if Feature.disabled?(:repository_languages, project.namespace) - try_obtain_lease do ::Projects::DetectRepositoryLanguagesService.new(project, user).execute end diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb index 6b8b972a440..25128caf72f 100644 --- a/app/workers/remove_expired_group_links_worker.rb +++ b/app/workers/remove_expired_group_links_worker.rb @@ -5,6 +5,6 @@ class RemoveExpiredGroupLinksWorker include CronjobQueue def perform - ProjectGroupLink.expired.destroy_all + ProjectGroupLink.expired.destroy_all # rubocop: disable DestroyAll end end diff --git a/app/workers/remove_old_web_hook_logs_worker.rb b/app/workers/remove_old_web_hook_logs_worker.rb index 17140ac4450..0f486f8991d 100644 --- a/app/workers/remove_old_web_hook_logs_worker.rb +++ b/app/workers/remove_old_web_hook_logs_worker.rb @@ -6,7 +6,9 @@ class RemoveOldWebHookLogsWorker WEB_HOOK_LOG_LIFETIME = 2.days + # rubocop: disable DestroyAll def perform WebHookLog.destroy_all(['created_at < ?', Time.now - WEB_HOOK_LOG_LIFETIME]) end + # rubocop: enable DestroyAll end diff --git a/app/workers/todos_destroyer/confidential_issue_worker.rb b/app/workers/todos_destroyer/confidential_issue_worker.rb index 9d640c14963..481fde8c83d 100644 --- a/app/workers/todos_destroyer/confidential_issue_worker.rb +++ b/app/workers/todos_destroyer/confidential_issue_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TodosDestroyer class ConfidentialIssueWorker include ApplicationWorker diff --git a/app/workers/todos_destroyer/entity_leave_worker.rb b/app/workers/todos_destroyer/entity_leave_worker.rb index e62d9876f4a..7db3f6c84b4 100644 --- a/app/workers/todos_destroyer/entity_leave_worker.rb +++ b/app/workers/todos_destroyer/entity_leave_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TodosDestroyer class EntityLeaveWorker include ApplicationWorker diff --git a/app/workers/todos_destroyer/group_private_worker.rb b/app/workers/todos_destroyer/group_private_worker.rb index 3e47eec7461..21ec4abe478 100644 --- a/app/workers/todos_destroyer/group_private_worker.rb +++ b/app/workers/todos_destroyer/group_private_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TodosDestroyer class GroupPrivateWorker include ApplicationWorker diff --git a/app/workers/todos_destroyer/private_features_worker.rb b/app/workers/todos_destroyer/private_features_worker.rb index f457d5e0471..1e68f0fd5ae 100644 --- a/app/workers/todos_destroyer/private_features_worker.rb +++ b/app/workers/todos_destroyer/private_features_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TodosDestroyer class PrivateFeaturesWorker include ApplicationWorker diff --git a/app/workers/todos_destroyer/project_private_worker.rb b/app/workers/todos_destroyer/project_private_worker.rb index 7a853c36370..064e001530c 100644 --- a/app/workers/todos_destroyer/project_private_worker.rb +++ b/app/workers/todos_destroyer/project_private_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TodosDestroyer class ProjectPrivateWorker include ApplicationWorker |