diff options
Diffstat (limited to 'app')
171 files changed, 1917 insertions, 1310 deletions
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index c14d69c5d18..6b54e8baefb 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -1,6 +1,8 @@ +import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys'; import FilteredSearchContainer from '../filtered_search/container'; import FilteredSearchManager from '../filtered_search/filtered_search_manager'; import boardsStore from './stores/boards_store'; +import { isEE } from '~/lib/utils/common_utils'; export default class FilteredSearchBoards extends FilteredSearchManager { constructor(store, updateUrl = false, cantEdit = []) { @@ -8,6 +10,8 @@ export default class FilteredSearchBoards extends FilteredSearchManager { page: 'boards', isGroupDecendent: true, stateFiltersSelector: '.issues-state-filters', + isGroup: isEE(), + filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, }); this.store = store; diff --git a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue index a8ee3f4ac10..70b5c6b0094 100644 --- a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue +++ b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue @@ -50,7 +50,7 @@ export default { }, modalText() { - const linkStart = `<a class="commit-sha" href="${_.escape(this.commitUrl)}">`; + const linkStart = `<a class="commit-sha mr-0" href="${_.escape(this.commitUrl)}">`; const commitId = _.escape(this.commitShortSha); const linkEnd = '</a>'; const name = _.escape(this.name); diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index c541ea3445b..f0e80cba753 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -504,22 +504,28 @@ export default { class="table-section section-10 deployment-column d-none d-sm-none d-md-block" role="gridcell" > - <span v-if="shouldRenderDeploymentID"> {{ deploymentInternalId }} </span> + <span v-if="shouldRenderDeploymentID" class="text-break-word"> + {{ deploymentInternalId }} + </span> - <span v-if="!model.isFolder && deploymentHasUser"> + <span v-if="!model.isFolder && deploymentHasUser" class="text-break-word"> by <user-avatar-link :link-href="deploymentUser.web_url" :img-src="deploymentUser.avatar_url" :img-alt="userImageAltDescription" :tooltip-text="deploymentUser.username" - class="js-deploy-user-container" + class="js-deploy-user-container float-none" /> </span> </div> <div class="table-section section-15 d-none d-sm-none d-md-block" role="gridcell"> - <a v-if="shouldRenderBuildName" :href="buildPath" class="build-link flex-truncate-parent"> + <a + v-if="shouldRenderBuildName" + :href="buildPath" + class="build-link cgray flex-truncate-parent" + > <span class="flex-truncate-child">{{ buildName }}</span> </a> </div> diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue index 266cdc42518..bafbc00597e 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.vue +++ b/app/assets/javascripts/environments/components/environment_rollback.vue @@ -72,10 +72,9 @@ export default { <gl-button v-gl-tooltip v-gl-modal.confirm-rollback-modal - variant="secondary" :disabled="isLoading" :title="title" - class="d-none d-md-block" + class="d-none d-md-block text-secondary" @click="onClick" > <icon v-if="isLastDeployment" name="repeat" /> <icon v-else name="redo" /> diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue index 6d74d136a94..13195d32cc4 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.vue +++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue @@ -39,7 +39,7 @@ export default { :aria-label="title" :href="terminalPath" :class="{ disabled: disabled }" - class="btn terminal-button d-none d-sm-none d-md-block" + class="btn terminal-button d-none d-sm-none d-md-block text-secondary" > <icon name="terminal" /> </a> diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js index b844e4c5e5b..ccbe591a63e 100644 --- a/app/assets/javascripts/issuable_bulk_update_actions.js +++ b/app/assets/javascripts/issuable_bulk_update_actions.js @@ -81,9 +81,6 @@ export default { const formData = { update: { state_event: this.form.find('input[name="update[state_event]"]').val(), - // For Merge Requests - assignee_id: this.form.find('input[name="update[assignee_id]"]').val(), - // For Issues assignee_ids: [this.form.find('input[name="update[assignee_ids][]"]').val()], milestone_id: this.form.find('input[name="update[milestone_id]"]').val(), issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(), diff --git a/app/assets/javascripts/lib/utils/webpack.js b/app/assets/javascripts/lib/utils/webpack.js index a4dad6f1615..37b5409a51d 100644 --- a/app/assets/javascripts/lib/utils/webpack.js +++ b/app/assets/javascripts/lib/utils/webpack.js @@ -6,5 +6,11 @@ export function resetServiceWorkersPublicPath() { // see: https://webpack.js.org/guides/public-path/ const relativeRootPath = (gon && gon.relative_url_root) || ''; const webpackAssetPath = `${relativeRootPath}/assets/webpack/`; + __webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase + + // monaco-editor-webpack-plugin currently (incorrectly) references the + // public path as a property of `window`. Once this is fixed upstream we + // can remove this line + // see: https://github.com/Microsoft/monaco-editor-webpack-plugin/pull/63 window.__webpack_public_path__ = webpackAssetPath; // eslint-disable-line } diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js index 9e5d0d0fd28..e97320fd682 100644 --- a/app/assets/javascripts/monitoring/constants.js +++ b/app/assets/javascripts/monitoring/constants.js @@ -18,5 +18,3 @@ export const timeWindows = { threeDays: __('3 days'), oneWeek: __('1 week'), }; - -export const msPerMinute = 60000; diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js index e379827b769..ef309c8a398 100644 --- a/app/assets/javascripts/monitoring/utils.js +++ b/app/assets/javascripts/monitoring/utils.js @@ -1,4 +1,4 @@ -import { timeWindows, msPerMinute } from './constants'; +import { timeWindows } from './constants'; /** * method that converts a predetermined time window to minutes @@ -6,27 +6,26 @@ import { timeWindows, msPerMinute } from './constants'; * @param {String} timeWindow - The time window to convert to minutes * @returns {number} The time window in minutes */ -const getTimeDifferenceMinutes = timeWindow => { +const getTimeDifferenceSeconds = timeWindow => { switch (timeWindow) { case timeWindows.thirtyMinutes: - return 30; + return 60 * 30; case timeWindows.threeHours: - return 60 * 3; + return 60 * 60 * 3; case timeWindows.oneDay: - return 60 * 24 * 1; + return 60 * 60 * 24 * 1; case timeWindows.threeDays: - return 60 * 24 * 3; + return 60 * 60 * 24 * 3; case timeWindows.oneWeek: - return 60 * 24 * 7 * 1; + return 60 * 60 * 24 * 7 * 1; default: - return 60 * 8; + return 60 * 60 * 8; } }; export const getTimeDiff = selectedTimeWindow => { - const end = Date.now(); - const timeDifferenceMinutes = getTimeDifferenceMinutes(selectedTimeWindow); - const start = new Date(end - timeDifferenceMinutes * msPerMinute).getTime(); + const end = Date.now() / 1000; // convert milliseconds to seconds + const start = end - getTimeDifferenceSeconds(selectedTimeWindow); return { start, end }; }; diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 1d6cb9485f7..b30d7fa9b73 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -115,8 +115,11 @@ export default { author() { return this.getUserData; }, - canUpdateIssue() { - return this.getNoteableData.current_user.can_update; + canToggleIssueState() { + return ( + this.getNoteableData.current_user.can_update && + this.getNoteableData.state !== constants.MERGED + ); }, endpoint() { return this.getNoteableData.create_note_path; @@ -415,7 +418,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" </div> <loading-button - v-if="canUpdateIssue" + v-if="canToggleIssueState" :loading="isToggleStateButtonLoading" :container-class="[ actionButtonClassNames, diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index ff303d0f55a..fbf75ed0e41 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -1,6 +1,7 @@ <script> import { mapActions } from 'vuex'; import $ from 'jquery'; +import getDiscussion from 'ee_else_ce/notes/mixins/get_discussion'; import noteEditedText from './note_edited_text.vue'; import noteAwardsList from './note_awards_list.vue'; import noteAttachment from './note_attachment.vue'; @@ -16,7 +17,7 @@ export default { noteForm, Suggestions, }, - mixins: [autosave], + mixins: [autosave, getDiscussion], props: { note: { type: Object, @@ -76,8 +77,8 @@ export default { renderGFM() { $(this.$refs['note-body']).renderGFM(); }, - handleFormUpdate(note, parentElement, callback) { - this.$emit('handleFormUpdate', note, parentElement, callback); + handleFormUpdate(note, parentElement, callback, resolveDiscussion) { + this.$emit('handleFormUpdate', note, parentElement, callback, resolveDiscussion); }, formCancelHandler(shouldConfirm, isDirty) { this.$emit('cancelForm', shouldConfirm, isDirty); @@ -111,6 +112,8 @@ export default { :line="line" :note="note" :help-page-path="helpPagePath" + :discussion="discussion" + :resolve-discussion="note.resolve_discussion" @handleFormUpdate="handleFormUpdate" @cancelForm="formCancelHandler" /> diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index d2cfeff53e8..47d74c2f892 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -4,6 +4,7 @@ import { mapGetters, mapActions } from 'vuex'; import { escape } from 'underscore'; import { truncateSha } from '~/lib/utils/text_utility'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; +import draftMixin from 'ee_else_ce/notes/mixins/draft'; import { s__, sprintf } from '../../locale'; import Flash from '../../flash'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; @@ -23,7 +24,7 @@ export default { noteBody, TimelineEntryItem, }, - mixins: [noteable, resolvable], + mixins: [noteable, resolvable, draftMixin], props: { note: { type: Object, @@ -73,9 +74,6 @@ export default { 'is-editable': this.note.current_user.can_edit, }; }, - canResolve() { - return this.note.resolvable && !!this.getUserData.id; - }, canReportAsAbuse() { return !!this.note.report_abuse_path && this.author.id !== this.getUserData.id; }, @@ -156,12 +154,16 @@ export default { this.$refs.noteBody.resetAutoSave(); this.$emit('updateSuccess'); }, - formUpdateHandler(noteText, parentElement, callback) { + formUpdateHandler(noteText, parentElement, callback, resolveDiscussion) { this.$emit('handleUpdateNote', { note: this.note, noteText, + resolveDiscussion, callback: () => this.updateSuccess(), }); + + if (this.isDraft) return; + const data = { endpoint: this.note.path, note: { @@ -234,6 +236,7 @@ export default { <div class="timeline-content"> <div class="note-header"> <note-header v-once :author="author" :created-at="note.created_at" :note-id="note.id"> + <slot slot="note-header-info" name="note-header-info"></slot> <span v-if="commit" v-html="actionText"></span> <span v-else class="d-none d-sm-inline">·</span> </note-header> @@ -247,12 +250,15 @@ export default { :can-award-emoji="note.current_user.can_award_emoji" :can-delete="note.current_user.can_edit" :can-report-as-abuse="canReportAsAbuse" - :can-resolve="note.current_user.can_resolve" + :can-resolve="canResolve" :report-abuse-path="note.report_abuse_path" - :resolvable="note.resolvable" - :is-resolved="note.resolved" + :resolvable="note.resolvable || note.isDraft" + :is-resolved="note.resolved || note.resolve_discussion" :is-resolving="isResolving" :resolved-by="note.resolved_by" + :is-draft="note.isDraft" + :resolve-discussion="note.isDraft && note.resolve_discussion" + :discussion-id="discussionId" @handleEdit="editHandler" @handleDelete="deleteHandler" @handleResolve="resolveHandler" diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js index fba3db8542c..bdfb6b8f105 100644 --- a/app/assets/javascripts/notes/constants.js +++ b/app/assets/javascripts/notes/constants.js @@ -7,6 +7,7 @@ export const COMMENT = 'comment'; export const OPENED = 'opened'; export const REOPENED = 'reopened'; export const CLOSED = 'closed'; +export const MERGED = 'merged'; export const EMOJI_THUMBSUP = 'thumbsup'; export const EMOJI_THUMBSDOWN = 'thumbsdown'; export const ISSUE_NOTEABLE_TYPE = 'issue'; diff --git a/app/assets/javascripts/notes/mixins/draft.js b/app/assets/javascripts/notes/mixins/draft.js new file mode 100644 index 00000000000..1370f3978df --- /dev/null +++ b/app/assets/javascripts/notes/mixins/draft.js @@ -0,0 +1,8 @@ +export default { + computed: { + isDraft: () => false, + canResolve() { + return this.note.current_user.can_resolve; + }, + }, +}; diff --git a/app/assets/javascripts/notes/mixins/get_discussion.js b/app/assets/javascripts/notes/mixins/get_discussion.js new file mode 100644 index 00000000000..b5d820fe083 --- /dev/null +++ b/app/assets/javascripts/notes/mixins/get_discussion.js @@ -0,0 +1,7 @@ +export default { + computed: { + discussion() { + return {}; + }, + }, +}; diff --git a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue index 52d4b75a3a1..6d908524da9 100644 --- a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue +++ b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue @@ -84,10 +84,7 @@ export default { </div> </div> <div> - <div - v-if="isFetchingMergeRequests" - class="related-related-merge-requests-icon qa-related-merge-requests-loading-icon" - > + <div v-if="isFetchingMergeRequests" class="qa-related-merge-requests-loading-icon"> <gl-loading-icon label="Fetching related merge requests" class="py-2" /> </div> <ul v-else class="content-list related-items-list"> diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue index d1a396182b3..ce378e24289 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue @@ -74,8 +74,7 @@ export default { } if (!this.users.length) { - const emptyTooltipLabel = - this.issuableType === 'issue' ? __('Assignee(s)') : __('Assignee'); + const emptyTooltipLabel = __('Assignee(s)'); names.push(emptyTooltipLabel); } @@ -90,6 +89,27 @@ export default { return counter; }, + mergeNotAllowedTooltipMessage() { + const assigneesCount = this.users.length; + + if (this.issuableType !== 'merge_request' || assigneesCount === 0) { + return null; + } + + const cannotMergeCount = this.users.filter(u => u.can_merge === false).length; + const canMergeCount = assigneesCount - cannotMergeCount; + + if (canMergeCount === assigneesCount) { + // Everyone can merge + return null; + } else if (cannotMergeCount === assigneesCount && assigneesCount > 1) { + return 'No one can merge'; + } else if (assigneesCount === 1) { + return 'Cannot merge'; + } + + return `${canMergeCount}/${assigneesCount} can merge`; + }, }, methods: { assignSelf() { @@ -154,6 +174,15 @@ export default { </button> </div> <div class="value hide-collapsed"> + <span + v-if="mergeNotAllowedTooltipMessage" + v-tooltip + :title="mergeNotAllowedTooltipMessage" + data-placement="left" + class="float-right cannot-be-merged" + > + <i aria-hidden="true" data-hidden="true" class="fa fa-exclamation-triangle"></i> + </span> <template v-if="hasNoUsers"> <span class="assign-yourself no-value"> No assignee diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index 3f282138bdf..944b9c0c083 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -162,7 +162,7 @@ export default { </template> <icon name="commit" class="commit-icon js-commit-icon" /> - <gl-link :href="commitUrl" class="commit-sha"> {{ shortSha }} </gl-link> + <gl-link :href="commitUrl" class="commit-sha mr-0"> {{ shortSha }} </gl-link> <div class="commit-title flex-truncate-parent"> <span v-if="title" class="flex-truncate-child"> diff --git a/app/assets/javascripts/vue_shared/components/notes/timeline_entry_item.vue b/app/assets/javascripts/vue_shared/components/notes/timeline_entry_item.vue index 06974a12aed..f316c4fe112 100644 --- a/app/assets/javascripts/vue_shared/components/notes/timeline_entry_item.vue +++ b/app/assets/javascripts/vue_shared/components/notes/timeline_entry_item.vue @@ -1,9 +1,3 @@ -<script> -export default { - name: 'TimelineEntryItem', -}; -</script> - <template> <li class="timeline-entry"> <div class="timeline-entry-inner"><slot></slot></div> diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 8aaa9772715..a2f518cd24e 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -28,6 +28,9 @@ // Component specific styles, will be moved to gitlab-ui @import "components/**/*"; +// Vendors specific styles +@import "vendors/**/*"; + // Styles for JS behaviors. @import "behaviors"; diff --git a/app/assets/stylesheets/components/related_items_list.scss b/app/assets/stylesheets/components/related_items_list.scss index 5a5601f2fa3..628dffc39f1 100644 --- a/app/assets/stylesheets/components/related_items_list.scss +++ b/app/assets/stylesheets/components/related_items_list.scss @@ -25,6 +25,18 @@ $item-weight-max-width: 48px; flex-grow: 1; } + .issue-token-state-icon-open { + color: $green-500; + } + + .issue-token-state-icon-closed { + color: $blue-500; + } + + .merge-request-status.closed { + color: $red-500; + } + .issue-token-state-icon-open, .issue-token-state-icon-closed, .confidential-icon, diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 7c10de828cd..bfd96a4bc05 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -163,88 +163,6 @@ } } -.atwho-view { - overflow-y: auto; - overflow-x: hidden; - - .name, - small.aliases, - small.params { - float: left; - } - - small.aliases, - small.params { - padding: 2px 5px; - } - - small.description { - float: right; - padding: 3px 5px; - } - - .avatar-inline { - margin-bottom: 0; - } - - .has-warning { - .name, - .description { - color: $orange-700; - } - } - - .cur { - .avatar { - @include disable-all-animation; - border: 1px solid $white-light; - } - } - - ul > li { - @include clearfix; - white-space: nowrap; - } - - // TODO: fallback to global style - .atwho-view-ul { - padding: 8px 1px; - - li { - padding: 8px 16px; - border: 0; - - &.cur { - background-color: $gray-darker; - color: $gl-text-color; - - small { - color: inherit; - } - - &.has-warning { - color: $orange-700; - background-color: $orange-100; - } - } - - div.avatar { - display: inline-flex; - justify-content: center; - align-items: center; - - .center { - line-height: 14px; - } - } - - strong { - color: $gl-text-color; - } - } - } -} - .md-suggestion-diff { display: table !important; border: 1px solid $border-color !important; @@ -269,15 +187,6 @@ } @include media-breakpoint-down(xs) { - .atwho-view-ul { - width: 350px; - } - - .atwho-view ul li { - overflow: hidden; - text-overflow: ellipsis; - } - .referenced-users { margin-right: 0; } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 8e1ee51628d..93dffb5ff09 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -12,34 +12,6 @@ .environments-container { .ci-table { - .deployment-column { - > span { - word-break: break-all; - } - - .avatar { - float: none; - } - } - - .btn-group { - > .btn:not(.btn-danger) { - color: $gl-text-color-secondary; - } - - svg path { - fill: $gl-text-color-secondary; - } - - .dropdown { - outline: none; - } - } - - .btn .text-center { - display: inline; - } - .commit-title { margin: 0; } @@ -49,47 +21,16 @@ color: $gl-text-color-secondary; } - .dropdown-menu { - .fa { - margin-right: 6px; - color: $gl-text-color-secondary; - } - } - .build-link, .ref-name { color: $gl-text-color; } - .stop-env-link, - .external-url { - color: $gl-text-color-secondary; - - .stop-env-icon { - font-size: 14px; - } - } - - .deployment .build-column { - .build-link { - color: $gl-text-color; - } - - .avatar { - float: none; - margin-right: 0; - } - } - .folder-icon { margin-right: 3px; color: $gl-text-color-secondary; display: inline-block; vertical-align: text-top; - - .fa:nth-child(1) { - margin-right: 3px; - } } .folder-name { @@ -103,12 +44,6 @@ text-align: center; } - .branch-commit { - .commit-sha { - margin-right: 0; - } - } - .no-btn { border: 0; background: none; @@ -168,11 +103,6 @@ opacity: 0.25; } -.prometheus-graph-overlay { - fill: none; - opacity: 0; - pointer-events: all; -} .rect-text-metric { fill: $white-light; @@ -203,276 +133,10 @@ stroke: $gray-darkest; } -.prometheus-graphs { - .dropdowns { - .dropdown-menu-toggle { - svg { - position: absolute; - right: 5%; - top: 25%; - } - } - - .dropdown-menu-toggle, - .dropdown-menu { - width: 240px; - } - } -} - .environments-actions { .external-url, .monitoring-url, - .terminal-button, - .stop-env-link { + .terminal-button { width: 38px; } } - -.prometheus-panel { - margin-top: 20px; -} - -.prometheus-graph-group { - display: flex; - flex-wrap: wrap; - padding: $gl-padding / 2; -} - -.prometheus-graph { - padding: $gl-padding / 2; -} - -.prometheus-graph-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: $gl-padding-8; - - h5 { - font-size: $gl-font-size-large; - margin: 0; - } -} - -.prometheus-graph-cursor { - position: absolute; - background: $gray-600; - width: 1px; -} - -.prometheus-graph-flag { - display: block; - min-width: 160px; - border: 0; - box-shadow: 0 1px 4px 0 $black-transparent; - - h5 { - padding: 0; - margin: 0; - font-size: 14px; - line-height: 1.2; - } - - .deploy-meta-content { - border-bottom: 1px solid $white-dark; - - svg { - height: 15px; - vertical-align: bottom; - } - } - - &.popover { - padding: 0; - - &.left { - left: auto; - right: 0; - margin-right: 10px; - - > .arrow { - right: -14px; - border-left-color: $border-color; - } - - > .arrow::after { - border-top: 6px solid transparent; - border-bottom: 6px solid transparent; - border-left: 4px solid $gray-50; - } - - .arrow-shadow { - right: -3px; - box-shadow: 1px 0 9px 0 $black-transparent; - } - } - - &.right { - left: 0; - right: auto; - margin-left: 10px; - - > .arrow { - left: -7px; - border-right-color: $border-color; - } - - > .arrow::after { - border-top: 6px solid transparent; - border-bottom: 6px solid transparent; - border-right: 4px solid $gray-50; - } - - .arrow-shadow { - left: -3px; - box-shadow: 1px 0 8px 0 $black-transparent; - } - } - - > .arrow { - top: 10px; - margin: 0; - } - - .arrow-shadow { - content: ''; - position: absolute; - width: 7px; - height: 7px; - background-color: transparent; - transform: rotate(45deg); - top: 13px; - } - - > .popover-title, - > .popover-content, - > .popover-header, - > .popover-body { - padding: 8px; - font-size: 12px; - white-space: nowrap; - position: relative; - } - - > .popover-title { - background-color: $gray-50; - border-radius: $border-radius-default $border-radius-default 0 0; - } - } - - strong { - font-weight: 600; - } -} - -.prometheus-table { - border-collapse: collapse; - padding: 0; - margin: 0; - - td { - vertical-align: middle; - - + td { - padding-left: 8px; - vertical-align: top; - } - } - - .legend-metric-title { - font-size: 12px; - vertical-align: middle; - } -} - -.prometheus-svg-container { - position: relative; - height: 0; - width: 100%; - padding: 0; - padding-bottom: 100%; - - .text-metric-usage { - fill: $black; - font-weight: $gl-font-weight-normal; - font-size: 12px; - } - - > svg { - position: absolute; - height: 100%; - width: 100%; - left: 0; - top: 0; - - text { - fill: $gl-text-color; - stroke-width: 0; - } - - .text-metric-bold { - font-weight: $gl-font-weight-bold; - } - - .label-axis-text { - fill: $black; - font-weight: $gl-font-weight-normal; - font-size: 10px; - } - - .legend-axis-text { - fill: $black; - } - - .tick { - > line { - stroke: $gray-darker; - } - - > text { - fill: $gray-600; - font-size: 10px; - } - } - - .y-label-text, - .x-label-text { - fill: $gray-darkest; - } - - .axis-tick { - stroke: $gray-darker; - } - - .deploy-info-text { - dominant-baseline: text-before-edge; - font-size: 12px; - } - - .deploy-info-text-link { - font-family: $monospace-font; - fill: $blue-600; - - &:hover { - fill: $blue-800; - } - } - - @include media-breakpoint-down(sm) { - .label-axis-text, - .text-metric-usage, - .legend-axis-text { - font-size: 8px; - } - - .tick > text { - font-size: 8px; - } - } - } -} - -.prometheus-table-row-highlight { - background-color: $gray-100; -} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 86b58c1b1b2..709940ba6c8 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -498,6 +498,16 @@ flex: 1; } + .issuable-meta { + .author-link { + display: inline-block; + } + + .issuable-comments { + height: 18px; + } + } + .merge-request-title { margin-bottom: 2px; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 792c618fd40..7778b4aab3d 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -67,6 +67,10 @@ } } +.classification-label { + background-color: $red-500; +} + .toggle-wrapper { margin-top: 5px; } @@ -1158,6 +1162,8 @@ pre.light-well { .cannot-be-merged:hover { color: $red-500; margin-top: 2px; + position: relative; + z-index: 2; } .private-forks-notice .private-fork-icon { diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss new file mode 100644 index 00000000000..c03554b287f --- /dev/null +++ b/app/assets/stylesheets/pages/prometheus.scss @@ -0,0 +1,270 @@ +.prometheus-graphs { + .dropdowns { + .dropdown-menu-toggle { + svg { + position: absolute; + right: 5%; + top: 25%; + } + } + + .dropdown-menu-toggle, + .dropdown-menu { + width: 240px; + } + } +} + +.prometheus-panel { + margin-top: 20px; +} + +.prometheus-graph-group { + display: flex; + flex-wrap: wrap; + padding: $gl-padding / 2; +} + +.prometheus-graph { + padding: $gl-padding / 2; +} + +.prometheus-graph-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: $gl-padding-8; + + h5 { + font-size: $gl-font-size-large; + margin: 0; + } +} + +.prometheus-graph-cursor { + position: absolute; + background: $gray-600; + width: 1px; +} + +.prometheus-graph-flag { + display: block; + min-width: 160px; + border: 0; + box-shadow: 0 1px 4px 0 $black-transparent; + + h5 { + padding: 0; + margin: 0; + font-size: 14px; + line-height: 1.2; + } + + .deploy-meta-content { + border-bottom: 1px solid $white-dark; + + svg { + height: 15px; + vertical-align: bottom; + } + } + + &.popover { + padding: 0; + + &.left { + left: auto; + right: 0; + margin-right: 10px; + + > .arrow { + right: -14px; + border-left-color: $border-color; + } + + > .arrow::after { + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + border-left: 4px solid $gray-50; + } + + .arrow-shadow { + right: -3px; + box-shadow: 1px 0 9px 0 $black-transparent; + } + } + + &.right { + left: 0; + right: auto; + margin-left: 10px; + + > .arrow { + left: -7px; + border-right-color: $border-color; + } + + > .arrow::after { + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + border-right: 4px solid $gray-50; + } + + .arrow-shadow { + left: -3px; + box-shadow: 1px 0 8px 0 $black-transparent; + } + } + + > .arrow { + top: 10px; + margin: 0; + } + + .arrow-shadow { + content: ''; + position: absolute; + width: 7px; + height: 7px; + background-color: transparent; + transform: rotate(45deg); + top: 13px; + } + + > .popover-title, + > .popover-content, + > .popover-header, + > .popover-body { + padding: 8px; + font-size: 12px; + white-space: nowrap; + position: relative; + } + + > .popover-title { + background-color: $gray-50; + border-radius: $border-radius-default $border-radius-default 0 0; + } + } + + strong { + font-weight: 600; + } +} + +.prometheus-table { + border-collapse: collapse; + padding: 0; + margin: 0; + + td { + vertical-align: middle; + + + td { + padding-left: 8px; + vertical-align: top; + } + } + + .legend-metric-title { + font-size: 12px; + vertical-align: middle; + } +} + +.prometheus-svg-container { + position: relative; + height: 0; + width: 100%; + padding: 0; + padding-bottom: 100%; + + .text-metric-usage { + fill: $black; + font-weight: $gl-font-weight-normal; + font-size: 12px; + } + + > svg { + position: absolute; + height: 100%; + width: 100%; + left: 0; + top: 0; + + text { + fill: $gl-text-color; + stroke-width: 0; + } + + .text-metric-bold { + font-weight: $gl-font-weight-bold; + } + + .label-axis-text { + fill: $black; + font-weight: $gl-font-weight-normal; + font-size: 10px; + } + + .legend-axis-text { + fill: $black; + } + + .tick { + > line { + stroke: $gray-darker; + } + + > text { + fill: $gray-600; + font-size: 10px; + } + } + + .y-label-text, + .x-label-text { + fill: $gray-darkest; + } + + .axis-tick { + stroke: $gray-darker; + } + + .deploy-info-text { + dominant-baseline: text-before-edge; + font-size: 12px; + } + + .deploy-info-text-link { + font-family: $monospace-font; + fill: $blue-600; + + &:hover { + fill: $blue-800; + } + } + + @include media-breakpoint-down(sm) { + .label-axis-text, + .text-metric-usage, + .legend-axis-text { + font-size: 8px; + } + + .tick > text { + font-size: 8px; + } + } + } +} + +.prometheus-table-row-highlight { + background-color: $gray-100; +} + +.prometheus-graph-overlay { + fill: none; + opacity: 0; + pointer-events: all; +} diff --git a/app/assets/stylesheets/vendors/atwho.scss b/app/assets/stylesheets/vendors/atwho.scss new file mode 100644 index 00000000000..ccf3824ea56 --- /dev/null +++ b/app/assets/stylesheets/vendors/atwho.scss @@ -0,0 +1,92 @@ +.atwho-view { + overflow-y: auto; + overflow-x: hidden; + + .name, + small.aliases, + small.params { + float: left; + } + + small.aliases, + small.params { + padding: 2px 5px; + } + + small.description { + float: right; + padding: 3px 5px; + } + + .avatar-inline { + margin-bottom: 0; + } + + .has-warning { + .name, + .description { + color: $orange-700; + } + } + + .cur { + .avatar { + @include disable-all-animation; + border: 1px solid $white-light; + } + } + + ul > li { + @include clearfix; + white-space: nowrap; + } + + // TODO: fallback to global style + .atwho-view-ul { + padding: 8px 1px; + + li { + padding: 8px 16px; + border: 0; + + &.cur { + background-color: $gray-darker; + color: $gl-text-color; + + small { + color: inherit; + } + + &.has-warning { + color: $orange-700; + background-color: $orange-100; + } + } + + div.avatar { + display: inline-flex; + justify-content: center; + align-items: center; + + .center { + line-height: 14px; + } + } + + strong { + color: $gl-text-color; + } + } + } +} + +@include media-breakpoint-down(xs) { + .atwho-view-ul { + width: 350px; + } + + .atwho-view ul li { + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb index 68e14f0c2e5..7d8016f763d 100644 --- a/app/controllers/abuse_reports_controller.rb +++ b/app/controllers/abuse_reports_controller.rb @@ -16,7 +16,7 @@ class AbuseReportsController < ApplicationController if @abuse_report.save @abuse_report.notify - message = "Thank you for your report. A GitLab administrator will look into it shortly." + message = _("Thank you for your report. A GitLab administrator will look into it shortly.") redirect_to @abuse_report.user, notice: message else render :new @@ -37,9 +37,9 @@ class AbuseReportsController < ApplicationController @user = User.find_by(id: params[:user_id]) if @user.nil? - redirect_to root_path, alert: "Cannot create the abuse report. The user has been deleted." + redirect_to root_path, alert: _("Cannot create the abuse report. The user has been deleted.") elsif @user.blocked? - redirect_to @user, alert: "Cannot create the abuse report. This user has been blocked." + redirect_to @user, alert: _("Cannot create the abuse report. This user has been blocked.") end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index ab792cf7403..b681949ab36 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -124,7 +124,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end def visible_application_setting_attributes - ApplicationSettingsHelper.visible_attributes + [ + [ + *::ApplicationSettingsHelper.visible_attributes, + *::ApplicationSettingsHelper.external_authorization_service_attributes, :domain_blacklist_file, disabled_oauth_sign_in_sources: [], import_sources: [], diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b7eb6af6d67..d5f1e35a79b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -293,7 +293,7 @@ class ApplicationController < ActionController::Base unless Gitlab::Auth::LDAP::Access.allowed?(current_user) sign_out current_user - flash[:alert] = "Access denied for your LDAP account." + flash[:alert] = _("Access denied for your LDAP account.") redirect_to new_user_session_path end end @@ -340,7 +340,7 @@ class ApplicationController < ActionController::Base def require_email if current_user && current_user.temp_oauth_email? && session[:impersonator_id].nil? - return redirect_to profile_path, notice: 'Please complete your profile with email address' + return redirect_to profile_path, notice: _('Please complete your profile with email address') end end diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 85aeecbf90b..065d2d3a4ec 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -192,12 +192,7 @@ module IssuableActions def bulk_update_params permitted_keys_array = permitted_keys.dup - - if resource_name == 'issue' - permitted_keys_array << { assignee_ids: [] } - else - permitted_keys_array.unshift(:assignee_id) - end + permitted_keys_array << { assignee_ids: [] } params.require(:update).permit(permitted_keys_array) end diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 6d6e0cc6c7f..91e875dca54 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -190,15 +190,15 @@ module IssuableCollections end end + # rubocop:disable Gitlab/ModuleWithInstanceVariables def preload_for_collection + common_attributes = [:author, :assignees, :labels, :milestone] @preload_for_collection ||= case collection_type when 'Issue' - [:project, :author, :assignees, :labels, :milestone, project: :namespace] + common_attributes + [:project, project: :namespace] when 'MergeRequest' - [ - :target_project, :author, :assignee, :labels, :milestone, - source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits - ] + common_attributes + [:target_project, source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits] end end + # rubocop:enable Gitlab/ModuleWithInstanceVariables end diff --git a/app/controllers/concerns/project_unauthorized.rb b/app/controllers/concerns/project_unauthorized.rb index f59440dbc59..d42363b8b17 100644 --- a/app/controllers/concerns/project_unauthorized.rb +++ b/app/controllers/concerns/project_unauthorized.rb @@ -1,10 +1,21 @@ # frozen_string_literal: true module ProjectUnauthorized - extend ActiveSupport::Concern - - # EE would override this def project_unauthorized_proc - # no-op + lambda do |project| + if project + label = project.external_authorization_classification_label + rejection_reason = nil + + unless ::Gitlab::ExternalAuthorization.access_allowed?(current_user, label) + rejection_reason = ::Gitlab::ExternalAuthorization.rejection_reason(current_user, label) + rejection_reason ||= _('External authorization denied access to this project') + end + + if rejection_reason + access_denied!(rejection_reason) + end + end + end end end diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 2c4aab67448..2ae500a2fdf 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -22,7 +22,7 @@ class ConfirmationsController < Devise::ConfirmationsController after_sign_in(resource) else Gitlab::AppLogger.info("Email Confirmed: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip}") - flash[:notice] = flash[:notice] + " Please sign in." + flash[:notice] = flash[:notice] + _(" Please sign in.") new_session_path(:user, anchor: 'login-pane') end end diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index 0a47736cad8..70811f5ea59 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -14,8 +14,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController respond_to do |format| format.html do - # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37434 - # Also https://gitlab.com/gitlab-org/gitlab-ce/issues/40260 + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/40260 Gitlab::GitalyClient.allow_n_plus_1_calls do render end diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index a9d6addd4a4..10cdce98437 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -22,7 +22,7 @@ class HelpController < ApplicationController end def show - @path = clean_path_info(path_params[:path]) + @path = Rack::Utils.clean_path_info(path_params[:path]) respond_to do |format| format.any(:markdown, :md, :html) do @@ -75,35 +75,4 @@ class HelpController < ApplicationController params end - - PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact) - - # Taken from ActionDispatch::FileHandler - # Cleans up the path, to prevent directory traversal outside the doc folder. - def clean_path_info(path_info) - parts = path_info.split(PATH_SEPS) - - clean = [] - - # Walk over each part of the path - parts.each do |part| - # Turn `one//two` or `one/./two` into `one/two`. - next if part.empty? || part == '.' - - if part == '..' - # Turn `one/two/../` into `one` - clean.pop - else - # Add simple folder names to the clean path. - clean << part - end - end - - # If the path was an absolute path (i.e. `/` or `/one/two`), - # add `/` to the front of the clean path. - clean.unshift '/' if parts.empty? || parts.first.empty? - - # Join all the clean path parts by the path separator. - ::File.join(*clean) - end end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 315d1375e02..a78d87eceea 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -13,9 +13,9 @@ class InvitesController < ApplicationController if member.accept_invite!(current_user) label, path = source_info(member.source) - redirect_to path, notice: "You have been granted #{member.human_access} access to #{label}." + redirect_to path, notice: _("You have been granted %{member_human_access} access to %{label}.") % { member_human_access: member.human_access, label: label } else - redirect_back_or_default(options: { alert: "The invitation could not be accepted." }) + redirect_back_or_default(options: { alert: _("The invitation could not be accepted.") }) end end @@ -30,9 +30,9 @@ class InvitesController < ApplicationController new_user_session_path end - redirect_to path, notice: "You have declined the invitation to join #{label}." + redirect_to path, notice: _("You have declined the invitation to join %{label}.") % { label: label } else - redirect_back_or_default(options: { alert: "The invitation could not be declined." }) + redirect_back_or_default(options: { alert: _("The invitation could not be declined.") }) end end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index e90e8278c13..d9b3b4bbbd9 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -105,11 +105,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController end def redirect_identity_link_failed(error_message) - redirect_to profile_account_path, notice: "Authentication failed: #{error_message}" + redirect_to profile_account_path, notice: _("Authentication failed: %{error_message}") % { error_message: error_message } end def redirect_identity_linked - redirect_to profile_account_path, notice: 'Authentication method updated' + redirect_to profile_account_path, notice: _('Authentication method updated') end def handle_service_ticket(provider, ticket) @@ -147,10 +147,10 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController def handle_signup_error label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider']) - message = ["Signing in using your #{label} account without a pre-existing GitLab account is not allowed."] + message = [_("Signing in using your %{label} account without a pre-existing GitLab account is not allowed.") % { label: label }] if Gitlab::CurrentSettings.allow_signup? - message << "Create a GitLab account first, and then connect it to your #{label} account." + message << _("Create a GitLab account first, and then connect it to your %{label} account.") % { label: label } end flash[:notice] = message.join(' ') @@ -168,14 +168,14 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController end def fail_auth0_login - flash[:alert] = 'Wrong extern UID provided. Make sure Auth0 is configured correctly.' + flash[:alert] = _('Wrong extern UID provided. Make sure Auth0 is configured correctly.') redirect_to new_user_session_path end def handle_disabled_provider label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider']) - flash[:alert] = "Signing in using #{label} has been disabled" + flash[:alert] = _("Signing in using %{label} has been disabled") % { label: label } redirect_to new_user_session_path end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 28f113b5cbe..77de5cb45c9 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -22,7 +22,7 @@ class PasswordsController < Devise::PasswordsController ).first_or_initialize unless user.reset_password_period_valid? - flash[:alert] = 'Your password reset token has expired.' + flash[:alert] = _('Your password reset token has expired.') redirect_to(new_user_password_url(user_email: user['email'])) end end @@ -52,7 +52,7 @@ class PasswordsController < Devise::PasswordsController end redirect_to after_sending_reset_password_instructions_path_for(resource_name), - alert: "Password authentication is unavailable." + alert: _("Password authentication is unavailable.") end def throttle_reset diff --git a/app/controllers/profiles/chat_names_controller.rb b/app/controllers/profiles/chat_names_controller.rb index 2e78b9e6dc7..80b8279e91e 100644 --- a/app/controllers/profiles/chat_names_controller.rb +++ b/app/controllers/profiles/chat_names_controller.rb @@ -15,9 +15,9 @@ class Profiles::ChatNamesController < Profiles::ApplicationController new_chat_name = current_user.chat_names.new(chat_name_params) if new_chat_name.save - flash[:notice] = "Authorized #{new_chat_name.chat_name}" + flash[:notice] = _("Authorized %{new_chat_name}") % { new_chat_name: new_chat_name.chat_name } else - flash[:alert] = "Could not authorize chat nickname. Try again!" + flash[:alert] = _("Could not authorize chat nickname. Try again!") end delete_chat_name_token @@ -27,7 +27,7 @@ class Profiles::ChatNamesController < Profiles::ApplicationController def deny delete_chat_name_token - flash[:notice] = "Denied authorization of chat nickname #{chat_name_params[:user_name]}." + flash[:notice] = _("Denied authorization of chat nickname %{user_name}.") % { user_name: chat_name_params[:user_name] } redirect_to profile_chat_names_path end @@ -36,9 +36,9 @@ class Profiles::ChatNamesController < Profiles::ApplicationController @chat_name = chat_names.find(params[:id]) if @chat_name.destroy - flash[:notice] = "Deleted chat nickname: #{@chat_name.chat_name}!" + flash[:notice] = _("Deleted chat nickname: %{chat_name}!") % { chat_name: @chat_name.chat_name } else - flash[:alert] = "Could not delete chat nickname #{@chat_name.chat_name}." + flash[:alert] = _("Could not delete chat nickname %{chat_name}.") % { chat_name: @chat_name.chat_name } end redirect_to profile_chat_names_path, status: :found diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index 4b6ec2697b7..213d900a563 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -11,7 +11,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController if @personal_access_token.save PersonalAccessToken.redis_store!(current_user.id, @personal_access_token.token) - redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created." + redirect_to profile_personal_access_tokens_path, notice: _("Your new personal access token has been created.") else set_index_vars render :index @@ -22,9 +22,9 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController @personal_access_token = finder.find(params[:id]) if @personal_access_token.revoke! - flash[:notice] = "Revoked personal access token #{@personal_access_token.name}!" + flash[:notice] = _("Revoked personal access token %{personal_access_token_name}!") % { personal_access_token_name: @personal_access_token.name } else - flash[:alert] = "Could not revoke personal access token #{@personal_access_token.name}." + flash[:alert] = _("Could not revoke personal access token %{personal_access_token_name}.") % { personal_access_token_name: @personal_access_token.name } end redirect_to profile_personal_access_tokens_path diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index 0227af2c266..0e30df1b15b 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -11,13 +11,13 @@ class Profiles::PreferencesController < Profiles::ApplicationController result = Users::UpdateService.new(current_user, preferences_params.merge(user: user)).execute if result[:status] == :success - flash[:notice] = 'Preferences saved.' + flash[:notice] = _('Preferences saved.') else - flash[:alert] = 'Failed to save preferences.' + flash[:alert] = _('Failed to save preferences.') end rescue ArgumentError => e # Raised when `dashboard` is given an invalid value. - flash[:alert] = "Failed to save preferences (#{e.message})." + flash[:alert] = _("Failed to save preferences (%{error_message}).") % { error_message: e.message } end respond_to do |format| diff --git a/app/controllers/profiles/u2f_registrations_controller.rb b/app/controllers/profiles/u2f_registrations_controller.rb index e6a154fb6aa..866c4dee6e2 100644 --- a/app/controllers/profiles/u2f_registrations_controller.rb +++ b/app/controllers/profiles/u2f_registrations_controller.rb @@ -4,6 +4,6 @@ class Profiles::U2fRegistrationsController < Profiles::ApplicationController def destroy u2f_registration = current_user.u2f_registrations.find(params[:id]) u2f_registration.destroy - redirect_to profile_two_factor_auth_path, status: 302, notice: "Successfully deleted U2F device." + redirect_to profile_two_factor_auth_path, status: 302, notice: _("Successfully deleted U2F device.") end end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 6ff2e222489..9c31ae6376a 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -25,7 +25,7 @@ class Projects::BranchesController < Projects::ApplicationController @refs_pipelines = @project.ci_pipelines.latest_successful_for_refs(@branches.map(&:name)) @merged_branch_names = repository.merged_branch_names(@branches.map(&:name)) - # n+1: https://gitlab.com/gitlab-org/gitaly/issues/992 + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/48097 Gitlab::GitalyClient.allow_n_plus_1_calls do @max_commits = @branches.reduce(0) do |memo, branch| diverging_commit_counts = repository.diverging_commit_counts(branch) diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 301449cfa90..e35f34be23c 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -193,7 +193,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController return unless Feature.enabled?(:metrics_time_window, project) return unless params[:start].present? || params[:end].present? - params.require([:start, :end]).values_at(:start, :end) + params.require([:start, :end]) end def search_environment_names diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb index 6045ee4e171..eb469d2d714 100644 --- a/app/controllers/projects/merge_requests/application_controller.rb +++ b/app/controllers/projects/merge_requests/application_controller.rb @@ -20,7 +20,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont def merge_request_params_attributes [ :allow_collaboration, - :assignee_id, :description, :force_remove_source_branch, :lock_version, @@ -35,6 +34,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont :title, :discussion_locked, label_ids: [], + assignee_ids: [], update_task: [:index, :checked, :line_number, :line_source] ] end diff --git a/app/controllers/projects/tags/releases_controller.rb b/app/controllers/projects/tags/releases_controller.rb index 334e1847cc8..5e4c601a693 100644 --- a/app/controllers/projects/tags/releases_controller.rb +++ b/app/controllers/projects/tags/releases_controller.rb @@ -12,16 +12,13 @@ class Projects::Tags::ReleasesController < Projects::ApplicationController end def update - # Release belongs to Tag which is not active record object, - # it exists only to save a description to each Tag. - # If description is empty we should destroy the existing record. if release_params[:description].present? release.update(release_params) else release.destroy end - redirect_to project_tag_path(@project, @tag.name) + redirect_to project_tag_path(@project, tag.name) end private @@ -30,11 +27,10 @@ class Projects::Tags::ReleasesController < Projects::ApplicationController @tag ||= @repository.find_tag(params[:tag_id]) end - # rubocop: disable CodeReuse/ActiveRecord def release - @release ||= @project.releases.find_or_initialize_by(tag: @tag.name) + @release ||= Releases::CreateService.new(project, current_user, tag: @tag.name) + .find_or_build_release end - # rubocop: enable CodeReuse/ActiveRecord def release_params params.require(:release).permit(:description) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 89dc43a48a1..62b97fc2590 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -345,6 +345,7 @@ class ProjectsController < Projects::ApplicationController :container_registry_enabled, :default_branch, :description, + :external_authorization_classification_label, :import_url, :issues_tracker, :issues_tracker_id, diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 8b8d87524a8..0fa4677ced1 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -27,7 +27,7 @@ class RegistrationsController < Devise::RegistrationsController persist_accepted_terms_if_required(new_user) end else - flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' + flash[:alert] = s_('Profiles|There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.') flash.delete :recaptcha_error render action: 'new' end diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb index 7b6657e1196..f1b39125a48 100644 --- a/app/controllers/root_controller.rb +++ b/app/controllers/root_controller.rb @@ -15,7 +15,7 @@ class RootController < Dashboard::ProjectsController before_action :redirect_logged_user, if: -> { current_user.present? } def index - # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37434 + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/40260 Gitlab::GitalyClient.allow_n_plus_1_calls do super end diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb index 2b76921ebd8..77757c4a3ef 100644 --- a/app/controllers/sent_notifications_controller.rb +++ b/app/controllers/sent_notifications_controller.rb @@ -16,7 +16,7 @@ class SentNotificationsController < ApplicationController noteable = @sent_notification.noteable noteable.unsubscribe(@sent_notification.recipient, @sent_notification.project) - flash[:notice] = "You have been unsubscribed from this thread." + flash[:notice] = _("You have been unsubscribed from this thread.") if current_user redirect_to noteable_path(noteable) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 4bd7d71e264..6943795e8ac 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -70,7 +70,7 @@ class SessionsController < Devise::SessionsController increment_failed_login_captcha_counter self.resource = resource_class.new - flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' + flash[:alert] = _('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.') flash.delete :recaptcha_error respond_with_navigational(resource) { render :new } @@ -122,7 +122,7 @@ class SessionsController < Devise::SessionsController end redirect_to edit_user_password_path(reset_password_token: @token), - notice: "Please create a password for your new account." + notice: _("Please create a password for your new account.") end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 64c88505a16..88ec77426d5 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -439,22 +439,6 @@ class IssuableFinder end # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord - def by_assignee(items) - if filter_by_no_assignee? - items.where(assignee_id: nil) - elsif filter_by_any_assignee? - items.where('assignee_id IS NOT NULL') - elsif assignee - items.where(assignee_id: assignee.id) - elsif assignee_id? || assignee_username? # assignee not found - items.none - else - items - end - end - # rubocop: enable CodeReuse/ActiveRecord - def filter_by_no_assignee? # Assignee_id takes precedence over assignee_username [NONE, FILTER_NONE].include?(params[:assignee_id].to_s.downcase) || params[:assignee_username].to_s == NONE @@ -478,6 +462,20 @@ class IssuableFinder end # rubocop: enable CodeReuse/ActiveRecord + def by_assignee(items) + if filter_by_no_assignee? + items.unassigned + elsif filter_by_any_assignee? + items.assigned + elsif assignee + items.assigned_to(assignee) + elsif assignee_id? || assignee_username? # assignee not found + items.none + else + items + end + end + # rubocop: disable CodeReuse/ActiveRecord def by_milestone(items) if milestones? diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index cb44575d6f1..e6a82f55856 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -144,18 +144,4 @@ class IssuesFinder < IssuableFinder current_user.blank? end - - def by_assignee(items) - if filter_by_no_assignee? - items.unassigned - elsif filter_by_any_assignee? - items.assigned - elsif assignee - items.assigned_to(assignee) - elsif assignee_id? || assignee_username? # assignee not found - items.none - else - items - end - end end diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 0319e95d439..93d3c991846 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -81,7 +81,7 @@ class ProjectsFinder < UnionFinder if private_only? current_user.authorized_projects else - Project.public_or_visible_to_user(current_user, params[:visibility_level]) + Project.public_or_visible_to_user(current_user) end end end diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index 53efd9042b1..1afe000c5f8 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true class GitlabSchema < GraphQL::Schema - # Took our current most complicated query in use, issues.graphql, - # with a complexity of 19, and added a 20 point buffer to it. + # Currently an IntrospectionQuery has a complexity of 179. # These values will evolve over time. - DEFAULT_MAX_COMPLEXITY = 40 - AUTHENTICATED_COMPLEXITY = 50 - ADMIN_COMPLEXITY = 60 + DEFAULT_MAX_COMPLEXITY = 200 + AUTHENTICATED_COMPLEXITY = 250 + ADMIN_COMPLEXITY = 300 use BatchLoader::GraphQL use Gitlab::Graphql::Authorize diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index e275e4278a4..5995ef57e26 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -119,6 +119,39 @@ module ApplicationSettingsHelper options_for_select(options, selected) end + def external_authorization_description + _("If enabled, access to projects will be validated on an external service"\ + " using their classification label.") + end + + def external_authorization_timeout_help_text + _("Time in seconds GitLab will wait for a response from the external "\ + "service. When the service does not respond in time, access will be "\ + "denied.") + end + + def external_authorization_url_help_text + _("When leaving the URL blank, classification labels can still be "\ + "specified without disabling cross project features or performing "\ + "external authorization checks.") + end + + def external_authorization_client_certificate_help_text + _("The X509 Certificate to use when mutual TLS is required to communicate "\ + "with the external authorization service. If left blank, the server "\ + "certificate is still validated when accessing over HTTPS.") + end + + def external_authorization_client_key_help_text + _("The private key to use when a client certificate is provided. This value "\ + "is encrypted at rest.") + end + + def external_authorization_client_pass_help_text + _("The passphrase required to decrypt the private key. This is optional "\ + "and the value is encrypted at rest.") + end + def visible_attributes [ :admin_notification_email, @@ -238,6 +271,18 @@ module ApplicationSettingsHelper ] end + def external_authorization_service_attributes + [ + :external_auth_client_cert, + :external_auth_client_key, + :external_auth_client_key_pass, + :external_authorization_service_default_label, + :external_authorization_service_enabled, + :external_authorization_service_timeout, + :external_authorization_service_url + ] + end + def expanded_by_default? Rails.env.test? end diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index be1e7016a1e..1640f4fc93f 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -69,7 +69,7 @@ module BoardsHelper end def board_sidebar_user_data - dropdown_options = issue_assignees_dropdown_options + dropdown_options = assignees_dropdown_options('issue') { toggle: 'dropdown', diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb index 769f75f57c4..30d8a19ecce 100644 --- a/app/helpers/clusters_helper.rb +++ b/app/helpers/clusters_helper.rb @@ -20,4 +20,9 @@ module ClustersHelper !cluster.provider.legacy_abac? end + + # EE overrides this + def show_cluster_health_graphs?(cluster) + false + end end diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index 8b3d270e873..f7c7f37cc38 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -17,8 +17,8 @@ module FormHelper end end - def issue_assignees_dropdown_options - { + def assignees_dropdown_options(issuable_type) + dropdown_data = { toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data', title: 'Select assignee', filter: true, @@ -28,8 +28,8 @@ module FormHelper first_user: current_user&.username, null_user: true, current_user: true, - project_id: @project&.id, - field_name: 'issue[assignee_ids][]', + project_id: (@target_project || @project)&.id, + field_name: "#{issuable_type}[assignee_ids][]", default_label: 'Unassigned', 'max-select': 1, 'dropdown-header': 'Assignee', @@ -39,5 +39,36 @@ module FormHelper current_user_info: UserSerializer.new.represent(current_user) } } + + type = issuable_type.to_s + + if type == 'issue' && issue_supports_multiple_assignees? || + type == 'merge_request' && merge_request_supports_multiple_assignees? + dropdown_data = multiple_assignees_dropdown_options(dropdown_data) + end + + dropdown_data + end + + # Overwritten + def issue_supports_multiple_assignees? + false + end + + # Overwritten + def merge_request_supports_multiple_assignees? + false + end + + private + + def multiple_assignees_dropdown_options(options) + new_options = options.dup + + new_options[:title] = 'Select assignee(s)' + new_options[:data][:'dropdown-header'] = 'Assignee(s)' + new_options[:data].delete(:'max-select') + + new_options end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 52c49498e9b..9a12db258d5 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -15,11 +15,14 @@ module IssuablesHelper sidebar_gutter_collapsed? ? _('Expand sidebar') : _('Collapse sidebar') end - def sidebar_assignee_tooltip_label(issuable) - if issuable.assignee - issuable.assignee.name + def assignees_label(issuable, include_value: true) + label = 'Assignee'.pluralize(issuable.assignees.count) + + if include_value + sanitized_list = sanitize_name(issuable.assignee_list) + "#{label}: #{sanitized_list}" else - issuable.allows_multiple_assignees? ? _('Assignee(s)') : _('Assignee') + label end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 009dd70c2c9..2ac90eb8d9f 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -303,6 +303,16 @@ module ProjectsHelper @path.present? end + def external_classification_label_help_message + default_label = ::Gitlab::CurrentSettings.current_application_settings + .external_authorization_service_default_label + + s_( + "ExternalAuthorizationService|When no classification label is set the "\ + "default label `%{default_label}` will be used." + ) % { default_label: default_label } + end + private def get_project_nav_tabs(project, current_user) diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 9ba8f92fcbf..63148831a24 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -24,10 +24,12 @@ module Emails end # rubocop: disable CodeReuse/ActiveRecord - def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id, reason = nil) + def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_ids, updated_by_user_id, reason = nil) setup_merge_request_mail(merge_request_id, recipient_id) - @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id + @previous_assignees = [] + @previous_assignees = User.where(id: previous_assignee_ids) if previous_assignee_ids.any? + mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason)) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index efa1233b434..0b740809f30 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -4,6 +4,7 @@ class Notify < BaseMailer include ActionDispatch::Routing::PolymorphicRoutes include GitlabRoutingHelper include EmailsHelper + include IssuablesHelper include Emails::Issues include Emails::MergeRequests @@ -24,6 +25,7 @@ class Notify < BaseMailer helper MembersHelper helper AvatarsHelper helper GitlabRoutingHelper + helper IssuablesHelper def test_email(recipient_email, subject, body) mail(to: recipient_email, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 7ec8505b33a..d28a12413bf 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -213,6 +213,40 @@ class ApplicationSetting < ApplicationRecord validate :terms_exist, if: :enforce_terms? + validates :external_authorization_service_default_label, + presence: true, + if: :external_authorization_service_enabled + + validates :external_authorization_service_url, + url: true, allow_blank: true, + if: :external_authorization_service_enabled + + validates :external_authorization_service_timeout, + numericality: { greater_than: 0, less_than_or_equal_to: 10 }, + if: :external_authorization_service_enabled + + validates :external_auth_client_key, + presence: true, + if: -> (setting) { setting.external_auth_client_cert.present? } + + validates_with X509CertificateCredentialsValidator, + certificate: :external_auth_client_cert, + pkey: :external_auth_client_key, + pass: :external_auth_client_key_pass, + if: -> (setting) { setting.external_auth_client_cert.present? } + + attr_encrypted :external_auth_client_key, + mode: :per_attribute_iv, + key: Settings.attr_encrypted_db_key_base_truncated, + algorithm: 'aes-256-gcm', + encode: true + + attr_encrypted :external_auth_client_key_pass, + mode: :per_attribute_iv, + key: Settings.attr_encrypted_db_key_base_truncated, + algorithm: 'aes-256-gcm', + encode: true + before_validation :ensure_uuid! before_validation :strip_sentry_values diff --git a/app/models/blob.rb b/app/models/blob.rb index c5766eb0327..d528bef8b19 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -110,7 +110,7 @@ class Blob < SimpleDelegator end def load_all_data! - # Endpoint needed: gitlab-org/gitaly#756 + # Endpoint needed: https://gitlab.com/gitlab-org/gitaly/issues/756 Gitlab::GitalyClient.allow_n_plus_1_calls do super(project.repository) if project end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 01d96754518..b81a3cf8362 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -750,6 +750,10 @@ module Ci self.sha == sha || self.source_sha == sha end + def triggered_by?(current_user) + user == current_user + end + private def ci_yaml_from_repo diff --git a/app/models/concerns/deprecated_assignee.rb b/app/models/concerns/deprecated_assignee.rb new file mode 100644 index 00000000000..7f12ce39c96 --- /dev/null +++ b/app/models/concerns/deprecated_assignee.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +# This module handles backward compatibility for import/export of Merge Requests after +# multiple assignees feature was introduced. Also, it handles the scenarios where +# the #26496 background migration hasn't finished yet. +# Ideally, most of this code should be removed at #59457. +module DeprecatedAssignee + extend ActiveSupport::Concern + + def assignee_ids=(ids) + nullify_deprecated_assignee + super + end + + def assignees=(users) + nullify_deprecated_assignee + super + end + + def assignee_id=(id) + self.assignee_ids = Array(id) + end + + def assignee=(user) + self.assignees = Array(user) + end + + def assignee + assignees.first + end + + def assignee_id + assignee_ids.first + end + + def assignee_ids + if Gitlab::Database.read_only? && pending_assignees_population? + return Array(deprecated_assignee_id) + end + + update_assignees_relation + super + end + + def assignees + if Gitlab::Database.read_only? && pending_assignees_population? + return User.where(id: deprecated_assignee_id) + end + + update_assignees_relation + super + end + + private + + # This will make the background migration process quicker (#26496) as it'll have less + # assignee_id rows to look through. + def nullify_deprecated_assignee + return unless persisted? && Gitlab::Database.read_only? + + update_column(:assignee_id, nil) + end + + # This code should be removed in the clean-up phase of the + # background migration (#59457). + def pending_assignees_population? + persisted? && deprecated_assignee_id && merge_request_assignees.empty? + end + + # If there's an assignee_id and no relation, it means the background + # migration at #26496 didn't reach this merge request yet. + # This code should be removed in the clean-up phase of the + # background migration (#59457). + def update_assignees_relation + if pending_assignees_population? + transaction do + merge_request_assignees.create!(user_id: deprecated_assignee_id, merge_request_id: id) + update_column(:assignee_id, nil) + end + end + end + + def deprecated_assignee_id + read_attribute(:assignee_id) + end +end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 17f94b4bd9b..3232c51bfbd 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -67,13 +67,6 @@ module Issuable allow_nil: true, prefix: true - delegate :name, - :email, - :public_email, - to: :assignee, - allow_nil: true, - prefix: true - validates :author, presence: true validates :title, presence: true, length: { maximum: 255 } validate :milestone_is_valid @@ -88,6 +81,19 @@ module Issuable scope :only_opened, -> { with_state(:opened) } scope :closed, -> { with_state(:closed) } + # rubocop:disable GitlabSecurity/SqlInjection + # The `to_ability_name` method is not an user input. + scope :assigned, -> do + where("EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE #{to_ability_name}_id = #{to_ability_name}s.id)") + end + scope :unassigned, -> do + where("NOT EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE #{to_ability_name}_id = #{to_ability_name}s.id)") + end + scope :assigned_to, ->(u) do + where("EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE user_id = ? AND #{to_ability_name}_id = #{to_ability_name}s.id)", u.id) + end + # rubocop:enable GitlabSecurity/SqlInjection + scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") } scope :order_milestone_due_desc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC') } scope :order_milestone_due_asc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC') } @@ -104,6 +110,7 @@ module Issuable participant :author participant :notes_with_associations + participant :assignees strip_attributes :title @@ -270,6 +277,10 @@ module Issuable end end + def assignee_or_author?(user) + author_id == user.id || assignees.exists?(user.id) + end + def today? Date.today == created_at.to_date end @@ -314,11 +325,7 @@ module Issuable end if old_assignees != assignees - if self.is_a?(Issue) - changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)] - else - changes[:assignee] = [old_assignees&.first&.hook_attrs, assignee&.hook_attrs] - end + changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)] end if self.respond_to?(:total_time_spent) @@ -355,10 +362,18 @@ module Issuable def card_attributes { 'Author' => author.try(:name), - 'Assignee' => assignee.try(:name) + 'Assignee' => assignee_list } end + def assignee_list + assignees.map(&:name).to_sentence + end + + def assignee_username_list + assignees.map(&:username).to_sentence + end + def notes_with_associations # If A has_many Bs, and B has_many Cs, and you do # `A.includes(b: :c).each { |a| a.b.includes(:c) }`, sadly ActiveRecord diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index 3c74034b527..423ce7e7db1 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -13,6 +13,14 @@ module Noteable end end + # The timestamp of the note (e.g. the :updated_at attribute if provided via + # API call) + def system_note_timestamp + @system_note_timestamp || Time.now # rubocop:disable Gitlab/ModuleWithInstanceVariables + end + + attr_writer :system_note_timestamp + def base_class_name self.class.base_class.name end diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb index 7f9ff7bbda6..46cac1d41bb 100644 --- a/app/models/gpg_signature.rb +++ b/app/models/gpg_signature.rb @@ -38,6 +38,15 @@ class GpgSignature < ApplicationRecord .safe_find_or_create_by!(commit_sha: attributes[:commit_sha]) end + # Find commits that are lacking a signature in the database at present + def self.unsigned_commit_shas(commit_shas) + return [] if commit_shas.empty? + + signed = GpgSignature.where(commit_sha: commit_shas).pluck(:commit_sha) + + commit_shas - signed + end + def gpg_key=(model) case model when GpgKey diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb index 11289887e00..a9b1962f24c 100644 --- a/app/models/instance_configuration.rb +++ b/app/models/instance_configuration.rb @@ -39,7 +39,7 @@ class InstanceConfiguration def gitlab_ci Settings.gitlab_ci .to_h - .merge(artifacts_max_size: { value: Settings.artifacts.max_size&.megabytes, + .merge(artifacts_max_size: { value: Gitlab::CurrentSettings.max_artifacts_size.megabytes, default: 100.megabytes }) end diff --git a/app/models/issue.rb b/app/models/issue.rb index 97c6dcc4745..eb5544f2a12 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -49,10 +49,6 @@ class Issue < ApplicationRecord scope :in_projects, ->(project_ids) { where(project_id: project_ids) } - scope :assigned, -> { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') } - scope :unassigned, -> { where('NOT EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') } - scope :assigned_to, ->(u) { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = ? AND issue_id = issues.id)', u.id)} - scope :with_due_date, -> { where.not(due_date: nil) } scope :without_due_date, -> { where(due_date: nil) } scope :due_before, ->(date) { where('issues.due_date < ?', date) } @@ -75,8 +71,6 @@ class Issue < ApplicationRecord attr_spammable :title, spam_title: true attr_spammable :description, spam_description: true - participant :assignees - state_machine :state, initial: :opened do event :close do transition [:opened] => :closed @@ -90,7 +84,7 @@ class Issue < ApplicationRecord state :closed before_transition any => :closed do |issue| - issue.closed_at = Time.zone.now + issue.closed_at = issue.system_note_timestamp end before_transition closed: :opened do |issue| @@ -155,22 +149,6 @@ class Issue < ApplicationRecord Gitlab::HookData::IssueBuilder.new(self).build end - # Returns a Hash of attributes to be used for Twitter card metadata - def card_attributes - { - 'Author' => author.try(:name), - 'Assignee' => assignee_list - } - end - - def assignee_or_author?(user) - author_id == user.id || assignees.exists?(user.id) - end - - def assignee_list - assignees.map(&:name).to_sentence - end - # `from` argument can be a Namespace or Project. def to_reference(from = nil, full: false) reference = "#{self.class.reference_prefix}#{iid}" @@ -230,7 +208,13 @@ class Issue < ApplicationRecord def visible_to_user?(user = nil) return false unless project && project.feature_available?(:issues, user) - user ? readable_by?(user) : publicly_visible? + return publicly_visible? unless user + + return false unless readable_by?(user) + + user.full_private_access? || + ::Gitlab::ExternalAuthorization.access_allowed?( + user, project.external_authorization_classification_label) end def check_for_spam? @@ -298,7 +282,7 @@ class Issue < ApplicationRecord # Returns `true` if this Issue is visible to everybody. def publicly_visible? - project.public? && !confidential? + project.public? && !confidential? && !::Gitlab::ExternalAuthorization.enabled? end def expire_etag_cache diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 458c57c1dc6..0a39a720766 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -16,6 +16,7 @@ class MergeRequest < ApplicationRecord include LabelEventable include ReactiveCaching include FromUnion + include DeprecatedAssignee self.reactive_cache_key = ->(model) { [model.project.id, model.iid] } self.reactive_cache_refresh_interval = 10.minutes @@ -69,8 +70,7 @@ class MergeRequest < ApplicationRecord has_many :suggestions, through: :notes has_many :merge_request_assignees - # Will be deprecated at https://gitlab.com/gitlab-org/gitlab-ce/issues/59457 - belongs_to :assignee, class_name: "User" + has_many :assignees, class_name: "User", through: :merge_request_assignees serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize @@ -79,10 +79,6 @@ class MergeRequest < ApplicationRecord after_update :reload_diff_if_branch_changed after_save :ensure_metrics - # Required until the codebase starts using this relation for single or multiple assignees. - # TODO: Remove at gitlab-ee#2004 implementation. - after_save :refresh_merge_request_assignees, if: :assignee_id_changed? - # When this attribute is true some MR validation is ignored # It allows us to close or modify broken merge requests attr_accessor :allow_broken @@ -188,19 +184,14 @@ class MergeRequest < ApplicationRecord end scope :join_project, -> { joins(:target_project) } scope :references_project, -> { references(:target_project) } - scope :assigned, -> { where("assignee_id IS NOT NULL") } - scope :unassigned, -> { where("assignee_id IS NULL") } - scope :assigned_to, ->(u) { where(assignee_id: u.id)} scope :with_api_entity_associations, -> { - preload(:author, :assignee, :notes, :labels, :milestone, :timelogs, + preload(:assignees, :author, :notes, :labels, :milestone, :timelogs, latest_merge_request_diff: [:merge_request_diff_commits], metrics: [:latest_closed_by, :merged_by], target_project: [:route, { namespace: :route }], source_project: [:route, { namespace: :route }]) } - participant :assignee - after_save :keep_around_commit alias_attribute :project, :target_project @@ -337,31 +328,6 @@ class MergeRequest < ApplicationRecord Gitlab::HookData::MergeRequestBuilder.new(self).build end - # Returns a Hash of attributes to be used for Twitter card metadata - def card_attributes - { - 'Author' => author.try(:name), - 'Assignee' => assignee.try(:name) - } - end - - # These method are needed for compatibility with issues to not mess view and other code - def assignees - Array(assignee) - end - - def assignee_ids - Array(assignee_id) - end - - def assignee_ids=(ids) - write_attribute(:assignee_id, ids.last) - end - - def assignee_or_author?(user) - author_id == user.id || assignee_id == user.id - end - # `from` argument can be a Namespace or Project. def to_reference(from = nil, full: false) reference = "#{self.class.reference_prefix}#{iid}" @@ -682,15 +648,6 @@ class MergeRequest < ApplicationRecord merge_request_diff || create_merge_request_diff end - def refresh_merge_request_assignees - transaction do - # Using it instead relation.delete_all in order to avoid adding a - # dependent: :delete_all (we already have foreign key cascade deletion). - MergeRequestAssignee.where(merge_request_id: self).delete_all - merge_request_assignees.create(user_id: assignee_id) if assignee_id - end - end - def create_merge_request_diff fetch_ref! @@ -1208,7 +1165,7 @@ class MergeRequest < ApplicationRecord variables.append(key: 'CI_MERGE_REQUEST_PROJECT_URL', value: project.web_url) variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME', value: target_branch.to_s) variables.append(key: 'CI_MERGE_REQUEST_TITLE', value: title) - variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee.username) if assignee + variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee_username_list) if assignees.any? variables.append(key: 'CI_MERGE_REQUEST_MILESTONE', value: milestone.title) if milestone variables.append(key: 'CI_MERGE_REQUEST_LABELS', value: label_names.join(',')) if labels.present? variables.concat(source_project_variables) diff --git a/app/models/project.rb b/app/models/project.rb index e2869fc2ad5..66fc83113ea 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -459,41 +459,14 @@ class Project < ApplicationRecord # Returns a collection of projects that is either public or visible to the # logged in user. - # - # requested_visiblity_levels: Normally all projects that are visible - # to the user (e.g. internal and public) are queried, but this - # parameter allows the caller to narrow the search space to optimize - # database queries. For instance, a caller may only want to see - # internal projects. Instead of querying for internal and public - # projects and throwing away public projects, this parameter allows - # the query to be targeted for only internal projects. - def self.public_or_visible_to_user(user = nil, requested_visibility_levels = []) - return public_to_user unless user - - visible_levels = Gitlab::VisibilityLevel.levels_for_user(user) - include_private = true - requested_visibility_levels = Array(requested_visibility_levels) - - if requested_visibility_levels.present? - visible_levels &= requested_visibility_levels - include_private = requested_visibility_levels.include?(Gitlab::VisibilityLevel::PRIVATE) - end - - public_or_internal_rel = - if visible_levels.present? - where('projects.visibility_level IN (?)', visible_levels) - else - Project.none - end - - private_rel = - if include_private - where('EXISTS (?)', user.authorizations_for_projects) - else - Project.none - end - - public_or_internal_rel.or(private_rel) + def self.public_or_visible_to_user(user = nil) + if user + where('EXISTS (?) OR projects.visibility_level IN (?)', + user.authorizations_for_projects, + Gitlab::VisibilityLevel.levels_for_user(user)) + else + public_to_user + end end # project features may be "disabled", "internal", "enabled" or "public". If "internal", @@ -674,6 +647,10 @@ class Project < ApplicationRecord { scope: :project, status: auto_devops&.enabled || Feature.enabled?(:force_autodevops_on_by_default, self) } end + def multiple_mr_assignees_enabled? + Feature.enabled?(:multiple_merge_request_assignees, self) + end + def daily_statistics_enabled? Feature.enabled?(:project_daily_statistics, self, default_enabled: true) end @@ -2062,6 +2039,11 @@ class Project < ApplicationRecord fetch_branch_allows_collaboration(user, branch_name) end + def external_authorization_classification_label + super || ::Gitlab::CurrentSettings.current_application_settings + .external_authorization_service_default_label + end + def licensed_features [] end diff --git a/app/models/release.rb b/app/models/release.rb index 746fc31a038..0f9e94373c7 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -15,6 +15,7 @@ class Release < ApplicationRecord accepts_nested_attributes_for :links, allow_destroy: true validates :description, :project, :tag, presence: true + validates :name, presence: true, on: :create scope :sorted, -> { order(created_at: :desc) } diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb index 72de04203a6..5dd2279ef99 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -22,6 +22,13 @@ class BasePolicy < DeclarativePolicy::Base Gitlab::CurrentSettings.current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) end - # This is prevented in some cases in `gitlab-ee` + condition(:external_authorization_enabled, scope: :global, score: 0) do + ::Gitlab::ExternalAuthorization.perform_check? + end + + rule { external_authorization_enabled & ~full_private_access }.policy do + prevent :read_cross_project + end + rule { default }.enable :read_cross_project end diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb index 2c90b8a73cd..662c29a0973 100644 --- a/app/policies/ci/pipeline_policy.rb +++ b/app/policies/ci/pipeline_policy.rb @@ -14,6 +14,10 @@ module Ci @subject.external? end + condition(:triggerer_of_pipeline) do + @subject.triggered_by?(@user) + end + # Disallow users without permissions from accessing internal pipelines rule { ~can?(:read_build) & ~external_pipeline }.policy do prevent :read_pipeline @@ -29,6 +33,14 @@ module Ci enable :destroy_pipeline end + rule { can?(:admin_pipeline) }.policy do + enable :read_pipeline_variable + end + + rule { can?(:update_pipeline) & triggerer_of_pipeline }.policy do + enable :read_pipeline_variable + end + def ref_protected?(user, project, tag, ref) access = ::Gitlab::UserAccess.new(user, project: project) diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 26d7d6e84c4..ba38af9c529 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -89,6 +89,15 @@ class ProjectPolicy < BasePolicy ::Gitlab::CurrentSettings.current_application_settings.mirror_available end + with_scope :subject + condition(:classification_label_authorized, score: 32) do + ::Gitlab::ExternalAuthorization.access_allowed?( + @user, + @subject.external_authorization_classification_label, + @subject.full_path + ) + end + # We aren't checking `:read_issue` or `:read_merge_request` in this case # because it could be possible for a user to see an issuable-iid # (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be @@ -417,6 +426,25 @@ class ProjectPolicy < BasePolicy rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster + rule { ~can?(:read_cross_project) & ~classification_label_authorized }.policy do + # Preventing access here still allows the projects to be listed. Listing + # projects doesn't check the `:read_project` ability. But instead counts + # on the `project_authorizations` table. + # + # All other actions should explicitly check read project, which would + # trigger the `classification_label_authorized` condition. + # + # `:read_project_for_iids` is not prevented by this condition, as it is + # used for cross-project reference checks. + prevent :guest_access + prevent :public_access + prevent :public_user_access + prevent :reporter_access + prevent :developer_access + prevent :maintainer_access + prevent :owner_access + end + private def team_member? diff --git a/app/serializers/issuable_sidebar_extras_entity.rb b/app/serializers/issuable_sidebar_extras_entity.rb index d60253564e1..fb35b7522c5 100644 --- a/app/serializers/issuable_sidebar_extras_entity.rb +++ b/app/serializers/issuable_sidebar_extras_entity.rb @@ -11,4 +11,6 @@ class IssuableSidebarExtrasEntity < Grape::Entity expose :subscribed do |issuable| issuable.subscribed?(request.current_user, issuable.project) end + + expose :assignees, using: API::Entities::UserBasic end diff --git a/app/serializers/issue_board_entity.rb b/app/serializers/issue_board_entity.rb index f7719447b92..e0041eff6cc 100644 --- a/app/serializers/issue_board_entity.rb +++ b/app/serializers/issue_board_entity.rb @@ -2,6 +2,7 @@ class IssueBoardEntity < Grape::Entity include RequestAwareEntity + include TimeTrackableEntity expose :id expose :iid diff --git a/app/serializers/issue_sidebar_extras_entity.rb b/app/serializers/issue_sidebar_extras_entity.rb index 7b6e860140b..dee891a50b7 100644 --- a/app/serializers/issue_sidebar_extras_entity.rb +++ b/app/serializers/issue_sidebar_extras_entity.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true class IssueSidebarExtrasEntity < IssuableSidebarExtrasEntity - expose :assignees, using: API::Entities::UserBasic end diff --git a/app/serializers/merge_request_assignee_entity.rb b/app/serializers/merge_request_assignee_entity.rb new file mode 100644 index 00000000000..6849c62e759 --- /dev/null +++ b/app/serializers/merge_request_assignee_entity.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class MergeRequestAssigneeEntity < ::API::Entities::UserBasic + expose :can_merge do |assignee, options| + options[:merge_request]&.can_be_merged_by?(assignee) + end +end diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb index 178e72f4f0a..973e971b4c0 100644 --- a/app/serializers/merge_request_basic_entity.rb +++ b/app/serializers/merge_request_basic_entity.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class MergeRequestBasicEntity < Grape::Entity - expose :assignee_id expose :merge_status expose :merge_error expose :state @@ -9,7 +8,7 @@ class MergeRequestBasicEntity < Grape::Entity expose :rebase_in_progress?, as: :rebase_in_progress expose :milestone, using: API::Entities::Milestone expose :labels, using: LabelEntity - expose :assignee, using: API::Entities::UserBasic + expose :assignees, using: API::Entities::UserBasic expose :task_status, :task_status_short expose :lock_version, :lock_version end diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb index 4cf84336aa4..6f589351670 100644 --- a/app/serializers/merge_request_serializer.rb +++ b/app/serializers/merge_request_serializer.rb @@ -8,9 +8,9 @@ class MergeRequestSerializer < BaseSerializer entity = case opts[:serializer] when 'sidebar' - MergeRequestSidebarBasicEntity + IssuableSidebarBasicEntity when 'sidebar_extras' - IssuableSidebarExtrasEntity + MergeRequestSidebarExtrasEntity when 'basic' MergeRequestBasicEntity else diff --git a/app/serializers/merge_request_sidebar_basic_entity.rb b/app/serializers/merge_request_sidebar_basic_entity.rb deleted file mode 100644 index 0ae7298a7c1..00000000000 --- a/app/serializers/merge_request_sidebar_basic_entity.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -class MergeRequestSidebarBasicEntity < IssuableSidebarBasicEntity - expose :assignee, if: lambda { |issuable| issuable.assignee } do - expose :assignee, merge: true, using: API::Entities::UserBasic - - expose :can_merge do |issuable| - issuable.can_be_merged_by?(issuable.assignee) - end - end -end diff --git a/app/serializers/merge_request_sidebar_extras_entity.rb b/app/serializers/merge_request_sidebar_extras_entity.rb new file mode 100644 index 00000000000..7276509c363 --- /dev/null +++ b/app/serializers/merge_request_sidebar_extras_entity.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class MergeRequestSidebarExtrasEntity < IssuableSidebarExtrasEntity + expose :assignees do |merge_request| + MergeRequestAssigneeEntity.represent(merge_request.assignees, merge_request: merge_request) + end +end diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index dbbeca9431d..95d73c6422d 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -7,23 +7,7 @@ class PipelineSerializer < BaseSerializer # rubocop: disable CodeReuse/ActiveRecord def represent(resource, opts = {}) if resource.is_a?(ActiveRecord::Relation) - resource = resource.preload([ - :stages, - :retryable_builds, - :cancelable_statuses, - :trigger_requests, - :manual_actions, - :scheduled_actions, - :artifacts, - :merge_request, - { - pending_builds: :project, - project: [:route, { namespace: :route }], - artifacts: { - project: [:route, { namespace: :route }] - } - } - ]) + resource = resource.preload(preloaded_relations) end if paginated? @@ -51,4 +35,26 @@ class PipelineSerializer < BaseSerializer data = represent(resource, { only: [{ details: [:stages] }], preload: true }) data.dig(:details, :stages) || [] end + + private + + def preloaded_relations + [ + :stages, + :retryable_builds, + :cancelable_statuses, + :trigger_requests, + :manual_actions, + :scheduled_actions, + :artifacts, + :merge_request, + { + pending_builds: :project, + project: [:route, { namespace: :route }], + artifacts: { + project: [:route, { namespace: :route }] + } + } + ] + end end diff --git a/app/services/after_branch_delete_service.rb b/app/services/after_branch_delete_service.rb deleted file mode 100644 index ece9fbbef43..00000000000 --- a/app/services/after_branch_delete_service.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -# Branch can be deleted either by DeleteBranchService or by Git::BranchPushService. -class AfterBranchDeleteService < BaseService - attr_reader :branch_name - - def execute(branch_name) - @branch_name = branch_name - - stop_environments - end - - private - - def stop_environments - Ci::StopEnvironmentsService - .new(project, current_user) - .execute(branch_name) - end -end diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb index 9146eb96533..7eeaf8aade1 100644 --- a/app/services/application_settings/update_service.rb +++ b/app/services/application_settings/update_service.rb @@ -2,9 +2,17 @@ module ApplicationSettings class UpdateService < ApplicationSettings::BaseService + include ValidatesClassificationLabel + attr_reader :params, :application_setting def execute + validate_classification_label(application_setting, :external_authorization_service_default_label) + + if application_setting.errors.any? + return false + end + update_terms(@params.delete(:terms)) if params.key?(:performance_bar_allowed_group_path) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 8973c5ffc9e..41dee4e5641 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -37,7 +37,7 @@ module Ci variables_attributes: params[:variables_attributes], project: project, current_user: current_user, - push_options: params[:push_options], + push_options: params[:push_options] || {}, chat_data: params[:chat_data], **extra_options(options)) diff --git a/app/services/clusters/applications/base_helm_service.rb b/app/services/clusters/applications/base_helm_service.rb index adaa68b1efb..3e7f55f0c63 100644 --- a/app/services/clusters/applications/base_helm_service.rb +++ b/app/services/clusters/applications/base_helm_service.rb @@ -16,6 +16,7 @@ module Clusters error_code: error.respond_to?(:error_code) ? error.error_code : nil, service: self.class.name, app_id: app.id, + app_name: app.name, project_ids: app.cluster.project_ids, group_ids: app.cluster.group_ids } @@ -30,6 +31,19 @@ module Clusters Gitlab::Sentry.track_acceptable_exception(error, extra: meta) end + def log_event(event) + meta = { + service: self.class.name, + app_id: app.id, + app_name: app.name, + project_ids: app.cluster.project_ids, + group_ids: app.cluster.group_ids, + event: event + } + + logger.info(meta) + end + def logger @logger ||= Gitlab::Kubernetes::Logger.build end diff --git a/app/services/clusters/applications/install_service.rb b/app/services/clusters/applications/install_service.rb index 5bd3623a558..1f62b3eb4de 100644 --- a/app/services/clusters/applications/install_service.rb +++ b/app/services/clusters/applications/install_service.rb @@ -7,8 +7,10 @@ module Clusters return unless app.scheduled? app.make_installing! + log_event(:begin_install) helm_api.install(install_command) + log_event(:schedule_wait_for_installation) ClusterWaitForAppInstallationWorker.perform_in( ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) rescue Kubeclient::HttpError => e diff --git a/app/services/clusters/applications/patch_service.rb b/app/services/clusters/applications/patch_service.rb index 20c739af7a2..c3d317e226b 100644 --- a/app/services/clusters/applications/patch_service.rb +++ b/app/services/clusters/applications/patch_service.rb @@ -8,8 +8,10 @@ module Clusters app.make_updating! + log_event(:begin_patch) helm_api.update(update_command) + log_event(:schedule_wait_for_patch) ClusterWaitForAppInstallationWorker.perform_in( ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) rescue Kubeclient::HttpError => e diff --git a/app/services/clusters/applications/upgrade_service.rb b/app/services/clusters/applications/upgrade_service.rb index a0ece1d2635..c34391bc8ad 100644 --- a/app/services/clusters/applications/upgrade_service.rb +++ b/app/services/clusters/applications/upgrade_service.rb @@ -9,10 +9,12 @@ module Clusters begin app.make_updating! + log_event(:begin_upgrade) # install_command works with upgrades too # as it basically does `helm upgrade --install` helm_api.update(install_command) + log_event(:schedule_wait_for_upgrade) ClusterWaitForAppInstallationWorker.perform_in( ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) rescue Kubeclient::HttpError => e diff --git a/app/services/concerns/validates_classification_label.rb b/app/services/concerns/validates_classification_label.rb new file mode 100644 index 00000000000..ebcf5c24ff8 --- /dev/null +++ b/app/services/concerns/validates_classification_label.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module ValidatesClassificationLabel + def validate_classification_label(record, attribute_name) + return unless ::Gitlab::ExternalAuthorization.enabled? + return unless classification_label_change?(record, attribute_name) + + new_label = params[attribute_name].presence + new_label ||= ::Gitlab::CurrentSettings.current_application_settings + .external_authorization_service_default_label + + unless ::Gitlab::ExternalAuthorization.access_allowed?(current_user, new_label) + reason = rejection_reason_for_label(new_label) + message = s_('ClassificationLabelUnavailable|is unavailable: %{reason}') % { reason: reason } + record.errors.add(attribute_name, message) + end + end + + def rejection_reason_for_label(label) + reason_from_service = ::Gitlab::ExternalAuthorization.rejection_reason(current_user, label).presence + reason_from_service || _("Access to '%{classification_label}' not allowed") % { classification_label: label } + end + + def classification_label_change?(record, attribute_name) + params.key?(attribute_name) || record.new_record? + end +end diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index 8322a3d74f4..4c3ac19f754 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -29,14 +29,4 @@ class DeleteBranchService < BaseService def success(message) super().merge(message: message) end - - def build_push_data(branch) - Gitlab::DataBuilder::Push.build( - project, - current_user, - branch.dereferenced_target.sha, - Gitlab::Git::BLANK_SHA, - "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", - []) - end end diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb new file mode 100644 index 00000000000..fce4040e390 --- /dev/null +++ b/app/services/git/base_hooks_service.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +module Git + class BaseHooksService < ::BaseService + include Gitlab::Utils::StrongMemoize + + # The N most recent commits to process in a single push payload. + PROCESS_COMMIT_LIMIT = 100 + + def execute + project.repository.after_create if project.empty_repo? + + create_events + create_pipelines + execute_project_hooks + + # Not a hook, but it needs access to the list of changed commits + enqueue_invalidate_cache + + push_data + end + + private + + def hook_name + raise NotImplementedError, "Please implement #{self.class}##{__method__}" + end + + def commits + raise NotImplementedError, "Please implement #{self.class}##{__method__}" + end + + def limited_commits + commits.last(PROCESS_COMMIT_LIMIT) + end + + def commits_count + commits.count + end + + def event_message + nil + end + + def invalidated_file_types + [] + end + + def create_events + EventCreateService.new.push(project, current_user, push_data) + end + + def create_pipelines + Ci::CreatePipelineService + .new(project, current_user, push_data) + .execute(:push, pipeline_options) + end + + def execute_project_hooks + project.execute_hooks(push_data, hook_name) + project.execute_services(push_data, hook_name) + end + + def enqueue_invalidate_cache + ProjectCacheWorker.perform_async( + project.id, + invalidated_file_types, + [:commit_count, :repository_size] + ) + end + + def push_data + @push_data ||= Gitlab::DataBuilder::Push.build( + project, + current_user, + params[:oldrev], + params[:newrev], + params[:ref], + limited_commits, + event_message, + commits_count: commits_count, + push_options: params[:push_options] || {} + ) + + # Dependent code may modify the push data, so return a duplicate each time + @push_data.dup + end + + # to be overridden in EE + def pipeline_options + {} + end + end +end diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb new file mode 100644 index 00000000000..d21a6bb1b9a --- /dev/null +++ b/app/services/git/branch_hooks_service.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true + +module Git + class BranchHooksService < ::Git::BaseHooksService + def execute + execute_branch_hooks + + super.tap do + enqueue_update_gpg_signatures + end + end + + private + + def hook_name + :push_hooks + end + + def commits + strong_memoize(:commits) do + if creating_default_branch? + # The most recent PROCESS_COMMIT_LIMIT commits in the default branch + offset = [count_commits_in_branch - PROCESS_COMMIT_LIMIT, 0].max + project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT) + elsif creating_branch? + # Use the pushed commits that aren't reachable by the default branch + # as a heuristic. This may include more commits than are actually + # pushed, but that shouldn't matter because we check for existing + # cross-references later. + project.repository.commits_between(project.default_branch, params[:newrev]) + elsif updating_branch? + project.repository.commits_between(params[:oldrev], params[:newrev]) + else # removing branch + [] + end + end + end + + def commits_count + return count_commits_in_branch if creating_default_branch? + + super + end + + def invalidated_file_types + return super unless default_branch? && !creating_branch? + + paths = limited_commits.each_with_object(Set.new) do |commit, set| + commit.raw_deltas.each do |diff| + set << diff.new_path + end + end + + Gitlab::FileDetector.types_in_paths(paths) + end + + def execute_branch_hooks + project.repository.after_push_commit(branch_name) + + branch_create_hooks if creating_branch? + branch_update_hooks if updating_branch? + branch_change_hooks if creating_branch? || updating_branch? + branch_remove_hooks if removing_branch? + end + + def branch_create_hooks + project.repository.after_create_branch + project.after_create_default_branch if default_branch? + end + + def branch_update_hooks + # Update the bare repositories info/attributes file using the contents of + # the default branch's .gitattributes file + project.repository.copy_gitattributes(params[:ref]) if default_branch? + end + + def branch_change_hooks + enqueue_process_commit_messages + end + + def branch_remove_hooks + project.repository.after_remove_branch + end + + # Schedules processing of commit messages + def enqueue_process_commit_messages + # don't process commits for the initial push to the default branch + return if creating_default_branch? + + limited_commits.each do |commit| + next unless commit.matches_cross_reference_regex? + + ProcessCommitWorker.perform_async( + project.id, + current_user.id, + commit.to_hash, + default_branch? + ) + end + end + + def enqueue_update_gpg_signatures + unsigned = GpgSignature.unsigned_commit_shas(limited_commits.map(&:sha)) + return if unsigned.empty? + + signable = Gitlab::Git::Commit.shas_with_signatures(project.repository, unsigned) + return if signable.empty? + + CreateGpgSignatureWorker.perform_async(signable, project.id) + end + + def creating_branch? + Gitlab::Git.blank_ref?(params[:oldrev]) + end + + def updating_branch? + !creating_branch? && !removing_branch? + end + + def removing_branch? + Gitlab::Git.blank_ref?(params[:newrev]) + end + + def creating_default_branch? + creating_branch? && default_branch? + end + + def count_commits_in_branch + strong_memoize(:count_commits_in_branch) do + project.repository.commit_count_for_ref(params[:ref]) + end + end + + def default_branch? + strong_memoize(:default_branch) do + [nil, branch_name].include?(project.default_branch) + end + end + + def branch_name + strong_memoize(:branch_name) { Gitlab::Git.ref_name(params[:ref]) } + end + end +end diff --git a/app/services/git/branch_push_service.rb b/app/services/git/branch_push_service.rb index b55aeb5f2b9..da053ce80c7 100644 --- a/app/services/git/branch_push_service.rb +++ b/app/services/git/branch_push_service.rb @@ -1,14 +1,10 @@ # frozen_string_literal: true module Git - class BranchPushService < BaseService - attr_accessor :push_data, :push_commits + class BranchPushService < ::BaseService include Gitlab::Access include Gitlab::Utils::StrongMemoize - # The N most recent commits to process in a single push payload. - PROCESS_COMMIT_LIMIT = 100 - # This method will be called after each git update # and only if the provided user and project are present in GitLab. # @@ -23,108 +19,43 @@ module Git # 6. Checks if the project's main language has changed # def execute - update_commits + return unless Gitlab::Git.branch_ref?(params[:ref]) + + enqueue_update_mrs + enqueue_detect_repository_languages + execute_related_hooks perform_housekeeping update_remote_mirrors - update_caches + stop_environments - update_signatures + true end - def update_commits - project.repository.after_create if project.empty_repo? - project.repository.after_push_commit(branch_name) - - if push_remove_branch? - project.repository.after_remove_branch - @push_commits = [] - elsif push_to_new_branch? - project.repository.after_create_branch - - # Re-find the pushed commits. - if default_branch? - # Initial push to the default branch. Take the full history of that branch as "newly pushed". - process_default_branch - else - # Use the pushed commits that aren't reachable by the default branch - # as a heuristic. This may include more commits than are actually pushed, but - # that shouldn't matter because we check for existing cross-references later. - @push_commits = project.repository.commits_between(project.default_branch, params[:newrev]) - - # don't process commits for the initial push to the default branch - process_commit_messages - end - elsif push_to_existing_branch? - # Collect data for this git push - @push_commits = project.repository.commits_between(params[:oldrev], params[:newrev]) - - process_commit_messages - - # Update the bare repositories info/attributes file using the contents of the default branches - # .gitattributes file - update_gitattributes if default_branch? - end - end - - def update_gitattributes - project.repository.copy_gitattributes(params[:ref]) - end - - def update_caches - if default_branch? - if push_to_new_branch? - # If this is the initial push into the default branch, the file type caches - # will already be reset as a result of `Project#change_head`. - types = [] - else - paths = Set.new - - last_pushed_commits.each do |commit| - commit.raw_deltas.each do |diff| - paths << diff.new_path - end - end - - types = Gitlab::FileDetector.types_in_paths(paths.to_a) - end - - DetectRepositoryLanguagesWorker.perform_async(@project.id, current_user.id) - else - types = [] - end - - ProjectCacheWorker.perform_async(project.id, types, [:commit_count, :repository_size]) + # Update merge requests that may be affected by this push. A new branch + # could cause the last commit of a merge request to change. + def enqueue_update_mrs + UpdateMergeRequestsWorker.perform_async( + project.id, + current_user.id, + params[:oldrev], + params[:newrev], + params[:ref] + ) end - # rubocop: disable CodeReuse/ActiveRecord - def update_signatures - commit_shas = last_pushed_commits.map(&:sha) + def enqueue_detect_repository_languages + return unless default_branch? - return if commit_shas.empty? - - shas_with_cached_signatures = GpgSignature.where(commit_sha: commit_shas).pluck(:commit_sha) - commit_shas -= shas_with_cached_signatures - - return if commit_shas.empty? - - commit_shas = Gitlab::Git::Commit.shas_with_signatures(project.repository, commit_shas) - - CreateGpgSignatureWorker.perform_async(commit_shas, project.id) + DetectRepositoryLanguagesWorker.perform_async(project.id, current_user.id) end - # rubocop: enable CodeReuse/ActiveRecord - # Schedules processing of commit messages. - def process_commit_messages - default = default_branch? + # Only stop environments if the ref is a branch that is being deleted + def stop_environments + return unless removing_branch? - last_pushed_commits.each do |commit| - if commit.matches_cross_reference_regex? - ProcessCommitWorker - .perform_async(project.id, current_user.id, commit.to_hash, default) - end - end + Ci::StopEnvironmentsService.new(project, current_user).execute(branch_name) end def update_remote_mirrors @@ -135,23 +66,7 @@ module Git end def execute_related_hooks - # Update merge requests that may be affected by this push. A new branch - # could cause the last commit of a merge request to change. - # - UpdateMergeRequestsWorker - .perform_async(project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref]) - - EventCreateService.new.push(project, current_user, build_push_data) - Ci::CreatePipelineService.new(project, current_user, build_push_data).execute(:push, pipeline_options) - - project.execute_hooks(build_push_data.dup, :push_hooks) - project.execute_services(build_push_data.dup, :push_hooks) - - if push_remove_branch? - AfterBranchDeleteService - .new(project, current_user) - .execute(branch_name) - end + BranchHooksService.new(project, current_user, params).execute end def perform_housekeeping @@ -161,84 +76,18 @@ module Git rescue Projects::HousekeepingService::LeaseTaken end - def process_default_branch - 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 - end - - def build_push_data - @push_data ||= Gitlab::DataBuilder::Push.build( - project, - current_user, - params[:oldrev], - params[:newrev], - params[:ref], - @push_commits, - commits_count: commits_count, - push_options: params[:push_options] || [] - ) - end - - def push_to_existing_branch? - # Return if this is not a push to a branch (e.g. new commits) - branch_ref? && !Gitlab::Git.blank_ref?(params[:oldrev]) - end - - def push_to_new_branch? - strong_memoize(:push_to_new_branch) do - branch_ref? && Gitlab::Git.blank_ref?(params[:oldrev]) - end - end - - def push_remove_branch? - strong_memoize(:push_remove_branch) do - branch_ref? && Gitlab::Git.blank_ref?(params[:newrev]) - end - end - - def default_branch? - branch_ref? && - (branch_name == project.default_branch || project.default_branch.nil?) - end - - def commit_user(commit) - commit.author || current_user + def removing_branch? + Gitlab::Git.blank_ref?(params[:newrev]) end def branch_name - strong_memoize(:branch_name) do - Gitlab::Git.ref_name(params[:ref]) - end + strong_memoize(:branch_name) { Gitlab::Git.ref_name(params[:ref]) } end - def branch_ref? - strong_memoize(:branch_ref) do - Gitlab::Git.branch_ref?(params[:ref]) - end - end - - 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]) + def default_branch? + strong_memoize(:default_branch) do + [nil, branch_name].include?(project.default_branch) end end - - def last_pushed_commits - @last_pushed_commits ||= @push_commits.last(PROCESS_COMMIT_LIMIT) - end - - private - - def pipeline_options - {} # to be overridden in EE - end end end diff --git a/app/services/git/tag_hooks_service.rb b/app/services/git/tag_hooks_service.rb new file mode 100644 index 00000000000..18eb780579f --- /dev/null +++ b/app/services/git/tag_hooks_service.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Git + class TagHooksService < ::Git::BaseHooksService + private + + def hook_name + :tag_push_hooks + end + + def commits + [tag_commit].compact + end + + def event_message + tag&.message + end + + def tag + strong_memoize(:tag) do + next if Gitlab::Git.blank_ref?(params[:newrev]) + + tag_name = Gitlab::Git.ref_name(params[:ref]) + tag = project.repository.find_tag(tag_name) + + tag if tag && tag.target == params[:newrev] + end + end + + def tag_commit + strong_memoize(:tag_commit) do + project.commit(tag.dereferenced_target) if tag + end + end + end +end diff --git a/app/services/git/tag_push_service.rb b/app/services/git/tag_push_service.rb index 9ce0fbdb206..ee4166dccd0 100644 --- a/app/services/git/tag_push_service.rb +++ b/app/services/git/tag_push_service.rb @@ -1,56 +1,14 @@ # frozen_string_literal: true module Git - class TagPushService < BaseService - attr_accessor :push_data - + class TagPushService < ::BaseService def execute - project.repository.after_create if project.empty_repo? - project.repository.before_push_tag - - @push_data = build_push_data - - EventCreateService.new.push(project, current_user, push_data) - Ci::CreatePipelineService.new(project, current_user, push_data).execute(:push, pipeline_options) + return unless Gitlab::Git.tag_ref?(params[:ref]) - project.execute_hooks(push_data.dup, :tag_push_hooks) - project.execute_services(push_data.dup, :tag_push_hooks) - - ProjectCacheWorker.perform_async(project.id, [], [:commit_count, :repository_size]) + project.repository.before_push_tag + TagHooksService.new(project, current_user, params).execute true end - - private - - def build_push_data - commits = [] - message = nil - - unless Gitlab::Git.blank_ref?(params[:newrev]) - tag_name = Gitlab::Git.ref_name(params[:ref]) - tag = project.repository.find_tag(tag_name) - - if tag && tag.target == params[:newrev] - commit = project.commit(tag.dereferenced_target) - commits = [commit].compact - message = tag.message - end - end - - Gitlab::DataBuilder::Push.build( - project, - current_user, - params[:oldrev], - params[:newrev], - params[:ref], - commits, - message, - push_options: params[:push_options] || []) - end - - def pipeline_options - {} # to be overridden in EE - end end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 7a4ccf0d178..26132f1824a 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -34,14 +34,20 @@ class IssuableBaseService < BaseService end def filter_assignee(issuable) - return unless params[:assignee_id].present? + return if params[:assignee_ids].blank? - assignee_id = params[:assignee_id] + unless issuable.allows_multiple_assignees? + params[:assignee_ids] = params[:assignee_ids].first(1) + end + + assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) } - if assignee_id.to_s == IssuableFinder::NONE - params[:assignee_id] = "" + if params[:assignee_ids].map(&:to_s) == [IssuableFinder::NONE] + params[:assignee_ids] = [] + elsif assignee_ids.any? + params[:assignee_ids] = assignee_ids else - params.delete(:assignee_id) unless assignee_can_read?(issuable, assignee_id) + params.delete(:assignee_ids) end end @@ -352,7 +358,7 @@ class IssuableBaseService < BaseService end def has_changes?(issuable, old_labels: [], old_assignees: []) - valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch] + valid_attrs = [:title, :description, :assignee_ids, :milestone_id, :target_branch] attrs_changed = valid_attrs.any? do |attr| issuable.previous_changes.include?(attr.to_s) diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index ef08adf4f92..48ed5afbc2a 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -20,7 +20,7 @@ module Issues private def create_assignee_note(issue, old_assignees) - SystemNoteService.change_issue_assignees( + SystemNoteService.change_issuable_assignees( issue, issue.project, current_user, old_assignees) end @@ -31,26 +31,6 @@ module Issues issue.project.execute_services(issue_data, hooks_scope) end - # rubocop: disable CodeReuse/ActiveRecord - def filter_assignee(issuable) - return if params[:assignee_ids].blank? - - unless issuable.allows_multiple_assignees? - params[:assignee_ids] = params[:assignee_ids].take(1) - end - - assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) } - - if params[:assignee_ids].map(&:to_s) == [IssuableFinder::NONE] - params[:assignee_ids] = [] - elsif assignee_ids.any? - params[:assignee_ids] = assignee_ids - else - params.delete(:assignee_ids) - end - end - # rubocop: enable CodeReuse/ActiveRecord - def update_project_counter_caches?(issue) super || issue.confidential_changed? end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index cec5b5734c0..cb2337d29d4 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -39,7 +39,7 @@ module Issues if issue.assignees != old_assignees create_assignee_note(issue, old_assignees) notification_service.async.reassigned_issue(issue, current_user, old_assignees) - todo_service.reassigned_issue(issue, current_user, old_assignees) + todo_service.reassigned_issuable(issue, current_user, old_assignees) end if issue.previous_changes.include?('confidential') diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 8a9e5ebb014..b8334a87f6d 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -49,9 +49,9 @@ module MergeRequests MergeRequestMetricsService.new(merge_request.metrics) end - def create_assignee_note(merge_request) - SystemNoteService.change_assignee( - merge_request, merge_request.project, current_user, merge_request.assignee) + def create_assignee_note(merge_request, old_assignees) + SystemNoteService.change_issuable_assignees( + merge_request, merge_request.project, current_user, old_assignees) end def create_pipeline_for(merge_request, user) diff --git a/app/services/merge_requests/push_options_handler_service.rb b/app/services/merge_requests/push_options_handler_service.rb new file mode 100644 index 00000000000..a24163331e8 --- /dev/null +++ b/app/services/merge_requests/push_options_handler_service.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +module MergeRequests + class PushOptionsHandlerService + LIMIT = 10 + + attr_reader :branches, :changes_by_branch, :current_user, :errors, + :project, :push_options, :target_project + + def initialize(project, current_user, changes, push_options) + @project = project + @target_project = @project.default_merge_request_target + @current_user = current_user + @branches = get_branches(changes) + @push_options = push_options + @errors = [] + end + + def execute + validate_service + return self if errors.present? + + branches.each do |branch| + execute_for_branch(branch) + rescue Gitlab::Access::AccessDeniedError + errors << 'User access was denied' + rescue StandardError => e + Gitlab::AppLogger.error(e) + errors << 'An unknown error occurred' + end + + self + end + + private + + def get_branches(raw_changes) + Gitlab::ChangesList.new(raw_changes).map do |changes| + next unless Gitlab::Git.branch_ref?(changes[:ref]) + + # Deleted branch + next if Gitlab::Git.blank_ref?(changes[:newrev]) + + # Default branch + branch_name = Gitlab::Git.branch_name(changes[:ref]) + next if branch_name == target_project.default_branch + + branch_name + end.compact.uniq + end + + def validate_service + errors << 'User is required' if current_user.nil? + + unless target_project.merge_requests_enabled? + errors << "Merge requests are not enabled for project #{target_project.full_path}" + end + + if branches.size > LIMIT + errors << "Too many branches pushed (#{branches.size} were pushed, limit is #{LIMIT})" + end + + if push_options[:target] && !target_project.repository.branch_exists?(push_options[:target]) + errors << "Branch #{push_options[:target]} does not exist" + end + end + + # Returns a Hash of branch => MergeRequest + def merge_requests + @merge_requests ||= MergeRequest.from_project(target_project) + .opened + .from_source_branches(branches) + .index_by(&:source_branch) + end + + def execute_for_branch(branch) + merge_request = merge_requests[branch] + + if merge_request + update!(merge_request) + else + create!(branch) + end + end + + def create!(branch) + unless push_options[:create] + errors << "A merge_request.create push option is required to create a merge request for branch #{branch}" + return + end + + # Use BuildService to assign the standard attributes of a merge request + merge_request = ::MergeRequests::BuildService.new( + project, + current_user, + create_params(branch) + ).execute + + unless merge_request.errors.present? + merge_request = ::MergeRequests::CreateService.new( + project, + current_user, + merge_request.attributes.merge(assignees: merge_request.assignees) + ).execute + end + + collect_errors_from_merge_request(merge_request) unless merge_request.persisted? + end + + def update!(merge_request) + merge_request = ::MergeRequests::UpdateService.new( + target_project, + current_user, + update_params + ).execute(merge_request) + + collect_errors_from_merge_request(merge_request) unless merge_request.valid? + end + + def create_params(branch) + params = { + assignees: [current_user], + source_branch: branch, + source_project: project, + target_branch: push_options[:target] || target_project.default_branch, + target_project: target_project + } + + if push_options.key?(:merge_when_pipeline_succeeds) + params.merge!( + merge_when_pipeline_succeeds: push_options[:merge_when_pipeline_succeeds], + merge_user: current_user + ) + end + + params + end + + def update_params + params = {} + + if push_options.key?(:merge_when_pipeline_succeeds) + params.merge!( + merge_when_pipeline_succeeds: push_options[:merge_when_pipeline_succeeds], + merge_user: current_user + ) + end + + if push_options.key?(:target) + params[:target_branch] = push_options[:target] + end + + params + end + + def collect_errors_from_merge_request(merge_request) + merge_request.errors.full_messages.each do |error| + errors << error + end + end + end +end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 51d27673787..3abea1ad1ae 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -14,13 +14,16 @@ module MergeRequests private def refresh_merge_requests! + # n + 1: https://gitlab.com/gitlab-org/gitlab-ce/issues/60289 Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits)) + # Be sure to close outstanding MRs before reloading them to avoid generating an # empty diff during a manual merge close_upon_missing_source_branch_ref post_merge_manually_merged reload_merge_requests outdate_suggestions + refresh_pipelines_on_merge_requests reset_merge_when_pipeline_succeeds mark_pending_todos_done cache_merge_requests_closing_issues @@ -107,8 +110,6 @@ module MergeRequests end merge_request.mark_as_unchecked - create_pipeline_for(merge_request, current_user) - UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id) end # Upcoming method calls need the refreshed version of @@ -134,6 +135,13 @@ module MergeRequests end end + def refresh_pipelines_on_merge_requests + merge_requests_for_source_branch.each do |merge_request| + create_pipeline_for(merge_request, current_user) + UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id) + end + end + def reset_merge_when_pipeline_succeeds merge_requests_for_source_branch.each(&:reset_merge_when_pipeline_succeeds) end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 8112c2a4299..faaa4d66726 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -24,13 +24,13 @@ module MergeRequests update_task_event(merge_request) || update(merge_request) end - # rubocop:disable Metrics/AbcSize def handle_changes(merge_request, options) old_associations = options.fetch(:old_associations, {}) old_labels = old_associations.fetch(:labels, []) old_mentioned_users = old_associations.fetch(:mentioned_users, []) + old_assignees = old_associations.fetch(:assignees, []) - if has_changes?(merge_request, old_labels: old_labels) + if has_changes?(merge_request, old_labels: old_labels, old_assignees: old_assignees) todo_service.mark_pending_todos_as_done(merge_request, current_user) end @@ -45,15 +45,10 @@ module MergeRequests merge_request.target_branch) end - if merge_request.previous_changes.include?('assignee_id') - reassigned_merge_request_args = [merge_request, current_user] - - old_assignee_id = merge_request.previous_changes['assignee_id'].first - reassigned_merge_request_args << User.find(old_assignee_id) if old_assignee_id - - create_assignee_note(merge_request) - notification_service.async.reassigned_merge_request(*reassigned_merge_request_args) - todo_service.reassigned_merge_request(merge_request, current_user) + if merge_request.assignees != old_assignees + create_assignee_note(merge_request, old_assignees) + notification_service.async.reassigned_merge_request(merge_request, current_user, old_assignees) + todo_service.reassigned_issuable(merge_request, current_user, old_assignees) end if merge_request.previous_changes.include?('target_branch') || @@ -81,7 +76,6 @@ module MergeRequests ) end end - # rubocop:enable Metrics/AbcSize def handle_task_changes(merge_request) todo_service.mark_pending_todos_as_done(merge_request, current_user) diff --git a/app/services/note_summary.rb b/app/services/note_summary.rb index 81f6f92f75c..60a68568833 100644 --- a/app/services/note_summary.rb +++ b/app/services/note_summary.rb @@ -5,7 +5,9 @@ class NoteSummary attr_reader :metadata def initialize(noteable, project, author, body, action: nil, commit_count: nil) - @note = { noteable: noteable, project: project, author: author, note: body } + @note = { noteable: noteable, + created_at: noteable.system_note_timestamp, + project: project, author: author, note: body } @metadata = { action: action, commit_count: commit_count }.compact set_commit_params if note[:noteable].is_a?(Commit) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 56f11b31110..760962346fb 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -247,15 +247,15 @@ module NotificationRecipientService attr_reader :target attr_reader :current_user attr_reader :action - attr_reader :previous_assignee + attr_reader :previous_assignees attr_reader :skip_current_user - def initialize(target, current_user, action:, custom_action: nil, previous_assignee: nil, skip_current_user: true) + def initialize(target, current_user, action:, custom_action: nil, previous_assignees: nil, skip_current_user: true) @target = target @current_user = current_user @action = action @custom_action = custom_action - @previous_assignee = previous_assignee + @previous_assignees = previous_assignees @skip_current_user = skip_current_user end @@ -270,11 +270,7 @@ module NotificationRecipientService # Re-assign is considered as a mention of the new assignee case custom_action - when :reassign_merge_request - add_recipients(previous_assignee, :mention, nil) - add_recipients(target.assignee, :mention, NotificationReason::ASSIGNED) - when :reassign_issue - previous_assignees = Array(previous_assignee) + when :reassign_merge_request, :reassign_issue add_recipients(previous_assignees, :mention, nil) add_recipients(target.assignees, :mention, NotificationReason::ASSIGNED) end @@ -287,17 +283,11 @@ module NotificationRecipientService # receive them, too. add_mentions(current_user, target: target) - # Add the assigned users, if any - assignees = case custom_action - when :new_issue - target.assignees - else - target.assignee - end - # We use the `:participating` notification level in order to match existing legacy behavior as captured # in existing specs (notification_service_spec.rb ~ line 507) - add_recipients(assignees, :participating, NotificationReason::ASSIGNED) if assignees + if target.is_a?(Issuable) + add_recipients(target.assignees, :participating, NotificationReason::ASSIGNED) + end add_labels_subscribers end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 1a65561dd70..8d3b569498f 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -95,8 +95,8 @@ class NotificationService # When we reassign an issue we should send an email to: # - # * issue old assignee if their notification level is not Disabled - # * issue new assignee if their notification level is not Disabled + # * issue old assignees if their notification level is not Disabled + # * issue new assignees if their notification level is not Disabled # * users with custom level checked with "reassign issue" # def reassigned_issue(issue, current_user, previous_assignees = []) @@ -104,7 +104,7 @@ class NotificationService issue, current_user, action: "reassign", - previous_assignee: previous_assignees + previous_assignees: previous_assignees ) previous_assignee_ids = previous_assignees.map(&:id) @@ -140,7 +140,7 @@ class NotificationService # When create a merge request we should send an email to: # # * mr author - # * mr assignee if their notification level is not Disabled + # * mr assignees if their notification level is not Disabled # * project team members with notification level higher then Participating # * watchers of the mr's labels # * users with custom level checked with "new merge request" @@ -184,23 +184,25 @@ class NotificationService # When we reassign a merge_request we should send an email to: # - # * merge_request old assignee if their notification level is not Disabled - # * merge_request assignee if their notification level is not Disabled + # * merge_request old assignees if their notification level is not Disabled + # * merge_request new assignees if their notification level is not Disabled # * users with custom level checked with "reassign merge request" # - def reassigned_merge_request(merge_request, current_user, previous_assignee = nil) + def reassigned_merge_request(merge_request, current_user, previous_assignees = []) recipients = NotificationRecipientService.build_recipients( merge_request, current_user, action: "reassign", - previous_assignee: previous_assignee + previous_assignees: previous_assignees ) + previous_assignee_ids = previous_assignees.map(&:id) + recipients.each do |recipient| mailer.reassigned_merge_request_email( recipient.user.id, merge_request.id, - previous_assignee&.id, + previous_assignee_ids, current_user.id, recipient.reason ).deliver_later diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index d03137b63b2..3723c5ef7d7 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -2,6 +2,8 @@ module Projects class CreateService < BaseService + include ValidatesClassificationLabel + def initialize(user, params) @current_user, @params = user, params.dup @skip_wiki = @params.delete(:skip_wiki) @@ -45,6 +47,8 @@ module Projects relations_block&.call(@project) yield(@project) if block_given? + validate_classification_label(@project, :external_authorization_classification_label) + # If the block added errors, don't try to save the project return @project if @project.errors.any? diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 6856009b395..bc36bb8659d 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -3,6 +3,7 @@ module Projects class UpdateService < BaseService include UpdateVisibilityLevel + include ValidatesClassificationLabel ValidationError = Class.new(StandardError) @@ -14,6 +15,8 @@ module Projects yield if block_given? + validate_classification_label(project, :external_authorization_classification_label) + # If the block added errors, don't try to save the project return update_failed! if project.errors.any? diff --git a/app/services/projects/update_statistics_service.rb b/app/services/projects/update_statistics_service.rb new file mode 100644 index 00000000000..f32a779fab0 --- /dev/null +++ b/app/services/projects/update_statistics_service.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Projects + class UpdateStatisticsService < BaseService + def execute + return unless project && project.repository.exists? + + Rails.logger.info("Updating statistics for project #{project.id}") + + project.statistics.refresh!(only: statistics.map(&:to_sym)) + end + + private + + def statistics + params[:statistics] + end + end +end diff --git a/app/services/releases/concerns.rb b/app/services/releases/concerns.rb index a04bb8f9e14..ff6b696ca96 100644 --- a/app/services/releases/concerns.rb +++ b/app/services/releases/concerns.rb @@ -15,7 +15,7 @@ module Releases end def name - params[:name] + params[:name] || tag_name end def description diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb index c6e143d440d..a271a7e5e49 100644 --- a/app/services/releases/create_service.rb +++ b/app/services/releases/create_service.rb @@ -15,6 +15,10 @@ module Releases create_release(tag) end + def find_or_build_release + release || build_release(existing_tag) + end + private def ensure_tag @@ -38,7 +42,17 @@ module Releases end def create_release(tag) - release = project.releases.create!( + release = build_release(tag) + + release.save! + + success(tag: tag, release: release) + rescue => e + error(e.message, 400) + end + + def build_release(tag) + project.releases.build( name: name, description: description, author: current_user, @@ -46,10 +60,6 @@ module Releases sha: tag.dereferenced_target.sha, links_attributes: params.dig(:assets, 'links') || [] ) - - success(tag: tag, release: release) - rescue => e - error(e.message, 400) end end end diff --git a/app/services/resource_events/change_labels_service.rb b/app/services/resource_events/change_labels_service.rb index 039d6e2ebad..b45e567079b 100644 --- a/app/services/resource_events/change_labels_service.rb +++ b/app/services/resource_events/change_labels_service.rb @@ -12,7 +12,7 @@ module ResourceEvents label_hash = { resource_column(resource) => resource.id, user_id: user.id, - created_at: Time.now + created_at: resource.system_note_timestamp } labels = added_labels.map do |label| diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index ea8ac7e4656..a39ff76b798 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -69,7 +69,7 @@ module SystemNoteService # Called when the assignees of an Issue is changed or removed # - # issue - Issue object + # issuable - Issuable object (responds to assignees) # project - Project owning noteable # author - User performing the change # assignees - Users being assigned, or nil @@ -85,9 +85,9 @@ module SystemNoteService # "assigned to @user1 and @user2" # # Returns the created Note object - def change_issue_assignees(issue, project, author, old_assignees) - unassigned_users = old_assignees - issue.assignees - added_users = issue.assignees.to_a - old_assignees + def change_issuable_assignees(issuable, project, author, old_assignees) + unassigned_users = old_assignees - issuable.assignees + added_users = issuable.assignees.to_a - old_assignees text_parts = [] text_parts << "assigned to #{added_users.map(&:to_reference).to_sentence}" if added_users.any? @@ -95,7 +95,7 @@ module SystemNoteService body = text_parts.join(' and ') - create_note(NoteSummary.new(issue, project, author, body, action: 'assignee')) + create_note(NoteSummary.new(issuable, project, author, body, action: 'assignee')) end # Called when the milestone of a Noteable is changed @@ -258,7 +258,7 @@ module SystemNoteService body = "created #{issue.to_reference} to continue this discussion" note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body) - note = Note.create(note_attributes.merge(system: true)) + note = Note.create(note_attributes.merge(system: true, created_at: issue.system_note_timestamp)) note.system_note_metadata = SystemNoteMetadata.new(action: 'discussion') note diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index f357dc37fe7..0ea230a44a1 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -49,12 +49,12 @@ class TodoService todo_users.each(&:update_todos_count_cache) end - # When we reassign an issue we should: + # When we reassign an issuable we should: # - # * create a pending todo for new assignee if issue is assigned + # * create a pending todo for new assignee if issuable is assigned # - def reassigned_issue(issue, current_user, old_assignees = []) - create_assignment_todo(issue, current_user, old_assignees) + def reassigned_issuable(issuable, current_user, old_assignees = []) + create_assignment_todo(issuable, current_user, old_assignees) end # When create a merge request we should: @@ -82,14 +82,6 @@ class TodoService mark_pending_todos_as_done(merge_request, current_user) end - # When we reassign a merge request we should: - # - # * creates a pending todo for new assignee if merge request is assigned - # - def reassigned_merge_request(merge_request, current_user) - create_assignment_todo(merge_request, current_user) - end - # When merge a merge request we should: # # * mark all pending todos related to the target for the current user as done diff --git a/app/services/verify_pages_domain_service.rb b/app/services/verify_pages_domain_service.rb index 07f7391f877..b53c3145caf 100644 --- a/app/services/verify_pages_domain_service.rb +++ b/app/services/verify_pages_domain_service.rb @@ -8,6 +8,7 @@ class VerifyPagesDomainService < BaseService # How long verification lasts for VERIFICATION_PERIOD = 7.days + REMOVAL_DELAY = 1.week.freeze attr_reader :domain @@ -36,7 +37,7 @@ class VerifyPagesDomainService < BaseService # Prevent any pre-existing grace period from being truncated reverify = [domain.enabled_until, VERIFICATION_PERIOD.from_now].compact.max - domain.assign_attributes(verified_at: Time.now, enabled_until: reverify) + domain.assign_attributes(verified_at: Time.now, enabled_until: reverify, remove_at: nil) domain.save!(validate: false) if was_disabled @@ -49,18 +50,20 @@ class VerifyPagesDomainService < BaseService end def unverify_domain! - if domain.verified? - domain.assign_attributes(verified_at: nil) - domain.save!(validate: false) + was_verified = domain.verified? - notify(:verification_failed) - end + domain.assign_attributes(verified_at: nil) + domain.remove_at ||= REMOVAL_DELAY.from_now unless domain.enabled? + domain.save!(validate: false) + + notify(:verification_failed) if was_verified error("Couldn't verify #{domain.domain}") end def disable_domain! domain.assign_attributes(verified_at: nil, enabled_until: nil) + domain.remove_at ||= REMOVAL_DELAY.from_now domain.save!(validate: false) notify(:disabled) diff --git a/app/validators/x509_certificate_credentials_validator.rb b/app/validators/x509_certificate_credentials_validator.rb new file mode 100644 index 00000000000..d2f18e956c3 --- /dev/null +++ b/app/validators/x509_certificate_credentials_validator.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +# X509CertificateCredentialsValidator +# +# Custom validator to check if certificate-attribute was signed using the +# private key stored in an attrebute. +# +# This can be used as an `ActiveModel::Validator` as follows: +# +# validates_with X509CertificateCredentialsValidator, +# certificate: :client_certificate, +# pkey: :decrypted_private_key, +# pass: :decrypted_passphrase +# +# +# Required attributes: +# - certificate: The name of the accessor that returns the certificate to check +# - pkey: The name of the accessor that returns the private key +# Optional: +# - pass: The name of the accessor that returns the passphrase to decrypt the +# private key +class X509CertificateCredentialsValidator < ActiveModel::Validator + def initialize(*args) + super + + # We can't validate if we don't have a private key or certificate attributes + # in which case this validator is useless. + if options[:pkey].nil? || options[:certificate].nil? + raise 'Provide at least `certificate` and `pkey` attribute names' + end + end + + def validate(record) + unless certificate = read_certificate(record) + record.errors.add(options[:certificate], _('is not a valid X509 certificate.')) + end + + unless private_key = read_private_key(record) + record.errors.add(options[:pkey], _('could not read private key, is the passphrase correct?')) + end + + return if private_key.nil? || certificate.nil? + + unless certificate.public_key.fingerprint == private_key.public_key.fingerprint + record.errors.add(options[:pkey], _('private key does not match certificate.')) + end + end + + private + + def read_private_key(record) + OpenSSL::PKey.read(pkey(record).to_s, pass(record).to_s) + rescue OpenSSL::PKey::PKeyError, ArgumentError + # When the primary key could not be read, an ArgumentError is raised. + # This hapens when the passed key is not valid or the passphrase is incorrect + nil + end + + def read_certificate(record) + OpenSSL::X509::Certificate.new(certificate(record).to_s) + rescue OpenSSL::X509::CertificateError + nil + end + + # rubocop:disable GitlabSecurity/PublicSend + # + # Allowing `#public_send` here because we don't want the validator to really + # care about the names of the attributes or where they come from. + # + # The credentials are mostly stored encrypted so we need to go through the + # accessors to get the values, `read_attribute` bypasses those. + def certificate(record) + record.public_send(options[:certificate]) + end + + def pkey(record) + record.public_send(options[:pkey]) + end + + def pass(record) + return unless options[:pass] + + record.public_send(options[:pass]) + end + # rubocop:enable GitlabSecurity/PublicSend +end diff --git a/app/views/admin/application_settings/_external_authorization_service_form.html.haml b/app/views/admin/application_settings/_external_authorization_service_form.html.haml new file mode 100644 index 00000000000..01f6c7afe61 --- /dev/null +++ b/app/views/admin/application_settings/_external_authorization_service_form.html.haml @@ -0,0 +1,51 @@ +%section.settings.as-external-auth.no-animate#js-external-auth-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('External authentication') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = _('External Classification Policy Authorization') + .settings-content + + = form_for @application_setting, url: admin_application_settings_path(anchor: 'js-external-auth-settings'), html: { class: 'fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + .form-check + = f.check_box :external_authorization_service_enabled, class: 'form-check-input' + = f.label :external_authorization_service_enabled, class: 'form-check-label' do + = _('Enable classification control using an external service') + %span.form-text.text-muted + = external_authorization_description + = link_to icon('question-circle'), help_page_path('user/admin_area/settings/external_authorization') + .form-group + = f.label :external_authorization_service_url, _('Service URL'), class: 'label-bold' + = f.text_field :external_authorization_service_url, class: 'form-control' + %span.form-text.text-muted + = external_authorization_url_help_text + .form-group + = f.label :external_authorization_service_timeout, _('External authorization request timeout'), class: 'label-bold' + = f.number_field :external_authorization_service_timeout, class: 'form-control', min: 0.001, max: 10, step: 0.001 + %span.form-text.text-muted + = external_authorization_timeout_help_text + = f.label :external_auth_client_cert, _('Client authentication certificate'), class: 'label-bold' + = f.text_area :external_auth_client_cert, class: 'form-control' + %span.form-text.text-muted + = external_authorization_client_certificate_help_text + .form-group + = f.label :external_auth_client_key, _('Client authentication key'), class: 'label-bold' + = f.text_area :external_auth_client_key, class: 'form-control' + %span.form-text.text-muted + = external_authorization_client_key_help_text + .form-group + = f.label :external_auth_client_key_pass, _('Client authentication key password'), class: 'label-bold' + = f.password_field :external_auth_client_key_pass, class: 'form-control' + %span.form-text.text-muted + = external_authorization_client_pass_help_text + .form-group + = f.label :external_authorization_service_default_label, _('Default classification label'), class: 'label-bold' + = f.text_field :external_authorization_service_default_label, class: 'form-control' + + = 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 fc9dd29b8ca..31f18ba0d56 100644 --- a/app/views/admin/application_settings/show.html.haml +++ b/app/views/admin/application_settings/show.html.haml @@ -68,7 +68,7 @@ .settings-content = render 'terms' -= render_if_exists 'admin/application_settings/external_authorization_service_form', expanded: expanded_by_default? += render 'admin/application_settings/external_authorization_service_form', expanded: expanded_by_default? %section.settings.as-terminal.no-animate#js-terminal-settings{ class: ('expanded' if expanded_by_default?) } .settings-header diff --git a/app/views/admin/users/_user_detail.html.haml b/app/views/admin/users/_user_detail.html.haml index 3319b4bad3a..13d10dcd625 100644 --- a/app/views/admin/users/_user_detail.html.haml +++ b/app/views/admin/users/_user_detail.html.haml @@ -6,7 +6,7 @@ = image_tag avatar_icon_for_user(user), class: 'avatar s16 d-xs-flex d-md-none mr-1 prepend-top-2', alt: _('Avatar for %{name}') % { name: sanitize_name(user.name) } = link_to user.name, admin_user_path(user), class: 'text-plain js-user-link', data: { user_id: user.id } - = render_if_exists 'admin/users/user_detail_note', user: user + = render_if_exists 'admin/users/user_listing_note', user: user - user_badges_in_admin_section(user).each do |badge| - css_badge = "badge badge-#{badge[:variant]}" if badge[:variant].present? diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 4ffa8d89504..c4178296e67 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -53,6 +53,8 @@ - else Disabled + = render_if_exists 'admin/namespace_plan_info', namespace: @user.namespace + %li %span.light External User: %strong @@ -134,6 +136,8 @@ %strong = link_to @user.created_by.name, [:admin, @user.created_by] + = render_if_exists partial: "namespaces/shared_runner_status", locals: { namespace: @user.namespace } + .col-md-6 - unless @user == current_user - unless @user.confirmed? @@ -146,6 +150,9 @@ %p This user has an unconfirmed email address#{email}. You may force a confirmation. %br = link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } + + = render_if_exists 'admin/users/user_detail_note' + - if @user.blocked? .card.border-info .card-header.bg-info.text-white diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml index 3b6fc85e70e..369b0f7e62c 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, container: 'body' } do + = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item d-flex', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do %span{ class: klass }= sprite_icon(status.icon) %span.ci-build-text.text-truncate.mw-70p.gl-pl-1= subject.name - else - .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } } + .menu-item.mini-pipeline-graph-dropdown-item.d-flex{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } } %span{ class: klass }= sprite_icon(status.icon) %span.ci-build-text.text-truncate.mw-70p.gl-pl-1= subject.name diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml index 62b947ca40d..e38a16e7a1a 100644 --- a/app/views/clusters/clusters/show.html.haml +++ b/app/views/clusters/clusters/show.html.haml @@ -34,7 +34,7 @@ = render 'banner' = render 'form' - = render_if_exists 'health' + = render_if_exists 'projects/clusters/prometheus_graphs' if show_cluster_health_graphs?(@cluster) .cluster-applications-table#js-cluster-applications diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index f40fdb0b86b..916f98a62d1 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -24,8 +24,9 @@ Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises. %br Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}. - %p= link_to 'Check the current instance configuration ', help_instance_configuration_url - %hr + +%p= link_to 'Check the current instance configuration ', help_instance_configuration_url +%hr .row.prepend-top-default .col-md-8 diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 1b2a4cd6780..26a1f1e119c 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -7,6 +7,7 @@ .alert-wrapper = render "layouts/broadcast" = render "layouts/header/read_only_banner" + = render "layouts/nav/classification_level_banner" = yield :flash_message = render "shared/ping_consent" - unless @hide_breadcrumbs diff --git a/app/views/layouts/nav/_classification_level_banner.html.haml b/app/views/layouts/nav/_classification_level_banner.html.haml new file mode 100644 index 00000000000..cc4caf079b8 --- /dev/null +++ b/app/views/layouts/nav/_classification_level_banner.html.haml @@ -0,0 +1,5 @@ +- if ::Gitlab::ExternalAuthorization.enabled? && @project + = content_for :header_content do + %span.badge.color-label.classification-label.has-tooltip{ title: s_('ExternalAuthorizationService|Classification label') } + = sprite_icon('lock-open', size: 8, css_class: 'inline') + = @project.external_authorization_classification_label diff --git a/app/views/notify/_reassigned_issuable_email.html.haml b/app/views/notify/_reassigned_issuable_email.html.haml new file mode 100644 index 00000000000..4ab40ff2659 --- /dev/null +++ b/app/views/notify/_reassigned_issuable_email.html.haml @@ -0,0 +1,10 @@ +%p + Assignee changed + - if previous_assignees.any? + from + %strong= sanitize_name(previous_assignees.map(&:name).to_sentence) + to + - if issuable.assignees.any? + %strong= sanitize_name(issuable.assignee_list) + - else + %strong Unassigned diff --git a/app/views/notify/_removal_notification.html.haml b/app/views/notify/_removal_notification.html.haml new file mode 100644 index 00000000000..590e0d569aa --- /dev/null +++ b/app/views/notify/_removal_notification.html.haml @@ -0,0 +1,9 @@ +- if @domain.remove_at + %p + Unless you verify your domain by + %strong= @domain.remove_at.strftime('%F %T,') + it will be removed from your GitLab project. +- else + %p + If you no longer wish to use this domain with GitLab Pages, please remove it + from your GitLab project and delete any related DNS records. diff --git a/app/views/notify/closed_merge_request_email.text.haml b/app/views/notify/closed_merge_request_email.text.haml index 1094d584a1c..6e84f9fb355 100644 --- a/app/views/notify/closed_merge_request_email.text.haml +++ b/app/views/notify/closed_merge_request_email.text.haml @@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m = merge_path_description(@merge_request, 'to') Author: #{sanitize_name(@merge_request.author_name)} -Assignee: #{sanitize_name(@merge_request.assignee_name)} += assignees_label(@merge_request) diff --git a/app/views/notify/issue_due_email.html.haml b/app/views/notify/issue_due_email.html.haml index e81144b8fcb..08bc98ca05c 100644 --- a/app/views/notify/issue_due_email.html.haml +++ b/app/views/notify/issue_due_email.html.haml @@ -3,7 +3,7 @@ - if @issue.assignees.any? %p - Assignee: #{@issue.assignee_list} + = assignees_label(@issue) %p This issue is due on: #{@issue.due_date.to_s(:medium)} diff --git a/app/views/notify/issue_due_email.text.erb b/app/views/notify/issue_due_email.text.erb index 3c7a57a8a2e..ae50b703fe3 100644 --- a/app/views/notify/issue_due_email.text.erb +++ b/app/views/notify/issue_due_email.text.erb @@ -2,6 +2,6 @@ The following issue is due on <%= @issue.due_date %>: Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %> Author: <%= @issue.author_name %> -Assignee: <%= @issue.assignee_list %> +<%= assignees_label(@issue) %> <%= @issue.description %> diff --git a/app/views/notify/merge_request_status_email.text.haml b/app/views/notify/merge_request_status_email.text.haml index b9b9e0c3ad7..e3b24bbd405 100644 --- a/app/views/notify/merge_request_status_email.text.haml +++ b/app/views/notify/merge_request_status_email.text.haml @@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m = merge_path_description(@merge_request, 'to') Author: #{sanitize_name(@merge_request.author_name)} -Assignee: #{sanitize_name(@merge_request.assignee_name)} += assignees_label(@merge_request) diff --git a/app/views/notify/merge_request_unmergeable_email.text.haml b/app/views/notify/merge_request_unmergeable_email.text.haml index 0c7bf1bb044..e9708a297d7 100644 --- a/app/views/notify/merge_request_unmergeable_email.text.haml +++ b/app/views/notify/merge_request_unmergeable_email.text.haml @@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m = merge_path_description(@merge_request, 'to') Author: #{sanitize_name(@merge_request.author_name)} -Assignee: #{sanitize_name(@merge_request.assignee_name)} += assignees_label(@merge_request) diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml index 045a43cbc84..d623e701a30 100644 --- a/app/views/notify/merged_merge_request_email.text.haml +++ b/app/views/notify/merged_merge_request_email.text.haml @@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m = merge_path_description(@merge_request, 'to') Author: #{sanitize_name(@merge_request.author_name)} -Assignee: #{sanitize_name(@merge_request.assignee_name)} += assignees_label(@merge_request) diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml index e6cdaf85c0d..8aa7939dd0b 100644 --- a/app/views/notify/new_issue_email.html.haml +++ b/app/views/notify/new_issue_email.html.haml @@ -4,7 +4,7 @@ - if @issue.assignees.any? %p - Assignee: #{@issue.assignee_list} + = assignees_label(@issue) - if @issue.description %div diff --git a/app/views/notify/new_issue_email.text.erb b/app/views/notify/new_issue_email.text.erb index 58a2bcbe5eb..ff258711b48 100644 --- a/app/views/notify/new_issue_email.text.erb +++ b/app/views/notify/new_issue_email.text.erb @@ -2,6 +2,6 @@ New Issue was created. Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %> Author: <%= sanitize_name(@issue.author_name) %> -Assignee: <%= @issue.assignee_list %> +<%= assignees_label(@issue) %> <%= @issue.description %> diff --git a/app/views/notify/new_mention_in_issue_email.text.erb b/app/views/notify/new_mention_in_issue_email.text.erb index 173091e4a80..8e95063b40f 100644 --- a/app/views/notify/new_mention_in_issue_email.text.erb +++ b/app/views/notify/new_mention_in_issue_email.text.erb @@ -2,6 +2,6 @@ You have been mentioned in an issue. Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %> Author: <%= sanitize_name(@issue.author_name) %> -Assignee: <%= sanitize_name(@issue.assignee_list) %> +<%= assignees_label(@issue) %> <%= @issue.description %> diff --git a/app/views/notify/new_mention_in_merge_request_email.text.erb b/app/views/notify/new_mention_in_merge_request_email.text.erb index 96a4f3f9eac..3c78e257a88 100644 --- a/app/views/notify/new_mention_in_merge_request_email.text.erb +++ b/app/views/notify/new_mention_in_merge_request_email.text.erb @@ -4,6 +4,6 @@ You have been mentioned in Merge Request <%= @merge_request.to_reference %> <%= merge_path_description(@merge_request, 'to') %> Author: <%= sanitize_name(@merge_request.author_name) %> -Assignee: <%= sanitize_name(@merge_request.assignee_name) %> += assignees_label(@merge_request) <%= @merge_request.description %> diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml index db23447dd39..77d2e65d285 100644 --- a/app/views/notify/new_merge_request_email.html.haml +++ b/app/views/notify/new_merge_request_email.html.haml @@ -5,9 +5,9 @@ %p.details != merge_path_description(@merge_request, '→') -- if @merge_request.assignee_id.present? +- if @merge_request.assignees.any? %p - Assignee: #{sanitize_name(@merge_request.assignee_name)} + = assignees_label(@merge_request) = render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb index 754f4bca1cd..e6c42f1cf5f 100644 --- a/app/views/notify/new_merge_request_email.text.erb +++ b/app/views/notify/new_merge_request_email.text.erb @@ -4,7 +4,7 @@ New Merge Request <%= @merge_request.to_reference %> <%= merge_path_description(@merge_request, 'to') %> Author: <%= @merge_request.author_name %> -Assignee: <%= @merge_request.assignee_name %> +<%= assignees_label(@merge_request) %> <%= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter %> <%= @merge_request.description %> diff --git a/app/views/notify/pages_domain_disabled_email.html.haml b/app/views/notify/pages_domain_disabled_email.html.haml index 34ce4238a12..224b79bfde8 100644 --- a/app/views/notify/pages_domain_disabled_email.html.haml +++ b/app/views/notify/pages_domain_disabled_email.html.haml @@ -10,6 +10,4 @@ If this domain has been disabled in error, please follow = link_to 'these instructions', help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') to verify and re-enable your domain. -%p - If you no longer wish to use this domain with GitLab Pages, please remove it - from your GitLab project and delete any related DNS records. += render 'removal_notification' diff --git a/app/views/notify/pages_domain_verification_failed_email.html.haml b/app/views/notify/pages_domain_verification_failed_email.html.haml index 0bb0eb09fd5..03b298f8e7c 100644 --- a/app/views/notify/pages_domain_verification_failed_email.html.haml +++ b/app/views/notify/pages_domain_verification_failed_email.html.haml @@ -12,6 +12,4 @@ Please visit = link_to 'these instructions', help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') for more information about custom domain verification. -%p - If you no longer wish to use this domain with GitLab Pages, please remove it - from your GitLab project and delete any related DNS records. += render 'removal_notification' diff --git a/app/views/notify/reassigned_issue_email.html.haml b/app/views/notify/reassigned_issue_email.html.haml index 6d25488a7e2..6b088927623 100644 --- a/app/views/notify/reassigned_issue_email.html.haml +++ b/app/views/notify/reassigned_issue_email.html.haml @@ -1,10 +1 @@ -%p - Assignee changed - - if @previous_assignees.any? - from - %strong= sanitize_name(@previous_assignees.map(&:name).to_sentence) - to - - if @issue.assignees.any? - %strong= @issue.assignee_list - - else - %strong Unassigned += render 'reassigned_issuable_email', issuable: @issue, previous_assignees: @previous_assignees diff --git a/app/views/notify/reassigned_merge_request_email.html.haml b/app/views/notify/reassigned_merge_request_email.html.haml index e4f19bc3200..0aefca6b14a 100644 --- a/app/views/notify/reassigned_merge_request_email.html.haml +++ b/app/views/notify/reassigned_merge_request_email.html.haml @@ -1,10 +1 @@ -%p - Assignee changed - - if @previous_assignee - from - %strong= sanitize_name(@previous_assignee.name) - to - - if @merge_request.assignee_id - %strong= sanitize_name(@merge_request.assignee_name) - - else - %strong Unassigned += render 'reassigned_issuable_email', issuable: @merge_request, previous_assignees: @previous_assignees diff --git a/app/views/notify/reassigned_merge_request_email.text.erb b/app/views/notify/reassigned_merge_request_email.text.erb index 96c770b5219..82ec7aa0fa4 100644 --- a/app/views/notify/reassigned_merge_request_email.text.erb +++ b/app/views/notify/reassigned_merge_request_email.text.erb @@ -2,5 +2,5 @@ Reassigned Merge Request <%= @merge_request.iid %> <%= url_for([@merge_request.project.namespace.becomes(Namespace), @merge_request.project, @merge_request, { only_path: false }]) %> -Assignee changed <%= "from #{sanitize_name(@previous_assignee.name)}" if @previous_assignee -%> - to <%= "#{@merge_request.assignee_id ? sanitize_name(@merge_request.assignee_name) : 'Unassigned'}" %> +Assignee changed <%= "from #{sanitize_name(@previous_assignees.map(&:name).to_sentence)}" if @previous_assignees.any? -%> + to <%= "#{@merge_request.assignees.any? ? @merge_request.assignee_list : 'Unassigned'}" %> diff --git a/app/views/projects/_classification_policy_settings.html.haml b/app/views/projects/_classification_policy_settings.html.haml new file mode 100644 index 00000000000..57c7a718d53 --- /dev/null +++ b/app/views/projects/_classification_policy_settings.html.haml @@ -0,0 +1,8 @@ +- if ::Gitlab::ExternalAuthorization.enabled? + .form-group + = f.label :external_authorization_classification_label, class: 'label-bold' do + = s_('ExternalAuthorizationService|Classification Label') + %span.light (optional) + = f.text_field :external_authorization_classification_label, class: "form-control" + %span.form-text.text-muted + = external_classification_label_help_message diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 9d069c025ba..6d48475d505 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -30,7 +30,7 @@ = custom_icon("icon_commit") - if commit_sha - = link_to job.short_sha, project_commit_path(job.project, job.sha), class: "commit-sha" + = link_to job.short_sha, project_commit_path(job.project, job.sha), class: "commit-sha mr-0" - if job.stuck? = icon('warning', class: 'text-warning has-tooltip', title: _('Job is stuck. Check runners.')) diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml index 282566eeadc..9774b797928 100644 --- a/app/views/projects/deployments/_commit.html.haml +++ b/app/views/projects/deployments/_commit.html.haml @@ -6,7 +6,7 @@ = link_to deployment.ref, project_ref_path(@project, deployment.ref), class: "ref-name" .icon-container.commit-icon = custom_icon("icon_commit") - = link_to deployment.short_sha, project_commit_path(@project, deployment.sha), class: "commit-sha" + = link_to deployment.short_sha, project_commit_path(@project, deployment.sha), class: "commit-sha mr-0" %p.commit-title.flex-truncate-parent %span.flex-truncate-child diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index 85bc8ec07e3..a11e23b6daa 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -18,7 +18,7 @@ - if deployment.user %div by - = user_avatar(user: deployment.user, size: 20) + = user_avatar(user: deployment.user, size: 20, css_class: "mr-0 float-none") .table-section.section-15{ role: 'gridcell' } .table-mobile-header{ role: 'rowheader' }= _("Created") diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 98017bea0c9..abf2fb7dc57 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -32,7 +32,7 @@ %span.light (optional) = f.text_area :description, class: "form-control", rows: 3, maxlength: 250 - = render_if_exists 'projects/classification_policy_settings', f: f + = render 'projects/classification_policy_settings', f: f = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index ce7c7091c93..377b2a6d8d9 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -46,7 +46,7 @@ CLOSED - if issue.assignees.any? %li - = render 'shared/issuable/assignees', project: @project, issue: issue + = render 'shared/issuable/assignees', project: @project, issuable: issue = render 'shared/issuable_meta_data', issuable: issue diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index b8e0b66e277..47c8e3d73f5 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -53,9 +53,9 @@ %li.issuable-pipeline-broken.d-none.d-sm-inline-block = link_to merge_request_path(merge_request), class: "has-tooltip", title: _('Cannot be merged automatically') do = icon('exclamation-triangle') - - if merge_request.assignee + - if merge_request.assignees.any? %li - = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: _('Assigned to :name')) + = render 'shared/issuable/assignees', project: merge_request.project, issuable: merge_request = render_if_exists 'projects/merge_requests/approvals_count', merge_request: merge_request = render 'shared/issuable_meta_data', issuable: merge_request diff --git a/app/views/shared/boards/components/sidebar/_assignee.html.haml b/app/views/shared/boards/components/sidebar/_assignee.html.haml index 1374da9d82c..af6a519a967 100644 --- a/app/views/shared/boards/components/sidebar/_assignee.html.haml +++ b/app/views/shared/boards/components/sidebar/_assignee.html.haml @@ -19,7 +19,7 @@ ":data-name" => "assignee.name", ":data-username" => "assignee.username" } .dropdown - - dropdown_options = issue_assignees_dropdown_options + - dropdown_options = assignees_dropdown_options('issue') %button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: board_sidebar_user_data, ":data-issuable-id" => "issue.iid" } = dropdown_options[:title] diff --git a/app/views/shared/issuable/_assignees.html.haml b/app/views/shared/issuable/_assignees.html.haml index ef3d44a9241..24734ed66cf 100644 --- a/app/views/shared/issuable/_assignees.html.haml +++ b/app/views/shared/issuable/_assignees.html.haml @@ -1,9 +1,9 @@ - max_render = 4 -- assignees_rendering_overflow = issue.assignees.size > max_render +- assignees_rendering_overflow = issuable.assignees.size > max_render - render_count = assignees_rendering_overflow ? max_render - 1 : max_render -- more_assignees_count = issue.assignees.size - render_count +- more_assignees_count = issuable.assignees.size - render_count -- issue.assignees.take(render_count).each do |assignee| # rubocop: disable CodeReuse/ActiveRecord +- issuable.assignees.take(render_count).each do |assignee| # rubocop: disable CodeReuse/ActiveRecord = link_to_member(@project, assignee, name: false, title: "Assigned to :name") - if more_assignees_count.positive? diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml index 909eb738f95..a05a13814ac 100644 --- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml +++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml @@ -21,10 +21,7 @@ .title Assignee .filter-item - - if type == :issues - - field_name = "update[assignee_ids][]" - - else - - field_name = "update[assignee_id]" + - field_name = "update[assignee_ids][]" = dropdown_tag("Select assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: field_name } }) .block diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml index 1a59055f652..ab01094ed6e 100644 --- a/app/views/shared/issuable/_sidebar_assignees.html.haml +++ b/app/views/shared/issuable/_sidebar_assignees.html.haml @@ -1,42 +1,10 @@ - issuable_type = issuable_sidebar[:type] - signed_in = !!issuable_sidebar.dig(:current_user, :id) -- can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit) -- if issuable_type == "issue" - #js-vue-sidebar-assignees{ data: { field: "#{issuable_type}[assignee_ids]", signed_in: signed_in } } - .title.hide-collapsed - = _('Assignee') - = icon('spinner spin') -- else - - assignee = assignees.first - .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body", boundary: 'viewport' }, title: (issuable_sidebar.dig(:assignee, :name) || _('Assignee')) } - - if issuable_sidebar[:assignee] - = link_to_member(@project, assignee, size: 24) - - else - = icon('user', 'aria-hidden': 'true') +#js-vue-sidebar-assignees{ data: { field: "#{issuable_type}[assignee_ids]", signed_in: signed_in } } .title.hide-collapsed = _('Assignee') - = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - - if can_edit_issuable - = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right' - - if !signed_in - %a.gutter-toggle.float-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => _('Toggle sidebar') } - = sidebar_gutter_toggle_icon - .value.hide-collapsed - - if issuable_sidebar[:assignee] - = link_to_member(@project, assignee, size: 32, extra_class: 'bold') do - - unless issuable_sidebar[:assignee][:can_merge] - %span.float-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: _('Not allowed to merge') } - = icon('exclamation-triangle', 'aria-hidden': 'true') - %span.username - @#{issuable_sidebar[:assignee][:username]} - - else - %span.assign-yourself.no-value - = _('No assignee') - - if can_edit_issuable - \- - %a.js-assign-yourself{ href: '#' } - = _('assign yourself') + = icon('spinner spin') .selectbox.hide-collapsed - if assignees.none? @@ -59,17 +27,15 @@ ability_name: issuable_type, null_user: true, display: 'static' } } - - title = _('Select assignee') - - if issuable_type == "issue" - - dropdown_options = issue_assignees_dropdown_options - - title = dropdown_options[:title] - - options[:toggle_class] += ' js-multiselect js-save-user-data' - - data = { field_name: "#{issuable_type}[assignee_ids][]" } - - data[:multi_select] = true - - data['dropdown-title'] = title - - data['dropdown-header'] = dropdown_options[:data][:'dropdown-header'] - - data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select'] - - options[:data].merge!(data) + - dropdown_options = assignees_dropdown_options(issuable_type) + - title = dropdown_options[:title] + - options[:toggle_class] += ' js-multiselect js-save-user-data' + - data = { field_name: "#{issuable_type}[assignee_ids][]" } + - data[:multi_select] = true + - data['dropdown-title'] = title + - data['dropdown-header'] = dropdown_options[:data][:'dropdown-header'] + - data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select'] + - options[:data].merge!(data) = dropdown_tag(title, options: options) diff --git a/app/views/shared/issuable/form/_merge_request_assignee.html.haml b/app/views/shared/issuable/form/_merge_request_assignee.html.haml deleted file mode 100644 index 05c03dedd91..00000000000 --- a/app/views/shared/issuable/form/_merge_request_assignee.html.haml +++ /dev/null @@ -1,31 +0,0 @@ -- merge_request = issuable -.block.assignee - .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: sidebar_assignee_tooltip_label(issuable) } - - if merge_request.assignee - = link_to_member(@project, merge_request.assignee, size: 24) - - else - = icon('user', 'aria-hidden': 'true') - .title.hide-collapsed - Assignee - = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - - if can_edit_issuable - = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link float-right' - .value.hide-collapsed - - if merge_request.assignee - = link_to_member(@project, merge_request.assignee, size: 32, extra_class: 'bold') do - - unless merge_request.can_be_merged_by?(merge_request.assignee) - %span.float-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' } - = icon('exclamation-triangle', 'aria-hidden': 'true') - %span.username - = merge_request.assignee.to_reference - - else - %span.assign-yourself.no-value - No assignee - - if can_edit_issuable - \- - %a.js-assign-yourself{ href: '#' } - assign yourself - - .selectbox.hide-collapsed - = f.hidden_field 'assignee_id', value: merge_request.assignee_id, id: 'issue_assignee_id' - = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: @project&.id, author_id: merge_request.author_id, field_name: 'merge_request[assignee_id]', issue_update: issuable_json_path(merge_request), ability_name: 'merge_request', null_user: true } }) diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml index e370dff9526..1e03440a5dc 100644 --- a/app/views/shared/issuable/form/_metadata.html.haml +++ b/app/views/shared/issuable/form/_metadata.html.haml @@ -8,11 +8,8 @@ %hr .row %div{ class: (has_due_date ? "col-lg-6" : "col-12") } - .form-group.row.issue-assignee - - if issuable.is_a?(Issue) - = render "shared/issuable/form/metadata_issue_assignee", issuable: issuable, form: form, has_due_date: has_due_date - - else - = render "shared/issuable/form/metadata_merge_request_assignee", issuable: issuable, form: form, has_due_date: has_due_date + .form-group.row.merge-request-assignee + = render "shared/issuable/form/metadata_issuable_assignee", issuable: issuable, form: form, has_due_date: has_due_date .form-group.row.issue-milestone = form.label :milestone_id, "Milestone", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}" .col-sm-10{ class: ("col-md-8" if has_due_date) } diff --git a/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml b/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml index 6d4f9ccd66f..5336159e762 100644 --- a/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml +++ b/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml @@ -1,4 +1,4 @@ -= form.label :assignee_ids, "Assignee", class: "col-form-label #{"col-md-2 col-lg-4" if has_due_date}" += form.label :assignee_id, "Assignee", class: "col-form-label #{has_due_date ? "col-lg-4" : "col-sm-2"}" .col-sm-10{ class: ("col-md-8" if has_due_date) } .issuable-form-select-holder.selectbox - issuable.assignees.each do |assignee| @@ -7,5 +7,5 @@ - if issuable.assignees.length === 0 = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' } - = dropdown_tag(users_dropdown_label(issuable.assignees), options: issue_assignees_dropdown_options) - = link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}" + = dropdown_tag(users_dropdown_label(issuable.assignees), options: assignees_dropdown_options(issuable.to_ability_name)) + = link_to 'Assign to me', '#', class: "assign-to-me-link qa-assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}" diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 3e8c2a1209a..f9b2e698fc9 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -143,6 +143,7 @@ - repository_remove_remote - system_hook_push - update_merge_requests +- update_project_statistics - upload_checksum - web_hook - repository_update_remote_mirror diff --git a/app/workers/concerns/application_worker.rb b/app/workers/concerns/application_worker.rb index d64c2f82a09..25c3a945077 100644 --- a/app/workers/concerns/application_worker.rb +++ b/app/workers/concerns/application_worker.rb @@ -53,7 +53,7 @@ module ApplicationWorker schedule = now + delay.to_i if schedule <= now - raise ArgumentError, 'The schedule time must be in the future!' + raise ArgumentError, _('The schedule time must be in the future!') end Sidekiq::Client.push_bulk('class' => self, 'args' => args_list, 'at' => schedule) diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb index bf637f82df2..c4bcda2da16 100644 --- a/app/workers/email_receiver_worker.rb +++ b/app/workers/email_receiver_worker.rb @@ -24,22 +24,22 @@ class EmailReceiverWorker reason = case error when Gitlab::Email::UnknownIncomingEmail - "We couldn't figure out what the email is for. Please create your issue or comment through the web interface." + s_("EmailError|We couldn't figure out what the email is for. Please create your issue or comment through the web interface.") when Gitlab::Email::SentNotificationNotFoundError - "We couldn't figure out what the email is in reply to. Please create your comment through the web interface." + s_("EmailError|We couldn't figure out what the email is in reply to. Please create your comment through the web interface.") when Gitlab::Email::ProjectNotFound - "We couldn't find the project. Please check if there's any typo." + s_("EmailError|We couldn't find the project. Please check if there's any typo.") when Gitlab::Email::EmptyEmailError can_retry = true - "It appears that the email is blank. Make sure your reply is at the top of the email, we can't process inline replies." + s_("EmailError|It appears that the email is blank. Make sure your reply is at the top of the email, we can't process inline replies.") when Gitlab::Email::UserNotFoundError - "We couldn't figure out what user corresponds to the email. Please create your comment through the web interface." + s_("EmailError|We couldn't figure out what user corresponds to the email. Please create your comment through the web interface.") when Gitlab::Email::UserBlockedError - "Your account has been blocked. If you believe this is in error, contact a staff member." + s_("EmailError|Your account has been blocked. If you believe this is in error, contact a staff member.") when Gitlab::Email::UserNotAuthorizedError - "You are not allowed to perform this action. If you believe this is in error, contact a staff member." + s_("EmailError|You are not allowed to perform this action. If you believe this is in error, contact a staff member.") when Gitlab::Email::NoteableNotFoundError - "The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member." + s_("EmailError|The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member.") when Gitlab::Email::InvalidAttachment error.message when Gitlab::Email::InvalidRecordError diff --git a/app/workers/object_storage/migrate_uploads_worker.rb b/app/workers/object_storage/migrate_uploads_worker.rb index 206eb71b898..12400d4e025 100644 --- a/app/workers/object_storage/migrate_uploads_worker.rb +++ b/app/workers/object_storage/migrate_uploads_worker.rb @@ -20,7 +20,7 @@ module ObjectStorage end def to_s - success? ? "Migration successful." : "Error while migrating #{upload.id}: #{error.message}" + success? ? _("Migration successful.") : _("Error while migrating %{upload_id}: %{error_message}") % { upload_id: upload.id, error_message: error.message } end end @@ -47,7 +47,7 @@ module ObjectStorage end def header(success, failures) - "Migrated #{success.count}/#{success.count + failures.count} files." + _("Migrated %{success_count}/%{total_count} files.") % { success_count: success.count, total_count: success.count + failures.count } end def failures(failures) @@ -75,9 +75,9 @@ module ObjectStorage model_types = uploads.map(&:model_type).uniq model_has_mount = mounted_as.nil? || model_class.uploaders[mounted_as] == uploader_class - raise(SanityCheckError, "Multiple uploaders found: #{uploader_types}") unless uploader_types.count == 1 - raise(SanityCheckError, "Multiple model types found: #{model_types}") unless model_types.count == 1 - raise(SanityCheckError, "Mount point #{mounted_as} not found in #{model_class}.") unless model_has_mount + raise(SanityCheckError, _("Multiple uploaders found: %{uploader_types}") % { uploader_types: uploader_types }) unless uploader_types.count == 1 + raise(SanityCheckError, _("Multiple model types found: %{model_types}") % { model_types: model_types }) unless model_types.count == 1 + raise(SanityCheckError, _("Mount point %{mounted_as} not found in %{model_class}.") % { mounted_as: mounted_as, model_class: model_class }) unless model_has_mount end # rubocop: disable CodeReuse/ActiveRecord @@ -110,9 +110,9 @@ module ObjectStorage return if args.count == 4 case args.count - when 3 then raise SanityCheckError, "Job is missing the `model_type` argument." + when 3 then raise SanityCheckError, _("Job is missing the `model_type` argument.") else - raise SanityCheckError, "Job has wrong arguments format." + raise SanityCheckError, _("Job has wrong arguments format.") end end diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 396f44396a3..a5554f07699 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -3,7 +3,7 @@ class PostReceive include ApplicationWorker - def perform(gl_repository, identifier, changes, push_options = []) + def perform(gl_repository, identifier, changes, push_options = {}) project, repo_type = Gitlab::GlRepository.parse(gl_repository) if project.nil? diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index b31099bc670..b2e0701008a 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -18,7 +18,7 @@ class ProjectCacheWorker return unless project && project.repository.exists? - update_statistics(project, statistics.map(&:to_sym)) + update_statistics(project, statistics) project.repository.refresh_method_caches(files.map(&:to_sym)) @@ -26,20 +26,28 @@ class ProjectCacheWorker end # rubocop: enable CodeReuse/ActiveRecord + # NOTE: triggering both an immediate update and one in 15 minutes if we + # successfully obtain the lease. That way, we only need to wait for the + # statistics to become accurate if they were already updated once in the + # last 15 minutes. def update_statistics(project, statistics = []) return if Gitlab::Database.read_only? - return unless try_obtain_lease_for(project.id, :update_statistics) + return unless try_obtain_lease_for(project.id, statistics) - Rails.logger.info("Updating statistics for project #{project.id}") + Projects::UpdateStatisticsService.new(project, nil, statistics: statistics).execute - project.statistics.refresh!(only: statistics) + UpdateProjectStatisticsWorker.perform_in(LEASE_TIMEOUT, project.id, statistics) end private - def try_obtain_lease_for(project_id, section) + def try_obtain_lease_for(project_id, statistics) Gitlab::ExclusiveLease - .new("project_cache_worker:#{project_id}:#{section}", timeout: LEASE_TIMEOUT) + .new(project_cache_worker_key(project_id, statistics), timeout: LEASE_TIMEOUT) .try_obtain end + + def project_cache_worker_key(project_id, statistics) + ["project_cache_worker", project_id, *statistics.sort].join(":") + end end diff --git a/app/workers/update_project_statistics_worker.rb b/app/workers/update_project_statistics_worker.rb new file mode 100644 index 00000000000..9a29cc12707 --- /dev/null +++ b/app/workers/update_project_statistics_worker.rb @@ -0,0 +1,18 @@ + +# frozen_string_literal: true + +# Worker for updating project statistics. +class UpdateProjectStatisticsWorker + include ApplicationWorker + + # project_id - The ID of the project for which to flush the cache. + # statistics - An Array containing columns from ProjectStatistics to + # refresh, if empty all columns will be refreshed + # rubocop: disable CodeReuse/ActiveRecord + def perform(project_id, statistics = []) + project = Project.find_by(id: project_id) + + Projects::UpdateStatisticsService.new(project, nil, statistics: statistics).execute + end + # rubocop: enable CodeReuse/ActiveRecord +end |