diff options
author | Marcia Ramos <virtua.creative@gmail.com> | 2019-04-10 17:05:46 +0100 |
---|---|---|
committer | Marcia Ramos <virtua.creative@gmail.com> | 2019-04-10 17:05:46 +0100 |
commit | cbd6841cac8185f181a5dcec33704f6e7c040732 (patch) | |
tree | 423bbc4fb873ab51590d0be4ae594769c80b739b | |
parent | 3402f8c817e9798eed9d86555f3f85fd10f49abf (diff) | |
parent | 490b31f740d23b54a62588cd9fd0e0cf7fdd9370 (diff) | |
download | gitlab-ce-cbd6841cac8185f181a5dcec33704f6e7c040732.tar.gz |
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into docs-pages-introdocs-pages-intro
498 files changed, 9167 insertions, 2945 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 98fdda3593e..9febc15e84a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,7 +4,12 @@ include: - local: /lib/gitlab/ci/templates/Code-Quality.gitlab-ci.yml .dedicated-runner: &dedicated-runner - retry: 1 + retry: + max: 1 # This is confusing but this means "2 runs at max". + when: + - unknown_failure + - api_failure + - runner_system_failure tags: - gitlab-org @@ -76,6 +81,11 @@ stages: - postgres:9.6 - redis:alpine +.use-pg-10: &use-pg-10 + services: + - postgres:10.0 + - redis:alpine + .use-mysql: &use-mysql services: - mysql:5.7 @@ -97,6 +107,15 @@ stages: - /(^docs[\/-].*|.*-docs$)/ - /(^qa[\/-].*|.*-qa$)/ +.only-schedules-master: &only-schedules-master + only: + - schedules@gitlab-org/gitlab-ce + - schedules@gitlab-org/gitlab-ee + - master@gitlab-org/gitlab-ce + - master@gitlab-org/gitlab-ee + - master@gitlab/gitlabhq + - master@gitlab/gitlab-ee + # Jobs that only need to pull cache .dedicated-no-docs-pull-cache-job: &dedicated-no-docs-pull-cache-job <<: *dedicated-runner @@ -172,6 +191,11 @@ stages: <<: *rspec-metadata <<: *use-pg +.rspec-metadata-pg-10: &rspec-metadata-pg-10 + <<: *rspec-metadata + <<: *use-pg-10 + image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.18-chrome-71.0-node-10.x-yarn-1.12-postgresql-10-graphicsmagick-1.3.29" + .rspec-metadata-mysql: &rspec-metadata-mysql <<: *rspec-metadata <<: *use-mysql @@ -457,6 +481,8 @@ setup-test-env: - schedules@gitlab-org/gitlab-ce - schedules@gitlab-org/gitlab-ee kubernetes: active + variables: + - $REVIEW_APP_CLEANUP except: refs: - tags @@ -525,12 +551,18 @@ rspec-pg: <<: *rspec-metadata-pg parallel: 50 +rspec-pg-10: + <<: *rspec-metadata-pg-10 + <<: *only-schedules-master + parallel: 50 + rspec-mysql: <<: *rspec-metadata-mysql + <<: *only-schedules-master parallel: 50 .rspec-quarantine: &rspec-quarantine - retry: 0 + <<: *only-schedules-master script: - export CACHE_CLASSES=true - scripts/gitaly-test-spawn @@ -561,7 +593,7 @@ static-analysis: - tmp/rubocop_cache # Documentation checks: -# - Check validity of relative links +# - Check validity of relative links, and anchors # - Make sure cURL examples in API docs use the full switches docs lint: <<: *dedicated-runner @@ -580,6 +612,8 @@ docs lint: - bundle exec nanoc # Check the internal links - bundle exec nanoc check internal_links + # Check the internal anchor links + - bundle exec nanoc check internal_anchors downtime_check: <<: *rake-exec @@ -1141,4 +1175,3 @@ schedule:review-performance: <<: *review-schedules-only script: - wait_for_job_to_be_done "schedule:review-deploy" - diff --git a/CHANGELOG.md b/CHANGELOG.md index c99e487d04b..4e91b9f29ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 11.9.7 (2019-04-09) + +- No changes. + ## 11.9.6 (2019-04-04) ### Fixed (3 changes) @@ -602,6 +606,10 @@ entry. - Creates mixin to reduce code duplication between CE and EE in graph component. +## 11.7.11 (2019-04-09) + +- No changes. + ## 11.7.10 (2019-03-28) ### Security (7 changes) diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 6d2890793d4..f9c71a52e2f 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -8.5.0 +8.5.1 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 diff --git a/changelogs/unreleased/30157-api-expose-single-environment.yml b/changelogs/unreleased/30157-api-expose-single-environment.yml new file mode 100644 index 00000000000..f9619dbcc7d --- /dev/null +++ b/changelogs/unreleased/30157-api-expose-single-environment.yml @@ -0,0 +1,5 @@ +--- +title: 'Add new API endpoint to expose a single environment.' +merge_request: 26887 +author: +type: added diff --git a/changelogs/unreleased/43263-git-push-option-to-create-mr.yml b/changelogs/unreleased/43263-git-push-option-to-create-mr.yml new file mode 100644 index 00000000000..d50c33da162 --- /dev/null +++ b/changelogs/unreleased/43263-git-push-option-to-create-mr.yml @@ -0,0 +1,5 @@ +--- +title: Allow merge requests to be created via git push options +merge_request: 26752 +author: +type: added diff --git a/changelogs/unreleased/47327-fix-github-import-visibility.yml b/changelogs/unreleased/47327-fix-github-import-visibility.yml new file mode 100644 index 00000000000..b02318f0e89 --- /dev/null +++ b/changelogs/unreleased/47327-fix-github-import-visibility.yml @@ -0,0 +1,5 @@ +--- +title: Fix GitHub project import visibility +merge_request: 27133 +author: Daniel Wyatt +type: fixed diff --git a/changelogs/unreleased/53198-git-push-option-merge-when-pipeline-succeeds.yml b/changelogs/unreleased/53198-git-push-option-merge-when-pipeline-succeeds.yml new file mode 100644 index 00000000000..6fefd05049c --- /dev/null +++ b/changelogs/unreleased/53198-git-push-option-merge-when-pipeline-succeeds.yml @@ -0,0 +1,6 @@ +--- +title: Allow merge requests to be set to merge when pipeline succeeds via git push + options +merge_request: 26842 +author: +type: added diff --git a/changelogs/unreleased/53279-fix-updated_at-api.yml b/changelogs/unreleased/53279-fix-updated_at-api.yml new file mode 100644 index 00000000000..c64dada7eaa --- /dev/null +++ b/changelogs/unreleased/53279-fix-updated_at-api.yml @@ -0,0 +1,5 @@ +--- +title: "Respect updated_at attribute in notes produced by API calls" +merge_request: 27124 +author: Ben Gamari +type: fixed diff --git a/changelogs/unreleased/57131-external_auth_to_core.yml b/changelogs/unreleased/57131-external_auth_to_core.yml new file mode 100644 index 00000000000..aacd3916c82 --- /dev/null +++ b/changelogs/unreleased/57131-external_auth_to_core.yml @@ -0,0 +1,5 @@ +--- +title: Move "Authorize project access with external service" to Core +merge_request: 26823 +author: +type: changed diff --git a/changelogs/unreleased/59708-vendor-css.yml b/changelogs/unreleased/59708-vendor-css.yml new file mode 100644 index 00000000000..ec7def7a9e6 --- /dev/null +++ b/changelogs/unreleased/59708-vendor-css.yml @@ -0,0 +1,5 @@ +--- +title: Creates a vendors folder for external CSS +merge_request: +author: +type: other diff --git a/changelogs/unreleased/always-link-instance-configuration.yml b/changelogs/unreleased/always-link-instance-configuration.yml new file mode 100644 index 00000000000..3f08747edf7 --- /dev/null +++ b/changelogs/unreleased/always-link-instance-configuration.yml @@ -0,0 +1,5 @@ +--- +title: Always show instance configuration link +merge_request: 26783 +author: Bastian Blank +type: fixed diff --git a/changelogs/unreleased/delay-update-statictics.yml b/changelogs/unreleased/delay-update-statictics.yml new file mode 100644 index 00000000000..d0201fb6db8 --- /dev/null +++ b/changelogs/unreleased/delay-update-statictics.yml @@ -0,0 +1,5 @@ +--- +title: Fix the bug that the project statistics is not updated +merge_request: 26854 +author: Hiroyuki Sato +type: fixed diff --git a/changelogs/unreleased/do-not-reopen-merged-mr.yml b/changelogs/unreleased/do-not-reopen-merged-mr.yml new file mode 100644 index 00000000000..14d1455cca4 --- /dev/null +++ b/changelogs/unreleased/do-not-reopen-merged-mr.yml @@ -0,0 +1,5 @@ +--- +title: Remove a "reopen merge request button" on a "merged" merge request +merge_request: 26965 +author: Hiroyuki Sato +type: fixed diff --git a/changelogs/unreleased/expose-pipeline-variables-via-api.yml b/changelogs/unreleased/expose-pipeline-variables-via-api.yml new file mode 100644 index 00000000000..f37bf0c5cd8 --- /dev/null +++ b/changelogs/unreleased/expose-pipeline-variables-via-api.yml @@ -0,0 +1,5 @@ +--- +title: Expose pipeline variables via API +merge_request: 26501 +author: Agustin Henze <tin@redhat.com> +type: added diff --git a/changelogs/unreleased/fix-pull-request-importer.yml b/changelogs/unreleased/fix-pull-request-importer.yml new file mode 100644 index 00000000000..5f642a0710b --- /dev/null +++ b/changelogs/unreleased/fix-pull-request-importer.yml @@ -0,0 +1,5 @@ +--- +title: Improve performance of PR import +merge_request: 27121 +author: +type: performance diff --git a/changelogs/unreleased/instance-configuration-artifact-size.yml b/changelogs/unreleased/instance-configuration-artifact-size.yml new file mode 100644 index 00000000000..077f8631af5 --- /dev/null +++ b/changelogs/unreleased/instance-configuration-artifact-size.yml @@ -0,0 +1,5 @@ +--- +title: Display maximum artifact size from runtime config +merge_request: 26784 +author: Bastian Blank +type: fixed diff --git a/changelogs/unreleased/issue-58418-release-notes.yml b/changelogs/unreleased/issue-58418-release-notes.yml new file mode 100644 index 00000000000..80e6529eb12 --- /dev/null +++ b/changelogs/unreleased/issue-58418-release-notes.yml @@ -0,0 +1,5 @@ +--- +title: Set release name when adding release notes to an existing tag +merge_request: 26807 +author: +type: fixed diff --git a/changelogs/unreleased/prevent-running-mr-pipelines-when-target-updated.yml b/changelogs/unreleased/prevent-running-mr-pipelines-when-target-updated.yml new file mode 100644 index 00000000000..d003ca55feb --- /dev/null +++ b/changelogs/unreleased/prevent-running-mr-pipelines-when-target-updated.yml @@ -0,0 +1,5 @@ +--- +title: Create pipelines for merge requests only when source branch is updated +merge_request: 26921 +author: +type: fixed diff --git a/changelogs/unreleased/remove-disabled-pages-domains.yml b/changelogs/unreleased/remove-disabled-pages-domains.yml new file mode 100644 index 00000000000..e23561329ef --- /dev/null +++ b/changelogs/unreleased/remove-disabled-pages-domains.yml @@ -0,0 +1,5 @@ +--- +title: Mark disabled pages domains for removal, but don't remove them yet +merge_request: 26212 +author: +type: added diff --git a/changelogs/unreleased/sh-optimize-projects-api.yml b/changelogs/unreleased/sh-optimize-projects-api.yml deleted file mode 100644 index 2f2459be77f..00000000000 --- a/changelogs/unreleased/sh-optimize-projects-api.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Optimize /api/v4/projects endpoint for visibility level -merge_request: 26481 -author: -type: performance diff --git a/changelogs/unreleased/update-workhorse-8-5-1.yml b/changelogs/unreleased/update-workhorse-8-5-1.yml new file mode 100644 index 00000000000..18193701678 --- /dev/null +++ b/changelogs/unreleased/update-workhorse-8-5-1.yml @@ -0,0 +1,5 @@ +--- +title: Update GitLab Workhorse to v8.5.1 +merge_request: 27217 +author: +type: fixed diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 8d9b6624995..bdac5b2a6a1 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -7,7 +7,8 @@ # * are being moved to ApplicationSetting model! # # If a setting requires an application restart say so in that screen. # # If you change this file in a Merge Request, please also create # -# a MR on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests # +# a MR on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests. # +# For more details see https://gitlab.com/gitlab-org/omnibus-gitlab/blob/0928cfb09f43993fd9454b0b14dbd1924b1407bc/doc/settings/gitlab.yml.md # ######################################################################## # # diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 2dc0da00919..8bc2426ec4c 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -89,4 +89,5 @@ - [project_daily_statistics, 1] - [import_issues_csv, 2] - [chat_notification, 2] - - [migrate_external_diffs, 1] + - [migrate_external_diffs, 1] + - [update_project_statistics, 1] diff --git a/danger/roulette/Dangerfile b/danger/roulette/Dangerfile index 81ee0397bdc..e6820f49ee2 100644 --- a/danger/roulette/Dangerfile +++ b/danger/roulette/Dangerfile @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'digest/md5' + MESSAGE = <<MARKDOWN ## Reviewer roulette @@ -29,17 +31,22 @@ Please consider creating a merge request to for them. MARKDOWN -def spin(team, project, category) +def spin(team, project, category, branch_name) + rng = Random.new(Digest::MD5.hexdigest(branch_name).to_i(16)) + reviewers = team.select { |member| member.reviewer?(project, category) } traintainers = team.select { |member| member.traintainer?(project, category) } maintainers = team.select { |member| member.maintainer?(project, category) } # TODO: filter out people who are currently not in the office + # https://gitlab.com/gitlab-org/gitlab-ce/issues/57652 + # # TODO: take CODEOWNERS into account? + # https://gitlab.com/gitlab-org/gitlab-ce/issues/57653 # Make traintainers have triple the chance to be picked as a reviewer - reviewer = (reviewers + traintainers + traintainers).sample - maintainer = maintainers.sample + reviewer = (reviewers + traintainers + traintainers).sample(random: rng) + maintainer = maintainers.sample(random: rng) "| #{helper.label_for_category(category)} | #{reviewer&.markdown_name} | #{maintainer&.markdown_name} |" end @@ -65,6 +72,12 @@ categories = changes.keys - [:unknown] # CSS Clean up MRs are reviewed using a slightly different process, so we # disable the review roulette for such MRs. if changes.any? && !gitlab.mr_labels.include?('single codebase') && !gitlab.mr_labels.include?('CSS cleanup') + # Strip leading and trailing CE/EE markers + canonical_branch_name = gitlab + .mr_json['source_branch'] + .gsub(/^[ce]e-/, '') + .gsub(/-[ce]e$/, '') + team = begin helper.project_team @@ -79,7 +92,7 @@ if changes.any? && !gitlab.mr_labels.include?('single codebase') && !gitlab.mr_l project = helper.project_name unknown = changes.fetch(:unknown, []) - rows = categories.map { |category| spin(team, project, category) } + rows = categories.map { |category| spin(team, project, category, canonical_branch_name) } markdown(MESSAGE) markdown(CATEGORY_TABLE_HEADER + rows.join("\n")) unless rows.empty? diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb index 1952f84ed62..43b69470d2c 100644 --- a/db/fixtures/development/10_merge_requests.rb +++ b/db/fixtures/development/10_merge_requests.rb @@ -21,7 +21,7 @@ Gitlab::Seeder.quiet do title: FFaker::Lorem.sentence(6), description: FFaker::Lorem.sentences(3).join(" "), milestone: project.milestones.sample, - assignee: project.team.users.sample, + assignees: [project.team.users.sample], label_ids: label_ids } diff --git a/db/migrate/20171211131502_add_external_classification_authorization_settings_to_appliction_settings.rb b/db/migrate/20171211131502_add_external_classification_authorization_settings_to_appliction_settings.rb new file mode 100644 index 00000000000..a7dec8732fb --- /dev/null +++ b/db/migrate/20171211131502_add_external_classification_authorization_settings_to_appliction_settings.rb @@ -0,0 +1,29 @@ +class AddExternalClassificationAuthorizationSettingsToApplictionSettings < ActiveRecord::Migration[4.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default :application_settings, + :external_authorization_service_enabled, + :boolean, + default: false + add_column :application_settings, + :external_authorization_service_url, + :string + add_column :application_settings, + :external_authorization_service_default_label, + :string + end + + def down + remove_column :application_settings, + :external_authorization_service_default_label + remove_column :application_settings, + :external_authorization_service_url + remove_column :application_settings, + :external_authorization_service_enabled + end +end diff --git a/db/migrate/20171218140451_add_external_authorization_service_classification_label_to_projects.rb b/db/migrate/20171218140451_add_external_authorization_service_classification_label_to_projects.rb new file mode 100644 index 00000000000..7b83580f025 --- /dev/null +++ b/db/migrate/20171218140451_add_external_authorization_service_classification_label_to_projects.rb @@ -0,0 +1,11 @@ +class AddExternalAuthorizationServiceClassificationLabelToProjects < ActiveRecord::Migration[4.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :projects, + :external_authorization_classification_label, + :string + end +end diff --git a/db/migrate/20180115094742_add_default_project_creation_setting.rb b/db/migrate/20180115094742_add_default_project_creation_setting.rb new file mode 100644 index 00000000000..465a89c39e8 --- /dev/null +++ b/db/migrate/20180115094742_add_default_project_creation_setting.rb @@ -0,0 +1,19 @@ +class AddDefaultProjectCreationSetting < ActiveRecord::Migration[4.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + unless column_exists?(:application_settings, :default_project_creation) + add_column_with_default(:application_settings, :default_project_creation, :integer, default: 2) + end + end + + def down + if column_exists?(:application_settings, :default_project_creation) + remove_column(:application_settings, :default_project_creation) + end + end +end diff --git a/db/migrate/20180115113902_add_project_creation_level_to_groups.rb b/db/migrate/20180115113902_add_project_creation_level_to_groups.rb new file mode 100644 index 00000000000..a10ce54087c --- /dev/null +++ b/db/migrate/20180115113902_add_project_creation_level_to_groups.rb @@ -0,0 +1,17 @@ +class AddProjectCreationLevelToGroups < ActiveRecord::Migration[4.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + unless column_exists?(:namespaces, :project_creation_level) + add_column(:namespaces, :project_creation_level, :integer) + end + end + + def down + if column_exists?(:namespaces, :project_creation_level) + remove_column(:namespaces, :project_creation_level, :integer) + end + end +end diff --git a/db/migrate/20180314100728_add_external_authorization_service_timeout_to_application_settings.rb b/db/migrate/20180314100728_add_external_authorization_service_timeout_to_application_settings.rb new file mode 100644 index 00000000000..c3c6aa0ddf8 --- /dev/null +++ b/db/migrate/20180314100728_add_external_authorization_service_timeout_to_application_settings.rb @@ -0,0 +1,18 @@ +class AddExternalAuthorizationServiceTimeoutToApplicationSettings < ActiveRecord::Migration[4.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + # We can use the regular `add_column` with a default since `application_settings` + # is a small table. + add_column :application_settings, + :external_authorization_service_timeout, + :float, + default: 0.5 + end + + def down + remove_column :application_settings, :external_authorization_service_timeout + end +end diff --git a/db/migrate/20180315160435_add_external_auth_mutual_tls_fields_to_project_settings.rb b/db/migrate/20180315160435_add_external_auth_mutual_tls_fields_to_project_settings.rb new file mode 100644 index 00000000000..ee3d1078f5e --- /dev/null +++ b/db/migrate/20180315160435_add_external_auth_mutual_tls_fields_to_project_settings.rb @@ -0,0 +1,16 @@ +class AddExternalAuthMutualTlsFieldsToProjectSettings < ActiveRecord::Migration[4.2] + DOWNTIME = false + + def change + add_column :application_settings, + :external_auth_client_cert, :text + add_column :application_settings, + :encrypted_external_auth_client_key, :text + add_column :application_settings, + :encrypted_external_auth_client_key_iv, :string + add_column :application_settings, + :encrypted_external_auth_client_key_pass, :string + add_column :application_settings, + :encrypted_external_auth_client_key_pass_iv, :string + end +end diff --git a/db/migrate/20190311132500_add_default_project_creation_application_setting.rb b/db/migrate/20190311132500_add_default_project_creation_application_setting.rb deleted file mode 100644 index 87427ad2930..00000000000 --- a/db/migrate/20190311132500_add_default_project_creation_application_setting.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. - -class AddDefaultProjectCreationApplicationSetting < ActiveRecord::Migration[5.0] - include Gitlab::Database::MigrationHelpers - - DOWNTIME = false - - def up - unless column_exists?(:application_settings, :default_project_creation) - add_column(:application_settings, :default_project_creation, :integer, default: 2, null: false) - end - end - - def down - if column_exists?(:application_settings, :default_project_creation) - remove_column(:application_settings, :default_project_creation) - end - end -end diff --git a/db/migrate/20190311132527_add_project_creation_level_to_namespaces.rb b/db/migrate/20190311132527_add_project_creation_level_to_namespaces.rb deleted file mode 100644 index 159e0a95ace..00000000000 --- a/db/migrate/20190311132527_add_project_creation_level_to_namespaces.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. - -class AddProjectCreationLevelToNamespaces < ActiveRecord::Migration[5.0] - include Gitlab::Database::MigrationHelpers - - DOWNTIME = false - - def up - unless column_exists?(:namespaces, :project_creation_level) - add_column :namespaces, :project_creation_level, :integer - end - end - - def down - unless column_exists?(:namespaces, :project_creation_level) - remove_column :namespaces, :project_creation_level, :integer - end - end -end diff --git a/db/migrate/20190312113229_add_remove_at_to_pages_domains.rb b/db/migrate/20190312113229_add_remove_at_to_pages_domains.rb new file mode 100644 index 00000000000..aa7d2841cd2 --- /dev/null +++ b/db/migrate/20190312113229_add_remove_at_to_pages_domains.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class AddRemoveAtToPagesDomains < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + def change + add_column :pages_domains, :remove_at, :datetime_with_timezone + end +end diff --git a/db/migrate/20190312113634_add_remove_at_index_to_pages_domains.rb b/db/migrate/20190312113634_add_remove_at_index_to_pages_domains.rb new file mode 100644 index 00000000000..b5ccebd9cfa --- /dev/null +++ b/db/migrate/20190312113634_add_remove_at_index_to_pages_domains.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddRemoveAtIndexToPagesDomains < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :pages_domains, :remove_at + end + + def down + remove_concurrent_index :pages_domains, :remove_at + end +end diff --git a/db/schema.rb b/db/schema.rb index ca5b04e810a..d1b3672725d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -178,6 +178,15 @@ ActiveRecord::Schema.define(version: 20190326164045) do t.integer "local_markdown_version", default: 0, null: false t.integer "first_day_of_week", default: 0, null: false t.integer "default_project_creation", default: 2, null: false + t.boolean "external_authorization_service_enabled", default: false, null: false + t.string "external_authorization_service_url" + t.string "external_authorization_service_default_label" + t.float "external_authorization_service_timeout", default: 0.5 + t.text "external_auth_client_cert" + t.text "encrypted_external_auth_client_key" + t.string "encrypted_external_auth_client_key_iv" + t.string "encrypted_external_auth_client_key_pass" + t.string "encrypted_external_auth_client_key_pass_iv" t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree end @@ -1538,9 +1547,11 @@ ActiveRecord::Schema.define(version: 20190326164045) do t.datetime_with_timezone "verified_at" t.string "verification_code", null: false t.datetime_with_timezone "enabled_until" + t.datetime_with_timezone "remove_at" t.index ["domain"], name: "index_pages_domains_on_domain", unique: true, using: :btree t.index ["project_id", "enabled_until"], name: "index_pages_domains_on_project_id_and_enabled_until", using: :btree t.index ["project_id"], name: "index_pages_domains_on_project_id", using: :btree + t.index ["remove_at"], name: "index_pages_domains_on_remove_at", using: :btree t.index ["verified_at", "enabled_until"], name: "index_pages_domains_on_verified_at_and_enabled_until", using: :btree t.index ["verified_at"], name: "index_pages_domains_on_verified_at", using: :btree end @@ -1756,6 +1767,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do t.string "runners_token_encrypted" t.string "bfg_object_map" t.boolean "detected_repository_languages" + t.string "external_authorization_classification_label" t.index ["ci_id"], name: "index_projects_on_ci_id", using: :btree t.index ["created_at"], name: "index_projects_on_created_at", using: :btree t.index ["creator_id"], name: "index_projects_on_creator_id", using: :btree diff --git a/doc/README.md b/doc/README.md index aead50ea97e..14a1eeffda0 100644 --- a/doc/README.md +++ b/doc/README.md @@ -288,7 +288,7 @@ The following documentation relates to the DevOps **Configure** stage: | [GitLab ChatOps](ci/chatops/README.md) | Interact with CI/CD jobs through chat services. | | [Installing Applications](user/project/clusters/index.md#installing-applications) | Deploy Helm, Ingress, and Prometheus on Kubernetes. | | [Mattermost slash commands](user/project/integrations/mattermost_slash_commands.md) | Enable and use slash commands from within Mattermost. | -| [Protected variables](ci/variables/README.md#protected-variables) | Restrict variables to protected branches and tags. | +| [Protected variables](ci/variables/README.md#protected-environment-variables) | Restrict variables to protected branches and tags. | | [Serverless](user/project/clusters/serverless/index.md) | Run serverless workloads on Kubernetes. | | [Slack slash commands](user/project/integrations/slack_slash_commands.md) | Enable and use slash commands from within Slack. | diff --git a/doc/administration/auth/okta.md b/doc/administration/auth/okta.md index 638405126a5..aa4e1b0d2e0 100644 --- a/doc/administration/auth/okta.md +++ b/doc/administration/auth/okta.md @@ -92,18 +92,23 @@ Now that the Okta app is configured, it's time to enable it in GitLab. 1. Add the provider configuration. >**Notes:** + > >- Change the value for `assertion_consumer_service_url` to match the HTTPS endpoint of GitLab (append `users/auth/saml/callback` to the HTTPS URL of your GitLab installation to generate the correct value). + > >- To get the `idp_cert_fingerprint` fingerprint, first download the certificate from the Okta app you registered and then run: `openssl x509 -in okta.cert -noout -fingerprint`. Substitute `okta.cert` with the location of your certificate. + > >- Change the value of `idp_sso_target_url`, with the value of the **Identity Provider Single Sign-On URL** from the step when you configured the Okta app. - >- Change the value of `issuer` to a unique name, which will identify the application + > + >- Change the value of `issuer` to the value of the **Audience Restriction** from your Okta app configuration. This will identify GitLab to the IdP. + > >- Leave `name_identifier_format` as-is. **For Omnibus GitLab installations** diff --git a/doc/administration/housekeeping.md b/doc/administration/housekeeping.md index 058346df56d..1b01419e062 100644 --- a/doc/administration/housekeeping.md +++ b/doc/administration/housekeeping.md @@ -30,7 +30,7 @@ the `pushes_since_gc` value is 200 a `git gc` will be run. `git add`. - `git repack` ([man page][man-repack]) re-organize existing packs into a single, more efficient pack. -You can find this option under your **[Project] > Edit Project**. +You can find this option under your project's **Settings > General > Advanced**. --- diff --git a/doc/administration/img/housekeeping_settings.png b/doc/administration/img/housekeeping_settings.png Binary files differindex acc4506993a..356de51f0cc 100644 --- a/doc/administration/img/housekeeping_settings.png +++ b/doc/administration/img/housekeeping_settings.png diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md index 25d85d1687b..2596e3fe68b 100644 --- a/doc/administration/integration/terminal.md +++ b/doc/administration/integration/terminal.md @@ -11,7 +11,7 @@ One of the things it uses these credentials for is providing access to ## How it works A detailed overview of the architecture of web terminals and how they work -can be found in [this document](https://gitlab.com/gitlab-org/gitlab-workhorse/blob/master/doc/terminal.md). +can be found in [this document](https://gitlab.com/gitlab-org/gitlab-workhorse/blob/master/doc/channel.md). In brief: - GitLab relies on the user to provide their own Kubernetes credentials, and to diff --git a/doc/administration/operations/fast_ssh_key_lookup.md b/doc/administration/operations/fast_ssh_key_lookup.md index c293df3fc57..f8d58c6ce28 100644 --- a/doc/administration/operations/fast_ssh_key_lookup.md +++ b/doc/administration/operations/fast_ssh_key_lookup.md @@ -58,6 +58,9 @@ sudo service sshd reload Confirm that SSH is working by removing your user's SSH key in the UI, adding a new one, and attempting to pull a repo. +> **Note:** For Omnibus Docker, `AuthorizedKeysCommand` is setup by default in +GitLab 11.11 and later. + > **Warning:** Do not disable writes until SSH is confirmed to be working perfectly, because the file will quickly become out-of-date. diff --git a/doc/api/commits.md b/doc/api/commits.md index e205307eeca..92f53c7b5e6 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -130,6 +130,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --header "Cont ``` Example response: + ```json { "id": "ed899a2f4b50b4370feeea94676502b42383c746", @@ -162,21 +163,21 @@ curl --request POST \ --form "branch=master" \ --form "commit_message=some commit message" \ --form "start_branch=master" \ - --form "actions[][action]=create" \ - --form "actions[][file_path]=foo/bar" \ - --form "actions[][content]=</path/to/local.file" \ - --form "actions[][action]=delete" \ - --form "actions[][file_path]=foo/bar2" \ - --form "actions[][action]=move" \ - --form "actions[][file_path]=foo/bar3" \ - --form "actions[][previous_path]=foo/bar4" \ - --form "actions[][content]=</path/to/local1.file" \ - --form "actions[][action]=update" \ + --form "actions[][action]=create" \ + --form "actions[][file_path]=foo/bar" \ + --form "actions[][content]=</path/to/local.file" \ + --form "actions[][action]=delete" \ + --form "actions[][file_path]=foo/bar2" \ + --form "actions[][action]=move" \ + --form "actions[][file_path]=foo/bar3" \ + --form "actions[][previous_path]=foo/bar4" \ + --form "actions[][content]=</path/to/local1.file" \ + --form "actions[][action]=update" \ --form "actions[][file_path]=foo/bar5" \ - --form "actions[][content]=</path/to/local2.file" \ - --form "actions[][action]=chmod" \ + --form "actions[][content]=</path/to/local2.file" \ + --form "actions[][action]=chmod" \ --form "actions[][file_path]=foo/bar5" \ - --form "actions[][execute_filemode]=true" \ + --form "actions[][execute_filemode]=true" \ --header "PRIVATE-TOKEN: <your_access_token>" \ "https://gitlab.example.com/api/v4/projects/1/repository/commits" ``` diff --git a/doc/api/environments.md b/doc/api/environments.md index 4a38dd73747..ebcdc546d08 100644 --- a/doc/api/environments.md +++ b/doc/api/environments.md @@ -29,6 +29,111 @@ Example response: ] ``` +## Get a specific environment + +``` +GET /projects/:id/environments/:environment_id +``` + +| Attribute | Type | Required | Description | +|-----------|---------|----------|---------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `environment_id` | integer | yes | The ID of the environment | + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/environments/1" +``` + +Example of response + +```json +{ + "id": 1, + "name": "review/fix-foo", + "slug": "review-fix-foo-dfjre3", + "external_url": "https://review-fix-foo-dfjre3.example.gitlab.com" + "last_deployment": { + "id": 100, + "iid": 34, + "ref": "fdroid", + "sha": "416d8ea11849050d3d1f5104cf8cf51053e790ab", + "created_at": "2019-03-25T18:55:13.252Z", + "user": { + "id": 1, + "name": "Administrator", + "state": "active", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://localhost:3000/root" + } + "deployable": { + "id": 710, + "status": "success", + "stage": "deploy", + "name": "staging", + "ref": "fdroid", + "tag": false, + "coverage": null, + "created_at": "2019-03-25T18:55:13.215Z", + "started_at": "2019-03-25T12:54:50.082Z", + "finished_at": "2019-03-25T18:55:13.216Z", + "duration": 21623.13423, + "user": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://gitlab.dev/root", + "created_at": "2015-12-21T13:14:24.077Z", + "bio": null, + "location": null, + "public_email": "", + "skype": "", + "linkedin": "", + "twitter": "", + "website_url": "", + "organization": null + } + "commit": { + "id": "416d8ea11849050d3d1f5104cf8cf51053e790ab", + "short_id": "416d8ea1", + "created_at": "2016-01-02T15:39:18.000Z", + "parent_ids": [ + "e9a4449c95c64358840902508fc827f1a2eab7df" + ], + "title": "Removed fabric to fix #40", + "message": "Removed fabric to fix #40\n", + "author_name": "Administrator", + "author_email": "admin@example.com", + "authored_date": "2016-01-02T15:39:18.000Z", + "committer_name": "Administrator", + "committer_email": "admin@example.com", + "committed_date": "2016-01-02T15:39:18.000Z" + }, + "pipeline": { + "id": 34, + "sha": "416d8ea11849050d3d1f5104cf8cf51053e790ab", + "ref": "fdroid", + "status": "success", + "web_url": "http://localhost:3000/Commit451/lab-coat/pipelines/34" + }, + "web_url": "http://localhost:3000/Commit451/lab-coat/-/jobs/710", + "artifacts": [ + { + "file_type": "trace", + "size": 1305, + "filename": "job.log", + "file_format": null + } + ], + "runner": null, + "artifacts_expire_at": null + } + } +} +``` + ## Create a new environment Creates a new environment with the given name and external_url. diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md index 43bbf463c8d..1a4310ef328 100644 --- a/doc/api/pipelines.md +++ b/doc/api/pipelines.md @@ -93,6 +93,36 @@ Example of response } ``` +### Get variables of a pipeline + +``` +GET /projects/:id/pipelines/:pipeline_id/variables +``` + +| Attribute | Type | Required | Description | +|------------|---------|----------|---------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `pipeline_id` | integer | yes | The ID of a pipeline | + +``` +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/pipelines/46/variables" +``` + +Example of response + +```json +[ + { + "key": "RUN_NIGHTLY_BUILD", + "value": "true" + }, + { + "key": "foo", + "value": "bar" + } +] +``` + ## Create a new pipeline > [Introduced][ce-7209] in GitLab 8.14 diff --git a/doc/ci/README.md b/doc/ci/README.md index 913e9d4d789..a7ad2018b09 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -62,6 +62,7 @@ into more features: | [ChatOps](chatops/README.md) | Trigger CI jobs from chat, with results sent back to the channel. | | [Interactive web terminals](interactive_web_terminal/index.md) | Open an interactive web terminal to debug the running jobs. | | [Review Apps](review_apps/index.md) | Configure GitLab CI/CD to preview code changes in a per-branch basis. | +| [Optimizing GitLab for large repositories](large_repositories/index.md) | Useful tips on how to optimize GitLab and GitLab Runner for big repositories. | | [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html) **[PREMIUM]** | Check the current health and status of each CI/CD environment running on Kubernetes. | | [GitLab CI/CD for external repositories](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/index.html) **[PREMIUM]** | Get the benefits of GitLab CI/CD combined with repositories in GitHub and BitBucket Cloud. | diff --git a/doc/ci/chatops/README.md b/doc/ci/chatops/README.md index a06fe6961a7..5ecdf0f8c54 100644 --- a/doc/ci/chatops/README.md +++ b/doc/ci/chatops/README.md @@ -24,7 +24,7 @@ Since ChatOps is built upon GitLab CI/CD, the job has all the same features and * It is strongly recommended to set `only: [chat]` so the job does not run as part of the standard CI pipeline. * If the job is set to `when: manual`, the pipeline will be created however the job will wait to be started. -* It is important to keep in mind that there is very limited support for access control. If the user who triggered the slash command is a developer in the project, the job will run. The job itself can utilize existing [CI/CD variables](../variables/README.html#predefined-environment-variables) like `GITLAB_USER_ID` to perform additional rights validation, however these variables can be [overridden](https://docs.gitlab.com/ce/ci/variables/README.html#priority-of-variables). +* It is important to keep in mind that there is very limited support for access control. If the user who triggered the slash command is a developer in the project, the job will run. The job itself can utilize existing [CI/CD variables](../variables/README.html#predefined-environment-variables) like `GITLAB_USER_ID` to perform additional rights validation, however these variables can be [overridden](../variables/README.html#priority-of-environment-variables). ### Controlling the ChatOps reply diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index 9266c4511be..5222cc45bc4 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -303,20 +303,19 @@ services: - docker:dind variables: - CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH DOCKER_HOST: tcp://docker:2375 DOCKER_DRIVER: overlay2 before_script: - - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY build: stage: build script: - - docker pull $CONTAINER_IMAGE:latest || true - - docker build --cache-from $CONTAINER_IMAGE:latest --tag $CONTAINER_IMAGE:$CI_COMMIT_SHA --tag $CONTAINER_IMAGE:latest . - - docker push $CONTAINER_IMAGE:$CI_COMMIT_SHA - - docker push $CONTAINER_IMAGE:latest + - docker pull $CI_REGISTRY_IMAGE:latest || true + - docker build --cache-from $CI_REGISTRY_IMAGE:latest --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA --tag $CI_REGISTRY_IMAGE:latest . + - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + - docker push $CI_REGISTRY_IMAGE:latest ``` The steps in the `script` section for the `build` stage can be summed up to: @@ -324,7 +323,7 @@ The steps in the `script` section for the `build` stage can be summed up to: 1. The first command tries to pull the image from the registry so that it can be used as a cache for the `docker build` command. 1. The second command builds a Docker image using the pulled image as a - cache (notice the `--cache-from $CONTAINER_IMAGE:latest` argument) if + cache (notice the `--cache-from $CI_REGISTRY_IMAGE:latest` argument) if available, and tags it. 1. The last two commands push the tagged Docker images to the container registry so that they may also be used as cache for subsequent builds. @@ -421,14 +420,14 @@ and depend on the visibility of your project. For all projects, mostly suitable for public ones: -- **Using the special `gitlab-ci-token` user**: This user is created for you in order to +- **Using the special `$CI_REGISTRY_USER` variable**: The user specified by this variable is created for you in order to push to the Registry connected to your project. Its password is automatically - set with the `$CI_JOB_TOKEN` variable. This allows you to automate building and deploying + set with the `$CI_REGISTRY_PASSWORD` variable. This allows you to automate building and deploying your Docker images and has read/write access to the Registry. This is ephemeral, so it's only valid for one job. You can use the following example as-is: ```sh - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY + docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY ``` For private and internal projects: @@ -436,8 +435,10 @@ For private and internal projects: - **Using a personal access token**: You can create and use a [personal access token](../../user/profile/personal_access_tokens.md) in case your project is private: - - For read (pull) access, the scope should be `read_registry`. - - For read/write (pull/push) access, use `api`. + + - For read (pull) access, the scope should be `read_registry`. + - For read/write (pull/push) access, use `api`. + Replace the `<username>` and `<access_token>` in the following example: ```sh @@ -469,9 +470,9 @@ could look like: DOCKER_DRIVER: overlay2 stage: build script: - - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com - - docker build -t registry.example.com/group/project/image:latest . - - docker push registry.example.com/group/project/image:latest + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - docker build -t $CI_REGISTRY/group/project/image:latest . + - docker push $CI_REGISTRY/group/project/image:latest ``` You can also make use of [other variables](../variables/README.md) to avoid hardcoding: @@ -486,7 +487,7 @@ variables: IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG before_script: - - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY build: stage: build @@ -526,7 +527,7 @@ variables: CONTAINER_RELEASE_IMAGE: $CI_REGISTRY_IMAGE:latest before_script: - - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY build: stage: build diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 3314c76d234..13c26bc5f47 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -500,7 +500,7 @@ To configure access for `registry.example.com:5000`, follow these steps: bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ= ``` -1. Create a [variable] `DOCKER_AUTH_CONFIG` with the content of the +1. Create a [variable](../variables/README.md#gitlab-cicd-environment-variables) `DOCKER_AUTH_CONFIG` with the content of the Docker configuration file as the value: ```json @@ -642,7 +642,6 @@ creation. [postgres-hub]: https://hub.docker.com/r/_/postgres/ [mysql-hub]: https://hub.docker.com/r/_/mysql/ [runner-priv-reg]: http://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-container-registry -[variable]: ../variables/README.md#variables [entrypoint]: https://docs.docker.com/engine/reference/builder/#entrypoint [cmd]: https://docs.docker.com/engine/reference/builder/#cmd [register]: https://docs.gitlab.com/runner/register/ diff --git a/doc/ci/environments.md b/doc/ci/environments.md index eaafc7bc1c0..3e52cc786dd 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -224,7 +224,7 @@ The `name` and `url` parameters for dynamic environments can use most available including: - [Predefined environment variables](variables/README.md#predefined-environment-variables) -- [Project and group variables](variables/README.md#variables) +- [Project and group variables](variables/README.md#gitlab-cicd-environment-variables) - [`.gitlab-ci.yml` variables](yaml/README.md#variables) However, you cannot use variables defined: diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md index 0f809ed05ca..f56d5429fb7 100644 --- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md @@ -116,7 +116,7 @@ cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys cat ~/.ssh/id_rsa ``` -Now, let's add it to your GitLab project as a [variable](../../variables/README.md#variables). +Now, let's add it to your GitLab project as a [variable](../../variables/README.md#gitlab-cicd-environment-variables). Variables are user-defined variables and are stored out of `.gitlab-ci.yml`, for security purposes. They can be added per project by navigating to the project's **Settings** > **CI/CD**. diff --git a/doc/ci/examples/test-scala-application.md b/doc/ci/examples/test-scala-application.md index fa18cb22aed..e1164b8d03a 100644 --- a/doc/ci/examples/test-scala-application.md +++ b/doc/ci/examples/test-scala-application.md @@ -69,5 +69,5 @@ in the `.gitlab-ci.yml` file with your application's name. ## Heroku API key You can look up your Heroku API key in your -[account](https://dashboard.heroku.com/account). Add a [protected variable](../variables/README.md#protected-variables) with +[account](https://dashboard.heroku.com/account). Add a [protected variable](../variables/README.md#protected-environment-variables) with this value in **Project ➔ Variables** with key `HEROKU_API_KEY`. diff --git a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md index 1a909e8892a..4a5fda661df 100644 --- a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md +++ b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md @@ -11,7 +11,7 @@ last_updated: 2019-03-06 [Phoenix][phoenix-site] is a web development framework written in [Elixir][elixir-site], which is a functional language designed for productivity and maintainability that runs on the -[Erlang VM][erlang-site]. Erlang VM is really really fast and can handle very large numbers of +[Erlang VM](https://www.erlang.org). Erlang VM is really really fast and can handle very large numbers of simultaneous users. That's why we're hearing so much about Phoenix today. @@ -27,7 +27,7 @@ and GitLab UI._ ### What is Phoenix? [Phoenix][phoenix-site] is a web development framework written in [Elixir][elixir-site] very useful - to build fast, reliable, and high-performance applications, as it uses [Erlang VM][erlang-site]. + to build fast, reliable, and high-performance applications, as it uses [Erlang VM](https://www.erlang.org). Many components and concepts are similar to Ruby on Rails or Python's Django. High developer productivity and high application performance are only a few advantages on learning how to use it. @@ -406,7 +406,6 @@ other reasons][ci-reasons] to keep using GitLab CI/CD. The benefits to our teams [elixir-site]: http://elixir-lang.org/ "Elixir" [elixir-mix]: http://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html "Introduction to mix" [elixir-docs]: http://elixir-lang.org/getting-started/introduction.html "Elixir Documentation" -[erlang-site]: http://erlang.org "Erlang" [elixir-install]: https://elixir-lang.org/install.html "Elixir Installation" [ecto]: http://hexdocs.pm/ecto "Ecto" [ecto-repo]: https://hexdocs.pm/ecto/Ecto.html#module-repositories "Ecto Repositories" diff --git a/doc/ci/large_repositories/index.md b/doc/ci/large_repositories/index.md new file mode 100644 index 00000000000..cfe638c0a22 --- /dev/null +++ b/doc/ci/large_repositories/index.md @@ -0,0 +1,235 @@ +# Optimizing GitLab for large repositories + +Large repositories consisting of more than 50k files in a worktree +often require special consideration because of +the time required to clone and check out. + +GitLab and GitLab Runner handle this scenario well +but require optimized configuration to efficiently perform its +set of operations. + +The general guidelines for handling big repositories are simple. +Each guideline is described in more detail in the sections below: + +- Always fetch incrementally. Do not clone in a way that results in recreating all of the worktree. +- Always use shallow clone to reduce data transfer. Be aware that this puts more burden + on GitLab instance due to higher CPU impact. +- Control the clone directory if you heavily use a fork-based workflow. +- Optimize `git clean` flags to ensure that you remove or keep data that might affect or speed-up your build. + +## Shallow cloning + +> Introduced in GitLab Runner 8.9. + +GitLab and GitLab Runner always perform a full clone by default. +While it means that all changes from GitLab are received, +it often results in receiving extra commit logs. + +Ideally, you should always use `GIT_DEPTH` with a small number +like 10. This will instruct GitLab Runner to perform shallow clones. +Shallow clones makes Git request only the latest set of changes for a given branch, +up to desired number of commits as defined by the `GIT_DEPTH` variable. + +This significantly speeds up fetching of changes from Git repositories, +especially if the repository has a very long backlog consisting of number +of big files as we effectively reduce amount of data transfer. + +The following example makes GitLab Runner shallow clone to fetch only a given branch, +it does not fetch any other branches nor tags. + +```yaml +variables: + GIT_DEPTH: 10 + +test: + script: + - ls -al +``` + +## Git strategy + +> Introduced in GitLab Runner 8.9. + +By default, GitLab is configured to always prefer the `GIT_STRATEGY: fetch` strategy. +The `GIT_STRATEGY: fetch` strategy will re-use existing worktrees if found +on disk. This is different to the `GIT_STRATEGY: clone` strategy +as in case of clones, if a worktree is found, it is removed before clone. + +Usage of `fetch` is preferred because it reduces the amount of data to transfer and +does not really impact the operations that you might do on a repository from CI. + +However, `fetch` does require access to the previous worktree. This works +well when using the `shell` or `docker` executor because these +try to preserve worktrees and try to re-use them by default. + +This does not work today for `kubernetes` executor and has limitations when using +`docker+machine`. `kubernetes` executor today always clones into ephemeral directory. + +GitLab also offers the `GIT_STRATEGY: none` strategy. This disables any `fetch` and `checkout` commands +done by GitLab, requiring you to do them. + +## Git clone path + +> Introduced in GitLab Runner 11.10. + +`GIT_CLONE_PATH` allows you to control where you clone your sources. +This can have implications if you heavily use big repositories with fork workflow. + +Fork workflow from GitLab Runner's perspective is stored as a separate repository +with separate worktree. That means that GitLab Runner cannot optimize the usage +of worktrees and you might have to instruct GitLab Runner to use that. + +In such cases, ideally you want to make the GitLab Runner executor be used only used only +for the given project and not shared across different projects to make this +process more efficient. + +The `GIT_CLONE_PATH` has to be within the `$CI_BUILDS_DIR`. Currently, +it is impossible to pick any path from disk. + +## Git clean flags + +> Introduced in GitLab Runner 11.10. + +`GIT_CLEAN_FLAGS` allows you to control whether or not you require +the `git clean` command to be executed for each CI job. +By default, GitLab ensures that you have your worktree on the given SHA, +and that your repository is clean. + +`GIT_CLEAN_FLAGS` is disabled when set to `none`. On very big repositories, this +might be desired because `git clean` is disk I/O intensive. Controlling that +with `GIT_CLEAN_FLAGS: -ffdx -e .build/`, for example, allows you to control and +disable removal of some directories within the worktree between subsequent runs, +which can speed-up the incremental builds. This has the biggest effect +if you re-use existing machines, and have an existing worktree that you can re-use +for builds. + +For exact parameters accepted by `GIT_CLEAN_FLAGS`, see the documentation +for [git clean](https://git-scm.com/docs/git-clean). The +available parameters are dependent on Git version. + +## Fork-based workflow + +> Introduced in GitLab Runner 11.10. + +Following the guidelines above, lets imagine that we want to: + +- Optimize for a big project (more than 50k files in directory). +- Use forks-based workflow for contributing. +- Reuse existing worktrees. Have preconfigured runners that are pre-cloned with repositories. +- Runner assigned only to project and all forks. + +Lets consider the following two examples, one using `shell` executor and +other using `docker` executor. + +### `shell` executor example + +Lets assume that you have the following [config.toml](https://docs.gitlab.com/runner/configuration/advanced-configuration.html). + +```toml +concurrent = 4 + +[[runners]] + url = "GITLAB_URL" + token = "TOKEN" + executor = "shell" + builds_dir = "/builds" + cache_dir = "/cache" + + [runners.custom_build_dir] + enabled = true +``` + +This `config.toml`: + +- Uses the `shell` executor, +- Specifies a custom `/builds` directory where all clones will be stored. +- Enables the ability to specify `GIT_CLONE_PATH`, +- Runs at most 4 jobs at once. + +### `docker` executor example + +Lets assume that you have the following [config.toml](https://docs.gitlab.com/runner/configuration/advanced-configuration.html). + +```toml +concurrent = 4 + +[[runners]] + url = "GITLAB_URL" + token = "TOKEN" + executor = "docker" + builds_dir = "/builds" + cache_dir = "/cache" + + [runners.docker] + volumes = ["/builds:/builds", "/cache:/cache"] +``` + +This `config.toml`: + +- Uses the `docker` executor, +- Specifies a custom `/builds` directory on disk where all clones will be stored. + We host mount the `/builds` directory to make it reusable between subsequent runs + and be allowed to override the cloning strategy. +- Doesn't enable the ability to specify `GIT_CLONE_PATH` as it is enabled by default. +- Runs at most 4 jobs at once. + +### Our `.gitlab-ci.yml` + +Once we have the executor configured, we need to fine tune our `.gitlab-ci.yml`. + +Our pipeline will be most performant if we use the following `.gitlab-ci.yml`: + +```yaml +variables: + GIT_DEPTH: 10 + GIT_CLONE_PATH: $CI_BUILDS_DIR/$CI_CONCURRENT_ID/$CI_PROJECT_NAME + +build: + script: ls -al +``` + +The above configures a: + +- Shallow clone of 10, to speed up subsequent `git fetch` commands. +- Custom clone path to make it possible to re-use worktrees between parent project and all forks + because we use the same clone path for all forks. + +Why use `$CI_CONCURRENT_ID`? The main reason is to ensure that worktrees used are not conflicting +between projects. The `$CI_CONCURRENT_ID` represents a unique identifier within the given executor, +so as long as we use it to construct the path, it is guaranteed that this directory will not conflict +with other concurrent jobs running. + +### Store custom clone options in `config.toml` + +Ideally, all job-related configuration should be stored in `.gitlab-ci.yml`. +However, sometimes it is desirable to make these schemes part of Runner configuration. + +In the above example of Forks, making this configuration discoverable for users may be preferred, +but this brings administrative overhead as the `.gitlab-ci.yml` needs to be updated for each branch. +In such cases, it might be desirable to keep the `.gitlab-ci.yml` clone path agnostic, but make it +a configuration of Runner. + +We can extend our [config.toml](https://docs.gitlab.com/runner/configuration/advanced-configuration.html) +with the following specification that will be used by Runner if `.gitlab-ci.yml` will not override it: + +```toml +concurrent = 4 + +[[runners]] + url = "GITLAB_URL" + token = "TOKEN" + executor = "docker" + builds_dir = "/builds" + cache_dir = "/cache" + + environment = [ + "GIT_DEPTH=10", + "GIT_CLONE_PATH=$CI_BUILDS_DIR/$CI_CONCURRENT_ID/$CI_PROJECT_NAME" + ] + + [runners.docker] + volumes = ["/builds:/builds", "/cache:/cache"] +``` + +This makes the cloning configuration to be part of given Runner, +and does not require us to update each `.gitlab-ci.yml`. diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index ff63d0bd69f..9ed1ec5aa5c 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -49,7 +49,7 @@ to access it. This is where an SSH key pair comes in handy. **Do not** add a passphrase to the SSH key, or the `before_script` will prompt for it. -1. Create a new [variable](../variables/README.md#variables). +1. Create a new [variable](../variables/README.md#gitlab-cicd-environment-variables). As **Key** enter the name `SSH_PRIVATE_KEY` and in the **Value** field paste the content of your _private_ key that you created earlier. @@ -157,7 +157,7 @@ ssh-keyscan example.com ssh-keyscan 1.2.3.4 ``` -Create a new [variable](../variables/README.md#variables) with +Create a new [variable](../variables/README.md#gitlab-cicd-environment-variables) with `SSH_KNOWN_HOSTS` as "Key", and as a "Value" add the output of `ssh-keyscan`. NOTE: **Note:** diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index 398b017277f..0e72bdddee7 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -20,7 +20,7 @@ A unique trigger token can be obtained when [adding a new trigger](#adding-a-new DANGER: **Danger:** Passing plain text tokens in public projects is a security issue. Potential attackers can impersonate the user that exposed their trigger token publicly in -their `.gitlab-ci.yml` file. Use [variables](../variables/README.md#variables) +their `.gitlab-ci.yml` file. Use [variables](../variables/README.md#gitlab-cicd-environment-variables) to protect trigger tokens. ## Adding a new trigger diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 592fdfd2873..61d1a904f76 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -3,7 +3,6 @@ table_display_block: true --- # GitLab CI/CD environment variables -{: #variables} After a brief overview over the use of environment variables, this document teaches you how to use GitLab CI/CD's @@ -65,7 +64,7 @@ To get started, choose one of the existing to be output by the Runner. For example, let's say that you want a given job you're running through your script to output the stage that job is running for. In your `.gitlab-ci.yml` file, -call the variable from your script according to the [syntaxes](#syntax-of-variables-in-job-scripts) available. To +call the variable from your script according to the [syntaxes](#syntax-of-environment-variables-in-job-scripts) available. To output the job stage, use the predefined variable `CI_JOB_STAGE`: ```yaml @@ -145,7 +144,6 @@ settings](../../user/project/pipelines/settings.md#visibility-of-pipelines). Follow the discussion in issue [#13784][ce-13784] for masking the variables. ### Syntax of environment variables in job scripts -{: #syntax-of-variables-in-job-scripts} All variables are set as environment variables in the build environment, and they are accessible with normal methods that are used to access such variables. @@ -278,7 +276,6 @@ script: ``` ### Group-level environment variables -{: #group-level-variables} > Introduced in GitLab 9.4. @@ -297,19 +294,18 @@ Any variables of [subgroups](../../user/group/subgroups/index.md) will be inheri Once you set them, they will be available for all subsequent pipelines. ## Priority of environment variables -{: #priority-of-variables} Variables of different types can take precedence over other variables, depending on where they are defined. The order of precedence for variables is (from highest to lowest): -1. [Trigger variables](../triggers/README.md#making-use-of-trigger-variables) or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#making-use-of-scheduled-pipeline-variables). -1. Project-level [variables](#creating-a-custom-environment-variable) or [protected variables](#protected-variables). -1. Group-level [variables](#group-level-variables) or [protected variables](#protected-variables). +1. [Trigger variables](../triggers/README.md#making-use-of-trigger-variables) or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#using-variables). +1. Project-level [variables](#creating-a-custom-environment-variable) or [protected variables](#protected-environment-variables). +1. Group-level [variables](#group-level-environment-variables) or [protected variables](#protected-environment-variables). 1. YAML-defined [job-level variables](../yaml/README.md#variables). 1. YAML-defined [global variables](../yaml/README.md#variables). -1. [Deployment variables](#deployment-variables). +1. [Deployment variables](#deployment-environment-variables). 1. [Predefined environment variables](predefined_variables.md). For example, if you define: @@ -329,7 +325,6 @@ about which variables are [not supported](where_variables_can_be_used.md). ## Advanced use ### Protected environment variables -{: #protected-variables} > Introduced in GitLab 9.3. @@ -345,7 +340,6 @@ Protected variables can be added by going to your project's Once you set them, they will be available for all subsequent pipelines. ### Deployment environment variables -{: #deployment-variables} > Introduced in GitLab 8.15. @@ -382,7 +376,7 @@ limitations with the current Auto DevOps scripting environment. [Manually triggered pipelines](../pipelines.md#manually-executing-pipelines) allow you to override the value of a current variable. For instance, suppose you added a -[custom variable `$TEST`](#creating-a-custom-variable) +[custom variable `$TEST`](#creating-a-custom-environment-variable) as exemplified above and you want to override it in a manual pipeline. Navigate to your project's **CI/CD > Pipelines** and click **Run pipeline**. Choose the branch you want to run the pipeline for, then add a new variable @@ -396,7 +390,6 @@ value you set for this specific pipeline: ![Manually overridden variable output](img/override_value_via_manual_pipeline_output.png) ## Environment variables expressions -{: #variables-expressions} > Introduced in GitLab 10.7. diff --git a/doc/ci/variables/img/ci_job_stage_output_example.png b/doc/ci/variables/img/ci_job_stage_output_example.png Binary files differindex 056238d5693..056238d5693 100755..100644 --- a/doc/ci/variables/img/ci_job_stage_output_example.png +++ b/doc/ci/variables/img/ci_job_stage_output_example.png diff --git a/doc/ci/variables/img/custom_variable_output.png b/doc/ci/variables/img/custom_variable_output.png Binary files differindex 50f3bceff9a..50f3bceff9a 100755..100644 --- a/doc/ci/variables/img/custom_variable_output.png +++ b/doc/ci/variables/img/custom_variable_output.png diff --git a/doc/ci/variables/img/new_custom_variable_example.png b/doc/ci/variables/img/new_custom_variable_example.png Binary files differindex d169c5f1806..d169c5f1806 100755..100644 --- a/doc/ci/variables/img/new_custom_variable_example.png +++ b/doc/ci/variables/img/new_custom_variable_example.png diff --git a/doc/ci/variables/img/override_value_via_manual_pipeline_output.png b/doc/ci/variables/img/override_value_via_manual_pipeline_output.png Binary files differindex 02369d57fb8..02369d57fb8 100755..100644 --- a/doc/ci/variables/img/override_value_via_manual_pipeline_output.png +++ b/doc/ci/variables/img/override_value_via_manual_pipeline_output.png diff --git a/doc/ci/variables/img/override_variable_manual_pipeline.png b/doc/ci/variables/img/override_variable_manual_pipeline.png Binary files differindex 3bcd354e096..3bcd354e096 100755..100644 --- a/doc/ci/variables/img/override_variable_manual_pipeline.png +++ b/doc/ci/variables/img/override_variable_manual_pipeline.png diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md index b429dc8c8be..846c539daab 100644 --- a/doc/ci/variables/predefined_variables.md +++ b/doc/ci/variables/predefined_variables.md @@ -36,7 +36,7 @@ future GitLab releases.** | `CI_COMMIT_TAG` | 9.0 | 0.5 | The commit tag name. Present only when building tags. | | `CI_COMMIT_TITLE` | 10.8 | all | The title of the commit - the full first line of the message | | `CI_CONFIG_PATH` | 9.4 | 0.5 | The path to CI config file. Defaults to `.gitlab-ci.yml` | -| `CI_DEBUG_TRACE` | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled | +| `CI_DEBUG_TRACE` | all | 1.7 | Whether [debug tracing](README.md#debug-tracing) is enabled | | `CI_DEPLOY_PASSWORD` | 10.8 | all | Authentication password of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.| | `CI_DEPLOY_USER` | 10.8 | all | Authentication username of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.| | `CI_DISPOSABLE_ENVIRONMENT` | all | 10.1 | Marks that the job is executed in a disposable environment (something that is created only for this job and disposed of/destroyed after the execution - all executors except `shell` and `ssh`). If the environment is disposable, it is set to true, otherwise it is not defined at all. | diff --git a/doc/ci/variables/where_variables_can_be_used.md b/doc/ci/variables/where_variables_can_be_used.md index 1218ac0b071..091ddcb0bae 100644 --- a/doc/ci/variables/where_variables_can_be_used.md +++ b/doc/ci/variables/where_variables_can_be_used.md @@ -111,4 +111,4 @@ They are: - Script execution shell. - Not supported: - For definitions where the ["Expansion place"](#gitlab-ciyml-file) is GitLab. - - In the `only` and `except` [variables expressions](README.md#variables-expressions). + - In the `only` and `except` [variables expressions](README.md#environment-variables-expressions). diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 1d76f911943..5e44de13b51 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -518,7 +518,7 @@ end-to-end: - $CI_COMMIT_MESSAGE =~ /skip-end-to-end-tests/ ``` -Learn more about [variables expressions](../variables/README.md#variables-expressions). +Learn more about [variables expressions](../variables/README.md#environment-variables-expressions). #### `only:changes`/`except:changes` @@ -2249,7 +2249,7 @@ Runner itself](../variables/README.md#predefined-environment-variables). One example would be `CI_COMMIT_REF_NAME` which has the value of the branch or tag name for which project is built. Apart from the variables you can set in `.gitlab-ci.yml`, there are also the so called -[Variables](../variables/README.md#variables) +[Variables](../variables/README.md#gitlab-cicd-environment-variables) which can be set in GitLab's UI. Learn more about [variables and their priority][variables]. diff --git a/doc/development/contributing/index.md b/doc/development/contributing/index.md index b39c302453b..8b1d014e101 100644 --- a/doc/development/contributing/index.md +++ b/doc/development/contributing/index.md @@ -3,9 +3,6 @@ Thank you for your interest in contributing to GitLab. This guide details how to contribute to GitLab in a way that is easy for everyone. -We want to create a welcoming environment for everyone who is interested in contributing. -Please visit our [Code of Conduct page](https://about.gitlab.com/contributing/code-of-conduct) to learn more about our commitment to an open and welcoming environment. - For a first-time step-by-step guide to the contribution process, please see ["Contributing to GitLab"](https://about.gitlab.com/contributing/). @@ -33,8 +30,8 @@ vulnerabilities. ## Code of conduct -Our code of conduct can be found on the -["Contributing to GitLab"](https://about.gitlab.com/contributing/) page. +We want to create a welcoming environment for everyone who is interested in contributing. +Please visit our [Code of Conduct page](https://about.gitlab.com/community/contribute/code-of-conduct/) to learn more about our commitment to an open and welcoming environment. ## Closing policy for issues and merge requests diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md index a4da34a50ce..4a2ff64807e 100644 --- a/doc/development/documentation/index.md +++ b/doc/development/documentation/index.md @@ -48,29 +48,21 @@ as its markdown rendering engine. See the [GitLab Markdown Guide](https://about. Adhere to the [Documentation Style Guide](styleguide.md). If a style standard is missing, you are welcome to suggest one via a merge request. -## Documentation directory structure +## Documentation types and organization The documentation is structured based on the GitLab UI structure itself, separated by [`user`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/user), [`administrator`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/administration), and [`contributor`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/development). -In order to have a [solid site structure](https://searchengineland.com/seo-benefits-developing-solid-site-structure-277456) for our documentation, -all docs should be linked. Every new document should be cross-linked to its related documentation, and linked from its topic-related index, when existent. - -The directories `/workflow/`, `/gitlab-basics/`, `/university/`, and `/articles/` have -been **deprecated** and the majority their docs have been moved to their correct location -in small iterations. Please do not create new docs in these folders. Organize docs by product area and subject, not type. +Organize docs by product area and subject, not type. For example, do not create groupings of similar media types +(e.g. indexes of all articles, videos, etc.). -### Documentation files - -- When you create a new directory, always start with an `index.md` file. - Do not use another file name and **do not** create `README.md` files. -- **Do not** use special chars and spaces, or capital letters in file names, - directory names, branch names, and anything that generates a path. -- Max screenshot size: 100KB. -- We do not support videos (yet). +Similarly, we do not use glossaries or FAQs. Such grouping of content by type makes +it difficult to browse for the information you need and difficult to maintain up-to-date content. +Instead, organize content by its subject (e.g. everything related to CI goes together) +and cross-link between any related content. -### Location and naming documents +### Location and naming of files Our goal is to have a clear hierarchical structure with meaningful URLs like `docs.gitlab.com/user/project/merge_requests/`. With this pattern, @@ -78,16 +70,8 @@ you can immediately tell that you are navigating to user-related documentation about project features; specifically about merge requests. Our site's paths match those of our repository, so the clear structure also makes documentation easier to update. -While the documentation is home to a variety of content types, we do not organize by content type. -For example, do not create groupings of similar media types (e.g. indexes of all articles, videos, etc.). -Similarly, we do not use glossaries or FAQs. Such grouping of content by type makes -it difficult to browse for the information you need and difficult to maintain up-to-date content. -Instead, organize content by its subject (e.g. everything related to CI goes together) -and cross-link between any related content. - -Do not simply link out to GitLab technical blog posts. There should be an up-to-date -single source of truth on the topic within the documentation, and the top of the -blog post should be updated to link to that doc. +In order to have a [solid site structure](https://searchengineland.com/seo-benefits-developing-solid-site-structure-277456) for our documentation, +all docs should be linked at least from its higher-level index page if not also from other relevant locations. The table below shows what kind of documentation goes where. @@ -102,13 +86,18 @@ The table below shows what kind of documentation goes where. | `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. | | `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`): all resources for that topic (user and admin documentation, articles, and third-party docs) | -**General rules & best practices:** +**Rules and best practices:** +1. When you create a new directory, always start with an `index.md` file. + Do not use another file name and **do not** create `README.md` files. +1. **Do not** use special chars and spaces, or capital letters in file names, + directory names, branch names, and anything that generates a path. 1. When creating a new document and it has more than one word in its name, make sure to use underscores instead of spaces or dashes (`-`). For example, a proper naming would be `import_projects_from_github.md`. The same rule applies to images. -1. Start a new directory with an `index.md` file. +1. For image files, do not exceed 100KB. +1. We do not yet support embedded videos. Please link out. 1. There are four main directories, `user`, `administration`, `api` and `development`. 1. The `doc/user/` directory has five main subdirectories: `project/`, `group/`, `profile/`, `dashboard/` and `admin_area/`. @@ -129,6 +118,9 @@ The table below shows what kind of documentation goes where. 1. The `doc/topics/` directory holds topic-related technical content. Create `doc/topics/topic-name/subtopic-name/index.md` when subtopics become necessary. General user- and admin- related documentation, should be placed accordingly. +1. The directories `/workflow/`, `/university/`, and `/articles/` have +been **deprecated** and the majority their docs have been moved to their correct location +in small iterations. If you are unsure where a document or a content addition should live, this should not stop you from authoring and contributing. You can use your best judgment and diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index c0386290785..1cefe48b4c1 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -14,9 +14,8 @@ For programmatic help adhering to the guidelines, see [linting](index.md#linting ## Files -- [Directory structure](index.md#location-and-naming-documents): place the docs - in the correct location. -- [Documentation files](index.md#documentation-files): name the files accordingly. +See the [Documentation types and organization](index.md#documentation-types-and-organization) section +on the index page for information on how to structure and name files across the GitLab documentation. DANGER: **Attention:** **Do not** use capital letters, spaces, or special chars in file names, @@ -28,41 +27,43 @@ a test that will fail if it spots a new `README.md` file. ### Markdown -The [documentation website](https://docs.gitlab.com) had its markdown engine migrated from [Redcarpet to GitLab Kramdown](https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/108) -in October 2018. +The [documentation website](https://docs.gitlab.com) uses GitLab Kramdown as its Markdown rendering engine [as of October 2018](https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/108). For a complete Kramdown reference, see the [GitLab Markdown Kramdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/). The [`gitlab-kramdown`](https://gitlab.com/gitlab-org/gitlab_kramdown) gem will support all [GFM markup](../../user/markdown.md) in the future. For now, -use regular markdown markup, following the rules on this style guide. For a complete -Kramdown reference, check the [GitLab Markdown Kramdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/). -Use Kramdown markup wisely: do not overuse its specific markup (e.g., `{:.class}`) as it will not render properly in -[`/help`](index.md#gitlab-help). +use regular markdown markup, following the rules in the linked style guide. + +Note that Kramdown-specific markup (e.g., `{:.class}`) will not render properly in on GitLab instances in [`/help`](index.md#gitlab-help). ## Content These guidelines help toward the goal of having every user's search of documentation yield a useful result, and ensuring content is helpful and easy to consume. -- What to include: - - Any and all helpful information, processes, and tips for implementing, - using, and troubleshooting GitLab features. [The documentation is the single source of truth](https://about.gitlab.com/handbook/documentation/#documentation-as-single-source-of-truth-ssot) - for this information. - - 'Risky' or niche problem-solving steps. There is no reason to withhold these or - store them elsewhere; simply include them along with the rest of the docs including all necessary - detail, such as specific warnings and caveats about potential ramifications. - - Any content types/sources, if relevant to users or admins. You can freely - include presentations, videos, etc.; no matter who it was originally written for, - if it is helpful to any of our audiences, we can include it. If an outside source - that's under copyright, rephrase, or summarize and link out; do not copy and paste. - - All applicable subsections as described on the [structure and template](structure.md) page, - with files organized in the [correct directory](index.md#documentation-directory-structure). +### Subject matter + +[The documentation is the single source of truth (SSOT)](https://about.gitlab.com/handbook/documentation/#documentation-as-single-source-of-truth-ssot) for any and all helpful information and processes needed to learn about, implement, use, and troubleshoot GitLab features. Note that this includes problem-solving actions that may address rare cases or be considered 'risky', so long as proper context is provided. See the SSOT link for more detail. + +### Media types and sources + +Include any media types/sources, if relevant to readers. You can freely include or link presentations, diagrams, videos, etc.; no matter who it was originally composed for, if it is helpful to any of our audiences, we can include it. + + - If you use an image that has a separate source file (for example, a vector or diagram format), link the image to the source file so that it may be reused or updated by anyone. + - Do not copy and paste content from other sources unless it is a limited quotation with the source cited. Typically it is better to either rephrase relevant information in your own words or link out to the other source. + +### Structure across documents + +- Place files in the correct directory per the [Documentation directory structure](index.md#documentation-types-and-organization) guidelines. +- To avoid duplication, do not include the same information in multiple places. Instead, choose one 'single source of truth (SSOT)' location and link from other relevant locations. +- When referencing other GitLab products and features, link to their respective docs. +- When referencing third-party products or technologies, link out to their external sites, documentation, and resources. + +### Structure within documents + +- Include any and all applicable subsections as described on the [structure and template](structure.md) page, - To ensure discoverability, link to each doc from its higher-level index page and other related pages. -- When referencing other GitLab products and features, link to their - respective docs; when referencing third-party products or technologies, - link out to their external sites, documentation, and resources. -- Do not duplicate information. - Structure content in alphabetical order in tables, lists, etc., unless there is - a logical reason not to (for example, when mirroring the UI or an ordered sequence). + a logical reason not to (for example, when mirroring the UI or an otherwise ordered sequence). ## Language diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index 9452593c510..c5a1d915be6 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -19,6 +19,11 @@ CE specs should remain untouched as much as possible and extra specs should be added for EE. Licensed features can be stubbed using the spec helper `stub_licensed_features` in `EE::LicenseHelpers`. +You can force Webpack to act as CE by either deleting the `ee/` directory or by +setting the [`IS_GITLAB_EE` environment variable](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/config/helpers/is_ee_env.js) +to something that evaluates as `false`. The same works for running tests +(for example `IS_GITLAB_EE=0 yarn jest`). + [ee-as-ce]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2500 ## Separation of EE code diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index a34d1fcec89..437ce9abc7d 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -38,7 +38,7 @@ _For consistency purposes, we recommend you to follow the same structure._ Let's look into each of them: -### A `index.js` file +### An `index.js` file This is the index file of your new feature. This is where the root Vue instance of the new feature should be. @@ -46,7 +46,7 @@ of the new feature should be. The Store and the Service should be imported and initialized in this file and provided as a prop to the main component. -Don't forget to follow [these steps][page_specific_javascript]. +Be sure to read about [page-specific JavaScript][page_specific_javascript]. ### Bootstrapping Gotchas #### Providing data from HAML to JavaScript @@ -240,7 +240,7 @@ One should apply to be a Vue.js expert by opening an MR when the Merge Request's [vue-docs]: http://vuejs.org/guide/index.html [issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards [environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments -[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript +[page_specific_javascript]: ./performance.md#page-specific-javascript [component-system]: https://vuejs.org/v2/guide/#Composing-with-Components [state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch [one-way-data-flow]: https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md index 7b54fa6289c..7d52cac5f7e 100644 --- a/doc/development/fe_guide/vuex.md +++ b/doc/development/fe_guide/vuex.md @@ -83,7 +83,7 @@ In this file, we will write the actions that will call the respective mutations: export const requestUsers = ({ commit }) => commit(types.REQUEST_USERS); export const receiveUsersSuccess = ({ commit }, data) => commit(types.RECEIVE_USERS_SUCCESS, data); - export const receiveUsersError = ({ commit }, error) => commit(types.REQUEST_USERS_ERROR, error); + export const receiveUsersError = ({ commit }, error) => commit(types.RECEIVE_USERS_ERROR, error); export const fetchUsers = ({ state, dispatch }) => { dispatch('requestUsers'); @@ -175,7 +175,7 @@ Remember that actions only describe that something happened, they don't describe state.users = data; state.isLoading = false; }, - [types.REQUEST_USERS_ERROR](state, error) { + [types.RECEIVE_USERS_ERROR](state, error) { state.isLoading = false; }, [types.REQUEST_ADD_USER](state, user) { diff --git a/doc/development/gitaly.md b/doc/development/gitaly.md index 27b69ba8278..bfde26dbe4a 100644 --- a/doc/development/gitaly.md +++ b/doc/development/gitaly.md @@ -6,7 +6,7 @@ Workhorse and GitLab-Shell. ## Beginner's guide Start by reading the gitaly repository's -[Beginner's guide to Gitaly contributions](https://gitlab.com/gitlab-org/gitaly/blob/master/doc/beginners_guide.md). +[Beginner's guide to Gitaly contributions](https://gitlab.com/gitlab-org/gitaly/blob/master/doc/beginners_guide.md). It describes how to setup gitaly, the various components of gitaly and what they do, and how to run its test suites. ## Developing new Git features @@ -192,7 +192,7 @@ GITALY_REPO_URL=https://gitlab+deploy-token-1000:token-here@gitlab.com/nick.thom To use a custom Gitaly repository in CI, for instance if you want your GitLab fork to always use your own Gitaly fork, set `GITALY_REPO_URL` -as a [CI environment variable](../ci/variables/README.md#variables). +as a [CI environment variable](../ci/variables/README.md#gitlab-cicd-environment-variables). --- diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index 7262c04d746..e41148360f2 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -216,8 +216,10 @@ project, one project will do for the entire file. This can be achieved by using reloads or recreates the model, _only_ if needed. That is, when you changed properties or destroyed the object. -There is one gotcha; you can't reference a model defined in a `let` block in a -`set` block. +Note that you can't reference a model defined in a `let` block in a `set` block. + +Also, `set` is not supported in `:js` specs since those don't use transactions +to clean up database state after each example. ### Time-sensitive tests diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md index 818a03a6f02..6e2a09fc030 100644 --- a/doc/gitlab-basics/create-issue.md +++ b/doc/gitlab-basics/create-issue.md @@ -1,5 +1,5 @@ --- -redirect_to: '../user/project/issues/index.md#new-issue' +redirect_to: '../user/project/issues/index.md#issue-actions' --- -This document was moved to [another location](../user/project/issues/index.md#new-issue). +This document was moved to [another location](../user/project/issues/index.md#issue-actions). diff --git a/doc/gitlab-basics/fork-project.md b/doc/gitlab-basics/fork-project.md index 6c232fe6086..a128a7c7dd3 100644 --- a/doc/gitlab-basics/fork-project.md +++ b/doc/gitlab-basics/fork-project.md @@ -1,8 +1,8 @@ # How to fork a project -A fork is a copy of an original repository that you can put in another namespace -where you can experiment and apply changes that you can later decide if -publishing or not, without affecting your original project. +A fork is a copy of an original repository that you put in another namespace +where you can experiment and apply changes that you can later decide whether or +not to share, without affecting the original project. It takes just a few steps to fork a project in GitLab. diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 574ba961cb0..56db7b5eb3a 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -409,7 +409,7 @@ an access key from the Google console first: 1. Select "Interoperability" and create an access key 1. Make note of the "Access Key" and "Secret" and replace them in the configurations below -1. In the buckets advanced settings ensure the Access Control option "Set object-level +1. In the buckets advanced settings ensure the Access Control option "Set object-level and bucket-level permissions" is selected 1. Make sure you already have a bucket created @@ -632,7 +632,7 @@ directory (repositories, uploads). To restore a backup, you will also need to restore `/etc/gitlab/gitlab-secrets.json` (for Omnibus packages) or `/home/git/gitlab/.secret` (for installations from source). This file contains the database encryption key, -[CI/CD variables](../ci/variables/README.md#variables), and +[CI/CD variables](../ci/variables/README.md#gitlab-cicd-environment-variables), and variables used for [two-factor authentication](../user/profile/account/two_factor_authentication.md). If you fail to restore this encryption key file along with the application data backup, users with two-factor authentication enabled and GitLab Runners will @@ -848,15 +848,24 @@ including (but not restricted to): * [Project mirroring](../workflow/repository_mirroring.md) * [Web hooks](../user/project/integrations/webhooks.md) -In the case of CI/CD, variables, you might experience some weird behavior, like -stuck jobs or 500 errors. In that case, you can try removing contents of the -`ci_group_variables` and `ci_project_variables` tables from the database. +In cases like CI/CD variables and Runner authentication, you might +experience some unexpected behavior such as: + +- Stuck jobs. +- 500 errors. + +In this case, you are required to reset all the tokens for CI/CD variables +and Runner Authentication, which is described in more detail below. After +resetting the tokens, you should be able to visit your project and the jobs +will have started running again. CAUTION: **Warning:** Use the following commands at your own risk, and make sure you've taken a backup beforehand. -1. Enter the Rails console: +#### Reset CI/CD variables + +1. Enter the DB console: For Omnibus GitLab packages: @@ -889,8 +898,39 @@ backup beforehand. 1. You may need to reconfigure or restart GitLab for the changes to take effect. -You should now be able to visit your project, and the jobs will start -running again. + +#### Reset Runner registration tokens + +1. Enter the DB console: + + For Omnibus GitLab packages: + + ```sh + sudo gitlab-rails dbconsole + ``` + + For installations from source: + + ```sh + sudo -u git -H bundle exec rails dbconsole RAILS_ENV=production + ``` + +1. Clear all the tokens for projects, groups, and the whole instance: + +CAUTION: **Caution:** +The last UPDATE operation will stop the runners being able to pick up +new jobs. You must register new runners. + + ```sql + -- Clear project tokens + UPDATE projects SET runners_token = null, runners_token_encrypted = null; + -- Clear group tokens + UPDATE namespaces SET runners_token = null, runners_token_encrypted = null; + -- Clear instance tokens + UPDATE application_settings SET runners_registration_token_encrypted = null; + -- Clear runner tokens + UPDATE ci_runners SET token = null, token_encrypted = null; + ``` A similar strategy can be employed for the remaining features - by removing the data that cannot be decrypted, GitLab can be brought back into working order, diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 9060360e6a2..1df492ba82c 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -141,7 +141,7 @@ in any of the following places: NOTE: **Note** The Auto DevOps base domain variable (`KUBE_INGRESS_BASE_DOMAIN`) follows the same order of precedence -as other environment [variables](../../ci/variables/README.md#priority-of-variables). +as other environment [variables](../../ci/variables/README.md#priority-of-environment-variables). A wildcard DNS A record matching the base domain(s) is required, for example, given a base domain of `example.com`, you'd need a DNS entry like: @@ -669,7 +669,7 @@ repo or by specifying a project variable: file in it, Auto DevOps will detect the chart and use it instead of the [default one](https://gitlab.com/charts/auto-deploy-app). This can be a great way to control exactly how your application is deployed. -- **Project variable** - Create a [project variable](../../ci/variables/README.md#variables) +- **Project variable** - Create a [project variable](../../ci/variables/README.md#gitlab-cicd-environment-variables) `AUTO_DEVOPS_CHART` with the URL of a custom chart to use or create two project variables `AUTO_DEVOPS_CHART_REPOSITORY` with the URL of a custom chart repository and `AUTO_DEVOPS_CHART` with the path to the chart. ### Custom Helm chart per environment **[PREMIUM]** @@ -770,7 +770,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac TIP: **Tip:** Set up the replica variables using a -[project variable](../../ci/variables/README.md#variables) +[project variable](../../ci/variables/README.md#gitlab-cicd-environment-variables) and scale your application by just redeploying it! CAUTION: **Caution:** diff --git a/doc/university/high-availability/aws/README.md b/doc/university/high-availability/aws/README.md index 1906655dfa5..01d5073ab7e 100644 --- a/doc/university/high-availability/aws/README.md +++ b/doc/university/high-availability/aws/README.md @@ -83,7 +83,7 @@ our newly created VPC. ### Internet Gateway Now still on the same dashboard head over to Internet Gateways and -create a new one. After its created pres on the `Attach to VPC` button and +create a new one. After its created press on the `Attach to VPC` button and select our VPC. ![Internet Gateway](img/ig.png) diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 9c3f6fcec9b..74735886350 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -58,7 +58,7 @@ By doing so: ## Issues and merge requests within a group Issues and merge requests are part of projects. For a given group, view all the -[issues](../project/issues/index.md#issues-per-group) and [merge requests](../project/merge_requests/index.md#merge-requests-per-group) across all the projects in that group, +[issues](../project/issues/index.md#issues-list) and [merge requests](../project/merge_requests/index.md#merge-requests-per-group) across all the projects in that group, together in a single list view. ## Create a new group diff --git a/doc/user/index.md b/doc/user/index.md index d408504249e..626246447f3 100644 --- a/doc/user/index.md +++ b/doc/user/index.md @@ -38,8 +38,8 @@ GitLab is a Git-based platform that integrates a great number of essential tools - Hosting code in repositories with version control. - Tracking proposals for new implementations, bug reports, and feedback with a - fully featured [Issue Tracker](project/issues/index.md#issue-tracker). -- Organizing and prioritizing with [Issue Boards](project/issues/index.md#issue-board). + fully featured [Issue Tracker](project/issues/index.md#issues-list). +- Organizing and prioritizing with [Issue Boards](project/issues/index.md#issue-boards). - Reviewing code in [Merge Requests](project/merge_requests/index.md) with live-preview changes per branch with [Review Apps](../ci/review_apps/index.md). - Building, testing, and deploying with built-in [Continuous Integration](../ci/README.md). diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 8239742969a..9891a43aa61 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -578,11 +578,11 @@ Alt-H2 ------ ``` -### Header IDs and links +#### Header IDs and links -All Markdown-rendered headers automatically get IDs, except in comments. +All Markdown-rendered headers automatically get IDs, which can be linked to, except in comments. -On hover, a link to those IDs becomes visible to make it easier to copy the link to the header to give it to someone else. +On hover, a link to those IDs becomes visible to make it easier to copy the link to the header to use it somewhere else. The IDs are generated from the content of the header according to the following rules: @@ -609,8 +609,8 @@ Would generate the following link IDs: 1. `this-header-has-spaces-in-it` 1. `this-header-has-a-in-it` 1. `this-header-has-unicode-in-it-한글` -1. `this-header-has-spaces-in-it` 1. `this-header-has-spaces-in-it-1` +1. `this-header-has-spaces-in-it-2` 1. `this-header-has-3-5-in-it-and-parentheses` Note that the Emoji processing happens before the header IDs are generated, so the Emoji is converted to an image which then gets removed from the ID. @@ -715,25 +715,25 @@ Becomes: There are two ways to create links, inline-style and reference-style. - [I'm an inline-style link](https://www.google.com) - - [I'm a reference-style link][Arbitrary case-insensitive reference text] - - [I'm a relative reference to a repository file](LICENSE) - - [I am an absolute reference within the repository](/doc/user/markdown.md) - - [I link to the Milestones page](/../milestones) +```markdown +[I'm an inline-style link](https://www.google.com) +[I'm a link to a repository file in the same directory](index.md) +[I am an absolute reference within the repository](/doc/user/index.md) +[I'm a relative link to the Milestones page](../README.md) - [You can use numbers for reference-style link definitions][1] +[I link to a section on a different markdown page, using a header ID](index.md#overview) +[I link to a different section on the same page, using the header ID](#header-ids-and-links) - Or leave it empty and use the [link text itself][] +[I'm a reference-style link][Arbitrary case-insensitive reference text] +[You can use numbers for reference-style link definitions][1] +Or leave it empty and use the [link text itself][] - Some text to show that the reference links can follow later. +Some text to show that the reference links can follow later. - [arbitrary case-insensitive reference text]: https://www.mozilla.org - [1]: http://slashdot.org - [link text itself]: https://www.reddit.com +[arbitrary case-insensitive reference text]: https://www.mozilla.org +[1]: http://slashdot.org +[link text itself]: https://www.reddit.com +``` >**Note:** Relative links do not allow referencing project files in a wiki page or wiki diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index b216b9f255c..61a30a775b0 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -33,6 +33,7 @@ From there, you can: - Set a [custom status](#current-status) for your profile - Manage your [commit email](#commit-email) for your profile - Manage [2FA](account/two_factor_authentication.md) +- Add details of [external accounts](#add-details-of-external-accounts). - Change your username and [delete your account](account/delete_account.md) - Manage applications that can [use GitLab as an OAuth provider](../../integration/oauth_provider.md#introduction-to-oauth) @@ -92,6 +93,16 @@ To enable private profile: NOTE: **Note:** You and GitLab admins can see your the abovementioned information on your profile even if it is private. +## Add details of external accounts + +GitLab allows you to add links to certain other external accounts you might have, like Skype and Twitter. They can help other users connect with you on other platforms. + +To add links to other accounts: + +1. Navigate to your **User Settings > Profile**. +1. In the **Main settings** section, locate and fill out fields for links to external accounts like Skype and Twitter. +1. Click the **Update profile settings** button. + ## Private contributions > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/14078) in GitLab 11.3. diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 878d30dddaa..1983513174c 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -314,7 +314,7 @@ install it manually. ## Installing applications -GitLab provides a one-click install for various applications which can +GitLab provides **GitLab Managed Apps**, a one-click install for various applications which can be added directly to your configured cluster. Those applications are needed for [Review Apps](../../../ci/review_apps/index.md) and [deployments](../../../ci/environments.md). You can install them after you @@ -545,7 +545,7 @@ The result will then be: ## Deployment variables The Kubernetes cluster integration exposes the following -[deployment variables](../../../ci/variables/README.md#deployment-variables) in the +[deployment variables](../../../ci/variables/README.md#deployment-environment-variables) in the GitLab CI/CD build environment. | Variable | Description | @@ -573,7 +573,7 @@ However, sometimes GitLab can not create them. In such instances, your job will This job failed because the necessary resources were not successfully created. ``` -To find the cause of this error when creating a namespace and service account, check the [logs](../../../administration/logs.md#sidekiqlog). +To find the cause of this error when creating a namespace and service account, check the [logs](../../../administration/logs.md#kuberneteslog). Common reasons for failure include: diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index ca19ce4d328..ad47b848bea 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -42,7 +42,7 @@ below. ## How it works The Issue Board builds on GitLab's existing -[issue tracking functionality](issues/index.md#issue-tracker) and +[issue tracking functionality](issues/index.md#issues-list) and leverages the power of [labels](labels.md) by utilizing them as lists of the scrum board. With the Issue Board you can have a different view of your issues while diff --git a/doc/user/project/issues/create_new_issue.md b/doc/user/project/issues/create_new_issue.md index 40040e44d64..9a147deecd4 100644 --- a/doc/user/project/issues/create_new_issue.md +++ b/doc/user/project/issues/create_new_issue.md @@ -7,7 +7,7 @@ the information illustrated on the image below. ![New issue from the issues list](img/new_issue.png) -Read through the [issues functionalities documentation](issues_functionalities.md#issues-functionalities) +Read through the [issue data and actions documentation](issue_data_and_actions.md#parts-of-an-issue) to understand these fields one by one. ## New issue from the Issue Tracker diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md index 675c280a12a..c82b7f100d2 100644 --- a/doc/user/project/issues/index.md +++ b/doc/user/project/issues/index.md @@ -1,170 +1,135 @@ # Issues -The GitLab Issue Tracker is an advanced and complete tool -for tracking the evolution of a new idea or the process -of solving a problem. +Issues are the fundamental medium for collaborating on ideas and planning work in GitLab. -It allows you, your team, and your collaborators to share -and discuss proposals before and while implementing them. +## Overview -GitLab Issues and the GitLab Issue Tracker are available in all -[GitLab Products](https://about.gitlab.com/pricing/) as -part of the [GitLab Workflow](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/). +The GitLab issue tracker is an advanced tool for collaboratively developing ideas, solving problems, and planning work. -## Use cases +Issues can allow you, your team, and your collaborators to share and discuss proposals before and during their implementation. +However, they can be used for a variety of other purposes, customized to your needs and workflow. -Issues can have endless applications. Just to exemplify, these are -some cases for which creating issues are most used: +Issues are always associated with a specific project, but if you have multiple projects in a group, +you can also view all the issues collectively at the group level. + +**Common use cases include:** - Discussing the implementation of a new idea -- Submitting feature proposals -- Asking questions -- Reporting bugs and malfunction -- Obtaining support +- Tracking tasks and work status +- Accepting feature proposals, questions, support requests, or bug reports - Elaborating new code implementations See also the blog post "[Always start a discussion with an issue](https://about.gitlab.com/2016/03/03/start-with-an-issue/)". -### Keep private things private - -For instance, let's assume you have a public project but want to start a discussion on something -you don't want to be public. With [Confidential Issues](#confidential-issues), -you can discuss private matters among the project members, and still keep -your project public, open to collaboration. - -### Streamline collaboration - -With [Multiple Assignees for Issues](https://docs.gitlab.com/ee/user/project/issues/multiple_assignees_for_issues.html), -available in [GitLab Starter](https://about.gitlab.com/pricing/) -you can streamline collaboration and allow shared responsibilities to be clearly displayed. -All assignees are shown across your workflows and receive notifications (as they -would as single assignees), simplifying communication and ownership. - -### Consistent collaboration - -Create [issue templates](#issue-templates) to make collaboration consistent and -containing all information you need. For example, you can create a template -for feature proposals and another one for bug reports. - -## Issue Tracker - -The Issue Tracker is the collection of opened and closed issues created in a project. -It is available for all projects, from the moment the project is created. - -Find the issue tracker by navigating to your **Project's homepage** > **Issues**. - -### Issues per project - -When you access your project's issues, GitLab will present them in a list, -and you can use the tabs available to quickly filter by open and closed issues. - -![Project issues list view](img/project_issues_list_view.png) - -You can also [search and filter](../../search/index.md#issues-and-merge-requests-per-project) the results more deeply with GitLab's search capacities. - -### Issues per group - -View issues in all projects in the group, including all projects of all descendant subgroups of the group. Navigate to **Group > Issues** to view these issues. This view also has the open and closed issues tabs. - -![Group Issues list view](img/group_issues_list_view.png) - -## GitLab Issues Functionalities - -The image bellow illustrates how an issue looks like: +## Parts of an issue + +Issues contain a variety of content and metadata, enabling a large range of flexibility in how they are used. Each issue can contain the following attributes, though some items may remain unset. + +<table class="borderless-table fixed-table"> +<tr> + <td> + <ul> + <li>Content</li> + <ul> + <li>Title</li> + <li>Description and tasks</li> + <li>Comments and other activity</li> + </ul> + <li>People</li> + <ul> + <li>Author</li> + <li>Assignee(s)</li> + </ul> + <li>State</li> + <ul> + <li>Status (open/closed)</li> + <li>Confidentiality</li> + <li>Tasks (completed vs. outstanding)</li> + </ul> + </ul> + </td> + <td> + <ul> + <li>Planning and tracking</li> + <ul> + <li>Milestone</li> + <li>Due date</li> + <li>Weight</li> + <li>Time tracking</li> + <li>Labels</li> + <li>Votes</li> + <li>Reaction emoji</li> + <li>Linked issues</li> + <li>Assigned epic</li> + <li>Unique issue number and URL</li> + </ul> + </ul> + </td> +</tr> +</table> + +## Viewing and managing issues + +While you can view and manage the full detail of an issue at its URL, you can also work with multiple issues at a time using the Issues List, Issue Boards, Epics **[ULTIMATE]**, and issue references. + +### Issue page ![Issue view](img/issues_main_view.png) -Learn more about it on the [GitLab Issues Functionalities documentation](issues_functionalities.md). - -## New issue +On an issue’s page, you can view all aspects of the issue, and you can also modify them if you you have the necessary [permissions](../../permissions.md). -Read through the [documentation on creating issues](create_new_issue.md). +For more information, see the [Issue Data and Actions](issue_data_and_actions.md) page. -## Closing issues +### Issues list -Learn distinct ways to [close issues](closing_issues.md) in GitLab. - -## Moving issues - -Read through the [documentation on moving issues](moving_issues.md). - -## Deleting issues +![Project issues list view](img/project_issues_list_view.png) -Read through the [documentation on deleting issues](deleting_issues.md) +On the Issues List, you can view all issues in the current project, or from multiple projects when opening the Issues List from the higher-level group context. Filter the issue list by [any search query](../../search/index.md#issues-and-merge-requests-per-project) and/or specific metadata, such as label(s), assignees(s), status, and more. From this view, you can also make certain changes [in bulk](../bulk_editing.md) to the displayed issues. -## Create a merge request from an issue +For more information, see the [Issue Data and Actions](issue_data_and_actions.md) page. -Learn more about it on the [GitLab Issues Functionalities documentation](issues_functionalities.md#18-new-merge-request). +### Issue boards -## Search for an issue +![Issue board](img/issue_board.png) -Learn how to [find an issue](../../search/index.md) by searching for and filtering them. +Issue boards are Kanban boards with columns that display issues based on their labels or their assignees**[PREMIUM]**. They offer the flexibility to manage issues using highly customizable workflows. -## Advanced features +You can reorder issues within a column, or drag an issue card to another column; its associated label or assignee will change to match that of the new column. The entire board can also be filtered to only include issues from a certain milestone or an overarching label. -### Confidential Issues +For more information, see the [Issue Boards](../issue_board.md) page. -Whenever you want to keep the discussion presented in a -issue within your team only, you can make that -[issue confidential](confidential_issues.md). Even if your project -is public, that issue will be preserved. The browser will -respond with a 404 error whenever someone who is not a project -member with at least [Reporter level](../../permissions.md#project-members-permissions) tries to -access that issue's URL. +### Epics **[ULTIMATE]** -Learn more about them on the [confidential issues documentation](confidential_issues.md). +Epics let you manage your portfolio of projects more efficiently and with less effort by tracking groups of issues that share a theme, across projects and milestones. -### Issue templates +For more information, see the [Epics](https://docs.gitlab.com/ee/user/group/epics/) page. -Create templates for every new issue. They will be available from -the dropdown menu **Choose a template** when you create a new issue: +### Related issues **[STARTER]** -![issue template](img/issue_template.png) +You can mark two issues as related, so that when viewing each one, the other is always listed in its Related Issues section. This can help display important context, such as past work, dependencies, or duplicates. -Learn more about them on the [issue templates documentation](../../project/description_templates.md#creating-issue-templates). +For more information, see [Related Issues](https://docs.gitlab.com/ee/user/project/issues/related_issues.html). ### Crosslinking issues -Learn more about [crosslinking](crosslinking_issues.md) issues and merge requests. - -### Issue Board - -The [GitLab Issue Board](https://about.gitlab.com/features/issueboard/) is a way to -enhance your workflow by organizing and prioritizing issues in GitLab. - -![Issue board](img/issue_board.png) - -Find GitLab Issue Boards by navigating to your **Project's Dashboard** > **Issues** > **Board**. - -Read through the documentation for [Issue Boards](../issue_board.md) -to find out more about this feature. - -With [GitLab Starter](https://about.gitlab.com/pricing/), you can also -create various boards per project with [Multiple Issue Boards](../issue_board.html#multiple-issue-boards-starter). - -### Import Issues from CSV - -You can import a CSV file containing issue titles and descriptions to create -a batch of issues simultaneously. - -When you navigate to the Issues list page, an import button is displayed. - -For further details, see [Importing issues from CSV](csv_import.md) - -### External Issue Tracker - -Alternatively to GitLab's built-in Issue Tracker, you can also use an [external -tracker](../../../integration/external-issue-tracker.md) such as Jira, Redmine, -YouTrack, or Bugzilla. - -### Issue API +When you reference an issue from another issue or merge request by including its URL or ID, the referenced issue displays a message in the Activity stream about the reference, with a link to the other issue or MR. -See the [API documentation](../../../api/issues.md). +For more information, see [Crosslinking issues](crosslinking_issues.md). -### Bulk editing issues +## Issue actions -See the [bulk editing issues](../../project/bulk_editing.md) page. +- [Create an issue](create_new_issue.md) +- [Create an issue from a template](../../project/description_templates.md#using-the-templates) +- [Close an issue](closing_issues.md) +- [Move an issue](moving_issues.md) +- [Delete an issue](deleting_issues.md) +- [Create a merge request from an issue](issue_data_and_actions.md#18-new-merge-request) -### Similar issues +## Advanced issue management -See the [similar issues](similar_issues.md) page. +- [Bulk edit issues](../bulk_editing.md) - From the Issues List, select multiple issues in order to change their status, assignee, milestone, or labels in bulk. +- [Import issues](csv_import.md) +- [Export issues](https://docs.gitlab.com/ee/user/project/issues/csv_export.html) **[STARTER]** +- [Issues API](../../../api/issues.md) +- Configure an [external issue tracker](../../../integration/external-issue-tracker.md) such as Jira, Redmine, +or Bugzilla. diff --git a/doc/user/project/issues/issues_functionalities.md b/doc/user/project/issues/issue_data_and_actions.md index 27b9dc51760..653bd94e513 100644 --- a/doc/user/project/issues/issues_functionalities.md +++ b/doc/user/project/issues/issue_data_and_actions.md @@ -1,8 +1,8 @@ -# GitLab Issues Functionalities +# Issue Data and Actions Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues. -## Issues Functionalities +## Parts of an Issue The image below illustrates what an issue looks like: @@ -77,7 +77,7 @@ can be changed as many times as needed. Categorize issues by giving them [labels](../labels.md). They help to organize workflows, and they enable you to work with the -[GitLab Issue Board](index.md#issue-board). +[GitLab Issue Board](index.md#issue-boards). Group Labels, which allow you to use the same labels for a group of projects, can be also given to issues. They work exactly the same, diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 01a3a5bbbe1..ba7d05a7ad7 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -219,6 +219,64 @@ apply the patches. The target branch can be specified using the [`/target_branch` quick action](../quick_actions.md). If the source branch already exists, the patches will be applied on top of it. +## Git push options + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/26752) in GitLab 11.10. + +NOTE: **Note:** +Git push options are only available with Git 2.10 or newer. + +GitLab supports using +[Git push options](https://git-scm.com/docs/git-push#Documentation/git-push.txt--oltoptiongt) +to perform the following actions against merge requests at the same time +as pushing changes: + +- Create a new merge request for the pushed branch. +- Set the target of the merge request to a particular branch. +- Set the merge request to merge when its pipeline succeeds. + +### Create a new merge request using git push options + +To create a new merge request for a branch, use the +`merge_request.create` push option: + +```sh +git push -o merge_request.create +``` + +### Set the target branch of a merge request using git push options + +To update an existing merge request's target branch, use the +`merge_request.target=<branch_name>` push option: + +```sh +git push -o merge_request.target=branch_name +``` + +You can also create a merge request and set its target branch at the +same time using a `-o` flag per push option: + +```sh +git push -o merge_request.create -o merge_request.target=branch_name +``` + +### Set merge when pipeline succeeds using git push options + +To set an existing merge request to +[merge when its pipeline succeeds](merge_when_pipeline_succeeds.md), use +the `merge_request.merge_when_pipeline_succeeds` push option: + +```sh +git push -o merge_request.merge_when_pipeline_succeeds +``` + +You can also create a merge request and set it to merge when its +pipeline succeeds at the same time using a `-o` flag per push option: + +```sh +git push -o merge_request.create -o merge_request.merge_when_pipeline_succeeds +``` + ## Find the merge request that introduced a change > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/2383) in GitLab 10.5. @@ -254,7 +312,7 @@ To prevent merge requests from accidentally being accepted before they're completely ready, GitLab blocks the "Accept" button for merge requests that have been marked as a **Work In Progress**. -[Learn more about settings a merge request as "Work In Progress".](work_in_progress_merge_requests.md) +[Learn more about setting a merge request as "Work In Progress".](work_in_progress_merge_requests.md) ## Merge request diff file navigation diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md index ba57b61f70a..9f2bc281f85 100644 --- a/doc/user/project/pages/getting_started_part_three.md +++ b/doc/user/project/pages/getting_started_part_three.md @@ -115,6 +115,8 @@ If using a [DNS A record](#dns-a-record), you can place the TXT record directly under the domain. If using a [DNS CNAME record](#dns-cname-record), the two record types won't co-exist, so you need to place the TXT record in a special subdomain of its own. +If the domain cannot be verified for 7 days, it will be removed from the GitLab project. + #### TL;DR For root domains (`domain.com`), set a DNS `A` record and verify your @@ -149,7 +151,7 @@ verify your domain's ownership with a TXT record: Once you've set the DNS record, you'll need navigate to your project's **Setting > Pages** and click **+ New domain** to add your custom domain to GitLab Pages. You can choose whether to add an [SSL/TLS certificate](#ssltls-certificates) -to make your website accessible under HTTPS or leave it blank. If don't add a certificate, +to make your website accessible under HTTPS or leave it blank. If you don't add a certificate, your site will be accessible only via HTTP: ![Add new domain](img/add_certificate_to_pages.png) diff --git a/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md b/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md index f639188684b..ea22f3e905b 100644 --- a/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md +++ b/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md @@ -134,14 +134,14 @@ Now that your certificate has been issued, let's add it to your Pages site: sudo cat /etc/letsencrypt/live/example.com/fullchain.pem | pbcopy ``` -1. Copy and paste the public key into the second field **Key (PEM)**: +1. Copy and paste the private key into the second field **Key (PEM)**: ```bash sudo cat /etc/letsencrypt/live/example.com/privkey.pem | pbcopy ``` 1. Click **Save changes** to apply them to your website. -1. Wait a few minutes for DNS propagation. +1. Wait a few minutes for the configuration changes to take effect. 1. Visit your website at `https://example.com`. To force `https` connections on your site, navigate to your diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index 4a989afad4d..8b762307ac4 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -182,4 +182,4 @@ https://example.gitlab.com/<namespace>/<project>/badges/<branch>/coverage.svg?st ## Environment Variables -[Environment variables](../../../ci/variables/README.html#variables) can be set in an environment to be available to a runner. +[Environment variables](../../../ci/variables/README.html#gitlab-cicd-environment-variables) can be set in an environment to be available to a runner. diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 2dd3120d3fc..4bdac278add 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -277,6 +277,7 @@ module API expose :statistics, using: 'API::Entities::ProjectStatistics', if: -> (project, options) { options[:statistics] && Ability.allowed?(options[:current_user], :read_statistics, project) } + expose :external_authorization_classification_label # rubocop: disable CodeReuse/ActiveRecord def self.preload_relation(projects_relation, options = {}) @@ -663,7 +664,11 @@ module API expose(:user_notes_count) { |merge_request, options| issuable_metadata(merge_request, options, :user_notes_count) } expose(:upvotes) { |merge_request, options| issuable_metadata(merge_request, options, :upvotes) } expose(:downvotes) { |merge_request, options| issuable_metadata(merge_request, options, :downvotes) } - expose :author, :assignee, using: Entities::UserBasic + expose :assignee, using: ::API::Entities::UserBasic do |merge_request| + merge_request.assignee + end + expose :author, :assignees, using: Entities::UserBasic + expose :source_project_id, :target_project_id expose :labels do |merge_request| # Avoids an N+1 query since labels are preloaded @@ -1116,6 +1121,8 @@ module API expose(:default_snippet_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_snippet_visibility) } expose(:default_group_visibility) { |setting, _options| Gitlab::VisibilityLevel.string_level(setting.default_group_visibility) } + expose(*::ApplicationSettingsHelper.external_authorization_service_attributes) + # support legacy names, can be removed in v5 expose :password_authentication_enabled_for_web, as: :password_authentication_enabled expose :password_authentication_enabled_for_web, as: :signin_enabled @@ -1294,10 +1301,6 @@ module API expose :id, :name, :slug, :external_url end - class Environment < EnvironmentBasic - expose :project, using: Entities::BasicProjectDetails - end - class Deployment < Grape::Entity expose :id, :iid, :ref, :sha, :created_at expose :user, using: Entities::UserBasic @@ -1305,6 +1308,11 @@ module API expose :deployable, using: Entities::Job end + class Environment < EnvironmentBasic + expose :project, using: Entities::BasicProjectDetails + expose :last_deployment, using: Entities::Deployment, if: { last_deployment: true } + end + class LicenseBasic < Grape::Entity expose :key, :name, :nickname expose :url, as: :html_url diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 5b0f3b914cb..6cd43923559 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -101,6 +101,21 @@ module API status 200 present environment, with: Entities::Environment, current_user: current_user end + + desc 'Get a single environment' do + success Entities::Environment + end + params do + requires :environment_id, type: Integer, desc: 'The environment ID' + end + get ':id/environments/:environment_id' do + authorize! :read_environment, user_project + + environment = user_project.environments.find(params[:environment_id]) + present environment, with: Entities::Environment, current_user: current_user, + except: [:project, { last_deployment: [:environment] }], + last_deployment: true + end end end end diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index 3fd824877ae..71c30ec99a5 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -43,6 +43,28 @@ module API ::MergeRequests::GetUrlsService.new(project).execute(params[:changes]) end + def process_mr_push_options(push_options, project, user, changes) + output = {} + + service = ::MergeRequests::PushOptionsHandlerService.new( + project, + user, + changes, + push_options + ).execute + + if service.errors.present? + output[:warnings] = push_options_warning(service.errors.join("\n\n")) + end + + output + end + + def push_options_warning(warning) + options = Array.wrap(params[:push_options]).map { |p| "'#{p}'" }.join(' ') + "Error encountered with push options #{options}: #{warning}" + end + def redis_ping result = Gitlab::Redis::SharedState.with { |redis| redis.ping } diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index 7b858dc2e72..aaf32dafca4 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -29,13 +29,13 @@ module API optional :printing_merge_request_link_enabled, type: Boolean, desc: 'Show link to create/view merge request when pushing from the command line' optional :merge_method, type: String, values: %w(ff rebase_merge merge), desc: 'The merge method used when merging merge requests' optional :initialize_with_readme, type: Boolean, desc: "Initialize a project with a README.md" + optional :external_authorization_classification_label, type: String, desc: 'The classification label for the project' end if Gitlab.ee? params :optional_project_params_ee do optional :repository_storage, type: String, desc: 'Which storage shard the repository is on. Available only to admins' optional :approvals_before_merge, type: Integer, desc: 'How many approvers should approve merge request by default' - optional :external_authorization_classification_label, type: String, desc: 'The classification label for the project' optional :mirror, type: Boolean, desc: 'Enables pull mirroring in a project' optional :mirror_trigger_builds, type: Boolean, desc: 'Pull mirroring triggers builds' end @@ -72,7 +72,8 @@ module API :tag_list, :visibility, :wiki_enabled, - :avatar + :avatar, + :external_authorization_classification_label ] end end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 9c7b9146c8f..00f0bbab231 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -256,19 +256,27 @@ module API post '/post_receive' do status 200 + output = {} # Messages to gitlab-shell + user = identify(params[:identifier]) + project = Gitlab::GlRepository.parse(params[:gl_repository]).first + push_options = Gitlab::PushOptions.new(params[:push_options]) + PostReceive.perform_async(params[:gl_repository], params[:identifier], - params[:changes], params[:push_options].to_a) + params[:changes], push_options.as_json) + + if Feature.enabled?(:mr_push_options, default_enabled: true) + mr_options = push_options.get(:merge_request) + output.merge!(process_mr_push_options(mr_options, project, user, params[:changes])) if mr_options.present? + end + broadcast_message = BroadcastMessage.current&.last&.message reference_counter_decreased = Gitlab::ReferenceCounter.new(params[:gl_repository]).decrease - output = { - merge_request_urls: merge_request_urls, + output.merge!( broadcast_message: broadcast_message, - reference_counter_decreased: reference_counter_decreased - } - - project = Gitlab::GlRepository.parse(params[:gl_repository]).first - user = identify(params[:identifier]) + reference_counter_decreased: reference_counter_decreased, + merge_request_urls: merge_request_urls + ) # A user is not guaranteed to be returned; an orphaned write deploy # key could be used diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 999a9cb5a82..000c00ea9f9 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -230,9 +230,13 @@ module API issue = user_project.issues.find_by!(iid: params.delete(:issue_iid)) authorize! :update_issue, issue - # Setting created_at time only allowed for admins and project/group owners - unless current_user.admin? || user_project.owner == current_user || current_user.owned_groups.include?(user_project.owner) - params.delete(:updated_at) + # Setting updated_at only allowed for admins and owners as well + if params[:updated_at].present? + if current_user.admin? || user_project.owner == current_user || current_user.owned_groups.include?(user_project.owner) + issue.system_note_timestamp = params[:updated_at] + else + params.delete(:updated_at) + end end update_params = declared_params(include_missing: false).merge(request: request, api: true) diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index e4b21b7d1c4..1cc0ecc6df8 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -20,6 +20,7 @@ module API def self.update_params_at_least_one_of %i[ assignee_id + assignee_ids description labels milestone_id @@ -184,6 +185,7 @@ module API params :optional_params do optional :description, type: String, desc: 'The description of the merge request' optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request' + optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue' optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request' optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names' optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging' @@ -231,6 +233,7 @@ module API mr_params = declared_params(include_missing: false) mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) + mr_params = convert_parameters_from_legacy_format(mr_params) merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute @@ -334,6 +337,7 @@ module API mr_params = declared_params(include_missing: false) mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present? + mr_params = convert_parameters_from_legacy_format(mr_params) merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request) diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb index ac8fe98e55e..f29a18e94cf 100644 --- a/lib/api/pipelines.rb +++ b/lib/api/pipelines.rb @@ -81,6 +81,19 @@ module API present pipeline, with: Entities::Pipeline end + desc 'Gets the variables for a given pipeline' do + detail 'This feature was introduced in GitLab 11.11' + success Entities::Variable + end + params do + requires :pipeline_id, type: Integer, desc: 'The pipeline ID' + end + get ':id/pipelines/:pipeline_id/variables' do + authorize! :read_pipeline_variable, pipeline + + present pipeline.variables, with: Entities::Variable + end + desc 'Deletes a pipeline' do detail 'This feature was introduced in GitLab 11.6' http_codes [[204, 'Pipeline was deleted'], [403, 'Forbidden']] diff --git a/lib/api/settings.rb b/lib/api/settings.rb index d96cdc31212..b064747e5fc 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -168,7 +168,9 @@ module API optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.' end - optional_attributes = ::ApplicationSettingsHelper.visible_attributes << :performance_bar_allowed_group_id + optional_attributes = [*::ApplicationSettingsHelper.visible_attributes, + *::ApplicationSettingsHelper.external_authorization_service_attributes, + :performance_bar_allowed_group_id] if Gitlab.ee? optional_attributes += EE::ApplicationSettingsHelper.possible_licensed_attributes diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index 2745905c5ff..199b3533cf4 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -150,7 +150,7 @@ module Banzai end def uri_type(path) - # https://gitlab.com/gitlab-org/gitlab-ce/issues/58011 + # https://gitlab.com/gitlab-org/gitlab-ce/issues/58657 Gitlab::GitalyClient.allow_n_plus_1_calls do @uri_types[path] ||= current_commit.uri_type(path) end diff --git a/lib/banzai/reference_parser/merge_request_parser.rb b/lib/banzai/reference_parser/merge_request_parser.rb index e8147ac591a..d7bf450465e 100644 --- a/lib/banzai/reference_parser/merge_request_parser.rb +++ b/lib/banzai/reference_parser/merge_request_parser.rb @@ -10,7 +10,7 @@ module Banzai nodes, MergeRequest.includes( :author, - :assignee, + :assignees, { # These associations are primarily used for checking permissions. # Eager loading these ensures we don't end up running dozens of diff --git a/lib/gitlab/background_migration.rb b/lib/gitlab/background_migration.rb index 5251e0fadf9..2e3a4f3b869 100644 --- a/lib/gitlab/background_migration.rb +++ b/lib/gitlab/background_migration.rb @@ -58,11 +58,31 @@ module Gitlab migration_class_for(class_name).new.perform(*arguments) end - def self.exists?(migration_class) + def self.exists?(migration_class, additional_queues = []) enqueued = Sidekiq::Queue.new(self.queue) scheduled = Sidekiq::ScheduledSet.new - [enqueued, scheduled].each do |queue| + enqueued_job?([enqueued, scheduled], migration_class) + end + + def self.dead_jobs?(migration_class) + dead_set = Sidekiq::DeadSet.new + + enqueued_job?([dead_set], migration_class) + end + + def self.retrying_jobs?(migration_class) + retry_set = Sidekiq::RetrySet.new + + enqueued_job?([retry_set], migration_class) + end + + def self.migration_class_for(class_name) + const_get(class_name) + end + + def self.enqueued_job?(queues, migration_class) + queues.each do |queue| queue.each do |job| return true if job.queue == self.queue && job.args.first == migration_class end @@ -70,9 +90,5 @@ module Gitlab false end - - def self.migration_class_for(class_name) - const_get(class_name) - end end end diff --git a/lib/gitlab/background_migration/migrate_stage_index.rb b/lib/gitlab/background_migration/migrate_stage_index.rb index f90f35a913d..f921233460d 100644 --- a/lib/gitlab/background_migration/migrate_stage_index.rb +++ b/lib/gitlab/background_migration/migrate_stage_index.rb @@ -21,8 +21,8 @@ module Gitlab AND stage_idx IS NOT NULL GROUP BY stage_id, stage_idx ), indexes AS ( - SELECT DISTINCT stage_id, last_value(stage_idx) - OVER (PARTITION BY stage_id ORDER BY freq ASC) AS index + SELECT DISTINCT stage_id, first_value(stage_idx) + OVER (PARTITION BY stage_id ORDER BY freq DESC) AS index FROM freqs ) diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb index 69a3a1aedef..5a13fd18504 100644 --- a/lib/gitlab/ci/config/entry/environment.rb +++ b/lib/gitlab/ci/config/entry/environment.rb @@ -36,10 +36,12 @@ module Gitlab validates :config, allowed_keys: ALLOWED_KEYS validates :url, + type: String, length: { maximum: 255 }, allow_nil: true validates :action, + type: String, inclusion: { in: %w[start stop], message: 'should be start or stop' }, allow_nil: true diff --git a/lib/gitlab/ci/pipeline/chain/skip.rb b/lib/gitlab/ci/pipeline/chain/skip.rb index 79bbcc1ed1e..7d6e0704d4a 100644 --- a/lib/gitlab/ci/pipeline/chain/skip.rb +++ b/lib/gitlab/ci/pipeline/chain/skip.rb @@ -8,7 +8,6 @@ module Gitlab include ::Gitlab::Utils::StrongMemoize SKIP_PATTERN = /\[(ci[ _-]skip|skip[ _-]ci)\]/i - SKIP_PUSH_OPTION = 'ci.skip' def perform! if skipped? @@ -35,7 +34,8 @@ module Gitlab end def push_option_skips_ci? - !!(@command.push_options&.include?(SKIP_PUSH_OPTION)) + @command.push_options.present? && + @command.push_options.deep_symbolize_keys.dig(:ci, :skip).present? end end end diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb index ea08b5f7eae..af385d7d4ca 100644 --- a/lib/gitlab/data_builder/push.rb +++ b/lib/gitlab/data_builder/push.rb @@ -32,10 +32,7 @@ module Gitlab } ], total_commits_count: 1, - push_options: [ - "ci.skip", - "custom option" - ] + push_options: { ci: { skip: true } } }.freeze # Produce a hash of post-receive data @@ -57,11 +54,11 @@ module Gitlab # }, # commits: Array, # total_commits_count: Fixnum, - # push_options: Array + # push_options: Hash # } # # rubocop:disable Metrics/ParameterLists - def build(project, user, oldrev, newrev, ref, commits = [], message = nil, commits_count: nil, push_options: []) + def build(project, user, oldrev, newrev, ref, commits = [], message = nil, commits_count: nil, push_options: {}) commits = Array(commits) # Total commits count diff --git a/lib/gitlab/external_authorization.rb b/lib/gitlab/external_authorization.rb new file mode 100644 index 00000000000..25f8b7b3628 --- /dev/null +++ b/lib/gitlab/external_authorization.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Gitlab + module ExternalAuthorization + extend ExternalAuthorization::Config + + RequestFailed = Class.new(StandardError) + + def self.access_allowed?(user, label, project_path = nil) + return true unless perform_check? + return false unless user + + access_for_user_to_label(user, label, project_path).has_access? + end + + def self.rejection_reason(user, label) + return unless enabled? + return unless user + + access_for_user_to_label(user, label, nil).reason + end + + def self.access_for_user_to_label(user, label, project_path) + if RequestStore.active? + RequestStore.fetch("external_authorisation:user-#{user.id}:label-#{label}") do + load_access(user, label, project_path) + end + else + load_access(user, label, project_path) + end + end + + def self.load_access(user, label, project_path) + access = ::Gitlab::ExternalAuthorization::Access.new(user, label).load! + ::Gitlab::ExternalAuthorization::Logger.log_access(access, project_path) + + access + end + end +end diff --git a/lib/gitlab/external_authorization/access.rb b/lib/gitlab/external_authorization/access.rb new file mode 100644 index 00000000000..e111c41fcc2 --- /dev/null +++ b/lib/gitlab/external_authorization/access.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Gitlab + module ExternalAuthorization + class Access + attr_reader :user, + :reason, + :loaded_at, + :label, + :load_type + + def initialize(user, label) + @user, @label = user, label + end + + def loaded? + loaded_at && (loaded_at > ExternalAuthorization::Cache::VALIDITY_TIME.ago) + end + + def has_access? + @access + end + + def load! + load_from_cache + load_from_service unless loaded? + self + end + + private + + def load_from_cache + @load_type = :cache + @access, @reason, @loaded_at = cache.load + end + + def load_from_service + @load_type = :request + response = Client.new(@user, @label).request_access + @access = response.successful? + @reason = response.reason + @loaded_at = Time.now + cache.store(@access, @reason, @loaded_at) if response.valid? + rescue ::Gitlab::ExternalAuthorization::RequestFailed => e + @access = false + @reason = e.message + @loaded_at = Time.now + end + + def cache + @cache ||= ExternalAuthorization::Cache.new(@user, @label) + end + end + end +end diff --git a/lib/gitlab/external_authorization/cache.rb b/lib/gitlab/external_authorization/cache.rb new file mode 100644 index 00000000000..acdc028b4dc --- /dev/null +++ b/lib/gitlab/external_authorization/cache.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Gitlab + module ExternalAuthorization + class Cache + VALIDITY_TIME = 6.hours + + def initialize(user, label) + @user, @label = user, label + end + + def load + @access, @reason, @refreshed_at = ::Gitlab::Redis::Cache.with do |redis| + redis.hmget(cache_key, :access, :reason, :refreshed_at) + end + + [access, reason, refreshed_at] + end + + def store(new_access, new_reason, new_refreshed_at) + ::Gitlab::Redis::Cache.with do |redis| + redis.pipelined do + redis.mapped_hmset( + cache_key, + { + access: new_access.to_s, + reason: new_reason.to_s, + refreshed_at: new_refreshed_at.to_s + } + ) + + redis.expire(cache_key, VALIDITY_TIME) + end + end + end + + private + + def access + ::Gitlab::Utils.to_boolean(@access) + end + + def reason + # `nil` if the cached value was an empty string + return unless @reason.present? + + @reason + end + + def refreshed_at + # Don't try to parse a time if there was no cache + return unless @refreshed_at.present? + + Time.parse(@refreshed_at) + end + + def cache_key + "external_authorization:user-#{@user.id}:label-#{@label}" + end + end + end +end diff --git a/lib/gitlab/external_authorization/client.rb b/lib/gitlab/external_authorization/client.rb new file mode 100644 index 00000000000..60aab2e7044 --- /dev/null +++ b/lib/gitlab/external_authorization/client.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +Excon.defaults[:ssl_verify_peer] = false + +module Gitlab + module ExternalAuthorization + class Client + include ExternalAuthorization::Config + + REQUEST_HEADERS = { + 'Content-Type' => 'application/json', + 'Accept' => 'application/json' + }.freeze + + def initialize(user, label) + @user, @label = user, label + end + + def request_access + response = Excon.post( + service_url, + post_params + ) + ::Gitlab::ExternalAuthorization::Response.new(response) + rescue Excon::Error => e + raise ::Gitlab::ExternalAuthorization::RequestFailed.new(e) + end + + private + + def post_params + params = { headers: REQUEST_HEADERS, + body: body.to_json, + connect_timeout: timeout, + read_timeout: timeout, + write_timeout: timeout } + + if has_tls? + params[:client_cert_data] = client_cert + params[:client_key_data] = client_key + params[:client_key_pass] = client_key_pass + end + + params + end + + def body + @body ||= begin + body = { + user_identifier: @user.email, + project_classification_label: @label + } + + if @user.ldap_identity + body[:user_ldap_dn] = @user.ldap_identity.extern_uid + end + + body + end + end + end + end +end diff --git a/lib/gitlab/external_authorization/config.rb b/lib/gitlab/external_authorization/config.rb new file mode 100644 index 00000000000..8654a8c1e2e --- /dev/null +++ b/lib/gitlab/external_authorization/config.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Gitlab + module ExternalAuthorization + module Config + extend self + + def timeout + application_settings.external_authorization_service_timeout + end + + def service_url + application_settings.external_authorization_service_url + end + + def enabled? + application_settings.external_authorization_service_enabled + end + + def perform_check? + enabled? && service_url.present? + end + + def client_cert + application_settings.external_auth_client_cert + end + + def client_key + application_settings.external_auth_client_key + end + + def client_key_pass + application_settings.external_auth_client_key_pass + end + + def has_tls? + client_cert.present? && client_key.present? + end + + private + + def application_settings + ::Gitlab::CurrentSettings.current_application_settings + end + end + end +end diff --git a/lib/gitlab/external_authorization/logger.rb b/lib/gitlab/external_authorization/logger.rb new file mode 100644 index 00000000000..61246cd870e --- /dev/null +++ b/lib/gitlab/external_authorization/logger.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Gitlab + module ExternalAuthorization + class Logger < ::Gitlab::Logger + def self.log_access(access, project_path) + status = access.has_access? ? "GRANTED" : "DENIED" + message = ["#{status} #{access.user.email} access to '#{access.label}'"] + + message << "(#{project_path})" if project_path.present? + message << "- #{access.load_type} #{access.loaded_at}" if access.load_type == :cache + + info(message.join(' ')) + end + + def self.file_name_noext + 'external-policy-access-control' + end + end + end +end diff --git a/lib/gitlab/external_authorization/response.rb b/lib/gitlab/external_authorization/response.rb new file mode 100644 index 00000000000..4f3fe5882db --- /dev/null +++ b/lib/gitlab/external_authorization/response.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Gitlab + module ExternalAuthorization + class Response + include ::Gitlab::Utils::StrongMemoize + + def initialize(excon_response) + @excon_response = excon_response + end + + def valid? + @excon_response && [200, 401, 403].include?(@excon_response.status) + end + + def successful? + valid? && @excon_response.status == 200 + end + + def reason + parsed_response['reason'] if parsed_response + end + + private + + def parsed_response + strong_memoize(:parsed_response) { parse_response! } + end + + def parse_response! + JSON.parse(@excon_response.body) + rescue JSON::JSONError + # The JSON response is optional, so don't fail when it's missing + nil + end + end + end +end diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb index 426436c2164..d98b85fecc4 100644 --- a/lib/gitlab/git_post_receive.rb +++ b/lib/gitlab/git_post_receive.rb @@ -5,7 +5,7 @@ module Gitlab include Gitlab::Identifier attr_reader :project, :identifier, :changes, :push_options - def initialize(project, identifier, changes, push_options) + def initialize(project, identifier, changes, push_options = {}) @project = project @identifier = identifier @changes = deserialize_changes(changes) diff --git a/lib/gitlab/hook_data/issuable_builder.rb b/lib/gitlab/hook_data/issuable_builder.rb index 0803df65632..b8da6731081 100644 --- a/lib/gitlab/hook_data/issuable_builder.rb +++ b/lib/gitlab/hook_data/issuable_builder.rb @@ -20,11 +20,7 @@ module Gitlab repository: issuable.project.hook_attrs.slice(:name, :url, :description, :homepage) } - if issuable.is_a?(Issue) - hook_data[:assignees] = issuable.assignees.map(&:hook_attrs) if issuable.assignees.any? - else - hook_data[:assignee] = issuable.assignee.hook_attrs if issuable.assignee - end + hook_data[:assignees] = issuable.assignees.map(&:hook_attrs) if issuable.assignees.any? hook_data end diff --git a/lib/gitlab/hook_data/merge_request_builder.rb b/lib/gitlab/hook_data/merge_request_builder.rb index d77b1d04644..a8e993e087e 100644 --- a/lib/gitlab/hook_data/merge_request_builder.rb +++ b/lib/gitlab/hook_data/merge_request_builder.rb @@ -34,7 +34,6 @@ module Gitlab end SAFE_HOOK_RELATIONS = %i[ - assignee labels total_time_spent ].freeze @@ -51,7 +50,9 @@ module Gitlab work_in_progress: merge_request.work_in_progress?, total_time_spent: merge_request.total_time_spent, human_total_time_spent: merge_request.human_total_time_spent, - human_time_estimate: merge_request.human_time_estimate + human_time_estimate: merge_request.human_time_estimate, + assignee_ids: merge_request.assignee_ids, + assignee_id: merge_request.assignee_ids.first # This key is deprecated } merge_request.attributes.with_indifferent_access.slice(*self.class.safe_hook_attributes) diff --git a/lib/gitlab/import/merge_request_helpers.rb b/lib/gitlab/import/merge_request_helpers.rb index b3fe1fc0685..4bc39868389 100644 --- a/lib/gitlab/import/merge_request_helpers.rb +++ b/lib/gitlab/import/merge_request_helpers.rb @@ -22,7 +22,7 @@ module Gitlab # additional work that is strictly necessary. merge_request_id = insert_and_return_id(attributes, project.merge_requests) - merge_request = project.merge_requests.reload.find(merge_request_id) + merge_request = project.merge_requests.reset.find(merge_request_id) [merge_request, false] end diff --git a/lib/gitlab/kubernetes/namespace.rb b/lib/gitlab/kubernetes/namespace.rb index 919f19c86d7..8a3bea95a04 100644 --- a/lib/gitlab/kubernetes/namespace.rb +++ b/lib/gitlab/kubernetes/namespace.rb @@ -19,11 +19,40 @@ module Gitlab def create! resource = ::Kubeclient::Resource.new(metadata: { name: name }) + log_event(:begin_create) @client.create_namespace(resource) end def ensure_exists! exists? || create! + rescue ::Kubeclient::HttpError => error + log_create_failed(error) + raise + end + + private + + def log_create_failed(error) + logger.error({ + exception: error.class.name, + status_code: error.error_code, + namespace: name, + class_name: self.class.name, + event: :failed_to_create_namespace, + message: error.message + }) + end + + def log_event(event) + logger.info( + namespace: name, + class_name: self.class.name, + event: event + ) + end + + def logger + @logger ||= Gitlab::Kubernetes::Logger.build end end end diff --git a/lib/gitlab/legacy_github_import/project_creator.rb b/lib/gitlab/legacy_github_import/project_creator.rb index ca1a1b8e9bd..b484b69c932 100644 --- a/lib/gitlab/legacy_github_import/project_creator.rb +++ b/lib/gitlab/legacy_github_import/project_creator.rb @@ -37,7 +37,7 @@ module Gitlab end def visibility_level - visibility_level = repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC + visibility_level = repo.private ? Gitlab::VisibilityLevel::PRIVATE : @namespace.visibility_level visibility_level = Gitlab::CurrentSettings.default_project_visibility if Gitlab::CurrentSettings.restricted_visibility_levels.include?(visibility_level) visibility_level diff --git a/lib/gitlab/legacy_github_import/release_formatter.rb b/lib/gitlab/legacy_github_import/release_formatter.rb index 8c0c17780ca..746786b5a66 100644 --- a/lib/gitlab/legacy_github_import/release_formatter.rb +++ b/lib/gitlab/legacy_github_import/release_formatter.rb @@ -7,6 +7,7 @@ module Gitlab { project: project, tag: raw_data.tag_name, + name: raw_data.name, description: raw_data.body, created_at: raw_data.created_at, updated_at: raw_data.created_at diff --git a/lib/gitlab/push_options.rb b/lib/gitlab/push_options.rb new file mode 100644 index 00000000000..810aba436cc --- /dev/null +++ b/lib/gitlab/push_options.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module Gitlab + class PushOptions + VALID_OPTIONS = HashWithIndifferentAccess.new({ + merge_request: { + keys: [:create, :merge_when_pipeline_succeeds, :target] + }, + ci: { + keys: [:skip] + } + }).freeze + + NAMESPACE_ALIASES = HashWithIndifferentAccess.new({ + mr: :merge_request + }).freeze + + OPTION_MATCHER = /(?<namespace>[^\.]+)\.(?<key>[^=]+)=?(?<value>.*)/ + + attr_reader :options + + def initialize(options = []) + @options = parse_options(options) + end + + def get(*args) + options.dig(*args) + end + + # Allow #to_json serialization + def as_json(*_args) + options + end + + private + + def parse_options(raw_options) + options = HashWithIndifferentAccess.new + + Array.wrap(raw_options).each do |option| + namespace, key, value = parse_option(option) + + next if [namespace, key].any?(&:nil?) + + options[namespace] ||= HashWithIndifferentAccess.new + options[namespace][key] = value + end + + options + end + + def parse_option(option) + parts = OPTION_MATCHER.match(option) + return unless parts + + namespace, key, value = parts.values_at(:namespace, :key, :value).map(&:strip) + namespace = NAMESPACE_ALIASES[namespace] if NAMESPACE_ALIASES[namespace] + value = value.presence || true + + return unless valid_option?(namespace, key) + + [namespace, key, value] + end + + def valid_option?(namespace, key) + keys = VALID_OPTIONS.dig(namespace, :keys) + keys && keys.include?(key.to_sym) + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f22b351ad78..7d6e23c80f6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -16,6 +16,9 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" +msgid " Please sign in." +msgstr "" + msgid " Status" msgstr "" @@ -408,6 +411,9 @@ msgstr "" msgid "Access Tokens" msgstr "" +msgid "Access denied for your LDAP account." +msgstr "" + msgid "Access denied! Please verify you can add deploy keys to this repository." msgstr "" @@ -417,6 +423,9 @@ msgstr "" msgid "Access forbidden. Check your access level." msgstr "" +msgid "Access to '%{classification_label}' not allowed" +msgstr "" + msgid "Account" msgstr "" @@ -981,9 +990,6 @@ msgstr "" msgid "Assigned Merge Requests" msgstr "" -msgid "Assigned to :name" -msgstr "" - msgid "Assigned to me" msgstr "" @@ -1008,12 +1014,18 @@ msgstr "" msgid "Authentication Log" msgstr "" +msgid "Authentication failed: %{error_message}" +msgstr "" + msgid "Authentication log" msgstr "" msgid "Authentication method" msgstr "" +msgid "Authentication method updated" +msgstr "" + msgid "Authentication via U2F device failed." msgstr "" @@ -1032,6 +1044,9 @@ msgstr "" msgid "Authorize %{link_to_client} to use your account?" msgstr "" +msgid "Authorized %{new_chat_name}" +msgstr "" + msgid "Authorized At" msgstr "" @@ -1467,6 +1482,12 @@ msgstr "" msgid "Cannot be merged automatically" msgstr "" +msgid "Cannot create the abuse report. The user has been deleted." +msgstr "" + +msgid "Cannot create the abuse report. This user has been blocked." +msgstr "" + msgid "Cannot modify managed Kubernetes cluster" msgstr "" @@ -1686,6 +1707,9 @@ msgstr "" msgid "CiVariable|Validation failed" msgstr "" +msgid "ClassificationLabelUnavailable|is unavailable: %{reason}" +msgstr "" + msgid "Clear" msgstr "" @@ -1716,6 +1740,15 @@ msgstr "" msgid "Click to expand text" msgstr "" +msgid "Client authentication certificate" +msgstr "" + +msgid "Client authentication key" +msgstr "" + +msgid "Client authentication key password" +msgstr "" + msgid "Clients" msgstr "" @@ -2525,12 +2558,18 @@ msgstr "" msgid "Copy token to clipboard" msgstr "" +msgid "Could not authorize chat nickname. Try again!" +msgstr "" + msgid "Could not connect to FogBugz, check your URL" msgstr "" msgid "Could not create Wiki Repository at this time. Please try again later." msgstr "" +msgid "Could not delete chat nickname %{chat_name}." +msgstr "" + msgid "Could not remove the trigger." msgstr "" @@ -2540,6 +2579,9 @@ msgstr "" msgid "Could not revoke impersonation token %{token_name}." msgstr "" +msgid "Could not revoke personal access token %{personal_access_token_name}." +msgstr "" + msgid "Coverage" msgstr "" @@ -2552,6 +2594,9 @@ msgstr "" msgid "Create New Domain" msgstr "" +msgid "Create a GitLab account first, and then connect it to your %{label} account." +msgstr "" + msgid "Create a new branch" msgstr "" @@ -2750,6 +2795,9 @@ msgstr "" msgid "Default artifacts expiration" msgstr "" +msgid "Default classification label" +msgstr "" + msgid "Default first day of the week" msgstr "" @@ -2804,6 +2852,12 @@ msgstr "" msgid "Deleted" msgstr "" +msgid "Deleted chat nickname: %{chat_name}!" +msgstr "" + +msgid "Denied authorization of chat nickname %{user_name}." +msgstr "" + msgid "Deny" msgstr "" @@ -3130,6 +3184,30 @@ msgstr "" msgid "Email patch" msgstr "" +msgid "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." +msgstr "" + +msgid "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." +msgstr "" + +msgid "EmailError|We couldn't figure out what the email is for. Please create your issue or comment through the web interface." +msgstr "" + +msgid "EmailError|We couldn't figure out what the email is in reply to. Please create your comment through the web interface." +msgstr "" + +msgid "EmailError|We couldn't figure out what user corresponds to the email. Please create your comment through the web interface." +msgstr "" + +msgid "EmailError|We couldn't find the project. Please check if there's any typo." +msgstr "" + +msgid "EmailError|You are not allowed to perform this action. If you believe this is in error, contact a staff member." +msgstr "" + +msgid "EmailError|Your account has been blocked. If you believe this is in error, contact a staff member." +msgstr "" + msgid "Emails" msgstr "" @@ -3160,6 +3238,9 @@ msgstr "" msgid "Enable and configure Prometheus metrics." msgstr "" +msgid "Enable classification control using an external service" +msgstr "" + msgid "Enable error tracking" msgstr "" @@ -3451,6 +3532,9 @@ msgstr "" msgid "Error while loading the merge request. Please try again." msgstr "" +msgid "Error while migrating %{upload_id}: %{error_message}" +msgstr "" + msgid "Error with Akismet. Please check the logs for more info." msgstr "" @@ -3589,12 +3673,33 @@ msgstr "" msgid "Explore public groups" msgstr "" +msgid "External Classification Policy Authorization" +msgstr "" + msgid "External URL" msgstr "" msgid "External Wiki" msgstr "" +msgid "External authentication" +msgstr "" + +msgid "External authorization denied access to this project" +msgstr "" + +msgid "External authorization request timeout" +msgstr "" + +msgid "ExternalAuthorizationService|Classification Label" +msgstr "" + +msgid "ExternalAuthorizationService|Classification label" +msgstr "" + +msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used." +msgstr "" + msgid "Facebook" msgstr "" @@ -3643,6 +3748,12 @@ msgstr "" msgid "Failed to save new settings" msgstr "" +msgid "Failed to save preferences (%{error_message})." +msgstr "" + +msgid "Failed to save preferences." +msgstr "" + msgid "Failed to update issues, please try again." msgstr "" @@ -4304,6 +4415,9 @@ msgstr "" msgid "If enabled" msgstr "" +msgid "If enabled, access to projects will be validated on an external service using their classification label." +msgstr "" + msgid "If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>." msgstr "" @@ -4592,6 +4706,12 @@ msgstr "" msgid "Job has been successfully erased!" msgstr "" +msgid "Job has wrong arguments format." +msgstr "" + +msgid "Job is missing the `model_type` argument." +msgstr "" + msgid "Job is stuck. Check runners." msgstr "" @@ -5175,6 +5295,12 @@ msgstr "" msgid "Metrics|Unexpected metrics data response from prometheus endpoint" msgstr "" +msgid "Migrated %{success_count}/%{total_count} files." +msgstr "" + +msgid "Migration successful." +msgstr "" + msgid "Milestone" msgstr "" @@ -5268,12 +5394,21 @@ msgstr "" msgid "Most stars" msgstr "" +msgid "Mount point %{mounted_as} not found in %{model_class}." +msgstr "" + msgid "Move" msgstr "" msgid "Move issue" msgstr "" +msgid "Multiple model types found: %{model_types}" +msgstr "" + +msgid "Multiple uploaders found: %{uploader_types}" +msgstr "" + msgid "Name" msgstr "" @@ -5429,9 +5564,6 @@ msgstr "" msgid "No activities found" msgstr "" -msgid "No assignee" -msgstr "" - msgid "No branches found" msgstr "" @@ -5519,9 +5651,6 @@ msgstr "" msgid "None" msgstr "" -msgid "Not allowed to merge" -msgstr "" - msgid "Not available" msgstr "" @@ -5800,6 +5929,9 @@ msgstr "" msgid "Password" msgstr "" +msgid "Password authentication is unavailable." +msgstr "" + msgid "Past due" msgstr "" @@ -6067,12 +6199,18 @@ msgstr "" msgid "Please choose a group URL with no special characters." msgstr "" +msgid "Please complete your profile with email address" +msgstr "" + msgid "Please convert them to %{link_to_git}, and go through the %{link_to_import_flow} again." msgstr "" msgid "Please convert them to Git on Google Code, and go through the %{link_to_import_flow} again." msgstr "" +msgid "Please create a password for your new account." +msgstr "" + msgid "Please create a username with only alphanumeric characters." msgstr "" @@ -6106,6 +6244,9 @@ msgstr "" msgid "Preferences" msgstr "" +msgid "Preferences saved." +msgstr "" + msgid "Preferences|Navigation theme" msgstr "" @@ -6298,6 +6439,9 @@ msgstr "" msgid "Profiles|The maximum file size allowed is 200KB." msgstr "" +msgid "Profiles|There was an error with the reCAPTCHA. Please solve the reCAPTCHA again." +msgstr "" + msgid "Profiles|This doesn't look like a public SSH key, are you sure you want to add it?" msgstr "" @@ -6992,6 +7136,9 @@ msgstr "" msgid "Revoked impersonation token %{token_name}!" msgstr "" +msgid "Revoked personal access token %{personal_access_token_name}!" +msgstr "" + msgid "Run untagged jobs" msgstr "" @@ -7214,9 +7361,6 @@ msgstr "" msgid "Select an existing Kubernetes cluster or create a new one" msgstr "" -msgid "Select assignee" -msgstr "" - msgid "Select branch/tag" msgstr "" @@ -7331,6 +7475,9 @@ msgstr "" msgid "Service Templates" msgstr "" +msgid "Service URL" +msgstr "" + msgid "Session duration (minutes)" msgstr "" @@ -7480,6 +7627,12 @@ msgstr "" msgid "Sign-up restrictions" msgstr "" +msgid "Signing in using %{label} has been disabled" +msgstr "" + +msgid "Signing in using your %{label} account without a pre-existing GitLab account is not allowed." +msgstr "" + msgid "Similar issues" msgstr "" @@ -7882,6 +8035,9 @@ msgstr "" msgid "Successfully confirmed" msgstr "" +msgid "Successfully deleted U2F device." +msgstr "" + msgid "Successfully removed email." msgstr "" @@ -8047,6 +8203,9 @@ msgstr "" msgid "Test failed." msgstr "" +msgid "Thank you for your report. A GitLab administrator will look into it shortly." +msgstr "" + msgid "The %{type} contains the following error:" msgid_plural "The %{type} contains the following errors:" msgstr[0] "" @@ -8061,6 +8220,9 @@ msgstr "" msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project." msgstr "" +msgid "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." +msgstr "" + msgid "The character highlighter helps you keep the subject line to %{titleLength} characters and wrap the body at %{bodyLength} so they are readable in git." msgstr "" @@ -8109,6 +8271,12 @@ msgstr "" msgid "The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination." msgstr "" +msgid "The invitation could not be accepted." +msgstr "" + +msgid "The invitation could not be declined." +msgstr "" + msgid "The invitation has already been accepted." msgstr "" @@ -8136,6 +8304,9 @@ msgstr "" msgid "The name %{entryName} is already taken in this directory." msgstr "" +msgid "The passphrase required to decrypt the private key. This is optional and the value is encrypted at rest." +msgstr "" + msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>" msgstr "" @@ -8148,6 +8319,9 @@ msgstr "" msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit." msgstr "" +msgid "The private key to use when a client certificate is provided. This value is encrypted at rest." +msgstr "" + msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle." msgstr "" @@ -8181,6 +8355,9 @@ msgstr "" msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." msgstr "" +msgid "The schedule time must be in the future!" +msgstr "" + msgid "The snippet can be accessed without any authentication." msgstr "" @@ -8511,6 +8688,9 @@ msgstr "" msgid "Time estimate" msgstr "" +msgid "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." +msgstr "" + msgid "Time remaining" msgstr "" @@ -8779,9 +8959,6 @@ msgstr "" msgid "Toggle navigation" msgstr "" -msgid "Toggle sidebar" -msgstr "" - msgid "ToggleButton|Toggle Status: OFF" msgstr "" @@ -9286,6 +9463,9 @@ msgstr "" msgid "When fast-forward merge is not possible, the user is given the option to rebase." msgstr "" +msgid "When leaving the URL blank, classification labels can still be specified without disabling cross project features or performing external authorization checks." +msgstr "" + msgid "When this merge request is accepted" msgid_plural "When these merge requests are accepted" msgstr[0] "" @@ -9465,6 +9645,9 @@ msgstr "" msgid "Write milestone description..." msgstr "" +msgid "Wrong extern UID provided. Make sure Auth0 is configured correctly." +msgstr "" + msgid "Yes" msgstr "" @@ -9591,6 +9774,15 @@ msgstr "" msgid "You don't have any deployments right now." msgstr "" +msgid "You have been granted %{member_human_access} access to %{label}." +msgstr "" + +msgid "You have been unsubscribed from this thread." +msgstr "" + +msgid "You have declined the invitation to join %{label}." +msgstr "" + msgid "You have no permissions" msgstr "" @@ -9747,6 +9939,12 @@ msgstr "" msgid "Your name" msgstr "" +msgid "Your new personal access token has been created." +msgstr "" + +msgid "Your password reset token has expired." +msgstr "" + msgid "Your project limit is %{limit} projects! Please contact your administrator to increase it" msgstr "" @@ -9771,9 +9969,6 @@ msgstr "" msgid "among other things" msgstr "" -msgid "assign yourself" -msgstr "" - msgid "attach a new file" msgstr "" @@ -9795,6 +9990,9 @@ msgstr "" msgid "connecting" msgstr "" +msgid "could not read private key, is the passphrase correct?" +msgstr "" + msgid "customize" msgstr "" @@ -9883,6 +10081,9 @@ msgstr "" msgid "index" msgstr "" +msgid "is not a valid X509 certificate." +msgstr "" + msgid "issue boards" msgstr "" @@ -10165,6 +10366,9 @@ msgstr "" msgid "private" msgstr "" +msgid "private key does not match certificate." +msgstr "" + msgid "processing" msgstr "" diff --git a/package.json b/package.json index 6d3e6764a46..115f6fd1e8d 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "prettier-staged-save": "node ./scripts/frontend/prettier.js save", "prettier-all": "node ./scripts/frontend/prettier.js check-all", "prettier-all-save": "node ./scripts/frontend/prettier.js save-all", - "stylelint": "node node_modules/stylelint/bin/stylelint.js app/assets/stylesheets/**/*.* --custom-formatter node_modules/stylelint-error-string-formatter", + "stylelint": "node node_modules/stylelint/bin/stylelint.js app/assets/stylesheets/**/*.* ee/app/assets/stylesheets/**/*.* !**/vendors/** --custom-formatter node_modules/stylelint-error-string-formatter", "stylelint-file": "node node_modules/stylelint/bin/stylelint.js", "stylelint-create-utility-map": "node scripts/frontend/stylelint/stylelint-utility-map.js", "test": "yarn jest && yarn karma", @@ -33,7 +33,7 @@ "@babel/plugin-syntax-import-meta": "^7.2.0", "@babel/preset-env": "^7.3.1", "@gitlab/csslab": "^1.9.0", - "@gitlab/svgs": "^1.58.0", + "@gitlab/svgs": "^1.59.0", "@gitlab/ui": "^3.2.0", "apollo-cache-inmemory": "^1.5.1", "apollo-client": "^2.5.1", diff --git a/qa/README.md b/qa/README.md index 9517d4f42b4..8efdd8514f1 100644 --- a/qa/README.md +++ b/qa/README.md @@ -39,7 +39,9 @@ following call would login to a local [GDK] instance and run all specs in `qa/specs/features`: ``` -bin/qa Test::Instance::All http://localhost:3000 +# Make sure to install the dependencies first with `bundle install` + +bundle exec bin/qa Test::Instance::All http://localhost:3000 ``` Note: If you want to run tests requiring SSH against GDK, you @@ -56,14 +58,14 @@ You can also supply specific tests to run as another parameter. For example, to run the repository-related specs, you can execute: ``` -bin/qa Test::Instance::All http://localhost -- qa/specs/features/browser_ui/3_create/repository +bundle exec bin/qa Test::Instance::All http://localhost -- qa/specs/features/browser_ui/3_create/repository ``` Since the arguments would be passed to `rspec`, you could use all `rspec` options there. For example, passing `--backtrace` and also line number: ``` -bin/qa Test::Instance::All http://localhost -- qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb:6 --backtrace +bundle exec bin/qa Test::Instance::All http://localhost -- qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb:6 --backtrace ``` Note that the separator `--` is required; all subsequent options will be @@ -78,7 +80,7 @@ If you need to authenticate as a different user, you can provide the `GITLAB_USERNAME` and `GITLAB_PASSWORD` environment variables: ``` -GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password bin/qa Test::Instance::All https://gitlab.example.com +GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password bundle exec bin/qa Test::Instance::All https://gitlab.example.com ``` If your user doesn't have permission to default sandbox group @@ -86,7 +88,7 @@ If your user doesn't have permission to default sandbox group `GITLAB_SANDBOX_NAME`: ``` -GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sandbox bin/qa Test::Instance::All https://gitlab.example.com +GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sandbox bundle exec bin/qa Test::Instance::All https://gitlab.example.com ``` All [supported environment variables are here](https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/what_tests_can_be_run.md#supported-environment-variables). @@ -121,7 +123,7 @@ tests that are expected to fail while a fix is in progress (similar to how can be used). ``` -bin/qa Test::Instance::All http://localhost -- --tag quarantine +bundle exec bin/qa Test::Instance::All http://localhost -- --tag quarantine ``` If `quarantine` is used with other tags, tests will only be run if they have at @@ -140,7 +142,7 @@ option `--enable-feature FEATURE_FLAG`. For example, to enable the feature flag that enforces Gitaly request limits, you would use the command: ``` -bin/qa Test::Instance::All http://localhost --enable-feature gitaly_enforce_requests_limits +bundle exec bin/qa Test::Instance::All http://localhost --enable-feature gitaly_enforce_requests_limits ``` This will instruct the QA framework to enable the `gitaly_enforce_requests_limits` @@ -153,4 +155,4 @@ flag during a single test, [as you can in unit tests](https://docs.gitlab.com/ee but [that capability is planned](https://gitlab.com/gitlab-org/quality/team-tasks/issues/77). Note also that the `--` separator isn't used because `--enable-feature` is a QA -framework option, not an `rspec` option.
\ No newline at end of file +framework option, not an `rspec` option. diff --git a/qa/qa/page/merge_request/new.rb b/qa/qa/page/merge_request/new.rb index 20d9c336367..56fbf59b9bc 100644 --- a/qa/qa/page/merge_request/new.rb +++ b/qa/qa/page/merge_request/new.rb @@ -26,7 +26,7 @@ module QA element :issuable_label end - view 'app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml' do + view 'app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml' do element :assign_to_me_link end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb index 6ca7af8a3af..c7db595284e 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module QA - context 'Create' do + # Failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/94 + context 'Create', :quarantine do describe 'Merge request creation from fork' do it 'user forks a project, submits a merge request and maintainer merges it' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb index f146636c49a..86692623790 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module QA - # Failure issue: https://gitlab.com/gitlab-org/quality/staging/issues/31 - context 'Create' do + # Failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/93 + context 'Create', :quarantine do describe 'Merge request squashing' do it 'user squashes commits while merging' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index bb1f775da75..0971e551db1 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -40,7 +40,7 @@ module QA Resource::CiVariable.fabricate! do |resource| resource.project = @project resource.key = 'K8S_SECRET_OPTIONAL_MESSAGE' - resource.value = 'You can see this application secret' + resource.value = 'you_can_see_this_variable' end # Connect K8s cluster @@ -99,7 +99,7 @@ module QA Page::Project::Operations::Environments::Show.perform do |show| show.view_deployment do expect(page).to have_content('Hello World!') - expect(page).to have_content('You can see this application secret') + expect(page).to have_content('you_can_see_this_variable') end end end diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index be13c3fb683..a235fddabca 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -34,7 +34,7 @@ RSpec.configure do |config| config.display_try_failure_messages = true config.around do |example| - retry_times = example.metadata.keys.include?(:quarantine) ? 1 : 3 + retry_times = example.metadata.keys.include?(:quarantine) ? 1 : 2 example.run_with_retry retry: retry_times end end diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh index e324f972640..22e47ce2c0a 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -38,13 +38,13 @@ function previousDeployFailed() { local deploy="${1}" echoinfo "Checking for previous deployment of ${deploy}" true - helm status ${deploy} >/dev/null 2>&1 + helm status "${deploy}" >/dev/null 2>&1 local status=$? # if `status` is `0`, deployment exists, has a status if [ $status -eq 0 ]; then echoinfo "Previous deployment found, checking status..." - deployment_status=$(helm status ${deploy} | grep ^STATUS | cut -d' ' -f2) + deployment_status=$(helm status "${deploy}" | grep ^STATUS | cut -d' ' -f2) echoinfo "Previous deployment state: ${deployment_status}" if [[ "$deployment_status" == "FAILED" || "$deployment_status" == "PENDING_UPGRADE" || "$deployment_status" == "PENDING_INSTALL" ]]; then status=0; @@ -64,13 +64,8 @@ function delete() { return fi - local track="${1-stable}" local name="$CI_ENVIRONMENT_SLUG" - if [[ "$track" != "stable" ]]; then - name="$name-$track" - fi - echoinfo "Deleting release '$name'..." true helm delete --purge "$name" || true @@ -97,7 +92,8 @@ function get_pod() { echoinfo "Running '${get_pod_cmd}'" true while true; do - local pod_name="$(eval $get_pod_cmd)" + local pod_name + pod_name="$(eval "${get_pod_cmd}")" [[ "${pod_name}" == "" ]] || break echoinfo "Waiting till '${app_name}' pod is ready"; @@ -113,7 +109,8 @@ function perform_review_app_deployment() { ensure_namespace install_tiller install_external_dns - time deploy + time deploy || true + wait_for_review_app_to_be_accessible add_license } @@ -156,7 +153,8 @@ function install_tiller() { function install_external_dns() { local release_name="dns-gitlab-review-app" - local domain=$(echo "${REVIEW_APPS_DOMAIN}" | awk -F. '{printf "%s.%s", $(NF-1), $NF}') + local domain + domain=$(echo "${REVIEW_APPS_DOMAIN}" | awk -F. '{printf "%s.%s", $(NF-1), $NF}') echoinfo "Installing external DNS for domain ${domain}..." true if ! deployExists "${KUBE_NAMESPACE}" "${release_name}" || previousDeployFailed "${release_name}" ; then @@ -182,17 +180,17 @@ function create_secret() { echoinfo "Creating the ${CI_ENVIRONMENT_SLUG}-gitlab-initial-root-password secret in the ${KUBE_NAMESPACE} namespace..." true kubectl create secret generic -n "$KUBE_NAMESPACE" \ - $CI_ENVIRONMENT_SLUG-gitlab-initial-root-password \ - --from-literal=password=$REVIEW_APPS_ROOT_PASSWORD \ + "${CI_ENVIRONMENT_SLUG}-gitlab-initial-root-password" \ + --from-literal="password=${REVIEW_APPS_ROOT_PASSWORD}" \ --dry-run -o json | kubectl apply -f - } function download_gitlab_chart() { echoinfo "Downloading the GitLab chart..." true - curl -o gitlab.tar.bz2 https://gitlab.com/charts/gitlab/-/archive/$GITLAB_HELM_CHART_REF/gitlab-$GITLAB_HELM_CHART_REF.tar.bz2 + curl -o gitlab.tar.bz2 "https://gitlab.com/charts/gitlab/-/archive/${GITLAB_HELM_CHART_REF}/gitlab-${GITLAB_HELM_CHART_REF}.tar.bz2" tar -xjf gitlab.tar.bz2 - cd gitlab-$GITLAB_HELM_CHART_REF + cd "gitlab-${GITLAB_HELM_CHART_REF}" echoinfo "Adding the gitlab repo to Helm..." helm repo add gitlab https://charts.gitlab.io @@ -202,18 +200,9 @@ function download_gitlab_chart() { } function deploy() { - local track="${1-stable}" local name="$CI_ENVIRONMENT_SLUG" - - if [[ "$track" != "stable" ]]; then - name="$name-$track" - fi echoinfo "Deploying ${name}..." true - replicas="1" - service_enabled="false" - postgres_enabled="$POSTGRES_ENABLED" - IMAGE_REPOSITORY="registry.gitlab.com/gitlab-org/build/cng-mirror" IMAGE_VERSION="${CI_PROJECT_NAME#gitlab-}" gitlab_migrations_image_repository="${IMAGE_REPOSITORY}/gitlab-rails-${IMAGE_VERSION}" @@ -224,24 +213,6 @@ function deploy() { gitlab_shell_image_repository="${IMAGE_REPOSITORY}/gitlab-shell" gitlab_workhorse_image_repository="${IMAGE_REPOSITORY}/gitlab-workhorse-${IMAGE_VERSION}" - # canary uses stable db - [[ "$track" == "canary" ]] && postgres_enabled="false" - - env_track=$( echo $track | tr -s '[:lower:]' '[:upper:]' ) - env_slug=$( echo ${CI_ENVIRONMENT_SLUG//-/_} | tr -s '[:lower:]' '[:upper:]' ) - - if [[ "$track" == "stable" ]]; then - # for stable track get number of replicas from `PRODUCTION_REPLICAS` - eval new_replicas=\$${env_slug}_REPLICAS - service_enabled="true" - else - # for all tracks get number of replicas from `CANARY_PRODUCTION_REPLICAS` - eval new_replicas=\$${env_track}_${env_slug}_REPLICAS - fi - if [[ -n "$new_replicas" ]]; then - replicas="$new_replicas" - fi - # Cleanup and previous installs, as FAILED and PENDING_UPGRADE will cause errors with `upgrade` if [ "$CI_ENVIRONMENT_SLUG" != "production" ] && previousDeployFailed "$CI_ENVIRONMENT_SLUG" ; then echo "Deployment in bad state, cleaning up $CI_ENVIRONMENT_SLUG" @@ -282,9 +253,9 @@ HELM_CMD=$(cat << EOF --set gitlab.unicorn.image.tag="$CI_COMMIT_REF_SLUG" \ --set gitlab.task-runner.image.repository="$gitlab_task_runner_image_repository" \ --set gitlab.task-runner.image.tag="$CI_COMMIT_REF_SLUG" \ - --set gitlab.gitaly.image.repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitaly" \ + --set gitlab.gitaly.image.repository="$gitlab_gitaly_image_repository" \ --set gitlab.gitaly.image.tag="v$GITALY_VERSION" \ - --set gitlab.gitlab-shell.image.repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-shell" \ + --set gitlab.gitlab-shell.image.repository="$gitlab_shell_image_repository" \ --set gitlab.gitlab-shell.image.tag="v$GITLAB_SHELL_VERSION" \ --set gitlab.unicorn.workhorse.image="$gitlab_workhorse_image_repository" \ --set gitlab.unicorn.workhorse.tag="$CI_COMMIT_REF_SLUG" \ @@ -302,6 +273,31 @@ EOF eval $HELM_CMD } +function wait_for_review_app_to_be_accessible() { + # In case the Review App isn't completely available yet. Keep trying for 5 minutes. + local interval=5 + local elapsed_seconds=0 + local max_seconds=$((5 * 60)) + while true; do + local review_app_http_code + review_app_http_code=$(curl --silent --output /dev/null --max-time 5 --write-out "%{http_code}" "${CI_ENVIRONMENT_URL}/users/sign_in") + if [[ "${review_app_http_code}" -eq "200" ]] || [[ "${elapsed_seconds}" -gt "${max_seconds}" ]]; then + break + fi + + printf "." + let "elapsed_seconds+=interval" + sleep ${interval} + done + + if [[ "${review_app_http_code}" == "200" ]]; then + echoinfo "The Review App at ${CI_ENVIRONMENT_URL} is ready!" + else + echoerr "The Review App at ${CI_ENVIRONMENT_URL} isn't ready after 5 minutes of polling..." + exit 1 + fi +} + function add_license() { if [ -z "${REVIEW_APPS_EE_LICENSE}" ]; then echo "License not found" && return; fi @@ -311,10 +307,10 @@ function add_license() { echoinfo "Installing license..." true echo "${REVIEW_APPS_EE_LICENSE}" > /tmp/license.gitlab - kubectl -n "$KUBE_NAMESPACE" cp /tmp/license.gitlab ${task_runner_pod}:/tmp/license.gitlab + kubectl -n "$KUBE_NAMESPACE" cp /tmp/license.gitlab "${task_runner_pod}":/tmp/license.gitlab rm /tmp/license.gitlab - kubectl -n "$KUBE_NAMESPACE" exec -it ${task_runner_pod} -- /srv/gitlab/bin/rails runner -e production \ + kubectl -n "$KUBE_NAMESPACE" exec -it "${task_runner_pod}" -- /srv/gitlab/bin/rails runner -e production \ ' content = File.read("/tmp/license.gitlab").strip; FileUtils.rm_f("/tmp/license.gitlab"); @@ -344,7 +340,8 @@ function get_job_id() { local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/pipelines/${CI_PIPELINE_ID}/jobs?per_page=100&page=${page}${query_string}" echoinfo "GET ${url}" - local job_id=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq "map(select(.name == \"${job_name}\")) | map(.id) | last") + local job_id + job_id=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq "map(select(.name == \"${job_name}\")) | map(.id) | last") [[ "${job_id}" == "null" && "${page}" -lt "$max_page" ]] || break let "page++" @@ -360,20 +357,23 @@ function get_job_id() { function play_job() { local job_name="${1}" - local job_id=$(get_job_id "${job_name}" "scope=manual"); + local job_id + job_id=$(get_job_id "${job_name}" "scope=manual"); if [ -z "${job_id}" ]; then return; fi local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/jobs/${job_id}/play" echoinfo "POST ${url}" - local job_url=$(curl --silent --show-error --request POST --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq ".web_url") + local job_url + job_url=$(curl --silent --show-error --request POST --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq ".web_url") echoinfo "Manual job '${job_name}' started at: ${job_url}" } function wait_for_job_to_be_done() { local job_name="${1}" local query_string="${2}" - local job_id=$(get_job_id "${job_name}" "${query_string}"); + local job_id + job_id=$(get_job_id "${job_name}" "${query_string}") if [ -z "${job_id}" ]; then return; fi echoinfo "Waiting for the '${job_name}' job to finish..." @@ -385,7 +385,8 @@ function wait_for_job_to_be_done() { local interval=30 local elapsed_seconds=0 while true; do - local job_status=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq ".status" | sed -e s/\"//g) + local job_status + job_status=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq ".status" | sed -e s/\"//g) [[ "${job_status}" == "pending" || "${job_status}" == "running" ]] || break printf "." diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb index 1a7be4c9a85..f3450a8289f 100644 --- a/spec/controllers/admin/application_settings_controller_spec.rb +++ b/spec/controllers/admin/application_settings_controller_spec.rb @@ -92,6 +92,28 @@ describe Admin::ApplicationSettingsController do expect(response).to redirect_to(admin_application_settings_path) expect(ApplicationSetting.current.default_project_creation).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS) end + + context 'external policy classification settings' do + let(:settings) do + { + external_authorization_service_enabled: true, + external_authorization_service_url: 'https://custom.service/', + external_authorization_service_default_label: 'default', + external_authorization_service_timeout: 3, + external_auth_client_cert: File.read('spec/fixtures/passphrase_x509_certificate.crt'), + external_auth_client_key: File.read('spec/fixtures/passphrase_x509_certificate_pk.key'), + external_auth_client_key_pass: "5iveL!fe" + } + end + + it 'updates settings when the feature is available' do + put :update, params: { application_setting: settings } + + settings.each do |attribute, value| + expect(ApplicationSetting.current.public_send(attribute)).to eq(value) + end + end + end end describe 'PUT #reset_registration_token' do diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb index 5eb05f01b8d..309cac47928 100644 --- a/spec/controllers/boards/issues_controller_spec.rb +++ b/spec/controllers/boards/issues_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Boards::IssuesController do + include ExternalAuthorizationServiceHelpers + let(:project) { create(:project, :private) } let(:board) { create(:board, project: project) } let(:user) { create(:user) } @@ -136,6 +138,30 @@ describe Boards::IssuesController do end end + context 'with external authorization' do + before do + sign_in(user) + enable_external_authorization_service_check + end + + it 'returns a 403 for group boards' do + group = create(:group) + group_board = create(:board, group: group) + + list_issues(user: user, board: group_board) + + expect(response).to have_gitlab_http_status(403) + end + + it 'is successful for project boards' do + project_board = create(:board, project: project) + + list_issues(user: user, board: project_board) + + expect(response).to have_gitlab_http_status(200) + end + end + def list_issues(user:, board:, list: nil) sign_in(user) diff --git a/spec/controllers/concerns/project_unauthorized_spec.rb b/spec/controllers/concerns/project_unauthorized_spec.rb new file mode 100644 index 00000000000..90b59b027cf --- /dev/null +++ b/spec/controllers/concerns/project_unauthorized_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe ProjectUnauthorized do + include ExternalAuthorizationServiceHelpers + let(:user) { create(:user) } + + before do + sign_in user + end + + render_views + + describe '#project_unauthorized_proc' do + controller(::Projects::ApplicationController) do + def show + head :ok + end + end + + let(:project) { create(:project) } + + before do + project.add_developer(user) + end + + it 'renders a 200 when the service allows access to the project' do + external_service_allow_access(user, project) + + get :show, params: { namespace_id: project.namespace.to_param, id: project.to_param } + + expect(response).to have_gitlab_http_status(200) + end + + it 'renders a 403 when the service denies access to the project' do + external_service_deny_access(user, project) + + get :show, params: { namespace_id: project.namespace.to_param, id: project.to_param } + + expect(response).to have_gitlab_http_status(403) + expect(response.body).to match("External authorization denied access to this project") + end + + it 'renders a 404 when the user cannot see the project at all' do + other_project = create(:project, :private) + + get :show, params: { namespace_id: other_project.namespace.to_param, id: other_project.to_param } + + expect(response).to have_gitlab_http_status(404) + end + end +end diff --git a/spec/controllers/dashboard/groups_controller_spec.rb b/spec/controllers/dashboard/groups_controller_spec.rb index c8d99f79277..775b3ca40b2 100644 --- a/spec/controllers/dashboard/groups_controller_spec.rb +++ b/spec/controllers/dashboard/groups_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Dashboard::GroupsController do + include ExternalAuthorizationServiceHelpers + let(:user) { create(:user) } before do @@ -11,33 +13,43 @@ describe Dashboard::GroupsController do expect(described_class).to include(GroupTree) end - it 'only includes projects the user is a member of' do - member_of_group = create(:group) - member_of_group.add_developer(user) - create(:group, :public) + describe '#index' do + it 'only includes projects the user is a member of' do + member_of_group = create(:group) + member_of_group.add_developer(user) + create(:group, :public) - get :index + get :index - expect(assigns(:groups)).to contain_exactly(member_of_group) - end + expect(assigns(:groups)).to contain_exactly(member_of_group) + end - context 'when rendering an expanded hierarchy with public groups you are not a member of', :nested_groups do - let!(:top_level_result) { create(:group, name: 'chef-top') } - let!(:top_level_a) { create(:group, name: 'top-a') } - let!(:sub_level_result_a) { create(:group, name: 'chef-sub-a', parent: top_level_a) } - let!(:other_group) { create(:group, name: 'other') } + context 'when rendering an expanded hierarchy with public groups you are not a member of', :nested_groups do + let!(:top_level_result) { create(:group, name: 'chef-top') } + let!(:top_level_a) { create(:group, name: 'top-a') } + let!(:sub_level_result_a) { create(:group, name: 'chef-sub-a', parent: top_level_a) } + let!(:other_group) { create(:group, name: 'other') } - before do - top_level_result.add_maintainer(user) - top_level_a.add_maintainer(user) + before do + top_level_result.add_maintainer(user) + top_level_a.add_maintainer(user) + end + + it 'renders only groups the user is a member of when searching hierarchy correctly' do + get :index, params: { filter: 'chef' }, format: :json + + expect(response).to have_gitlab_http_status(200) + all_groups = [top_level_result, top_level_a, sub_level_result_a] + expect(assigns(:groups)).to contain_exactly(*all_groups) + end end - it 'renders only groups the user is a member of when searching hierarchy correctly' do - get :index, params: { filter: 'chef' }, format: :json + it 'works when the external authorization service is enabled' do + enable_external_authorization_service_check + + get :index expect(response).to have_gitlab_http_status(200) - all_groups = [top_level_result, top_level_a, sub_level_result_a] - expect(assigns(:groups)).to contain_exactly(*all_groups) end end end diff --git a/spec/controllers/dashboard/labels_controller_spec.rb b/spec/controllers/dashboard/labels_controller_spec.rb index a3bfb2f3a87..01de896f9f4 100644 --- a/spec/controllers/dashboard/labels_controller_spec.rb +++ b/spec/controllers/dashboard/labels_controller_spec.rb @@ -13,13 +13,17 @@ describe Dashboard::LabelsController do describe "#index" do let!(:unrelated_label) { create(:label, project: create(:project, :public)) } + subject { get :index, format: :json } + it 'returns global labels for projects the user has a relationship with' do - get :index, format: :json + subject expect(json_response).to be_kind_of(Array) expect(json_response.size).to eq(1) expect(json_response[0]["id"]).to be_nil expect(json_response[0]["title"]).to eq(label.title) end + + it_behaves_like 'disabled when using an external authorization service' end end diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb index 828de0e7ca5..1614739db05 100644 --- a/spec/controllers/dashboard/milestones_controller_spec.rb +++ b/spec/controllers/dashboard/milestones_controller_spec.rb @@ -81,5 +81,11 @@ describe Dashboard::MilestonesController do expect(response.body).to include("Open\n<span class=\"badge badge-pill\">2</span>") expect(response.body).to include("Closed\n<span class=\"badge badge-pill\">0</span>") end + + context 'external authorization' do + subject { get :index } + + it_behaves_like 'disabled when using an external authorization service' + end end end diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb index 649441f4917..c17cb49e460 100644 --- a/spec/controllers/dashboard/projects_controller_spec.rb +++ b/spec/controllers/dashboard/projects_controller_spec.rb @@ -1,7 +1,29 @@ require 'spec_helper' describe Dashboard::ProjectsController do - it_behaves_like 'authenticates sessionless user', :index, :atom + include ExternalAuthorizationServiceHelpers + + describe '#index' do + context 'user not logged in' do + it_behaves_like 'authenticates sessionless user', :index, :atom + end + + context 'user logged in' do + before do + sign_in create(:user) + end + + context 'external authorization' do + it 'works when the external authorization service is enabled' do + enable_external_authorization_service_check + + get :index + + expect(response).to have_gitlab_http_status(200) + end + end + end + end context 'json requests' do render_views diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb index d88beaff0e1..abbf0b52306 100644 --- a/spec/controllers/dashboard/todos_controller_spec.rb +++ b/spec/controllers/dashboard/todos_controller_spec.rb @@ -105,6 +105,12 @@ describe Dashboard::TodosController do end end end + + context 'external authorization' do + subject { get :index } + + it_behaves_like 'disabled when using an external authorization service' + end end describe 'PATCH #restore' do diff --git a/spec/controllers/groups/avatars_controller_spec.rb b/spec/controllers/groups/avatars_controller_spec.rb index 772d1d0c1dd..6ececa6f372 100644 --- a/spec/controllers/groups/avatars_controller_spec.rb +++ b/spec/controllers/groups/avatars_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Groups::AvatarsController do + include ExternalAuthorizationServiceHelpers + let(:user) { create(:user) } let(:group) { create(:group, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) } @@ -15,4 +17,12 @@ describe Groups::AvatarsController do expect(@group.avatar.present?).to be_falsey expect(@group).to be_valid end + + it 'works when external authorization service is enabled' do + enable_external_authorization_service_check + + delete :destroy, params: { group_id: group } + + expect(response).to have_gitlab_http_status(302) + end end diff --git a/spec/controllers/groups/boards_controller_spec.rb b/spec/controllers/groups/boards_controller_spec.rb index 27ee37b3817..0ca5ce51750 100644 --- a/spec/controllers/groups/boards_controller_spec.rb +++ b/spec/controllers/groups/boards_controller_spec.rb @@ -82,6 +82,10 @@ describe Groups::BoardsController do end end + it_behaves_like 'disabled when using an external authorization service' do + subject { list_boards } + end + def list_boards(format: :html) get :index, params: { group_id: group }, format: format end @@ -160,6 +164,10 @@ describe Groups::BoardsController do end end + it_behaves_like 'disabled when using an external authorization service' do + subject { read_board board: board } + end + def read_board(board:, format: :html) get :show, params: { group_id: group, diff --git a/spec/controllers/groups/children_controller_spec.rb b/spec/controllers/groups/children_controller_spec.rb index e1b97013408..4085c8f95a9 100644 --- a/spec/controllers/groups/children_controller_spec.rb +++ b/spec/controllers/groups/children_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Groups::ChildrenController do + include ExternalAuthorizationServiceHelpers + let(:group) { create(:group, :public) } let(:user) { create(:user) } let!(:group_member) { create(:group_member, group: group, user: user) } @@ -317,5 +319,15 @@ describe Groups::ChildrenController do end end end + + context 'external authorization' do + it 'works when external authorization service is enabled' do + enable_external_authorization_service_check + + get :index, params: { group_id: group }, format: :json + + expect(response).to have_gitlab_http_status(200) + end + end end end diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb index 3a801fabafc..96a58d6d87c 100644 --- a/spec/controllers/groups/group_members_controller_spec.rb +++ b/spec/controllers/groups/group_members_controller_spec.rb @@ -1,8 +1,11 @@ require 'spec_helper' describe Groups::GroupMembersController do + include ExternalAuthorizationServiceHelpers + let(:user) { create(:user) } let(:group) { create(:group, :public, :access_requestable) } + let(:membership) { create(:group_member, group: group) } describe 'GET index' do it 'renders index with 200 status code' do @@ -263,4 +266,87 @@ describe Groups::GroupMembersController do end end end + + context 'with external authorization enabled' do + before do + enable_external_authorization_service_check + group.add_owner(user) + sign_in(user) + end + + describe 'GET #index' do + it 'is successful' do + get :index, params: { group_id: group } + + expect(response).to have_gitlab_http_status(200) + end + end + + describe 'POST #create' do + it 'is successful' do + post :create, params: { group_id: group, users: user, access_level: Gitlab::Access::GUEST } + + expect(response).to have_gitlab_http_status(302) + end + end + + describe 'PUT #update' do + it 'is successful' do + put :update, + params: { + group_member: { access_level: Gitlab::Access::GUEST }, + group_id: group, + id: membership + }, + format: :js + + expect(response).to have_gitlab_http_status(200) + end + end + + describe 'DELETE #destroy' do + it 'is successful' do + delete :destroy, params: { group_id: group, id: membership } + + expect(response).to have_gitlab_http_status(302) + end + end + + describe 'POST #destroy' do + it 'is successful' do + sign_in(create(:user)) + + post :request_access, params: { group_id: group } + + expect(response).to have_gitlab_http_status(302) + end + end + + describe 'POST #approve_request_access' do + it 'is successful' do + access_request = create(:group_member, :access_request, group: group) + post :approve_access_request, params: { group_id: group, id: access_request } + + expect(response).to have_gitlab_http_status(302) + end + end + + describe 'DELETE #leave' do + it 'is successful' do + group.add_owner(create(:user)) + + delete :leave, params: { group_id: group } + + expect(response).to have_gitlab_http_status(302) + end + end + + describe 'POST #resend_invite' do + it 'is successful' do + post :resend_invite, params: { group_id: group, id: membership } + + expect(response).to have_gitlab_http_status(302) + end + end + end end diff --git a/spec/controllers/groups/labels_controller_spec.rb b/spec/controllers/groups/labels_controller_spec.rb index fa664a29066..9af47114838 100644 --- a/spec/controllers/groups/labels_controller_spec.rb +++ b/spec/controllers/groups/labels_controller_spec.rb @@ -37,6 +37,12 @@ describe Groups::LabelsController do expect(label_ids).to match_array([group_label_1.title, subgroup_label_1.title]) end end + + context 'external authorization' do + subject { get :index, params: { group_id: group.to_param } } + + it_behaves_like 'disabled when using an external authorization service' + end end describe 'POST #toggle_subscription' do diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index 043cf28514b..d70946cbc8f 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -80,6 +80,12 @@ describe Groups::MilestonesController do expect(response.content_type).to eq 'application/json' end end + + context 'external authorization' do + subject { get :index, params: { group_id: group.to_param } } + + it_behaves_like 'disabled when using an external authorization service' + end end describe '#show' do diff --git a/spec/controllers/groups/settings/ci_cd_controller_spec.rb b/spec/controllers/groups/settings/ci_cd_controller_spec.rb index 3290ed8b088..b998f64ef72 100644 --- a/spec/controllers/groups/settings/ci_cd_controller_spec.rb +++ b/spec/controllers/groups/settings/ci_cd_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Groups::Settings::CiCdController do + include ExternalAuthorizationServiceHelpers + let(:group) { create(:group) } let(:user) { create(:user) } @@ -33,6 +35,19 @@ describe Groups::Settings::CiCdController do expect(response).to have_gitlab_http_status(404) end end + + context 'external authorization' do + before do + enable_external_authorization_service_check + group.add_owner(user) + end + + it 'renders show with 200 status code' do + get :show, params: { group_id: group } + + expect(response).to have_gitlab_http_status(200) + end + end end describe 'PUT #reset_registration_token' do diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb index 29ec3588316..40f05167350 100644 --- a/spec/controllers/groups/variables_controller_spec.rb +++ b/spec/controllers/groups/variables_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Groups::VariablesController do + include ExternalAuthorizationServiceHelpers + let(:group) { create(:group) } let(:user) { create(:user) } @@ -34,4 +36,36 @@ describe Groups::VariablesController do include_examples 'PATCH #update updates variables' end + + context 'with external authorization enabled' do + before do + enable_external_authorization_service_check + end + + describe 'GET #show' do + let!(:variable) { create(:ci_group_variable, group: group) } + + it 'is successful' do + get :show, params: { group_id: group }, format: :json + + expect(response).to have_gitlab_http_status(200) + end + end + + describe 'PATCH #update' do + let!(:variable) { create(:ci_group_variable, group: group) } + let(:owner) { group } + + it 'is successful' do + patch :update, + params: { + group_id: group, + variables_attributes: [{ id: variable.id, key: 'hello' }] + }, + format: :json + + expect(response).to have_gitlab_http_status(200) + end + end + end end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 4a28a27da79..431627cf85a 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe GroupsController do + include ExternalAuthorizationServiceHelpers + let(:user) { create(:user) } let(:admin) { create(:admin) } let(:group) { create(:group, :public) } @@ -665,4 +667,98 @@ describe GroupsController do end end end + + describe 'external authorization' do + before do + group.add_owner(user) + sign_in(user) + end + + context 'with external authorization service enabled' do + before do + enable_external_authorization_service_check + end + + describe 'GET #show' do + it 'is successful' do + get :show, params: { id: group.to_param } + + expect(response).to have_gitlab_http_status(200) + end + + it 'does not allow other formats' do + get :show, params: { id: group.to_param }, format: :atom + + expect(response).to have_gitlab_http_status(403) + end + end + + describe 'GET #edit' do + it 'is successful' do + get :edit, params: { id: group.to_param } + + expect(response).to have_gitlab_http_status(200) + end + end + + describe 'GET #new' do + it 'is successful' do + get :new + + expect(response).to have_gitlab_http_status(200) + end + end + + describe 'GET #index' do + it 'is successful' do + get :index + + # Redirects to the dashboard + expect(response).to have_gitlab_http_status(302) + end + end + + describe 'POST #create' do + it 'creates a group' do + expect do + post :create, params: { group: { name: 'a name', path: 'a-name' } } + end.to change { Group.count }.by(1) + end + end + + describe 'PUT #update' do + it 'updates a group' do + expect do + put :update, params: { id: group.to_param, group: { name: 'world' } } + end.to change { group.reload.name } + end + end + + describe 'DELETE #destroy' do + it 'deletes the group' do + delete :destroy, params: { id: group.to_param } + + expect(response).to have_gitlab_http_status(302) + end + end + end + + describe 'GET #activity' do + subject { get :activity, params: { id: group.to_param } } + + it_behaves_like 'disabled when using an external authorization service' + end + + describe 'GET #issues' do + subject { get :issues, params: { id: group.to_param } } + + it_behaves_like 'disabled when using an external authorization service' + end + + describe 'GET #merge_requests' do + subject { get :merge_requests, params: { id: group.to_param } } + + it_behaves_like 'disabled when using an external authorization service' + end + end end diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb index 1eeded06459..b1203fd00b0 100644 --- a/spec/controllers/projects/boards_controller_spec.rb +++ b/spec/controllers/projects/boards_controller_spec.rb @@ -98,6 +98,10 @@ describe Projects::BoardsController do end end + it_behaves_like 'unauthorized when external service denies access' do + subject { list_boards } + end + def list_boards(format: :html) get :index, params: { namespace_id: project.namespace, diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index 43639875265..168c0168bba 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -419,6 +419,17 @@ describe Projects::EnvironmentsController do expect(json_response['data']).to eq({}) expect(json_response['last_update']).to eq(42) end + + context 'when time params are provided' do + it 'returns a metrics JSON document' do + additional_metrics(start: '1554702993.5398998', end: '1554717396.996232') + + expect(response).to be_ok + expect(json_response['success']).to be(true) + expect(json_response['data']).to eq({}) + expect(json_response['last_update']).to eq(42) + end + end end context 'when only one time param is provided' do diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index c34d7c13d57..bfa23af76d5 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -127,6 +127,17 @@ describe Projects::IssuesController do expect(assigns(:issues).size).to eq(2) end end + + context 'external authorization' do + before do + sign_in user + project.add_developer(user) + end + + it_behaves_like 'unauthorized when external service denies access' do + subject { get :index, params: { namespace_id: project.namespace, project_id: project } } + end + end end describe 'GET #new' do diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 017162519d8..a125e470522 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -238,11 +238,11 @@ describe Projects::MergeRequestsController do assignee = create(:user) project.add_developer(assignee) - update_merge_request({ assignee_id: assignee.id }, format: :json) + update_merge_request({ assignee_ids: [assignee.id] }, format: :json) + body = JSON.parse(response.body) - expect(body['assignee'].keys) - .to match_array(%w(name username avatar_url id state web_url)) + expect(body['assignees']).to all(include(*%w(name username avatar_url id state web_url))) end end diff --git a/spec/controllers/projects/tags/releases_controller_spec.rb b/spec/controllers/projects/tags/releases_controller_spec.rb index 29f206c574b..66eff4844c2 100644 --- a/spec/controllers/projects/tags/releases_controller_spec.rb +++ b/spec/controllers/projects/tags/releases_controller_spec.rb @@ -18,40 +18,85 @@ describe Projects::Tags::ReleasesController do tag_id = release.tag project.releases.destroy_all # rubocop: disable DestroyAll - get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: tag_id } + response = get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: tag_id } release = assigns(:release) expect(release).not_to be_nil expect(release).not_to be_persisted + expect(response).to have_http_status(:ok) end it 'retrieves an existing release' do - get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: release.tag } + response = get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: release.tag } release = assigns(:release) expect(release).not_to be_nil expect(release).to be_persisted + expect(response).to have_http_status(:ok) end end describe 'PUT #update' do it 'updates release note description' do - update_release('description updated') + response = update_release(release.tag, "description updated") - release = project.releases.find_by_tag(tag) + release = project.releases.find_by(tag: tag) expect(release.description).to eq("description updated") + expect(response).to have_http_status(:found) end - it 'deletes release note when description is null' do - expect { update_release('') }.to change(project.releases, :count).by(-1) + it 'creates a release if one does not exist' do + tag_without_release = create_new_tag + + expect do + update_release(tag_without_release.name, "a new release") + end.to change { project.releases.count }.by(1) + + expect(response).to have_http_status(:found) + end + + it 'sets the release name, sha, and author for a new release' do + tag_without_release = create_new_tag + + response = update_release(tag_without_release.name, "a new release") + + release = project.releases.find_by(tag: tag_without_release.name) + expect(release.name).to eq(tag_without_release.name) + expect(release.sha).to eq(tag_without_release.target_commit.sha) + expect(release.author.id).to eq(user.id) + expect(response).to have_http_status(:found) + end + + it 'deletes release when description is empty' do + initial_releases_count = project.releases.count + + response = update_release(release.tag, "") + + expect(initial_releases_count).to eq(1) + expect(project.releases.count).to eq(0) + expect(response).to have_http_status(:found) + end + + it 'does nothing when description is empty and the tag does not have a release' do + tag_without_release = create_new_tag + + expect do + update_release(tag_without_release.name, "") + end.not_to change { project.releases.count } + + expect(response).to have_http_status(:found) end end - def update_release(description) + def create_new_tag + project.repository.add_tag(user, 'mytag', 'master') + end + + def update_release(tag_id, description) put :update, params: { namespace_id: project.namespace.to_param, project_id: project, - tag_id: release.tag, + tag_id: tag_id, release: { description: description } } end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index af437c5561b..1ce06bc877c 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -1,6 +1,7 @@ require('spec_helper') describe ProjectsController do + include ExternalAuthorizationServiceHelpers include ProjectForksHelper let(:project) { create(:project) } @@ -411,6 +412,37 @@ describe ProjectsController do it_behaves_like 'updating a project' end + + context 'as maintainer' do + before do + project.add_maintainer(user) + sign_in(user) + end + + it_behaves_like 'unauthorized when external service denies access' do + subject do + put :update, + params: { + namespace_id: project.namespace, + id: project, + project: { description: 'Hello world' } + } + project.reload + end + + it 'updates when the service allows access' do + external_service_allow_access(user, project) + + expect { subject }.to change(project, :description) + end + + it 'does not update when the service rejects access' do + external_service_deny_access(user, project) + + expect { subject }.not_to change(project, :description) + end + end + end end describe '#transfer' do diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index 02a0cfe0272..752d6ae55cc 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe SearchController do + include ExternalAuthorizationServiceHelpers + let(:user) { create(:user) } before do @@ -76,4 +78,41 @@ describe SearchController do expect(assigns[:search_objects].count).to eq(0) end end + + context 'with external authorization service enabled' do + let(:project) { create(:project, namespace: user.namespace) } + let(:note) { create(:note_on_issue, project: project) } + + before do + enable_external_authorization_service_check + end + + describe 'GET #show' do + it 'renders a 403 when no project is given' do + get :show, params: { scope: 'notes', search: note.note } + + expect(response).to have_gitlab_http_status(403) + end + + it 'renders a 200 when a project was set' do + get :show, params: { project_id: project.id, scope: 'notes', search: note.note } + + expect(response).to have_gitlab_http_status(200) + end + end + + describe 'GET #autocomplete' do + it 'renders a 403 when no project is given' do + get :autocomplete, params: { term: 'hello' } + + expect(response).to have_gitlab_http_status(403) + end + + it 'renders a 200 when a project was set' do + get :autocomplete, params: { project_id: project.id, term: 'hello' } + + expect(response).to have_gitlab_http_status(200) + end + end + end end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 4f6a6881193..42d28c53d34 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -223,6 +223,12 @@ describe UsersController do end end + context 'external authorization' do + subject { get :calendar_activities, params: { username: user.username } } + + it_behaves_like 'disabled when using an external authorization service' + end + def create_push_event push_data = Gitlab::DataBuilder::Push.build_sample(project, public_user) EventCreateService.new.push(project, public_user, push_data) @@ -286,6 +292,12 @@ describe UsersController do expect(JSON.parse(response.body)).to have_key('html') end end + + context 'external authorization' do + subject { get :snippets, params: { username: user.username } } + + it_behaves_like 'disabled when using an external authorization service' + end end describe 'GET #exists' do diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb index 20671da016e..b74f72f2bd3 100644 --- a/spec/factories/pages_domains.rb +++ b/spec/factories/pages_domains.rb @@ -41,6 +41,10 @@ nNp/xedE1YxutQ== enabled_until nil end + trait :scheduled_for_removal do + remove_at { 1.day.from_now } + end + trait :unverified do verified_at nil end diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index dfdb8d589eb..b358c6b9c34 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -2,6 +2,7 @@ require 'rails_helper' describe 'Issue Boards', :js do include BoardHelpers + include FilteredSearchHelpers let(:user) { create(:user) } let(:user2) { create(:user) } @@ -129,6 +130,7 @@ describe 'Issue Boards', :js do click_link 'Unassigned' end + close_dropdown_menu_if_visible wait_for_requests expect(page).to have_content('No assignee') diff --git a/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb b/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb new file mode 100644 index 00000000000..4098dd02141 --- /dev/null +++ b/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe 'The group dashboard' do + include ExternalAuthorizationServiceHelpers + + let(:user) { create(:user) } + + before do + sign_in user + end + + describe 'The top navigation' do + it 'has all the expected links' do + visit dashboard_groups_path + + within('.navbar') do + expect(page).to have_button('Projects') + expect(page).to have_button('Groups') + expect(page).to have_link('Activity') + expect(page).to have_link('Milestones') + expect(page).to have_link('Snippets') + end + end + + it 'hides some links when an external authorization service is enabled' do + enable_external_authorization_service_check + visit dashboard_groups_path + + within('.navbar') do + expect(page).to have_button('Projects') + expect(page).to have_button('Groups') + expect(page).not_to have_link('Activity') + expect(page).not_to have_link('Milestones') + expect(page).to have_link('Snippets') + end + end + end +end diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb index fbc2e5cc3d3..50b71368e13 100644 --- a/spec/features/dashboard/issuables_counter_spec.rb +++ b/spec/features/dashboard/issuables_counter_spec.rb @@ -8,7 +8,7 @@ describe 'Navigation bar counter', :use_clean_rails_memory_store_caching do before do issue.assignees = [user] - merge_request.update(assignee: user) + merge_request.update(assignees: [user]) sign_in(user) end @@ -33,7 +33,7 @@ describe 'Navigation bar counter', :use_clean_rails_memory_store_caching do expect_counters('merge_requests', '1') - merge_request.update(assignee: nil) + merge_request.update(assignees: []) user.invalidate_cache_counts diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb index 4965770605a..0c6713f623c 100644 --- a/spec/features/dashboard/merge_requests_spec.rb +++ b/spec/features/dashboard/merge_requests_spec.rb @@ -48,14 +48,14 @@ describe 'Dashboard Merge Requests' do let!(:assigned_merge_request) do create(:merge_request, - assignee: current_user, + assignees: [current_user], source_project: project, author: create(:user)) end let!(:assigned_merge_request_from_fork) do create(:merge_request, - source_branch: 'markdown', assignee: current_user, + source_branch: 'markdown', assignees: [current_user], target_project: public_project, source_project: forked_project, author: create(:user)) end diff --git a/spec/features/groups/group_page_with_external_authorization_service_spec.rb b/spec/features/groups/group_page_with_external_authorization_service_spec.rb new file mode 100644 index 00000000000..c05c3f4f3d6 --- /dev/null +++ b/spec/features/groups/group_page_with_external_authorization_service_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'The group page' do + include ExternalAuthorizationServiceHelpers + + let(:user) { create(:user) } + let(:group) { create(:group) } + + before do + sign_in user + group.add_owner(user) + end + + def expect_all_sidebar_links + within('.nav-sidebar') do + expect(page).to have_link('Overview') + expect(page).to have_link('Details') + expect(page).to have_link('Activity') + expect(page).to have_link('Issues') + expect(page).to have_link('Merge Requests') + expect(page).to have_link('Members') + end + end + + describe 'The sidebar' do + it 'has all the expected links' do + visit group_path(group) + + expect_all_sidebar_links + end + + it 'shows all project features when policy control is enabled' do + stub_application_setting(external_authorization_service_enabled: true) + + visit group_path(group) + + expect_all_sidebar_links + end + + it 'hides some links when an external authorization service configured with an url' do + enable_external_authorization_service_check + visit group_path(group) + + within('.nav-sidebar') do + expect(page).to have_link('Overview') + expect(page).to have_link('Details') + expect(page).not_to have_link('Activity') + expect(page).not_to have_link('Contribution Analytics') + + expect(page).not_to have_link('Issues') + expect(page).not_to have_link('Merge Requests') + expect(page).to have_link('Members') + end + end + end +end diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb index 54a8016c157..e1bc4eca619 100644 --- a/spec/features/groups/merge_requests_spec.rb +++ b/spec/features/groups/merge_requests_spec.rb @@ -38,7 +38,7 @@ describe 'Group merge requests page' do context 'when merge request assignee to user' do before do - issuable.update!(assignee: user) + issuable.update!(assignees: [user]) visit path end diff --git a/spec/features/issuables/shortcuts_issuable_spec.rb b/spec/features/issuables/shortcuts_issuable_spec.rb index a0ae6720a9f..a19101366a0 100644 --- a/spec/features/issuables/shortcuts_issuable_spec.rb +++ b/spec/features/issuables/shortcuts_issuable_spec.rb @@ -13,7 +13,7 @@ describe 'Blob shortcuts', :js do end shared_examples "quotes the selected text" do - it "quotes the selected text" do + it "quotes the selected text", :quarantine do select_element('.note-text') find('body').native.send_key('r') diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb index 7584339ccc0..7a6f76cb382 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -139,7 +139,7 @@ describe 'Dropdown milestone', :js do expect_filtered_search_input_empty end - it 'fills in the milestone name when the milestone is partially filled' do + it 'fills in the milestone name when the milestone is partially filled', :quarantine do filtered_search.send_keys('v') click_milestone(milestone.title) diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index 26c781350e5..6fa2ad8711f 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -30,8 +30,8 @@ describe 'New/edit issue', :js do # the original method, resulting in infinite recursion when called. # This is likely a bug with helper modules included into dynamically generated view classes. # To work around this, we have to hold on to and call to the original implementation manually. - original_issue_dropdown_options = FormHelper.instance_method(:issue_assignees_dropdown_options) - allow_any_instance_of(FormHelper).to receive(:issue_assignees_dropdown_options).and_wrap_original do |original, *args| + original_issue_dropdown_options = FormHelper.instance_method(:assignees_dropdown_options) + allow_any_instance_of(FormHelper).to receive(:assignees_dropdown_options).and_wrap_original do |original, *args| options = original_issue_dropdown_options.bind(original.receiver).call(*args) options[:data][:per_page] = 2 diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index 3050f23c130..321da8f44d7 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -130,7 +130,7 @@ describe 'Issue Sidebar' do end end - context 'creating a project label', :js do + context 'creating a project label', :js, :quarantine do before do page.within('.block.labels') do click_link 'Create project' diff --git a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb index 0a006011c89..b69fba0db00 100644 --- a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb +++ b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb @@ -29,7 +29,7 @@ describe 'User creates branch and merge request on issue page', :js do end # In order to improve tests performance, all UI checks are placed in this test. - it 'shows elements' do + it 'shows elements', :quarantine do button_create_merge_request = find('.js-create-merge-request') button_toggle_dropdown = find('.create-mr-dropdown-wrap .dropdown-toggle') @@ -139,7 +139,7 @@ describe 'User creates branch and merge request on issue page', :js do visit project_issue_path(project, issue) end - it 'disables the create branch button' do + it 'disables the create branch button', :quarantine do expect(page).to have_css('.create-mr-dropdown-wrap .unavailable:not(.hidden)') expect(page).to have_css('.create-mr-dropdown-wrap .available.hidden', visible: false) expect(page).to have_content /Related merge requests/ diff --git a/spec/features/issues/user_interacts_with_awards_spec.rb b/spec/features/issues/user_interacts_with_awards_spec.rb index afa425c2cec..d117620a2b1 100644 --- a/spec/features/issues/user_interacts_with_awards_spec.rb +++ b/spec/features/issues/user_interacts_with_awards_spec.rb @@ -14,7 +14,7 @@ describe 'User interacts with awards' do visit(project_issue_path(project, issue)) end - it 'toggles the thumbsup award emoji' do + it 'toggles the thumbsup award emoji', :quarantine do page.within('.awards') do thumbsup = page.first('.award-control') thumbsup.click @@ -75,7 +75,7 @@ describe 'User interacts with awards' do end end - it 'shows the list of award emoji categories' do + it 'shows the list of award emoji categories', :quarantine do page.within('.awards') do page.find('.js-add-award').click end diff --git a/spec/features/issues/user_uses_quick_actions_spec.rb b/spec/features/issues/user_uses_quick_actions_spec.rb index ea474759547..68af8303c2f 100644 --- a/spec/features/issues/user_uses_quick_actions_spec.rb +++ b/spec/features/issues/user_uses_quick_actions_spec.rb @@ -60,34 +60,7 @@ describe 'Issues > User uses quick actions', :js do it_behaves_like 'remove_due_date quick action' it_behaves_like 'duplicate quick action' it_behaves_like 'create_merge_request quick action' - - describe 'adding a due date from note' do - let(:issue) { create(:issue, project: project) } - - it_behaves_like 'due quick action available and date can be added' - - context 'when the current user cannot update the due date' do - let(:guest) { create(:user) } - before do - project.add_guest(guest) - gitlab_sign_out - sign_in(guest) - visit project_issue_path(project, issue) - end - - it_behaves_like 'due quick action not available' - end - end - - describe 'toggling the WIP prefix from the title from note' do - let(:issue) { create(:issue, project: project) } - - it 'does not recognize the command nor create a note' do - add_note("/wip") - - expect(page).not_to have_content '/wip' - end - end + it_behaves_like 'due quick action' describe 'move the issue to another project' do let(:issue) { create(:issue, project: project) } diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb index bac297de4a6..489651fea15 100644 --- a/spec/features/labels_hierarchy_spec.rb +++ b/spec/features/labels_hierarchy_spec.rb @@ -21,7 +21,7 @@ describe 'Labels Hierarchy', :js, :nested_groups do end shared_examples 'assigning labels from sidebar' do - it 'can assign all ancestors labels' do + it 'can assign all ancestors labels', :quarantine do [grandparent_group_label, parent_group_label, project_label_1].each do |label| page.within('.block.labels') do find('.edit-link').click diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb index c837a6752f9..65de0afae0c 100644 --- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb +++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb @@ -113,7 +113,7 @@ describe 'Merge request > User creates image diff notes', :js do create_image_diff_note end - it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do + it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes', :quarantine do indicator = find('.js-image-badge', match: :first) badge = find('.user-avatar-link .badge', match: :first) diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb index ea2bb1503bb..bcc11217389 100644 --- a/spec/features/merge_request/user_creates_merge_request_spec.rb +++ b/spec/features/merge_request/user_creates_merge_request_spec.rb @@ -68,15 +68,15 @@ describe "User creates a merge request", :js do fill_in("Title", with: title) end - click_button("Assignee") - expect(find(".js-assignee-search")["data-project-id"]).to eq(project.id.to_s) + find('.js-assignee-search').click page.within(".dropdown-menu-user") do expect(page).to have_content("Unassigned") .and have_content(user.name) .and have_content(project.users.first.name) end + find('.js-assignee-search').click click_button("Submit merge request") diff --git a/spec/features/merge_request/user_creates_mr_spec.rb b/spec/features/merge_request/user_creates_mr_spec.rb index c169a68cd1c..c9dedab048a 100644 --- a/spec/features/merge_request/user_creates_mr_spec.rb +++ b/spec/features/merge_request/user_creates_mr_spec.rb @@ -1,11 +1,18 @@ require 'rails_helper' describe 'Merge request > User creates MR' do - it_behaves_like 'a creatable merge request' + include ProjectForksHelper - context 'from a forked project' do - include ProjectForksHelper + before do + stub_licensed_features(multiple_merge_request_assignees: false) + end + context 'non-fork merge request' do + include_context 'merge request create context' + it_behaves_like 'a creatable merge request' + end + + context 'from a forked project' do let(:canonical_project) { create(:project, :public, :repository) } let(:source_project) do @@ -15,6 +22,7 @@ describe 'Merge request > User creates MR' do end context 'to canonical project' do + include_context 'merge request create context' it_behaves_like 'a creatable merge request' end @@ -25,6 +33,7 @@ describe 'Merge request > User creates MR' do namespace: user.namespace) end + include_context 'merge request create context' it_behaves_like 'a creatable merge request' end end diff --git a/spec/features/merge_request/user_edits_mr_spec.rb b/spec/features/merge_request/user_edits_mr_spec.rb index 3152707136c..25979513ead 100644 --- a/spec/features/merge_request/user_edits_mr_spec.rb +++ b/spec/features/merge_request/user_edits_mr_spec.rb @@ -1,13 +1,21 @@ -require 'rails_helper' +require 'spec_helper' describe 'Merge request > User edits MR' do include ProjectForksHelper - it_behaves_like 'an editable merge request' + before do + stub_licensed_features(multiple_merge_request_assignees: false) + end + + context 'non-fork merge request' do + include_context 'merge request edit context' + it_behaves_like 'an editable merge request' + end context 'for a forked project' do - it_behaves_like 'an editable merge request' do - let(:source_project) { fork_project(target_project, nil, repository: true) } - end + let(:source_project) { fork_project(target_project, nil, repository: true) } + + include_context 'merge request edit context' + it_behaves_like 'an editable merge request' end end diff --git a/spec/features/merge_requests/user_filters_by_assignees_spec.rb b/spec/features/merge_requests/user_filters_by_assignees_spec.rb index d6c770c93f1..0cbf1bcae30 100644 --- a/spec/features/merge_requests/user_filters_by_assignees_spec.rb +++ b/spec/features/merge_requests/user_filters_by_assignees_spec.rb @@ -7,7 +7,7 @@ describe 'Merge Requests > User filters by assignees', :js do let(:user) { project.creator } before do - create(:merge_request, assignee: user, title: 'Bugfix1', source_project: project, target_project: project, source_branch: 'bugfix1') + create(:merge_request, assignees: [user], title: 'Bugfix1', source_project: project, target_project: project, source_branch: 'bugfix1') create(:merge_request, title: 'Bugfix2', source_project: project, target_project: project, source_branch: 'bugfix2') sign_in(user) diff --git a/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb b/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb index 1615899a047..4627931f26a 100644 --- a/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb +++ b/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb @@ -10,7 +10,7 @@ describe 'Merge requests > User filters by multiple criteria', :js do before do sign_in(user) - mr = create(:merge_request, title: 'Bugfix2', author: user, assignee: user, source_project: project, target_project: project, milestone: milestone) + mr = create(:merge_request, title: 'Bugfix2', author: user, assignees: [user], source_project: project, target_project: project, milestone: milestone) mr.labels << wontfix visit project_merge_requests_path(project) diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb index c691011b9ca..bd91fae1453 100644 --- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb @@ -12,7 +12,7 @@ describe 'Merge requests > User lists merge requests' do title: 'fix', source_project: project, source_branch: 'fix', - assignee: user, + assignees: [user], milestone: create(:milestone, project: project, due_date: '2013-12-11'), created_at: 1.minute.ago, updated_at: 1.minute.ago) @@ -20,7 +20,7 @@ describe 'Merge requests > User lists merge requests' do title: 'markdown', source_project: project, source_branch: 'markdown', - assignee: user, + assignees: [user], milestone: create(:milestone, project: project, due_date: '2013-12-12'), created_at: 2.minutes.ago, updated_at: 2.minutes.ago) diff --git a/spec/features/merge_requests/user_mass_updates_spec.rb b/spec/features/merge_requests/user_mass_updates_spec.rb index e535c7e5811..c2dd105324d 100644 --- a/spec/features/merge_requests/user_mass_updates_spec.rb +++ b/spec/features/merge_requests/user_mass_updates_spec.rb @@ -54,8 +54,7 @@ describe 'Merge requests > User mass updates', :js do describe 'remove assignee' do before do - merge_request.assignee = user - merge_request.save + merge_request.assignees = [user] visit project_merge_requests_path(project) end diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb index 9909bfb5904..1b3718968b9 100644 --- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb +++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb @@ -63,7 +63,7 @@ describe 'User visits the profile preferences page' do end describe 'User changes their language', :js do - it 'creates a flash message' do + it 'creates a flash message', :quarantine do select2('en', from: '#user_preferred_language') click_button 'Save' diff --git a/spec/features/projects/classification_label_on_project_pages_spec.rb b/spec/features/projects/classification_label_on_project_pages_spec.rb new file mode 100644 index 00000000000..92f8aa8eb8d --- /dev/null +++ b/spec/features/projects/classification_label_on_project_pages_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Classification label on project pages' do + let(:project) do + create(:project, external_authorization_classification_label: 'authorized label') + end + let(:user) { create(:user) } + + before do + stub_application_setting(external_authorization_service_enabled: true) + project.add_maintainer(user) + sign_in(user) + end + + it 'shows the classification label on the project page' do + visit project_path(project) + + expect(page).to have_content('authorized label') + end +end diff --git a/spec/features/projects/forks/fork_list_spec.rb b/spec/features/projects/forks/fork_list_spec.rb new file mode 100644 index 00000000000..2c41c61a660 --- /dev/null +++ b/spec/features/projects/forks/fork_list_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe 'listing forks of a project' do + include ProjectForksHelper + include ExternalAuthorizationServiceHelpers + + let(:source) { create(:project, :public, :repository) } + let!(:fork) { fork_project(source, nil, repository: true) } + let(:user) { create(:user) } + + before do + source.add_maintainer(user) + sign_in(user) + end + + it 'shows the forked project in the list with commit as description' do + visit project_forks_path(source) + + page.within('li.project-row') do + expect(page).to have_content(fork.full_name) + expect(page).to have_css('a.commit-row-message') + end + end + + it 'does not show the commit message when an external authorization service is used' do + enable_external_authorization_service_check + + visit project_forks_path(source) + + page.within('li.project-row') do + expect(page).to have_content(fork.full_name) + expect(page).not_to have_css('a.commit-row-message') + end + end +end diff --git a/spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb b/spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb new file mode 100644 index 00000000000..a8612d77a5e --- /dev/null +++ b/spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb @@ -0,0 +1,128 @@ +require 'spec_helper' + +describe 'viewing an issue with cross project references' do + include ExternalAuthorizationServiceHelpers + include Gitlab::Routing.url_helpers + + let(:user) { create(:user) } + let(:other_project) do + create(:project, :public, + external_authorization_classification_label: 'other_label') + end + let(:other_issue) do + create(:issue, :closed, + title: 'I am in another project', + project: other_project) + end + let(:other_confidential_issue) do + create(:issue, :confidential, :closed, + title: 'I am in another project and confidential', + project: other_project) + end + let(:other_merge_request) do + create(:merge_request, :closed, + title: 'I am a merge request in another project', + source_project: other_project) + end + let(:description_referencing_other_issue) do + "Referencing: #{other_issue.to_reference(project)}, "\ + "a confidential issue #{confidential_issue.to_reference}, "\ + "a cross project confidential issue #{other_confidential_issue.to_reference(project)}, and "\ + "a cross project merge request #{other_merge_request.to_reference(project)}" + end + let(:project) { create(:project) } + let(:issue) do + create(:issue, + project: project, + description: description_referencing_other_issue ) + end + let(:confidential_issue) do + create(:issue, :confidential, :closed, + title: "I am in the same project and confidential", + project: project) + end + + before do + project.add_developer(user) + sign_in(user) + end + + it 'shows all information related to the cross project reference' do + visit project_issue_path(project, issue) + + expect(page).to have_link("#{other_issue.to_reference(project)} (#{other_issue.state})") + expect(page).to have_xpath("//a[@title='#{other_issue.title}']") + end + + it 'shows a link to the confidential issue in the same project' do + visit project_issue_path(project, issue) + + expect(page).to have_link("#{confidential_issue.to_reference(project)} (#{confidential_issue.state})") + expect(page).to have_xpath("//a[@title='#{confidential_issue.title}']") + end + + it 'does not show the link to a cross project confidential issue when the user does not have access' do + visit project_issue_path(project, issue) + + expect(page).not_to have_link("#{other_confidential_issue.to_reference(project)} (#{other_confidential_issue.state})") + expect(page).not_to have_xpath("//a[@title='#{other_confidential_issue.title}']") + end + + it 'shows the link to a cross project confidential issue when the user has access' do + other_project.add_developer(user) + + visit project_issue_path(project, issue) + + expect(page).to have_link("#{other_confidential_issue.to_reference(project)} (#{other_confidential_issue.state})") + expect(page).to have_xpath("//a[@title='#{other_confidential_issue.title}']") + end + + context 'when an external authorization service is enabled' do + before do + enable_external_authorization_service_check + end + + it 'only hits the external service for the project the user is viewing' do + expect(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(user, 'default_label', any_args).at_least(1).and_return(true) + expect(::Gitlab::ExternalAuthorization) + .not_to receive(:access_allowed?).with(user, 'other_label', any_args) + + visit project_issue_path(project, issue) + end + + it 'shows only the link to the cross project references' do + visit project_issue_path(project, issue) + + expect(page).to have_link("#{other_issue.to_reference(project)}") + expect(page).to have_link("#{other_merge_request.to_reference(project)}") + expect(page).not_to have_content("#{other_issue.to_reference(project)} (#{other_issue.state})") + expect(page).not_to have_xpath("//a[@title='#{other_issue.title}']") + expect(page).not_to have_content("#{other_merge_request.to_reference(project)} (#{other_merge_request.state})") + expect(page).not_to have_xpath("//a[@title='#{other_merge_request.title}']") + end + + it 'does not link a cross project confidential issue if the user does not have access' do + visit project_issue_path(project, issue) + + expect(page).not_to have_link("#{other_confidential_issue.to_reference(project)}") + expect(page).not_to have_xpath("//a[@title='#{other_confidential_issue.title}']") + end + + it 'links a cross project confidential issue without exposing information when the user has access' do + other_project.add_developer(user) + + visit project_issue_path(project, issue) + + expect(page).to have_link("#{other_confidential_issue.to_reference(project)}") + expect(page).not_to have_xpath("//a[@title='#{other_confidential_issue.title}']") + end + + it 'shows a link to the confidential issue in the same project' do + visit project_issue_path(project, issue) + + expect(page).to have_link("#{confidential_issue.to_reference(project)} (#{confidential_issue.state})") + expect(page).to have_xpath("//a[@title='#{confidential_issue.title}']") + end + end +end diff --git a/spec/features/projects/settings/external_authorization_service_settings_spec.rb b/spec/features/projects/settings/external_authorization_service_settings_spec.rb new file mode 100644 index 00000000000..31b2892cf6f --- /dev/null +++ b/spec/features/projects/settings/external_authorization_service_settings_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Projects > Settings > External Authorization Classification Label setting' do + let(:user) { create(:user) } + let(:project) { create(:project_empty_repo) } + + before do + project.add_maintainer(user) + sign_in(user) + end + + it 'shows the field to set a classification label' do + stub_application_setting(external_authorization_service_enabled: true) + + visit edit_project_path(project) + + expect(page).to have_selector('#project_external_authorization_classification_label') + end +end diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index efb7b01f5ad..bcbba6f14da 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -43,7 +43,7 @@ describe "User creates wiki page" do expect(page).to have_content("Create Page") end - it "shows non-escaped link in the pages list", :js do + it "shows non-escaped link in the pages list", :js, :quarantine do fill_in(:wiki_title, with: "one/two/three-test") page.within(".wiki-form") do diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb index 444de26733f..1cc47cd6bd1 100644 --- a/spec/features/search/user_uses_header_search_field_spec.rb +++ b/spec/features/search/user_uses_header_search_field_spec.rb @@ -36,7 +36,7 @@ describe 'User uses header search field' do end context 'when clicking merge requests' do - let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) } + let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignees: [user]) } it 'shows assigned merge requests' do find('.search-input-container .dropdown-menu').click_link('Merge requests assigned to me') @@ -100,7 +100,7 @@ describe 'User uses header search field' do end context 'when clicking merge requests' do - let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) } + let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignees: [user]) } it 'shows assigned merge requests' do find('.dropdown-menu').click_link('Merge requests assigned to me') diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb index 86379164cf0..351750c0179 100644 --- a/spec/features/users/show_spec.rb +++ b/spec/features/users/show_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe 'User page' do + include ExternalAuthorizationServiceHelpers + let(:user) { create(:user) } context 'with public profile' do @@ -86,4 +88,24 @@ describe 'User page' do end end end + + context 'most recent activity' do + it 'shows the most recent activity' do + visit(user_path(user)) + + expect(page).to have_content('Most Recent Activity') + end + + context 'when external authorization is enabled' do + before do + enable_external_authorization_service_check + end + + it 'hides the most recent activity' do + visit(user_path(user)) + + expect(page).not_to have_content('Most Recent Activity') + end + end + end end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index fe53fabe54c..6a47cd013f8 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -13,60 +13,32 @@ describe IssuesFinder do expect(issues).to contain_exactly(issue1, issue2, issue3, issue4) end - context 'filtering by assignee ID' do - let(:params) { { assignee_id: user.id } } + context 'assignee filtering' do + let(:issuables) { issues } - it 'returns issues assigned to that user' do - expect(issues).to contain_exactly(issue1, issue2) - end - end - - context 'filtering by assignee usernames' do - set(:user3) { create(:user) } - let(:params) { { assignee_username: [user2.username, user3.username] } } - - before do - project2.add_developer(user3) - - issue3.assignees = [user2, user3] + it_behaves_like 'assignee ID filter' do + let(:params) { { assignee_id: user.id } } + let(:expected_issuables) { [issue1, issue2] } end - it 'returns issues assigned to those users' do - expect(issues).to contain_exactly(issue3) - end - end - - context 'filtering by no assignee' do - let(:params) { { assignee_id: 'None' } } - - it 'returns issues not assigned to any assignee' do - expect(issues).to contain_exactly(issue4) - end - - it 'returns issues not assigned to any assignee' do - params[:assignee_id] = 0 - - expect(issues).to contain_exactly(issue4) - end - - it 'returns issues not assigned to any assignee' do - params[:assignee_id] = 'none' + it_behaves_like 'assignee username filter' do + before do + project2.add_developer(user3) + issue3.assignees = [user2, user3] + end - expect(issues).to contain_exactly(issue4) + set(:user3) { create(:user) } + let(:params) { { assignee_username: [user2.username, user3.username] } } + let(:expected_issuables) { [issue3] } end - end - - context 'filtering by any assignee' do - let(:params) { { assignee_id: 'Any' } } - it 'returns issues assigned to any assignee' do - expect(issues).to contain_exactly(issue1, issue2, issue3) + it_behaves_like 'no assignee filter' do + set(:user3) { create(:user) } + let(:expected_issuables) { [issue4] } end - it 'returns issues assigned to any assignee' do - params[:assignee_id] = 'any' - - expect(issues).to contain_exactly(issue1, issue2, issue3) + it_behaves_like 'any assignee filter' do + let(:expected_issuables) { [issue1, issue2, issue3] } end end @@ -559,6 +531,13 @@ describe IssuesFinder do expect(issues.count).to eq 0 end end + + context 'external authorization' do + it_behaves_like 'a finder with external authorization service' do + let!(:subject) { create(:issue, project: project) } + let(:project_params) { { project_id: project.id } } + end + end end describe '#row_count', :request_store do diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb index 3f060ba0553..98b4933fef6 100644 --- a/spec/finders/labels_finder_spec.rb +++ b/spec/finders/labels_finder_spec.rb @@ -226,5 +226,12 @@ describe LabelsFinder do expect(finder.execute).to eq [project_label_1] end end + + context 'external authorization' do + it_behaves_like 'a finder with external authorization service' do + let!(:subject) { create(:label, project: project) } + let(:project_params) { { project_id: project.id } } + end + end end end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index f508b9bdb6f..117f4a03735 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -136,21 +136,50 @@ describe MergeRequestsFinder do end end - context 'filtering by group milestone' do - let(:group_milestone) { create(:milestone, group: group) } + context 'assignee filtering' do + let(:issuables) { described_class.new(user, params).execute } - before do - project2.update(namespace: group) - merge_request2.update(milestone: group_milestone) - merge_request3.update(milestone: group_milestone) + it_behaves_like 'assignee ID filter' do + let(:params) { { assignee_id: user.id } } + let(:expected_issuables) { [merge_request1, merge_request2] } end - it 'returns merge requests assigned to that group milestone' do - params = { milestone_title: group_milestone.title } + it_behaves_like 'assignee username filter' do + before do + project2.add_developer(user3) + merge_request3.assignees = [user2, user3] + end - merge_requests = described_class.new(user, params).execute + set(:user3) { create(:user) } + let(:params) { { assignee_username: [user2.username, user3.username] } } + let(:expected_issuables) { [merge_request3] } + end - expect(merge_requests).to contain_exactly(merge_request2, merge_request3) + it_behaves_like 'no assignee filter' do + set(:user3) { create(:user) } + let(:expected_issuables) { [merge_request4, merge_request5] } + end + + it_behaves_like 'any assignee filter' do + let(:expected_issuables) { [merge_request1, merge_request2, merge_request3] } + end + + context 'filtering by group milestone' do + let(:group_milestone) { create(:milestone, group: group) } + + before do + project2.update(namespace: group) + merge_request2.update(milestone: group_milestone) + merge_request3.update(milestone: group_milestone) + end + + it 'returns merge requests assigned to that group milestone' do + params = { milestone_title: group_milestone.title } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests).to contain_exactly(merge_request2, merge_request3) + end end end @@ -253,6 +282,13 @@ describe MergeRequestsFinder do expect(finder.row_count).to eq(1) end end + + context 'external authorization' do + it_behaves_like 'a finder with external authorization service' do + let!(:subject) { create(:merge_request, source_project: project) } + let(:project_params) { { project_id: project.id } } + end + end end context 'when projects require different access levels for merge requests' do diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index 93287f3e9b8..d367f9015c7 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe SnippetsFinder do + include ExternalAuthorizationServiceHelpers include Gitlab::Allowable describe '#initialize' do @@ -164,4 +165,35 @@ describe SnippetsFinder do end it_behaves_like 'snippet visibility' + + context 'external authorization' do + let(:user) { create(:user) } + let(:project) { create(:project) } + let!(:snippet) { create(:project_snippet, :public, project: project) } + + before do + project.add_maintainer(user) + end + + it_behaves_like 'a finder with external authorization service' do + let!(:subject) { create(:project_snippet, project: project) } + let(:project_params) { { project: project } } + end + + it 'includes the result if the external service allows access' do + external_service_allow_access(user, project) + + results = described_class.new(user, project: project).execute + + expect(results).to contain_exactly(snippet) + end + + it 'does not include any results if the external service denies access' do + external_service_deny_access(user, project) + + results = described_class.new(user, project: project).execute + + expect(results).to be_empty + end + end end diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb index d4ed41d54f0..22318a9946a 100644 --- a/spec/finders/todos_finder_spec.rb +++ b/spec/finders/todos_finder_spec.rb @@ -47,6 +47,13 @@ describe TodosFinder do end end end + + context 'external authorization' do + it_behaves_like 'a finder with external authorization service' do + let!(:subject) { create(:todo, project: project, user: user) } + let(:project_params) { { project_id: project.id } } + end + end end describe '#sort' do diff --git a/spec/fixtures/api/graphql/introspection.graphql b/spec/fixtures/api/graphql/introspection.graphql new file mode 100644 index 00000000000..7b712068fcd --- /dev/null +++ b/spec/fixtures/api/graphql/introspection.graphql @@ -0,0 +1,92 @@ +# pulled from GraphiQL query +query IntrospectionQuery { + __schema { + queryType { name } + mutationType { name } + subscriptionType { name } + types { + ...FullType + } + directives { + name + description + locations + args { + ...InputValue + } + } + } +} + +fragment FullType on __Type { + kind + name + description + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } +} + +fragment InputValue on __InputValue { + name + description + type { ...TypeRef } + defaultValue +} + +fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } +} diff --git a/spec/fixtures/api/schemas/entities/issue_board.json b/spec/fixtures/api/schemas/entities/issue_board.json index f7b270ffa8d..7cb65e1f2f5 100644 --- a/spec/fixtures/api/schemas/entities/issue_board.json +++ b/spec/fixtures/api/schemas/entities/issue_board.json @@ -9,6 +9,9 @@ "project_id": { "type": "integer" }, "relative_position": { "type": ["integer", "null"] }, "time_estimate": { "type": "integer" }, + "total_time_spent": { "type": "integer" }, + "human_time_estimate": { "type": ["string", "null"] }, + "human_total_time_spent": { "type": ["string", "null"] }, "weight": { "type": ["integer", "null"] }, "project": { "type": "object", diff --git a/spec/fixtures/api/schemas/entities/merge_request_basic.json b/spec/fixtures/api/schemas/entities/merge_request_basic.json index 3006b482d41..88a600398b1 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_basic.json +++ b/spec/fixtures/api/schemas/entities/merge_request_basic.json @@ -6,14 +6,14 @@ "source_branch_exists": { "type": "boolean" }, "merge_error": { "type": ["string", "null"] }, "rebase_in_progress": { "type": "boolean" }, - "assignee_id": { "type": ["integer", "null"] }, "allow_collaboration": { "type": "boolean"}, "allow_maintainer_to_push": { "type": "boolean"}, - "assignee": { - "oneOf": [ - { "type": "null" }, - { "$ref": "user.json" } - ] + "assignees": { + "type": ["array"], + "items": { + "type": "object", + "$ref": "../public_api/v4/user/basic.json" + } }, "milestone": { "type": [ "object", "null" ] diff --git a/spec/fixtures/api/schemas/public_api/v4/artifact.json b/spec/fixtures/api/schemas/public_api/v4/artifact.json new file mode 100644 index 00000000000..9df957b1498 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/artifact.json @@ -0,0 +1,16 @@ +{ + "type": "object", + "required": [ + "file_type", + "size", + "filename", + "file_format" + ], + "properties": { + "file_type": { "type": "string"}, + "size": { "type": "integer"}, + "filename": { "type": "string"}, + "file_format": { "type": "string"} + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/artifact_file.json b/spec/fixtures/api/schemas/public_api/v4/artifact_file.json new file mode 100644 index 00000000000..4017e6bdabc --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/artifact_file.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "required": [ + "filename", + "size" + ], + "properties": { + "filename": { "type": "string"}, + "size": { "type": "integer"} + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/deployment.json b/spec/fixtures/api/schemas/public_api/v4/deployment.json new file mode 100644 index 00000000000..3af2dc27d55 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/deployment.json @@ -0,0 +1,32 @@ +{ + "type": "object", + "required": [ + "id", + "iid", + "ref", + "sha", + "created_at", + "user", + "deployable" + ], + "properties": { + "id": { "type": "integer" }, + "iid": { "type": "integer" }, + "ref": { "type": "string" }, + "sha": { "type": "string" }, + "created_at": { "type": "string" }, + "user": { + "oneOf": [ + { "type": "null" }, + { "$ref": "user/basic.json" } + ] + }, + "deployable": { + "oneOf": [ + { "type": "null" }, + { "$ref": "job.json" } + ] + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/environment.json b/spec/fixtures/api/schemas/public_api/v4/environment.json new file mode 100644 index 00000000000..242e90fb7ac --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/environment.json @@ -0,0 +1,23 @@ +{ + "type": "object", + "required": [ + "id", + "name", + "slug", + "external_url", + "last_deployment" + ], + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "slug": { "type": "string" }, + "external_url": { "$ref": "../../types/nullable_string.json" }, + "last_deployment": { + "oneOf": [ + { "type": "null" }, + { "$ref": "deployment.json" } + ] + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/job.json b/spec/fixtures/api/schemas/public_api/v4/job.json new file mode 100644 index 00000000000..454935422a0 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/job.json @@ -0,0 +1,63 @@ +{ + "type": "object", + "required": [ + "id", + "status", + "stage", + "name", + "ref", + "tag", + "coverage", + "created_at", + "started_at", + "finished_at", + "duration", + "user", + "commit", + "pipeline", + "web_url", + "artifacts", + "artifacts_expire_at", + "runner" + ], + "properties": { + "id": { "type": "integer" }, + "status": { "type": "string" }, + "stage": { "type": "string" }, + "name": { "type": "string" }, + "ref": { "type": "string" }, + "tag": { "type": "boolean" }, + "coverage": { "type": ["number", "null"] }, + "created_at": { "type": "string" }, + "started_at": { "type": ["null", "string"] }, + "finished_at": { "type": ["null", "string"] }, + "duration": { "type": ["null", "number"] }, + "user": { "$ref": "user/basic.json" }, + "commit": { + "oneOf": [ + { "type": "null" }, + { "$ref": "commit/basic.json" } + ] + }, + "pipeline": { "$ref": "pipeline/basic.json" }, + "web_url": { "type": "string" }, + "artifacts": { + "type": "array", + "items": { "$ref": "artifact.json" } + }, + "artifacts_file": { + "oneOf": [ + { "type": "null" }, + { "$ref": "artifact_file.json" } + ] + }, + "artifacts_expire_at": { "type": ["null", "string"] }, + "runner": { + "oneOf": [ + { "type": "null" }, + { "$ref": "runner.json" } + ] + } + }, + "additionalProperties":false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_request.json b/spec/fixtures/api/schemas/public_api/v4/merge_request.json index 918f2c4b47d..a423bf70b69 100644 --- a/spec/fixtures/api/schemas/public_api/v4/merge_request.json +++ b/spec/fixtures/api/schemas/public_api/v4/merge_request.json @@ -64,6 +64,11 @@ }, "additionalProperties": false }, + "assignees": { + "items": { + "$ref": "./merge_request.json" + } + }, "source_project_id": { "type": "integer" }, "target_project_id": { "type": "integer" }, "labels": { diff --git a/spec/fixtures/api/schemas/public_api/v4/runner.json b/spec/fixtures/api/schemas/public_api/v4/runner.json new file mode 100644 index 00000000000..d97d74a93f2 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/runner.json @@ -0,0 +1,24 @@ +{ + "type": "object", + "required": [ + "id", + "description", + "ip_address", + "active", + "is_shared", + "name", + "online", + "status" + ], + "properties": { + "id": { "type": "integer" }, + "description": { "type": "string" }, + "ip_address": { "type": "string" }, + "active": { "type": "boolean" }, + "is_shared": { "type": "boolean" }, + "name": { "type": ["null", "string"] }, + "online": { "type": "boolean" }, + "status": { "type": "string" } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/passphrase_x509_certificate.crt b/spec/fixtures/passphrase_x509_certificate.crt new file mode 100644 index 00000000000..6973163b79e --- /dev/null +++ b/spec/fixtures/passphrase_x509_certificate.crt @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEpTCCAo0CAQEwDQYJKoZIhvcNAQEFBQAwFDESMBAGA1UEAwwJYXV0aG9yaXR5 +MB4XDTE4MDMyMzE0MDIwOFoXDTE5MDMyMzE0MDIwOFowHTEbMBkGA1UEAwwSZ2l0 +bGFiLXBhc3NwaHJhc2VkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +zpsWHOewP/khfDsLUWxaRCinrBzVJm2C01bVahKVR3g/JD4vEH901Wod9Pvbh/9e +PEfE+YZmgSUUopbL3JUheMnyW416F43HKE/fPW4+QeuIEceuhCXg20eOXmvnWWNM +0hXZh4hq69rwvMPREC/LkZy/QkTDKhJNLNAqAQu2AJ3C7Yga8hFQYEhx1hpfGtwD +z/Nf3efat9WN/d6yW9hfJ98NCmImTm5l9Pc0YPNWCAf96vsqsNHBrTkFy6CQwkhH +K1ynVYuqnHYxSc4FPCT5SAleD9gR/xFBAHb7pPy4yGxMSEmiWaMjjZCVPsghj1jM +Ej77MTDL3U9LeDfiILhvZ+EeQxqPiFwwG2eaIn3ZEs2Ujvw7Z2VpG9VMcPTnB4jK +ot6qPM1YXnkGWQ6iT0DTPS3h7zg1xIJXI5N2sI6GXuKrXXwZ1wPqzFLKPv+xBjp8 +P6dih+EImfReFi9zIO1LqGMY+XmRcqodsb6jzsmBimJkqBtatJM7FuUUUN56wiaj +q9+BWbm+ZdQ2lvqndMljjUjTh6pNERfGAJgkNuLn3X9hXVE0TSpmn0nOgaL5izP3 +7FWUt0PTyGgK2zq9SEhZmK2TKckLkKMk/ZBBBVM/nrnjs72IlbsqdcVoTnApytZr +xVYTj1hV7QlAfaU3w/M534qXDiy8+HfX5ksWQMtSklECAwEAATANBgkqhkiG9w0B +AQUFAAOCAgEAMMhzSRq9PqCpui74nwjhmn8Dm2ky7A+MmoXNtk70cS/HWrjzaacb +B/rxsAUp7f0pj4QMMM0ETMFpbNs8+NPd2FRY0PfWE4yyDpvZO2Oj1HZKLHX72Gjn +K5KB9DYlVsXhGPfuFWXpxGWF2Az9hDWnj58M3DOAps+6tHuAtudQUuwf5ENQZWwE +ySpr7yoHm1ykgl0Tsb9ZHi9qLrWRRMNYXRT+gvwP1bba8j9jOtjO/xYiIskwMPLM +W8SFmQxbg0Cvi8Q89PB6zoTNOhPQyoyeSlw9meeZJHAMK2zxeglEm8C4EQ+I9Y6/ +yylM5/Sc55TjWAvRFgbsq+OozgMvffk/Q2fzcGF44J9DEQ7nrhmJxJ+X4enLknR5 +Hw4+WhdYA+bwjx3YZBNTh9/YMgNPYwQhf5gtcZGTd6X4j6qZfJ6CXBmhkC1Cbfyl +yM7B7i4JAqPWMeDP50pXCgyKlwgw1JuFW+xkbkYQAj7wtggQ6z1Vjb5W8R8kYn9q +LXClVtThEeSV5KkVwNX21aFcUs8qeQ+zsgKqpEyM5oILQQ1gDSxLTtrr2KuN+WJN +wM0acwD45X7gA/aZYpCGkIgHIBq0zIDP1s6IqeebFJjW8lWofhRxOEWomWdRweJG +N7qQ1WCTQxAPGAkDI8QPjaspvnAhFKmpBG/mR5IXLFKDbttu7WNdYDo= +-----END CERTIFICATE----- diff --git a/spec/fixtures/passphrase_x509_certificate_pk.key b/spec/fixtures/passphrase_x509_certificate_pk.key new file mode 100644 index 00000000000..f9760dfe70e --- /dev/null +++ b/spec/fixtures/passphrase_x509_certificate_pk.key @@ -0,0 +1,54 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,79CCB506B0FD42A6F1BAE6D72E1CB20C + +EuZQOfgaO6LVCNytTHNJmbiq1rbum9xg6ohfBTVt7Cw4+8yLezWva/3sJQtnEk2P +M2yEQYWIiCX+clPkRiRL8WLjRfLTNcYS6QxxuJdpOrowPrBYr4Aig8jBUUBI4VQf +w1ZEUQd0mxQGnyzkKpsudFOntCtZbvbrBsIAQUNLcrKEFk3XW/BqE1Q/ja6WfWqX +b6EKg6DoXi92V90O6sLDfpmTKZq3ThvVDFuWeJ2K/GVp2cs+MkBIBJ8XX+NT1nWg +g+Ok+yaSI/N9ILX4XDgXunJGwcooI8PhHSjkDWRusi8vbo7RFqIKiSF+h6tIwktF +Uss3JESKgXZCQ7upCnHSzK/aWFtwHtXxqOi7esqEZd+1sB0LY+XMnbaxweCMx2Kj +czktKYvoXUs69Whln+yyXULtl5XhJ8lbvlbIG2FbZ9y+/hHOyBqZyeUyCnXDzv8/ +0U0iZwreP3XPVMsy578pIdcdL27q+r05j4yjrJfbX3T9xp2u3F9uVubCa4euEBwV +yrFdsxJLKON8pFeDS49m5gHNsHmeZ0sUeTPZVGNXdabVetkOA0eAAGK4zAoqG79L +hEN7cDenz+E4XHp8gMzwwMiVyU4FuAb6SXkfSodctmSTWVbzNBja0FBek3UXy+pn +9qq7cIpe7NY5gzcbyoy9lSkyYVkAm8j6BIYtY1ZUAmtCklC2ADWARTjd7dI7aEbO +QbXxNIq2+O/zMOXfougSPoDP8SLyLuE1p6SwfWV7Dwf119hn+mjWlGzAZDxxHhsR +yYUQCUe0NIKzuUp3WYIx8xIb7/WFwit/JaFaxurjBnhkkEviBn+TgXiuFBO3tv/d +URpZ39rH0mrDsR61pCiIcoNVkQkynHcAFPd5VtaeSJPvZP280uOCPPS31cr6/0LB +1JX3lZoWWCuA+JQjxtZDaDTcvEUbfOQ2rexQQo4uylNkBF9F5WOdQBkKG/AfqBq8 +S/TdubYzvpcKhFAlXsI67JdbxGlU4HCsxOLwWzSUYclN4W3l7s7KZ5zxt+MU03Uf +vara9uuZHiKUjZohjXeqcXTc+UyC8VH1dF19M3Cj9RNrwl2xEDUMtIiALBjbGp1E +pu2nPj9NhWf9Vw5MtSszutesxXba2nPmvvGvvZ7N3h/k4NsKL7JdENF7XqkI0D2K +jpO1t6d3cazS1VpMWLZS45kWaM3Y07tVR3V+4Iv9Vo1e9H2u/Z5U4YeJ44sgMsct +dBOAhHdUAI5+P+ocLXiCKo+EcS0cKvz+CC4ux0vvcF3JrTqZJN1U/JxRka2EyJ1B +2Xtu3DF36XpBJcs+MJHjJ+kUn6DHYoYxZa+bB8LX6+FQ+G7ue+Dx/RsGlP7if1nq +DAaM6kZg7/FbFzOZyl5xhwAJMxfgNNU7nSbk9lrvQ4mdwgFjvgGu3jlER4+TcleE +4svXInxp1zK6ES44tI9fXkhPaFkafxAL7eUSyjjEwMC06h+FtqK3mmoKLo5NrGJE +zVl69r2WdoSQEylVN1Kbp+U4YbfncInLJqBq2q5w9ASL/8Rhe8b52q6PuVX/bjoz +0pkSu+At4jVbAhRpER5NGlzG884IaqqvBvMYR5zFJeRroIijyUyH0KslK37/sXRk +ty0yKrkm31De9gDa3+XlgAVDAgbEQmGVwVVcV0IYYJbjIf36lUdGh4+3krwxolr/ +vZct5Z7QxfJlBtdOstjz5U9o05yOhjoNrPZJXuKMmWOQjSwr7rRSdqmAABF9IrBf +Pa/ChF1y5j3gJESAFMyiea3kvLq1EbZRaKoybsQE2ctBQ8EQjzUz+OOxVO6GJ4W9 +XHyfcviFrpsVcJEpXQlEtGtKdfKLp48cytob1Fu1JOYPDCrafUQINCZP4H3Nt892 +zZiTmdwux7pbgf4KbONImN5XkpvdCGjQHSkYMmm5ETRK8s7Fmvt2aBPtlyXxJDOq +iJUqwDV5HZXOnQVE/v/yESKgo2Cb8BWqPZ4/8Ubgu/OADYyv/dtjQel8QQ2FMhO4 +2tnwWbBBJk8VpR/vjFHkGSnj+JJfW/vUVQ+06D3wHYhNp7mh4M+37AngwzGCp7k+ +9aFwb2FBGghArB03E4lIO/959T0cX95WZ6tZtLLEsf3+ug7PPOSswCqsoPsXzFJH +MgXVGKFXccNSsWol7VvrX/uja7LC1OE+pZNXxCRzSs4aljJBpvQ6Mty0lk2yBC0R +MdujMoZH9PG9U6stwFd+P17tlGrQdRD3H2uimn82Ck+j2l0z0pzN0JB2WBYEyK0O +1MC36wLICWjgIPLPOxDEEBeZPbc24DCcYfs/F/hSCHv/XTJzVVILCX11ShGPSXlI +FL9qyq6jTNh/pVz6NiN/WhUPBFfOSzLRDyU0MRsSHM8b/HPpf3NOI3Ywmmj65c2k +2kle1F2M5ZTL+XvLS61qLJ/8AgXWvDHP3xWuKGG/pM40CRTUkRW6NAokMr2/pEFw +IHTE2+84dOKnUIEczzMY3aqzNmYDCmhOY0jD/Ieb4hy9tN+1lbQ/msYMIJ1w7CFR +38yB/UbDD90NcuDhjrMbzVUv1At2rW7GM9lSbxGOlYDmtMNEL63md1pQ724v4gSE +mzoFcMkqdh+hjFvv11o4H32lF3mPYcXuL+po76tqxGOiUrLKe/ZqkT5XAclYV/7H +k3Me++PCh4ZqXBRPvR8Xr90NETtiFCkBQXLdhNWXrRe2v0EbSX+cYAWk68FQKCHa +HKTz9T7wAvB6QWBXFhH9iCP8rnQLCEhLEhdrt+4v2KFkIVzBgOlMoHsZsMp0sBeq +c5ZVbJdiKik3P/8ZQTn4jmOnQXCEyWx+LU4acks8Aho4lqq9yKq2DZpwbIRED47E +r7R/NUevhqqzEHZ2SGD6EDqRN+bHJEi64vq0ryaEielusYXZqlnFXDHJcfLCmR5X +3bj5pCwQF4ScTukrGQB/c4henG4vlF4CaD0CIIK3W6tH+AoDohYJts6YK49LGxmK +yXiyKNak8zHYBBoRvd2avRHyGuR5yC9KrN8cbC/kZqMDvAyM65pIK+U7exJwYJhv +ezCcbiH3bK3anpiRpdeNOot2ba/Y+/ks+DRC+xs4QDIhrmSEBCsLv1JbcWjtHSaG +lm+1DSVduUk/kN+fBnlfif+TQV9AP3/wb8ekk8jjKXsL7H1tJKHsLLIIvrgrpxjw +-----END RSA PRIVATE KEY----- diff --git a/spec/fixtures/x509_certificate.crt b/spec/fixtures/x509_certificate.crt new file mode 100644 index 00000000000..8a84890b928 --- /dev/null +++ b/spec/fixtures/x509_certificate.crt @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEnDCCAoQCAQEwDQYJKoZIhvcNAQEFBQAwFDESMBAGA1UEAwwJYXV0aG9yaXR5 +MB4XDTE4MDMxOTE1MjYzMloXDTE5MDMxOTE1MjYzMlowFDESMBAGA1UEAwwJbG9j +YWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+tcM7iphsLlR +ccUph2ixabRYnw1HeLCiA4O9a4O31oVUBuzAn/eVU4jyVWkaBym6MHa8CiDOro9H +OXodITMw+3G1sG/yQZ8Y/5dsOP2hEoSfs63/2FAgFWzrB2HnYSShiN8tBeeDI5cJ +ii4JVMfpfi9cvXZUXFR8+P0XR1HDxx6or6UTK37k2kbDQZ41rv1ng2w0AUZt0LRA +NWVE48zvUWIU0y+2JLP1yhrKj85RRjQc5cMK88zzWSZBcSjDGGeJ4C8B5Zh2gFlQ ++1aJkyyklORR3v/RyYO9prTeXPqQ3x/nNsNkI+cyv0Gle6tk+CkOfE1m0CvNWlNg +b8LdQ0XZsOYLZvxfpHk3gHA5GrHXvn5StkM5xMXpdUCsh22CZZHe/4SeFE64amkf +1/LuqY0LYc5UdG2SeJ0SDauPRAIuAr4OV7+Q/nLdY8haMC6KOtpbAWvKX/Jqq0z1 +nUXzQn1JWCNw1QMdq9Uz8wiWOjLTr2D/mIVrVef0pb2mfdtzjzUrYCP0PtnQExPB +rocP6BDXN7Ragcdis5/IfLuCOD6pAkmzy6o8RSvAoEUs9VbPiUfN7WAyU1K1rTYH +KV+zPfWF254nZ2SBeReN9CMKbMJE+TX2chRlq07Q5LDz33h9KXw1LZT8MWRinVJf +RePsQiyHpRBWRG0AhbD+YpiGKHzsat0CAwEAATANBgkqhkiG9w0BAQUFAAOCAgEA +Skp0tbvVsg3RG2pX0GP25j0ix+f78zG0+BJ6LiKGMoCIBtGKitfUjBg83ru/ILpa +fpgrQpNQVUnGQ9tmpnqV605ZBBRUC1CRDsvUnyN6p7+yQAq6Fl+2ZKONHpPk+Bl4 +CIewgdkHjTwTpvIM/1DFVCz4R1FxNjY3uqOVcNDczMYEk2Pn2GZNNN35hUHHxWh4 +89ZvI+XKuRFZq3cDPA60PySeJJpCRScWGgnkdEX1gTtWH3WUlq9llxIvRexyNyzZ +Yqvcfx5UT75/Pp+JPh9lpUCcKLHeUiadjkiLxu3IcrYa4gYx4lA8jgm7adNEahd0 +oMAHoO9DU6XMo7o6tnQH3xQv9RAbQanjuyJR9N7mwmc59bQ6mW+pxCk843GwT73F +slseJ1nE1fQQQD7mn/KGjmeWtxY2ElUjTay9ff9/AgJeQYRW+oH0cSdo8WCpc2+G ++LZtLWfBgFLHseRlmarSe2pP8KmbaTd3q7Bu0GekVQOxYcNX59Pj4muQZDVLh8aX +mSQ+Ifts/ljT649MISHn2AZMR4+BUx63tFcatQhbAGGH5LeFdbaGcaVdsUVyZ9a2 +HBmFWNsgEPtcC+WmNzCXbv7jQsLAJXufKG5MnurJgNf/n5uKCmpGsEJDT/KF1k/3 +x9YnqM7zTyV6un+LS3HjEJvwQmqPWe+vFAeXWGCoWxE= +-----END CERTIFICATE----- diff --git a/spec/fixtures/x509_certificate_pk.key b/spec/fixtures/x509_certificate_pk.key new file mode 100644 index 00000000000..c02a3cf6189 --- /dev/null +++ b/spec/fixtures/x509_certificate_pk.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKAIBAAKCAgEA+tcM7iphsLlRccUph2ixabRYnw1HeLCiA4O9a4O31oVUBuzA +n/eVU4jyVWkaBym6MHa8CiDOro9HOXodITMw+3G1sG/yQZ8Y/5dsOP2hEoSfs63/ +2FAgFWzrB2HnYSShiN8tBeeDI5cJii4JVMfpfi9cvXZUXFR8+P0XR1HDxx6or6UT +K37k2kbDQZ41rv1ng2w0AUZt0LRANWVE48zvUWIU0y+2JLP1yhrKj85RRjQc5cMK +88zzWSZBcSjDGGeJ4C8B5Zh2gFlQ+1aJkyyklORR3v/RyYO9prTeXPqQ3x/nNsNk +I+cyv0Gle6tk+CkOfE1m0CvNWlNgb8LdQ0XZsOYLZvxfpHk3gHA5GrHXvn5StkM5 +xMXpdUCsh22CZZHe/4SeFE64amkf1/LuqY0LYc5UdG2SeJ0SDauPRAIuAr4OV7+Q +/nLdY8haMC6KOtpbAWvKX/Jqq0z1nUXzQn1JWCNw1QMdq9Uz8wiWOjLTr2D/mIVr +Vef0pb2mfdtzjzUrYCP0PtnQExPBrocP6BDXN7Ragcdis5/IfLuCOD6pAkmzy6o8 +RSvAoEUs9VbPiUfN7WAyU1K1rTYHKV+zPfWF254nZ2SBeReN9CMKbMJE+TX2chRl +q07Q5LDz33h9KXw1LZT8MWRinVJfRePsQiyHpRBWRG0AhbD+YpiGKHzsat0CAwEA +AQKCAgBf1urJ1Meeji/gGETVx9qBWLbDjn9QTayZSyyEd78155tDShIPDLmxQRHW +MGIReo/5FGSkOgS+DWBZRZ77oGOGrtuMnjkheXhDr8dZvw5b1PBv5ntqWrLnfMYP +/Ag7xZMyiJLbPqmMX5j1gsFt8zPzUoVMnnl9DYryV0Edrs/utHgfJCM+6yzleUQB +PkGkqo1yWVVFZ3Nt2nDt9dNsdlC594+dYQ1m2JuArNvYNiw3dpHT98GnhRc1aLh4 +U+q22FiFn3BKGQat43JdlaLa6KO5f8MIQRYWuI8tss2DGPlhRv9AnUcVsLBjAuIH +bmUVrBosxCYUQ6giatjd2sZPfdC+VIDCbIWRthxkXJ9I/Ap8R98xx/7qIcPFc+XA +hcK1xOM7zIq2xgAOFeeh8O8Wq9cH8NmUhMCgzIE0WT32Zo0JAW6l0kZc82Y/Yofz +U+TJKo0NOFZe687HOhanOHbbQSG29XOqxMYTABZ7Ixf+4RZPD5+yQgZWP1BhLluy +PxZhsLl67xvbfB2i9VVorMN7PbFx5hbni3C7/p63Z0rG5q4/uJBbX3Uuh6KdhIo+ +Zh9UC6u29adIthdxz+ZV5wBccTOgaeHB9wRL9Hbp6ZxyqesQB4RTsFtPNXxZ7K43 +fmJgHZvHhF5gSbeB8JAeBf0cy3pytJM49ZxplifeGVzUJP2gAQKCAQEA/1T9quz5 +sOD03FxV//oRWD1kqfunq3v56sIBG4ZMVZKUqc6wLjTmeklLYKq85AWX8gnCHi0g +nmG/xDh/rt1/IngMWP98WVuD67hFbrj87g7A7YGIiwZ2gi6hqhqmALN+5JjCSTPp +XOiPvNnXP0XM4gIHBXV8diHq5rF9NsSh4vx3OExr8KQqVzWoDcnnWNfnDlrFB8cq +ViII+UqdovXp59hAVOsc+pYAe+8JeQDX17H3U/NMkUw4gU2aWUCvUVjxi9oBG/CW +ncIdYuW8zne4qXbX7YLC0QUUIDVOWzhLauAUBduTqRTldJo0KAxu887tf+uStXs8 +RACLGIaBQw7BXQKCAQEA+38NFnpflKquU92xRtmqWAVaW7rm865ZO6EIaS4JII/N +/Ebu1YZrAhT0ruGJQaolYj8w79BEZRF2CYDPZxKFv/ye0O7rWCAGtCdWQ0BXcrIU +7SdlsdfTNXO1R3WbwCyVxyjg6YF7FjbTaaOAoTiosTjDs2ZOgkbdh/sMeWkSN5HB +aQz4c8rqq0kkYucLqp4nWYSWSJn88bL8ctwEwW77MheJiSpo1ohNRP3ExHnbCbYw +RIj7ATSz74ebpd9NMauB5clvMMh4jRG0EQyt7KCoOyfPRFc3fddvTr03LlgFfX/n +qoxd2nejgAS3NnG1XMxdcUa7cPannt46Sef1uZo3gQKCAQB454zquCYQDKXGBu8u +NAKsjv2wxBqESENyV4VgvDo/NxawRdAFQUV12GkaEB87ti5aDSbfVS0h8lV1G+/S +JM5DyybFqcz/Hyebofk20d/q9g+DJ5g5hMjvIhepTc8Xe+d1ZaRyN2Oke/c8TMbx +DiNTTfR3MEfMRIlPzfHl0jx6GGR3wzBFleb6vsyiIt4qoqmlkXPFGBlDCgDH0v5M +ITgucacczuw8+HSoOut4Yd7TI1FjbkzubHJBQDb7VnbuBTjzqTpnOYiIkVeK8hBy +kBxgGodqz0Vi5o2+Jp/A8Co+JHc2wt/r65ovmali4WhUiMLLlQg2aXGDHeK/rUle +MIl9AoIBAQCPKCYSCnyHypRK5uG3W8VsLzfdCUnXogHnQGXiQTMu1szA8ruWzdnx +qG4TcgxIVYrMHv5DNAEKquLOzATDPjbmLu1ULvvGAQzv1Yhz5ZchkZ7507g+gIUY +YxHoaFjNDlP/txQ3tt2SqoizFD/vBap4nsA/SVgdLiuB8PSL07Rr70rx+lEe0H2+ +HHda2Pu6FiZ9/Uvybb0e8+xhkT4fwYW5YM6IRpzAqXuabv1nfZmiMJPPH04JxK88 +BKwjwjVVtbPOUlg5o5ODcXVXUylZjaXVbna8Bw1uU4hngKt9dNtDMeB0I0x1RC7M +e2Ky2g0LksUJ6uJdjfmiJAt38FLeYJuBAoIBAC2oqaqr86Dug5v8xHpgFoC5u7z7 +BRhaiHpVrUr+wnaNJEXfAEmyKf4xF5xDJqldnYG3c9ETG/7bLcg1dcrMPzXx94Si +MI3ykwiPeI/sVWYmUlq4U8zCIC7MY6sWzWt3oCBNoCN/EeYx9e7+eLNBB+fADAXq +v9RMGlUIy7beX0uac8Bs771dsxIb/RrYw58wz+jrwGlzuDmcPWiu+ARu7hnBqCAV +AITlCV/tsEk7u08oBuv47+rVGCh1Qb19pNswyTtTZARAGErJO0Q+39BNuu0M2TIn +G3M8eNmGHC+mNsZTVgKRuyk9Ye0s4Bo0KcqSndiPFGHjcrF7/t+RqEOXr/E= +-----END RSA PRIVATE KEY----- diff --git a/spec/frontend/.eslintrc.yml b/spec/frontend/.eslintrc.yml index 054dc27cda6..ff18f0e4a2d 100644 --- a/spec/frontend/.eslintrc.yml +++ b/spec/frontend/.eslintrc.yml @@ -12,3 +12,8 @@ globals: loadFixtures: false preloadFixtures: false setFixtures: false +rules: + jest/no-identical-title: error + jest/no-focused-tests: error + jest/valid-describe: error + jest/no-jasmine-globals: error diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js index 503af3920a8..67e5dc399ac 100644 --- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js +++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js @@ -31,7 +31,7 @@ describe('ErrorTrackingList', () => { actions = { getErrorList: () => {}, startPolling: () => {}, - restartPolling: jasmine.createSpy('restartPolling'), + restartPolling: jest.fn().mockName('restartPolling'), }; const state = { diff --git a/spec/frontend/filtered_search/services/recent_searches_service_error_spec.js b/spec/frontend/filtered_search/services/recent_searches_service_error_spec.js index ea7c146fa4f..0e62bc94517 100644 --- a/spec/frontend/filtered_search/services/recent_searches_service_error_spec.js +++ b/spec/frontend/filtered_search/services/recent_searches_service_error_spec.js @@ -8,7 +8,7 @@ describe('RecentSearchesServiceError', () => { }); it('instantiates an instance of RecentSearchesServiceError and not an Error', () => { - expect(recentSearchesServiceError).toEqual(jasmine.any(RecentSearchesServiceError)); + expect(recentSearchesServiceError).toEqual(expect.any(RecentSearchesServiceError)); expect(recentSearchesServiceError.name).toBe('RecentSearchesServiceError'); }); diff --git a/spec/frontend/ide/lib/common/disposable_spec.js b/spec/frontend/ide/lib/common/disposable_spec.js index af12ca15369..8596642eb7a 100644 --- a/spec/frontend/ide/lib/common/disposable_spec.js +++ b/spec/frontend/ide/lib/common/disposable_spec.js @@ -8,7 +8,7 @@ describe('Multi-file editor library disposable class', () => { instance = new Disposable(); disposableClass = { - dispose: jasmine.createSpy('dispose'), + dispose: jest.fn().mockName('dispose'), }; }); diff --git a/spec/frontend/ide/lib/diff/diff_spec.js b/spec/frontend/ide/lib/diff/diff_spec.js index 57f3ac3d365..d9b088e2c12 100644 --- a/spec/frontend/ide/lib/diff/diff_spec.js +++ b/spec/frontend/ide/lib/diff/diff_spec.js @@ -9,60 +9,57 @@ describe('Multi-file editor library diff calculator', () => { }); describe('modified', () => { - it('', () => { - const diff = computeDiff('123', '1234')[0]; - - expect(diff.added).toBeTruthy(); - expect(diff.modified).toBeTruthy(); - expect(diff.removed).toBeUndefined(); - }); - - it('', () => { - const diff = computeDiff('123\n123\n123', '123\n1234\n123')[0]; - - expect(diff.added).toBeTruthy(); - expect(diff.modified).toBeTruthy(); - expect(diff.removed).toBeUndefined(); - expect(diff.lineNumber).toBe(2); - }); + it.each` + originalContent | newContent | lineNumber + ${'123'} | ${'1234'} | ${1} + ${'123\n123\n123'} | ${'123\n1234\n123'} | ${2} + `( + 'marks line $lineNumber as added and modified but not removed', + ({ originalContent, newContent, lineNumber }) => { + const diff = computeDiff(originalContent, newContent)[0]; + + expect(diff.added).toBeTruthy(); + expect(diff.modified).toBeTruthy(); + expect(diff.removed).toBeUndefined(); + expect(diff.lineNumber).toBe(lineNumber); + }, + ); }); describe('added', () => { - it('', () => { - const diff = computeDiff('123', '123\n123')[0]; - - expect(diff.added).toBeTruthy(); - expect(diff.modified).toBeUndefined(); - expect(diff.removed).toBeUndefined(); - }); - - it('', () => { - const diff = computeDiff('123\n123\n123', '123\n123\n1234\n123')[0]; - - expect(diff.added).toBeTruthy(); - expect(diff.modified).toBeUndefined(); - expect(diff.removed).toBeUndefined(); - expect(diff.lineNumber).toBe(3); - }); + it.each` + originalContent | newContent | lineNumber + ${'123'} | ${'123\n123'} | ${1} + ${'123\n123\n123'} | ${'123\n123\n1234\n123'} | ${3} + `( + 'marks line $lineNumber as added but not modified and not removed', + ({ originalContent, newContent, lineNumber }) => { + const diff = computeDiff(originalContent, newContent)[0]; + + expect(diff.added).toBeTruthy(); + expect(diff.modified).toBeUndefined(); + expect(diff.removed).toBeUndefined(); + expect(diff.lineNumber).toBe(lineNumber); + }, + ); }); describe('removed', () => { - it('', () => { - const diff = computeDiff('123', '')[0]; - - expect(diff.added).toBeUndefined(); - expect(diff.modified).toBeUndefined(); - expect(diff.removed).toBeTruthy(); - }); - - it('', () => { - const diff = computeDiff('123\n123\n123', '123\n123')[0]; - - expect(diff.added).toBeUndefined(); - expect(diff.modified).toBeTruthy(); - expect(diff.removed).toBeTruthy(); - expect(diff.lineNumber).toBe(2); - }); + it.each` + originalContent | newContent | lineNumber | modified + ${'123'} | ${''} | ${1} | ${undefined} + ${'123\n123\n123'} | ${'123\n123'} | ${2} | ${true} + `( + 'marks line $lineNumber as removed', + ({ originalContent, newContent, lineNumber, modified }) => { + const diff = computeDiff(originalContent, newContent)[0]; + + expect(diff.added).toBeUndefined(); + expect(diff.modified).toBe(modified); + expect(diff.removed).toBeTruthy(); + expect(diff.lineNumber).toBe(lineNumber); + }, + ); }); it('includes line number of change', () => { diff --git a/spec/frontend/ide/lib/editor_options_spec.js b/spec/frontend/ide/lib/editor_options_spec.js index d149a883166..b07a583b7c8 100644 --- a/spec/frontend/ide/lib/editor_options_spec.js +++ b/spec/frontend/ide/lib/editor_options_spec.js @@ -2,7 +2,7 @@ import editorOptions from '~/ide/lib/editor_options'; describe('Multi-file editor library editor options', () => { it('returns an array', () => { - expect(editorOptions).toEqual(jasmine.any(Array)); + expect(editorOptions).toEqual(expect.any(Array)); }); it('contains readOnly option', () => { diff --git a/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js b/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js index 8e0e3ae99a1..8e8b7f06ca2 100644 --- a/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js +++ b/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js @@ -9,7 +9,7 @@ describe('IDE file templates mutations', () => { state = createState(); }); - describe(types.REQUEST_TEMPLATE_TYPES, () => { + describe(`${types.REQUEST_TEMPLATE_TYPES}`, () => { it('sets isLoading', () => { mutations[types.REQUEST_TEMPLATE_TYPES](state); @@ -17,7 +17,7 @@ describe('IDE file templates mutations', () => { }); }); - describe(types.RECEIVE_TEMPLATE_TYPES_ERROR, () => { + describe(`${types.RECEIVE_TEMPLATE_TYPES_ERROR}`, () => { it('sets isLoading', () => { state.isLoading = true; @@ -27,7 +27,7 @@ describe('IDE file templates mutations', () => { }); }); - describe(types.RECEIVE_TEMPLATE_TYPES_SUCCESS, () => { + describe(`${types.RECEIVE_TEMPLATE_TYPES_SUCCESS}`, () => { it('sets isLoading to false', () => { state.isLoading = true; @@ -43,7 +43,7 @@ describe('IDE file templates mutations', () => { }); }); - describe(types.SET_SELECTED_TEMPLATE_TYPE, () => { + describe(`${types.SET_SELECTED_TEMPLATE_TYPE}`, () => { it('sets selectedTemplateType', () => { mutations[types.SET_SELECTED_TEMPLATE_TYPE](state, 'type'); @@ -59,7 +59,7 @@ describe('IDE file templates mutations', () => { }); }); - describe(types.SET_UPDATE_SUCCESS, () => { + describe(`${types.SET_UPDATE_SUCCESS}`, () => { it('sets updateSuccess', () => { mutations[types.SET_UPDATE_SUCCESS](state, true); diff --git a/spec/frontend/import_projects/store/mutations_spec.js b/spec/frontend/import_projects/store/mutations_spec.js index 8db8e9819ba..505545f7aa5 100644 --- a/spec/frontend/import_projects/store/mutations_spec.js +++ b/spec/frontend/import_projects/store/mutations_spec.js @@ -2,7 +2,7 @@ import * as types from '~/import_projects/store/mutation_types'; import mutations from '~/import_projects/store/mutations'; describe('import_projects store mutations', () => { - describe(types.RECEIVE_IMPORT_SUCCESS, () => { + describe(`${types.RECEIVE_IMPORT_SUCCESS}`, () => { it('removes repoId from reposBeingImported and providerRepos, adds to importedProjects', () => { const repoId = 1; const state = { @@ -20,7 +20,7 @@ describe('import_projects store mutations', () => { }); }); - describe(types.RECEIVE_JOBS_SUCCESS, () => { + describe(`${types.RECEIVE_JOBS_SUCCESS}`, () => { it('updates importStatus of existing importedProjects', () => { const repoId = 1; const state = { importedProjects: [{ id: repoId, importStatus: 'started' }] }; diff --git a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js index 1e0bc708c31..7e9aec84016 100644 --- a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js +++ b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js @@ -25,14 +25,14 @@ describe('Abuse Reports', () => { it('should truncate long messages', () => { const $longMessage = findMessage('LONG MESSAGE'); - expect($longMessage.data('originalMessage')).toEqual(jasmine.anything()); + expect($longMessage.data('originalMessage')).toEqual(expect.anything()); assertMaxLength($longMessage); }); it('should not truncate short messages', () => { const $shortMessage = findMessage('SHORT MESSAGE'); - expect($shortMessage.data('originalMessage')).not.toEqual(jasmine.anything()); + expect($shortMessage.data('originalMessage')).not.toEqual(expect.anything()); }); it('should allow clicking a truncated message to expand and collapse the full message', () => { diff --git a/spec/frontend/pages/profiles/show/emoji_menu_spec.js b/spec/frontend/pages/profiles/show/emoji_menu_spec.js index efc338b36eb..6ac1e83829f 100644 --- a/spec/frontend/pages/profiles/show/emoji_menu_spec.js +++ b/spec/frontend/pages/profiles/show/emoji_menu_spec.js @@ -13,7 +13,7 @@ describe('EmojiMenu', () => { let dummyEmojiList; beforeEach(() => { - dummySelectEmojiCallback = jasmine.createSpy('dummySelectEmojiCallback'); + dummySelectEmojiCallback = jest.fn().mockName('dummySelectEmojiCallback'); dummyEmojiList = { glEmojiTag() { return dummyEmojiTag; @@ -75,19 +75,19 @@ describe('EmojiMenu', () => { expect(emojiMenu.registerEventListener).toHaveBeenCalledWith( 'one', - jasmine.anything(), + expect.anything(), 'mouseenter focus', dummyToggleButtonSelector, 'mouseenter focus', - jasmine.anything(), + expect.anything(), ); expect(emojiMenu.registerEventListener).toHaveBeenCalledWith( 'on', - jasmine.anything(), + expect.anything(), 'click', dummyToggleButtonSelector, - jasmine.anything(), + expect.anything(), ); }); @@ -96,10 +96,10 @@ describe('EmojiMenu', () => { expect(emojiMenu.registerEventListener).toHaveBeenCalledWith( 'on', - jasmine.anything(), + expect.anything(), 'click', `.js-awards-block .js-emoji-btn, .${dummyMenuClass} .js-emoji-btn`, - jasmine.anything(), + expect.anything(), ); }); }); diff --git a/spec/frontend/serverless/components/functions_spec.js b/spec/frontend/serverless/components/functions_spec.js index 5533de1a70a..7af33ceaadc 100644 --- a/spec/frontend/serverless/components/functions_spec.js +++ b/spec/frontend/serverless/components/functions_spec.js @@ -4,14 +4,21 @@ import axios from '~/lib/utils/axios_utils'; import functionsComponent from '~/serverless/components/functions.vue'; import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createStore } from '~/serverless/store'; +import { TEST_HOST } from 'helpers/test_constants'; import { mockServerlessFunctions } from '../mock_data'; describe('functionsComponent', () => { + const statusPath = `${TEST_HOST}/statusPath`; + let component; let store; let localVue; + let axiosMock; beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + axiosMock.onGet(statusPath).reply(200); + localVue = createLocalVue(); localVue.use(Vuex); @@ -20,6 +27,7 @@ describe('functionsComponent', () => { afterEach(() => { component.vm.$destroy(); + axiosMock.restore(); }); it('should render empty state when Knative is not installed', () => { @@ -80,11 +88,7 @@ describe('functionsComponent', () => { ); }); - fit('should render the functions list', () => { - const statusPath = 'statusPath'; - const axiosMock = new AxiosMockAdapter(axios); - axiosMock.onGet(statusPath).reply(200); - + it('should render the functions list', () => { component = shallowMount(functionsComponent, { localVue, store, diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js index 866d6eb05c6..c8deac1c086 100644 --- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js +++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js @@ -23,7 +23,7 @@ const newLine = { type: 'new', }; -describe(SuggestionDiffRow.name, () => { +describe('SuggestionDiffRow', () => { let wrapper; const factory = (options = {}) => { diff --git a/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js b/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js index c15635f2105..be6c58f0683 100644 --- a/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js +++ b/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js @@ -1,7 +1,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; -describe(TimelineEntryItem.name, () => { +describe(`TimelineEntryItem`, () => { let wrapper; const factory = (options = {}) => { diff --git a/spec/frontend/vuex_shared/modules/modal/mutations_spec.js b/spec/frontend/vuex_shared/modules/modal/mutations_spec.js index d07f8ba1e65..eaaf196d1ec 100644 --- a/spec/frontend/vuex_shared/modules/modal/mutations_spec.js +++ b/spec/frontend/vuex_shared/modules/modal/mutations_spec.js @@ -2,7 +2,7 @@ import mutations from '~/vuex_shared/modules/modal/mutations'; import * as types from '~/vuex_shared/modules/modal/mutation_types'; describe('Vuex ModalModule mutations', () => { - describe(types.SHOW, () => { + describe(`${types.SHOW}`, () => { it('sets isVisible to true', () => { const state = { isVisible: false, @@ -16,7 +16,7 @@ describe('Vuex ModalModule mutations', () => { }); }); - describe(types.HIDE, () => { + describe(`${types.HIDE}`, () => { it('sets isVisible to false', () => { const state = { isVisible: true, @@ -30,7 +30,7 @@ describe('Vuex ModalModule mutations', () => { }); }); - describe(types.OPEN, () => { + describe(`${types.OPEN}`, () => { it('sets data and sets isVisible to true', () => { const data = { id: 7 }; const state = { diff --git a/spec/javascripts/monitoring/utils_spec.js b/spec/javascripts/monitoring/utils_spec.js new file mode 100644 index 00000000000..e3c455d1686 --- /dev/null +++ b/spec/javascripts/monitoring/utils_spec.js @@ -0,0 +1,29 @@ +import { getTimeDiff } from '~/monitoring/utils'; +import { timeWindows } from '~/monitoring/constants'; + +describe('getTimeDiff', () => { + it('defaults to an 8 hour (28800s) difference', () => { + const params = getTimeDiff(); + + expect(params.end - params.start).toEqual(28800); + }); + + it('accepts time window as an argument', () => { + const params = getTimeDiff(timeWindows.thirtyMinutes); + + expect(params.end - params.start).not.toEqual(28800); + }); + + it('returns a value for every defined time window', () => { + const nonDefaultWindows = Object.keys(timeWindows).filter(window => window !== 'eightHours'); + + nonDefaultWindows.forEach(window => { + const params = getTimeDiff(timeWindows[window]); + const diff = params.end - params.start; + + // Ensure we're not returning the default, 28800 (the # of seconds in 8 hrs) + expect(diff).not.toEqual(28800); + expect(typeof diff).toEqual('number'); + }); + }); +}); diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js index 57b16b12cb0..47fee5d2b21 100644 --- a/spec/javascripts/sidebar/assignees_spec.js +++ b/spec/javascripts/sidebar/assignees_spec.js @@ -132,9 +132,94 @@ describe('Assignee component', () => { -1, ); }); + + it('has correct "cannot merge" tooltip when user cannot merge', () => { + const user = Object.assign({}, UsersMock.user, { can_merge: false }); + + component = new AssigneeComponent({ + propsData: { + rootPath: 'http://localhost:3000/', + users: [user], + editable: true, + issuableType: 'merge_request', + }, + }).$mount(); + + expect(component.mergeNotAllowedTooltipMessage).toEqual('Cannot merge'); + }); }); describe('Two or more assignees/users', () => { + it('has correct "cannot merge" tooltip when one user can merge', () => { + const users = UsersMockHelper.createNumberRandomUsers(3); + users[0].can_merge = true; + users[1].can_merge = false; + users[2].can_merge = false; + + component = new AssigneeComponent({ + propsData: { + rootPath: 'http://localhost:3000/', + users, + editable: true, + issuableType: 'merge_request', + }, + }).$mount(); + + expect(component.mergeNotAllowedTooltipMessage).toEqual('1/3 can merge'); + }); + + it('has correct "cannot merge" tooltip when no user can merge', () => { + const users = UsersMockHelper.createNumberRandomUsers(2); + users[0].can_merge = false; + users[1].can_merge = false; + + component = new AssigneeComponent({ + propsData: { + rootPath: 'http://localhost:3000/', + users, + editable: true, + issuableType: 'merge_request', + }, + }).$mount(); + + expect(component.mergeNotAllowedTooltipMessage).toEqual('No one can merge'); + }); + + it('has correct "cannot merge" tooltip when more than one user can merge', () => { + const users = UsersMockHelper.createNumberRandomUsers(3); + users[0].can_merge = false; + users[1].can_merge = true; + users[2].can_merge = true; + + component = new AssigneeComponent({ + propsData: { + rootPath: 'http://localhost:3000/', + users, + editable: true, + issuableType: 'merge_request', + }, + }).$mount(); + + expect(component.mergeNotAllowedTooltipMessage).toEqual('2/3 can merge'); + }); + + it('has no "cannot merge" tooltip when every user can merge', () => { + const users = UsersMockHelper.createNumberRandomUsers(2); + users[0].can_merge = true; + users[1].can_merge = true; + + component = new AssigneeComponent({ + propsData: { + rootPath: 'http://localhost:3000/', + users, + editable: true, + issuableType: 'merge_request', + }, + }).$mount(); + + expect(component.mergeNotAllowedTooltipMessage).toEqual(null); + }); + it('displays two assignee icons when collapsed', () => { const users = UsersMockHelper.createNumberRandomUsers(2); component = new AssigneeComponent({ diff --git a/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb b/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb index f8107dd40b9..4db829b1e7b 100644 --- a/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb @@ -30,6 +30,6 @@ describe Gitlab::BackgroundMigration::MigrateStageIndex, :migration, schema: 201 described_class.new.perform(100, 101) - expect(stages.all.pluck(:position)).to eq [2, 3] + expect(stages.all.order(:id).pluck(:position)).to eq [2, 3] end end diff --git a/spec/lib/gitlab/background_migration_spec.rb b/spec/lib/gitlab/background_migration_spec.rb index 7d3d8a949ef..1d0ffb5e9df 100644 --- a/spec/lib/gitlab/background_migration_spec.rb +++ b/spec/lib/gitlab/background_migration_spec.rb @@ -195,4 +195,44 @@ describe Gitlab::BackgroundMigration do end end end + + describe '.dead_jobs?' do + let(:queue) do + [double(args: ['Foo', [10, 20]], queue: described_class.queue)] + end + + context 'when there are dead jobs present' do + before do + allow(Sidekiq::DeadSet).to receive(:new).and_return(queue) + end + + it 'returns true if specific job exists' do + expect(described_class.dead_jobs?('Foo')).to eq(true) + end + + it 'returns false if specific job does not exist' do + expect(described_class.dead_jobs?('Bar')).to eq(false) + end + end + end + + describe '.retrying_jobs?' do + let(:queue) do + [double(args: ['Foo', [10, 20]], queue: described_class.queue)] + end + + context 'when there are dead jobs present' do + before do + allow(Sidekiq::RetrySet).to receive(:new).and_return(queue) + end + + it 'returns true if specific job exists' do + expect(described_class.retrying_jobs?('Foo')).to eq(true) + end + + it 'returns false if specific job does not exist' do + expect(described_class.retrying_jobs?('Bar')).to eq(false) + end + end + end end diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb index 3c0007f4d57..0bc9e8bd3cd 100644 --- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb @@ -100,6 +100,26 @@ describe Gitlab::Ci::Config::Entry::Environment do end end + context 'when wrong action type is used' do + let(:config) do + { name: 'production', + action: ['stop'] } + end + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + + describe '#errors' do + it 'contains error about wrong action type' do + expect(entry.errors) + .to include 'environment action should be a string' + end + end + end + context 'when invalid action is used' do let(:config) do { name: 'production', @@ -151,6 +171,26 @@ describe Gitlab::Ci::Config::Entry::Environment do end end + context 'when wrong url type is used' do + let(:config) do + { name: 'production', + url: ['https://meow.meow'] } + end + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + + describe '#errors' do + it 'contains error about wrong url type' do + expect(entry.errors) + .to include 'environment url should be a string' + end + end + end + context 'when variables are used for environment' do let(:config) do { name: 'review/$CI_COMMIT_REF_NAME', diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 8b39c4e4dd0..b7b30e60d44 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -615,7 +615,7 @@ module Gitlab subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config), opts) } context "when validating a ci config file with no project context" do - context "when a single string is provided" do + context "when a single string is provided", :quarantine do let(:include_content) { "/local.gitlab-ci.yml" } it "does not return any error" do diff --git a/spec/lib/gitlab/external_authorization/access_spec.rb b/spec/lib/gitlab/external_authorization/access_spec.rb new file mode 100644 index 00000000000..5dc2521b310 --- /dev/null +++ b/spec/lib/gitlab/external_authorization/access_spec.rb @@ -0,0 +1,142 @@ +require 'spec_helper' + +describe Gitlab::ExternalAuthorization::Access, :clean_gitlab_redis_cache do + subject(:access) { described_class.new(build(:user), 'dummy_label') } + + describe '#loaded?' do + it 'is `true` when it was loaded recently' do + Timecop.freeze do + allow(access).to receive(:loaded_at).and_return(5.minutes.ago) + + expect(access).to be_loaded + end + end + + it 'is `false` when there is no loading time' do + expect(access).not_to be_loaded + end + + it 'is `false` when there the result was loaded a long time ago' do + Timecop.freeze do + allow(access).to receive(:loaded_at).and_return(2.weeks.ago) + + expect(access).not_to be_loaded + end + end + end + + describe 'load!' do + let(:fake_client) { double('ExternalAuthorization::Client') } + let(:fake_response) do + double( + 'Response', + 'successful?' => true, + 'valid?' => true, + 'reason' => nil + ) + end + + before do + allow(access).to receive(:load_from_cache) + allow(fake_client).to receive(:request_access).and_return(fake_response) + allow(Gitlab::ExternalAuthorization::Client).to receive(:new) { fake_client } + end + + context 'when loading from the webservice' do + it 'loads from the webservice it the cache was empty' do + expect(access).to receive(:load_from_cache) + expect(access).to receive(:load_from_service).and_call_original + + access.load! + + expect(access).to be_loaded + end + + it 'assigns the accessibility, reason and loaded_at' do + allow(fake_response).to receive(:successful?).and_return(false) + allow(fake_response).to receive(:reason).and_return('Inaccessible label') + + access.load! + + expect(access.reason).to eq('Inaccessible label') + expect(access).not_to have_access + expect(access.loaded_at).not_to be_nil + end + + it 'returns itself' do + expect(access.load!).to eq(access) + end + + it 'stores the result in redis' do + Timecop.freeze do + fake_cache = double + expect(fake_cache).to receive(:store).with(true, nil, Time.now) + expect(access).to receive(:cache).and_return(fake_cache) + + access.load! + end + end + + context 'when the request fails' do + before do + allow(fake_client).to receive(:request_access) do + raise ::Gitlab::ExternalAuthorization::RequestFailed.new('Service unavailable') + end + end + + it 'is loaded' do + access.load! + + expect(access).to be_loaded + end + + it 'assigns the correct accessibility, reason and loaded_at' do + access.load! + + expect(access.reason).to eq('Service unavailable') + expect(access).not_to have_access + expect(access.loaded_at).not_to be_nil + end + + it 'does not store the result in redis' do + fake_cache = double + expect(fake_cache).not_to receive(:store) + allow(access).to receive(:cache).and_return(fake_cache) + + access.load! + end + end + end + + context 'When loading from cache' do + let(:fake_cache) { double('ExternalAuthorization::Cache') } + + before do + allow(access).to receive(:cache).and_return(fake_cache) + end + + it 'does not load from the webservice' do + Timecop.freeze do + expect(fake_cache).to receive(:load).and_return([true, nil, Time.now]) + + expect(access).to receive(:load_from_cache).and_call_original + expect(access).not_to receive(:load_from_service) + + access.load! + end + end + + it 'loads from the webservice when the cached result was too old' do + Timecop.freeze do + expect(fake_cache).to receive(:load).and_return([true, nil, 2.days.ago]) + + expect(access).to receive(:load_from_cache).and_call_original + expect(access).to receive(:load_from_service).and_call_original + allow(fake_cache).to receive(:store) + + access.load! + end + end + end + end +end diff --git a/spec/lib/gitlab/external_authorization/cache_spec.rb b/spec/lib/gitlab/external_authorization/cache_spec.rb new file mode 100644 index 00000000000..58e7d626707 --- /dev/null +++ b/spec/lib/gitlab/external_authorization/cache_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Gitlab::ExternalAuthorization::Cache, :clean_gitlab_redis_cache do + let(:user) { build_stubbed(:user) } + let(:cache_key) { "external_authorization:user-#{user.id}:label-dummy_label" } + + subject(:cache) { described_class.new(user, 'dummy_label') } + + def read_from_redis(key) + Gitlab::Redis::Cache.with do |redis| + redis.hget(cache_key, key) + end + end + + def set_in_redis(key, value) + Gitlab::Redis::Cache.with do |redis| + redis.hmset(cache_key, key, value) + end + end + + describe '#load' do + it 'reads stored info from redis' do + Timecop.freeze do + set_in_redis(:access, false) + set_in_redis(:reason, 'Access denied for now') + set_in_redis(:refreshed_at, Time.now) + + access, reason, refreshed_at = cache.load + + expect(access).to eq(false) + expect(reason).to eq('Access denied for now') + expect(refreshed_at).to be_within(1.second).of(Time.now) + end + end + end + + describe '#store' do + it 'sets the values in redis' do + Timecop.freeze do + cache.store(true, 'the reason', Time.now) + + expect(read_from_redis(:access)).to eq('true') + expect(read_from_redis(:reason)).to eq('the reason') + expect(read_from_redis(:refreshed_at)).to eq(Time.now.to_s) + end + end + end +end diff --git a/spec/lib/gitlab/external_authorization/client_spec.rb b/spec/lib/gitlab/external_authorization/client_spec.rb new file mode 100644 index 00000000000..fa18c1e56e8 --- /dev/null +++ b/spec/lib/gitlab/external_authorization/client_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' + +describe Gitlab::ExternalAuthorization::Client do + let(:user) { build(:user, email: 'dummy_user@example.com') } + let(:dummy_url) { 'https://dummy.net/' } + subject(:client) { described_class.new(user, 'dummy_label') } + + before do + stub_application_setting(external_authorization_service_url: dummy_url) + end + + describe '#request_access' do + it 'performs requests to the configured endpoint' do + expect(Excon).to receive(:post).with(dummy_url, any_args) + + client.request_access + end + + it 'adds the correct params for the user to the body of the request' do + expected_body = { + user_identifier: 'dummy_user@example.com', + project_classification_label: 'dummy_label' + }.to_json + expect(Excon).to receive(:post) + .with(dummy_url, hash_including(body: expected_body)) + + client.request_access + end + + it 'respects the the timeout' do + stub_application_setting( + external_authorization_service_timeout: 3 + ) + + expect(Excon).to receive(:post).with(dummy_url, + hash_including( + connect_timeout: 3, + read_timeout: 3, + write_timeout: 3 + )) + + client.request_access + end + + it 'adds the mutual tls params when they are present' do + stub_application_setting( + external_auth_client_cert: 'the certificate data', + external_auth_client_key: 'the key data', + external_auth_client_key_pass: 'open sesame' + ) + expected_params = { + client_cert_data: 'the certificate data', + client_key_data: 'the key data', + client_key_pass: 'open sesame' + } + + expect(Excon).to receive(:post).with(dummy_url, hash_including(expected_params)) + + client.request_access + end + + it 'returns an expected response' do + expect(Excon).to receive(:post) + + expect(client.request_access) + .to be_kind_of(::Gitlab::ExternalAuthorization::Response) + end + + it 'wraps exceptions if the request fails' do + expect(Excon).to receive(:post) { raise Excon::Error.new('the request broke') } + + expect { client.request_access } + .to raise_error(::Gitlab::ExternalAuthorization::RequestFailed) + end + + describe 'for ldap users' do + let(:user) do + create(:omniauth_user, + email: 'dummy_user@example.com', + extern_uid: 'external id', + provider: 'ldapprovider') + end + + it 'includes the ldap dn for ldap users' do + expected_body = { + user_identifier: 'dummy_user@example.com', + project_classification_label: 'dummy_label', + user_ldap_dn: 'external id' + }.to_json + expect(Excon).to receive(:post) + .with(dummy_url, hash_including(body: expected_body)) + + client.request_access + end + end + end +end diff --git a/spec/lib/gitlab/external_authorization/logger_spec.rb b/spec/lib/gitlab/external_authorization/logger_spec.rb new file mode 100644 index 00000000000..81f1b2390e6 --- /dev/null +++ b/spec/lib/gitlab/external_authorization/logger_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe Gitlab::ExternalAuthorization::Logger do + let(:request_time) { Time.parse('2018-03-26 20:22:15') } + + def fake_access(has_access, user, load_type = :request) + access = double('access') + allow(access).to receive_messages(user: user, + has_access?: has_access, + loaded_at: request_time, + label: 'dummy_label', + load_type: load_type) + + access + end + + describe '.log_access' do + it 'logs a nice message for an access request' do + expected_message = "GRANTED admin@example.com access to 'dummy_label' (the/project/path)" + fake_access = fake_access(true, build(:user, email: 'admin@example.com')) + + expect(described_class).to receive(:info).with(expected_message) + + described_class.log_access(fake_access, 'the/project/path') + end + + it 'does not trip without a project path' do + expected_message = "DENIED admin@example.com access to 'dummy_label'" + fake_access = fake_access(false, build(:user, email: 'admin@example.com')) + + expect(described_class).to receive(:info).with(expected_message) + + described_class.log_access(fake_access, nil) + end + + it 'adds the load time for cached accesses' do + expected_message = "DENIED admin@example.com access to 'dummy_label' - cache #{request_time}" + fake_access = fake_access(false, build(:user, email: 'admin@example.com'), :cache) + + expect(described_class).to receive(:info).with(expected_message) + + described_class.log_access(fake_access, nil) + end + end +end diff --git a/spec/lib/gitlab/external_authorization/response_spec.rb b/spec/lib/gitlab/external_authorization/response_spec.rb new file mode 100644 index 00000000000..43211043eca --- /dev/null +++ b/spec/lib/gitlab/external_authorization/response_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe Gitlab::ExternalAuthorization::Response do + let(:excon_response) { double } + subject(:response) { described_class.new(excon_response) } + + describe '#valid?' do + it 'is valid for 200, 401, and 403 responses' do + [200, 401, 403].each do |status| + allow(excon_response).to receive(:status).and_return(status) + + expect(response).to be_valid + end + end + + it "is invalid for other statuses" do + expect(excon_response).to receive(:status).and_return(500) + + expect(response).not_to be_valid + end + end + + describe '#reason' do + it 'returns a reason if it was included in the response body' do + expect(excon_response).to receive(:body).and_return({ reason: 'Not authorized' }.to_json) + + expect(response.reason).to eq('Not authorized') + end + + it 'returns nil when there was no body' do + expect(excon_response).to receive(:body).and_return('') + + expect(response.reason).to eq(nil) + end + end + + describe '#successful?' do + it 'is `true` if the status is 200' do + allow(excon_response).to receive(:status).and_return(200) + + expect(response).to be_successful + end + + it 'is `false` if the status is 401 or 403' do + [401, 403].each do |status| + allow(excon_response).to receive(:status).and_return(status) + + expect(response).not_to be_successful + end + end + end +end diff --git a/spec/lib/gitlab/external_authorization_spec.rb b/spec/lib/gitlab/external_authorization_spec.rb new file mode 100644 index 00000000000..7394fbfe0ce --- /dev/null +++ b/spec/lib/gitlab/external_authorization_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Gitlab::ExternalAuthorization, :request_store do + include ExternalAuthorizationServiceHelpers + + let(:user) { build(:user) } + let(:label) { 'dummy_label' } + + describe '#access_allowed?' do + it 'is always true when the feature is disabled' do + # Not using `stub_application_setting` because the method is prepended in + # `EE::ApplicationSetting` which breaks when using `any_instance` + # https://gitlab.com/gitlab-org/gitlab-ce/issues/33587 + expect(::Gitlab::CurrentSettings.current_application_settings) + .to receive(:external_authorization_service_enabled) { false } + + expect(described_class).not_to receive(:access_for_user_to_label) + + expect(described_class.access_allowed?(user, label)).to be_truthy + end + end + + describe '#rejection_reason' do + it 'is always nil when the feature is disabled' do + expect(::Gitlab::CurrentSettings.current_application_settings) + .to receive(:external_authorization_service_enabled) { false } + + expect(described_class).not_to receive(:access_for_user_to_label) + + expect(described_class.rejection_reason(user, label)).to be_nil + end + end + + describe '#access_for_user_to_label' do + it 'only loads the access once per request' do + enable_external_authorization_service_check + + expect(::Gitlab::ExternalAuthorization::Access) + .to receive(:new).with(user, label).once.and_call_original + + 2.times { described_class.access_for_user_to_label(user, label, nil) } + end + + it 'logs the access request once per request' do + expect(::Gitlab::ExternalAuthorization::Logger) + .to receive(:log_access) + .with(an_instance_of(::Gitlab::ExternalAuthorization::Access), + 'the/project/path') + .once + + 2.times { described_class.access_for_user_to_label(user, label, 'the/project/path') } + end + end +end diff --git a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb index 26529c4759d..569d5dcc757 100644 --- a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb @@ -97,13 +97,13 @@ describe Gitlab::HookData::IssuableBuilder do end context 'merge_request is assigned' do - let(:merge_request) { create(:merge_request, assignee: user) } + let(:merge_request) { create(:merge_request, assignees: [user]) } let(:data) { described_class.new(merge_request).build(user: user) } it 'returns correct hook data' do expect(data[:object_attributes]['assignee_id']).to eq(user.id) - expect(data[:assignee]).to eq(user.hook_attrs) - expect(data).not_to have_key(:assignees) + expect(data[:assignees].first).to eq(user.hook_attrs) + expect(data).not_to have_key(:assignee) end end end diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb index 9ce697adbba..39f80f92fa6 100644 --- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb @@ -10,6 +10,7 @@ describe Gitlab::HookData::MergeRequestBuilder do it 'includes safe attribute' do %w[ assignee_id + assignee_ids author_id created_at description diff --git a/spec/lib/gitlab/import/merge_request_helpers_spec.rb b/spec/lib/gitlab/import/merge_request_helpers_spec.rb new file mode 100644 index 00000000000..cc0f2baf905 --- /dev/null +++ b/spec/lib/gitlab/import/merge_request_helpers_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Import::MergeRequestHelpers, type: :helper do + set(:project) { create(:project, :repository) } + set(:user) { create(:user) } + + describe '.create_merge_request_without_hooks' do + let(:iid) { 42 } + + let(:attributes) do + { + iid: iid, + title: 'My Pull Request', + description: 'This is my pull request', + source_project_id: project.id, + target_project_id: project.id, + source_branch: 'master-42', + target_branch: 'master', + state: :merged, + author_id: user.id, + assignee_id: user.id + } + end + + subject { helper.create_merge_request_without_hooks(project, attributes, iid) } + + context 'when merge request does not exist' do + it 'returns a new object' do + expect(subject.first).not_to be_nil + expect(subject.second).to eq(false) + end + + it 'does load all existing objects' do + 5.times do |iid| + MergeRequest.create!( + attributes.merge(iid: iid, source_branch: iid.to_s)) + end + + # does ensure that we only load object twice + # 1. by #insert_and_return_id + # 2. by project.merge_requests.find + expect_any_instance_of(MergeRequest).to receive(:attributes) + .twice.times.and_call_original + + expect(subject.first).not_to be_nil + expect(subject.second).to eq(false) + end + end + + context 'when merge request does exist' do + before do + MergeRequest.create!(attributes) + end + + it 'returns an existing object' do + expect(subject.first).not_to be_nil + expect(subject.second).to eq(true) + end + end + + context 'when project is deleted' do + before do + project.delete + end + + it 'returns an existing object' do + expect(subject.first).to be_nil + end + end + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index e418516569a..ed557ffd4e3 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -102,6 +102,7 @@ merge_requests: - merge_request_pipelines - merge_request_assignees - suggestions +- assignees merge_request_diff: - merge_request - merge_request_diff_commits @@ -336,6 +337,9 @@ push_event_payload: issue_assignees: - issue - assignee +merge_request_assignees: +- merge_request +- assignee lfs_file_locks: - user project_badges: diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index d0ed588f05f..ebb62124cb1 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -496,6 +496,7 @@ Project: - merge_requests_ff_only_enabled - merge_requests_rebase_enabled - jobs_cache_index +- external_authorization_classification_label - pages_https_only Author: - name @@ -621,3 +622,7 @@ Suggestion: - outdated - lines_above - lines_below +MergeRequestAssignee: +- id +- user_id +- merge_request_id diff --git a/spec/lib/gitlab/issuable_metadata_spec.rb b/spec/lib/gitlab/issuable_metadata_spec.rb index 6ec86163233..916f3876a8e 100644 --- a/spec/lib/gitlab/issuable_metadata_spec.rb +++ b/spec/lib/gitlab/issuable_metadata_spec.rb @@ -19,7 +19,7 @@ describe Gitlab::IssuableMetadata do let!(:closed_issue) { create(:issue, state: :closed, author: user, project: project) } let!(:downvote) { create(:award_emoji, :downvote, awardable: closed_issue) } let!(:upvote) { create(:award_emoji, :upvote, awardable: issue) } - let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test") } + let!(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, title: "Test") } let!(:closing_issues) { create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request) } it 'aggregates stats on issues' do @@ -39,7 +39,7 @@ describe Gitlab::IssuableMetadata do end context 'merge requests' do - let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test") } + let!(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, title: "Test") } let!(:merge_request_closed) { create(:merge_request, state: "closed", source_project: project, target_project: project, title: "Closed Test") } let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request) } let!(:upvote) { create(:award_emoji, :upvote, awardable: merge_request) } diff --git a/spec/lib/gitlab/kubernetes/namespace_spec.rb b/spec/lib/gitlab/kubernetes/namespace_spec.rb index e1c35c355f4..e91a755aa03 100644 --- a/spec/lib/gitlab/kubernetes/namespace_spec.rb +++ b/spec/lib/gitlab/kubernetes/namespace_spec.rb @@ -62,5 +62,32 @@ describe Gitlab::Kubernetes::Namespace do subject.ensure_exists! end + + context 'when client errors' do + let(:exception) { Kubeclient::HttpError.new(500, 'system failure', nil) } + + before do + allow(client).to receive(:get_namespace).with(name).once.and_raise(exception) + end + + it 'raises the exception' do + expect { subject.ensure_exists! }.to raise_error(exception) + end + + it 'logs the error' do + expect(subject.send(:logger)).to receive(:error).with( + hash_including( + exception: 'Kubeclient::HttpError', + status_code: 500, + namespace: 'a_namespace', + class_name: 'Gitlab::Kubernetes::Namespace', + event: :failed_to_create_namespace, + message: 'system failure' + ) + ) + + expect { subject.ensure_exists! }.to raise_error(exception) + end + end end end diff --git a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb index 3d4240fa4ba..8675d8691c8 100644 --- a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb @@ -47,12 +47,22 @@ describe Gitlab::LegacyGithubImport::ProjectCreator do end context 'when GitHub project is public' do - it 'sets project visibility to public' do + it 'sets project visibility to namespace visibility level' do repo.private = false - project = service.execute - expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + expect(project.visibility_level).to eq(namespace.visibility_level) + end + + context 'when importing into a user namespace' do + subject(:service) { described_class.new(repo, repo.name, user.namespace, user, github_access_token: 'asdffg') } + + it 'sets project visibility to user namespace visibility level' do + repo.private = false + project = service.execute + + expect(project.visibility_level).to eq(user.namespace.visibility_level) + end end end diff --git a/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb index 082e3b36dd0..c57b96fb00d 100644 --- a/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb @@ -25,6 +25,7 @@ describe Gitlab::LegacyGithubImport::ReleaseFormatter do expected = { project: project, tag: 'v1.0.0', + name: 'First release', description: 'Release v1.0.0', created_at: created_at, updated_at: created_at diff --git a/spec/lib/gitlab/push_options_spec.rb b/spec/lib/gitlab/push_options_spec.rb new file mode 100644 index 00000000000..fc9e421bea6 --- /dev/null +++ b/spec/lib/gitlab/push_options_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::PushOptions do + describe 'namespace and key validation' do + it 'ignores unrecognised namespaces' do + options = described_class.new(['invalid.key=value']) + + expect(options.get(:invalid)).to eq(nil) + end + + it 'ignores unrecognised keys' do + options = described_class.new(['merge_request.key=value']) + + expect(options.get(:merge_request)).to eq(nil) + end + + it 'ignores blank keys' do + options = described_class.new(['merge_request']) + + expect(options.get(:merge_request)).to eq(nil) + end + + it 'parses recognised namespace and key pairs' do + options = described_class.new(['merge_request.target=value']) + + expect(options.get(:merge_request)).to include({ + target: 'value' + }) + end + end + + describe '#get' do + it 'can emulate Hash#dig' do + options = described_class.new(['merge_request.target=value']) + + expect(options.get(:merge_request, :target)).to eq('value') + end + end + + describe '#as_json' do + it 'returns all options' do + options = described_class.new(['merge_request.target=value']) + + expect(options.as_json).to include( + merge_request: { + target: 'value' + } + ) + end + end + + it 'can parse multiple push options' do + options = described_class.new([ + 'merge_request.create', + 'merge_request.target=value' + ]) + + expect(options.get(:merge_request)).to include({ + create: true, + target: 'value' + }) + expect(options.get(:merge_request, :create)).to eq(true) + expect(options.get(:merge_request, :target)).to eq('value') + end + + it 'stores options internally as a HashWithIndifferentAccess' do + options = described_class.new([ + 'merge_request.create' + ]) + + expect(options.get('merge_request', 'create')).to eq(true) + expect(options.get(:merge_request, :create)).to eq(true) + end + + it 'selects the last option when options contain duplicate namespace and key pairs' do + options = described_class.new([ + 'merge_request.target=value1', + 'merge_request.target=value2' + ]) + + expect(options.get(:merge_request, :target)).to eq('value2') + end + + it 'defaults values to true' do + options = described_class.new(['merge_request.create']) + + expect(options.get(:merge_request, :create)).to eq(true) + end + + it 'expands aliases' do + options = described_class.new(['mr.target=value']) + + expect(options.get(:merge_request, :target)).to eq('value') + end + + it 'forgives broken push options' do + options = described_class.new(['merge_request . target = value']) + + expect(options.get(:merge_request, :target)).to eq('value') + end +end diff --git a/spec/mailers/emails/pages_domains_spec.rb b/spec/mailers/emails/pages_domains_spec.rb index c74fd66ad22..050af587061 100644 --- a/spec/mailers/emails/pages_domains_spec.rb +++ b/spec/mailers/emails/pages_domains_spec.rb @@ -26,6 +26,26 @@ describe Emails::PagesDomains do end end + shared_examples 'notification about upcoming domain removal' do + context 'when domain is not scheduled for removal' do + it 'asks user to remove it' do + is_expected.to have_body_text 'please remove it' + end + end + + context 'when domain is scheduled for removal' do + before do + domain.update!(remove_at: 1.week.from_now) + end + it 'notifies user that domain will be removed automatically' do + aggregate_failures do + is_expected.to have_body_text domain.remove_at.strftime('%F %T') + is_expected.to have_body_text "it will be removed from your GitLab project" + end + end + end + end + describe '#pages_domain_enabled_email' do let(:email_subject) { "#{project.path} | GitLab Pages domain '#{domain.domain}' has been enabled" } @@ -43,6 +63,8 @@ describe Emails::PagesDomains do it_behaves_like 'a pages domain email' + it_behaves_like 'notification about upcoming domain removal' + it { is_expected.to have_body_text 'has been disabled' } end @@ -63,6 +85,8 @@ describe Emails::PagesDomains do it_behaves_like 'a pages domain email' + it_behaves_like 'notification about upcoming domain removal' + it 'says verification has failed and when the domain is enabled until' do is_expected.to have_body_text 'Verification has failed' is_expected.to have_body_text domain.enabled_until.strftime('%F %T') diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 5fa1369c00a..fee1d701e3a 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -19,7 +19,7 @@ describe Notify do create(:merge_request, source_project: project, target_project: project, author: current_user, - assignee: assignee, + assignees: [assignee], description: 'Awesome description') end @@ -275,7 +275,7 @@ describe Notify do context 'for merge requests' do describe 'that are new' do - subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) } + subject { described_class.new_merge_request_email(merge_request.assignee_ids.first, merge_request.id) } it_behaves_like 'an assignee email' it_behaves_like 'an email starting a new thread with reply-by-email enabled' do @@ -300,7 +300,7 @@ describe Notify do end context 'when sent with a reason' do - subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id, NotificationReason::ASSIGNED) } + subject { described_class.new_merge_request_email(merge_request.assignee_ids.first, merge_request.id, NotificationReason::ASSIGNED) } it_behaves_like 'appearance header and footer enabled' it_behaves_like 'appearance header and footer not enabled' @@ -324,7 +324,7 @@ describe Notify do describe 'that are reassigned' do let(:previous_assignee) { create(:user, name: 'Previous Assignee') } - subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) } + subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id) } it_behaves_like 'a multiple recipients email' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do @@ -351,7 +351,7 @@ describe Notify do end context 'when sent with a reason' do - subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, NotificationReason::ASSIGNED) } + subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, NotificationReason::ASSIGNED) } it_behaves_like 'appearance header and footer enabled' it_behaves_like 'appearance header and footer not enabled' @@ -364,11 +364,11 @@ describe Notify do text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(NotificationReason::ASSIGNED) is_expected.to have_body_text(text) - new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, NotificationReason::MENTIONED) + new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, NotificationReason::MENTIONED) text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(NotificationReason::MENTIONED) expect(new_subject).to have_body_text(text) - new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, nil) + new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, nil) text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(nil) expect(new_subject).to have_body_text(text) end @@ -376,7 +376,7 @@ describe Notify do end describe 'that are new with a description' do - subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) } + subject { described_class.new_merge_request_email(merge_request.assignee_ids.first, merge_request.id) } it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like "an unsubscribeable thread" @@ -476,7 +476,7 @@ describe Notify do source_project: project, target_project: project, author: current_user, - assignee: assignee, + assignees: [assignee], description: 'Awesome description') end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index c81572d739e..c7d7dbac736 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe ApplicationSetting do - let(:setting) { described_class.create_from_defaults } + subject(:setting) { described_class.create_from_defaults } it { include(CacheableAttributes) } it { include(ApplicationSettingImplementation) } @@ -284,6 +284,52 @@ describe ApplicationSetting do expect(subject).to be_valid end end + + describe 'when external authorization service is enabled' do + before do + setting.external_authorization_service_enabled = true + end + + it { is_expected.not_to allow_value('not a URL').for(:external_authorization_service_url) } + it { is_expected.to allow_value('https://example.com').for(:external_authorization_service_url) } + it { is_expected.to allow_value('').for(:external_authorization_service_url) } + it { is_expected.not_to allow_value(nil).for(:external_authorization_service_default_label) } + it { is_expected.not_to allow_value(11).for(:external_authorization_service_timeout) } + it { is_expected.not_to allow_value(0).for(:external_authorization_service_timeout) } + it { is_expected.not_to allow_value('not a certificate').for(:external_auth_client_cert) } + it { is_expected.to allow_value('').for(:external_auth_client_cert) } + it { is_expected.to allow_value('').for(:external_auth_client_key) } + + context 'when setting a valid client certificate for external authorization' do + let(:certificate_data) { File.read('spec/fixtures/passphrase_x509_certificate.crt') } + + before do + setting.external_auth_client_cert = certificate_data + end + + it 'requires a valid client key when a certificate is set' do + expect(setting).not_to allow_value('fefefe').for(:external_auth_client_key) + end + + it 'requires a matching certificate' do + other_private_key = File.read('spec/fixtures/x509_certificate_pk.key') + + expect(setting).not_to allow_value(other_private_key).for(:external_auth_client_key) + end + + it 'the credentials are valid when the private key can be read and matches the certificate' do + tls_attributes = [:external_auth_client_key_pass, + :external_auth_client_key, + :external_auth_client_cert] + setting.external_auth_client_key = File.read('spec/fixtures/passphrase_x509_certificate_pk.key') + setting.external_auth_client_key_pass = '5iveL!fe' + + setting.validate + + expect(setting.errors).not_to include(*tls_attributes) + end + end + end end context 'restrict creating duplicates' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 83b0f172f03..f3e78630c1b 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -684,12 +684,12 @@ describe Ci::Pipeline, :mailer do source_branch: 'feature', target_project: project, target_branch: 'master', - assignee: assignee, + assignees: assignees, milestone: milestone, labels: labels) end - let(:assignee) { create(:user) } + let(:assignees) { create_list(:user, 2) } let(:milestone) { create(:milestone, project: project) } let(:labels) { create_list(:label, 2) } @@ -710,7 +710,7 @@ describe Ci::Pipeline, :mailer do 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s, 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => pipeline.source_sha.to_s, 'CI_MERGE_REQUEST_TITLE' => merge_request.title, - 'CI_MERGE_REQUEST_ASSIGNEES' => assignee.username, + 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list, 'CI_MERGE_REQUEST_MILESTONE' => milestone.title, 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).join(',')) end @@ -730,7 +730,7 @@ describe Ci::Pipeline, :mailer do end context 'without assignee' do - let(:assignee) { nil } + let(:assignees) { [] } it 'does not expose assignee variable' do expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_ASSIGNEES') diff --git a/spec/models/concerns/deprecated_assignee_spec.rb b/spec/models/concerns/deprecated_assignee_spec.rb new file mode 100644 index 00000000000..e394de0aa34 --- /dev/null +++ b/spec/models/concerns/deprecated_assignee_spec.rb @@ -0,0 +1,160 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe DeprecatedAssignee do + let(:user) { create(:user) } + + describe '#assignee_id=' do + it 'creates the merge_request_assignees relation' do + merge_request = create(:merge_request, assignee_id: user.id) + + merge_request.reload + + expect(merge_request.merge_request_assignees.count).to eq(1) + end + + it 'nullifies the assignee_id column' do + merge_request = create(:merge_request, assignee_id: user.id) + + merge_request.reload + + expect(merge_request.read_attribute(:assignee_id)).to be_nil + end + + context 'when relation already exists' do + it 'overwrites existing assignees' do + other_user = create(:user) + merge_request = create(:merge_request, assignee_id: nil) + merge_request.merge_request_assignees.create!(user_id: user.id) + merge_request.merge_request_assignees.create!(user_id: other_user.id) + + expect { merge_request.update!(assignee_id: other_user.id) } + .to change { merge_request.reload.merge_request_assignees.count } + .from(2).to(1) + end + end + end + + describe '#assignee=' do + it 'creates the merge_request_assignees relation' do + merge_request = create(:merge_request, assignee: user) + + merge_request.reload + + expect(merge_request.merge_request_assignees.count).to eq(1) + end + + it 'nullifies the assignee_id column' do + merge_request = create(:merge_request, assignee: user) + + merge_request.reload + + expect(merge_request.read_attribute(:assignee_id)).to be_nil + end + + context 'when relation already exists' do + it 'overwrites existing assignees' do + other_user = create(:user) + merge_request = create(:merge_request, assignee: nil) + merge_request.merge_request_assignees.create!(user_id: user.id) + merge_request.merge_request_assignees.create!(user_id: other_user.id) + + expect { merge_request.update!(assignee: other_user) } + .to change { merge_request.reload.merge_request_assignees.count } + .from(2).to(1) + end + end + end + + describe '#assignee_id' do + it 'returns the first assignee ID' do + other_user = create(:user) + merge_request = create(:merge_request, assignees: [user, other_user]) + + merge_request.reload + + expect(merge_request.assignee_id).to eq(merge_request.assignee_ids.first) + end + end + + describe '#assignees' do + context 'when assignee_id exists and there is no relation' do + it 'creates the relation' do + merge_request = create(:merge_request, assignee_id: nil) + merge_request.update_column(:assignee_id, user.id) + + expect { merge_request.assignees }.to change { merge_request.merge_request_assignees.count }.from(0).to(1) + end + + it 'nullifies the assignee_id' do + merge_request = create(:merge_request, assignee_id: nil) + merge_request.update_column(:assignee_id, user.id) + + expect { merge_request.assignees } + .to change { merge_request.read_attribute(:assignee_id) } + .from(user.id).to(nil) + end + end + + context 'when DB is read-only' do + before do + allow(Gitlab::Database).to receive(:read_only?) { true } + end + + it 'returns a users relation' do + merge_request = create(:merge_request, assignee_id: user.id) + + expect(merge_request.assignees).to be_a(ActiveRecord::Relation) + expect(merge_request.assignees).to eq([user]) + end + + it 'returns an empty relation if no assignee_id is set' do + merge_request = create(:merge_request, assignee_id: nil) + + expect(merge_request.assignees).to be_a(ActiveRecord::Relation) + expect(merge_request.assignees).to eq([]) + end + end + end + + describe '#assignee_ids' do + context 'when assignee_id exists and there is no relation' do + it 'creates the relation' do + merge_request = create(:merge_request, assignee_id: nil) + merge_request.update_column(:assignee_id, user.id) + + expect { merge_request.assignee_ids }.to change { merge_request.merge_request_assignees.count }.from(0).to(1) + end + + it 'nullifies the assignee_id' do + merge_request = create(:merge_request, assignee_id: nil) + merge_request.update_column(:assignee_id, user.id) + + expect { merge_request.assignee_ids } + .to change { merge_request.read_attribute(:assignee_id) } + .from(user.id).to(nil) + end + end + + context 'when DB is read-only' do + before do + allow(Gitlab::Database).to receive(:read_only?) { true } + end + + it 'returns a list of user IDs' do + merge_request = create(:merge_request, assignee_id: user.id) + + expect(merge_request.assignee_ids).to be_a(Array) + expect(merge_request.assignee_ids).to eq([user.id]) + end + + it 'returns an empty relation if no assignee_id is set' do + merge_request = create(:merge_request, assignee_id: nil) + + expect(merge_request.assignee_ids).to be_a(Array) + expect(merge_request.assignee_ids).to eq([]) + end + end + end +end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 27ed298ae08..64f02978d79 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -502,8 +502,8 @@ describe Issuable do let(:user2) { create(:user) } before do - merge_request.update(assignee: user) - merge_request.update(assignee: user2) + merge_request.update(assignees: [user]) + merge_request.update(assignees: [user, user2]) expect(Gitlab::HookData::IssuableBuilder) .to receive(:new).with(merge_request).and_return(builder) end @@ -512,8 +512,7 @@ describe Issuable do expect(builder).to receive(:build).with( user: user, changes: hash_including( - 'assignee_id' => [user.id, user2.id], - 'assignee' => [user.hook_attrs, user2.hook_attrs] + 'assignees' => [[user.hook_attrs], [user.hook_attrs, user2.hook_attrs]] )) merge_request.to_hook_data(user, old_associations: { assignees: [user] }) diff --git a/spec/models/concerns/protected_ref_access_spec.rb b/spec/models/concerns/protected_ref_access_spec.rb index 94798f0590d..f63ad958ed3 100644 --- a/spec/models/concerns/protected_ref_access_spec.rb +++ b/spec/models/concerns/protected_ref_access_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe ProtectedRefAccess do + include ExternalAuthorizationServiceHelpers + subject(:protected_ref_access) do create(:protected_branch, :maintainers_can_push).push_access_levels.first end @@ -29,5 +31,15 @@ describe ProtectedRefAccess do expect(protected_ref_access.check_access(developer)).to be_falsy end + + context 'external authorization' do + it 'is false if external authorization denies access' do + maintainer = create(:user) + project.add_maintainer(maintainer) + external_service_deny_access(maintainer, project) + + expect(protected_ref_access.check_access(maintainer)).to be_falsey + end + end end end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index d192fe70506..e91b5c4c86f 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -263,7 +263,7 @@ describe Event do context 'merge request diff note event' do let(:project) { create(:project, :public) } - let(:merge_request) { create(:merge_request, source_project: project, author: author, assignee: assignee) } + let(:merge_request) { create(:merge_request, source_project: project, author: author, assignees: [assignee]) } let(:note_on_merge_request) { create(:legacy_diff_note_on_merge_request, noteable: merge_request, project: project) } let(:target) { note_on_merge_request } diff --git a/spec/models/instance_configuration_spec.rb b/spec/models/instance_configuration_spec.rb index e65f97df3c3..43954511858 100644 --- a/spec/models/instance_configuration_spec.rb +++ b/spec/models/instance_configuration_spec.rb @@ -82,6 +82,13 @@ describe InstanceConfiguration do it 'returns the key artifacts_max_size' do expect(gitlab_ci.keys).to include(:artifacts_max_size) end + + it 'returns the key artifacts_max_size with values' do + stub_application_setting(max_artifacts_size: 200) + + expect(gitlab_ci[:artifacts_max_size][:default]).to eq(100.megabytes) + expect(gitlab_ci[:artifacts_max_size][:value]).to eq(200.megabytes) + end end end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 892dd053e39..0cd69cb4817 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe Issue do + include ExternalAuthorizationServiceHelpers + describe "Associations" do it { is_expected.to belong_to(:milestone) } it { is_expected.to have_many(:assignees) } @@ -779,4 +781,47 @@ describe Issue do it_behaves_like 'throttled touch' do subject { create(:issue, updated_at: 1.hour.ago) } end + + context 'when an external authentication service' do + before do + enable_external_authorization_service_check + end + + describe '#visible_to_user?' do + it 'is `false` when an external authorization service is enabled' do + issue = build(:issue, project: build(:project, :public)) + + expect(issue).not_to be_visible_to_user + end + + it 'checks the external service to determine if an issue is readable by a user' do + project = build(:project, :public, + external_authorization_classification_label: 'a-label') + issue = build(:issue, project: project) + user = build(:user) + + expect(::Gitlab::ExternalAuthorization).to receive(:access_allowed?).with(user, 'a-label') { false } + expect(issue.visible_to_user?(user)).to be_falsy + end + + it 'does not check the external service if a user does not have access to the project' do + project = build(:project, :private, + external_authorization_classification_label: 'a-label') + issue = build(:issue, project: project) + user = build(:user) + + expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?) + expect(issue.visible_to_user?(user)).to be_falsy + end + + it 'does not check the external webservice for admins' do + issue = build(:issue) + user = build(:admin) + + expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?) + + issue.visible_to_user?(user) + end + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 6f34ef9c1bc..f61857ea5ff 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -13,7 +13,7 @@ describe MergeRequest do it { is_expected.to belong_to(:target_project).class_name('Project') } it { is_expected.to belong_to(:source_project).class_name('Project') } it { is_expected.to belong_to(:merge_user).class_name("User") } - it { is_expected.to belong_to(:assignee) } + it { is_expected.to have_many(:assignees).through(:merge_request_assignees) } it { is_expected.to have_many(:merge_request_diffs) } context 'for forks' do @@ -181,31 +181,6 @@ describe MergeRequest do expect(MergeRequest::Metrics.count).to eq(1) end end - - describe '#refresh_merge_request_assignees' do - set(:user) { create(:user) } - - it 'creates merge request assignees relation upon MR creation' do - merge_request = create(:merge_request, assignee: nil) - - expect(merge_request.merge_request_assignees).to be_empty - - expect { merge_request.update!(assignee: user) } - .to change { merge_request.reload.merge_request_assignees.count } - .from(0).to(1) - end - - it 'updates merge request assignees relation upon MR assignee change' do - another_user = create(:user) - merge_request = create(:merge_request, assignee: user) - - expect { merge_request.update!(assignee: another_user) } - .to change { merge_request.reload.merge_request_assignees.first.assignee } - .from(user).to(another_user) - - expect(merge_request.merge_request_assignees.count).to eq(1) - end - end end describe 'respond to' do @@ -337,34 +312,18 @@ describe MergeRequest do describe '#card_attributes' do it 'includes the author name' do allow(subject).to receive(:author).and_return(double(name: 'Robert')) - allow(subject).to receive(:assignee).and_return(nil) + allow(subject).to receive(:assignees).and_return([]) expect(subject.card_attributes) - .to eq({ 'Author' => 'Robert', 'Assignee' => nil }) + .to eq({ 'Author' => 'Robert', 'Assignee' => "" }) end - it 'includes the assignee name' do + it 'includes the assignees name' do allow(subject).to receive(:author).and_return(double(name: 'Robert')) - allow(subject).to receive(:assignee).and_return(double(name: 'Douwe')) + allow(subject).to receive(:assignees).and_return([double(name: 'Douwe'), double(name: 'Robert')]) expect(subject.card_attributes) - .to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' }) - end - end - - describe '#assignee_ids' do - it 'returns an array of the assigned user id' do - subject.assignee_id = 123 - - expect(subject.assignee_ids).to eq([123]) - end - end - - describe '#assignee_ids=' do - it 'sets assignee_id to the last id in the array' do - subject.assignee_ids = [123, 456] - - expect(subject.assignee_id).to eq(456) + .to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe and Robert' }) end end @@ -372,7 +331,7 @@ describe MergeRequest do let(:user) { create(:user) } it 'returns true for a user that is assigned to a merge request' do - subject.assignee = user + subject.assignees = [user] expect(subject.assignee_or_author?(user)).to eq(true) end @@ -1949,15 +1908,14 @@ describe MergeRequest do it 'updates when assignees change' do user1 = create(:user) user2 = create(:user) - mr = create(:merge_request, assignee: user1) + mr = create(:merge_request, assignees: [user1]) mr.project.add_developer(user1) mr.project.add_developer(user2) expect(user1.assigned_open_merge_requests_count).to eq(1) expect(user2.assigned_open_merge_requests_count).to eq(0) - mr.assignee = user2 - mr.save + mr.assignees = [user2] expect(user1.assigned_open_merge_requests_count).to eq(0) expect(user2.assigned_open_merge_requests_count).to eq(1) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 5eb31430ccd..7f8d2ff91fd 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' describe Project do include ProjectForksHelper include GitHelpers + include ExternalAuthorizationServiceHelpers it_behaves_like 'having unique enum values' @@ -2721,7 +2722,7 @@ describe Project do end describe '#any_lfs_file_locks?', :request_store do - let!(:project) { create(:project) } + set(:project) { create(:project) } it 'returns false when there are no LFS file locks' do expect(project.any_lfs_file_locks?).to be_falsey @@ -3159,53 +3160,6 @@ describe Project do expect(projects).to eq([public_project]) end end - - context 'with requested visibility levels' do - set(:internal_project) { create(:project, :internal, :repository) } - set(:private_project_2) { create(:project, :private) } - - context 'with admin user' do - set(:admin) { create(:admin) } - - it 'returns all projects' do - projects = described_class.all.public_or_visible_to_user(admin, []) - - expect(projects).to match_array([public_project, private_project, private_project_2, internal_project]) - end - - it 'returns all public and private projects' do - projects = described_class.all.public_or_visible_to_user(admin, [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::PRIVATE]) - - expect(projects).to match_array([public_project, private_project, private_project_2]) - end - - it 'returns all private projects' do - projects = described_class.all.public_or_visible_to_user(admin, [Gitlab::VisibilityLevel::PRIVATE]) - - expect(projects).to match_array([private_project, private_project_2]) - end - end - - context 'with regular user' do - it 'returns authorized projects' do - projects = described_class.all.public_or_visible_to_user(user, []) - - expect(projects).to match_array([public_project, private_project, internal_project]) - end - - it "returns user's public and private projects" do - projects = described_class.all.public_or_visible_to_user(user, [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::PRIVATE]) - - expect(projects).to match_array([public_project, private_project]) - end - - it 'returns one private project' do - projects = described_class.all.public_or_visible_to_user(user, [Gitlab::VisibilityLevel::PRIVATE]) - - expect(projects).to eq([private_project]) - end - end - end end describe '.with_feature_available_for_user' do @@ -4417,6 +4371,25 @@ describe Project do end end + describe '#external_authorization_classification_label' do + it 'falls back to the default when none is configured' do + enable_external_authorization_service_check + + expect(build(:project).external_authorization_classification_label) + .to eq('default_label') + end + + it 'returns the classification label if it was configured on the project' do + enable_external_authorization_service_check + + project = build(:project, + external_authorization_classification_label: 'hello') + + expect(project.external_authorization_classification_label) + .to eq('hello') + end + end + describe "#pages_https_only?" do subject { build(:project) } diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb index b4b32c95dee..0b19a4f8efc 100644 --- a/spec/models/release_spec.rb +++ b/spec/models/release_spec.rb @@ -18,6 +18,22 @@ RSpec.describe Release do describe 'validation' do it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:description) } + it { is_expected.to validate_presence_of(:name) } + + context 'when a release exists in the database without a name' do + it 'does not require name' do + existing_release_without_name = build(:release, project: project, author: user, name: nil) + existing_release_without_name.save(validate: false) + + existing_release_without_name.description = "change" + existing_release_without_name.save + existing_release_without_name.reload + + expect(existing_release_without_name).to be_valid + expect(existing_release_without_name.description).to eq("change") + expect(existing_release_without_name.name).to be_nil + end + end end describe '#assets_count' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a45a2737b13..d1338e34bb8 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2816,9 +2816,9 @@ describe User do project = create(:project, :public) archived_project = create(:project, :public, :archived) - create(:merge_request, source_project: project, author: user, assignee: user) - create(:merge_request, :closed, source_project: project, author: user, assignee: user) - create(:merge_request, source_project: archived_project, author: user, assignee: user) + create(:merge_request, source_project: project, author: user, assignees: [user]) + create(:merge_request, :closed, source_project: project, author: user, assignees: [user]) + create(:merge_request, source_project: archived_project, author: user, assignees: [user]) expect(user.assigned_open_merge_requests_count(force: true)).to eq 1 end diff --git a/spec/policies/base_policy_spec.rb b/spec/policies/base_policy_spec.rb index c03d95b34db..09be831dcd5 100644 --- a/spec/policies/base_policy_spec.rb +++ b/spec/policies/base_policy_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe BasePolicy do + include ExternalAuthorizationServiceHelpers + describe '.class_for' do it 'detects policy class based on the subject ancestors' do expect(DeclarativePolicy.class_for(GenericCommitStatus.new)).to eq(CommitStatusPolicy) @@ -16,4 +18,25 @@ describe BasePolicy do expect(DeclarativePolicy.class_for(:global)).to eq(GlobalPolicy) end end + + describe 'read cross project' do + let(:current_user) { create(:user) } + let(:user) { create(:user) } + + subject { described_class.new(current_user, [user]) } + + it { is_expected.to be_allowed(:read_cross_project) } + + context 'when an external authorization service is enabled' do + before do + enable_external_authorization_service_check + end + + it { is_expected.not_to be_allowed(:read_cross_project) } + + it 'allows admins' do + expect(described_class.new(build(:admin), nil)).to be_allowed(:read_cross_project) + end + end + end end diff --git a/spec/policies/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb index 844d96017de..126d44d1860 100644 --- a/spec/policies/ci/pipeline_policy_spec.rb +++ b/spec/policies/ci/pipeline_policy_spec.rb @@ -100,5 +100,51 @@ describe Ci::PipelinePolicy, :models do end end end + + describe 'read_pipeline_variable' do + let(:project) { create(:project, :public) } + + context 'when user has owner access' do + let(:user) { project.owner } + + it 'is enabled' do + expect(policy).to be_allowed :read_pipeline_variable + end + end + + context 'when user is developer and the creator of the pipeline' do + let(:pipeline) { create(:ci_empty_pipeline, project: project, user: user) } + + before do + project.add_developer(user) + create(:protected_branch, :developers_can_merge, + name: pipeline.ref, project: project) + end + + it 'is enabled' do + expect(policy).to be_allowed :read_pipeline_variable + end + end + + context 'when user is developer and it is not the creator of the pipeline' do + let(:pipeline) { create(:ci_empty_pipeline, project: project, user: project.owner) } + + before do + project.add_developer(user) + create(:protected_branch, :developers_can_merge, + name: pipeline.ref, project: project) + end + + it 'is disabled' do + expect(policy).to be_disallowed :read_pipeline_variable + end + end + + context 'when user is not owner nor developer' do + it 'is disabled' do + expect(policy).not_to be_allowed :read_pipeline_variable + end + end + end end end diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb index 008d118b557..b149dbcf871 100644 --- a/spec/policies/issue_policy_spec.rb +++ b/spec/policies/issue_policy_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe IssuePolicy do + include ExternalAuthorizationServiceHelpers + let(:guest) { create(:user) } let(:author) { create(:user) } let(:assignee) { create(:user) } @@ -204,4 +206,21 @@ describe IssuePolicy do end end end + + context 'with external authorization enabled' do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project) } + let(:policies) { described_class.new(user, issue) } + + before do + enable_external_authorization_service_check + end + + it 'can read the issue iid without accessing the external service' do + expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?) + + expect(policies).to be_allowed(:read_issue_iid) + end + end end diff --git a/spec/policies/merge_request_policy_spec.rb b/spec/policies/merge_request_policy_spec.rb index 1efa70addc2..81279225d61 100644 --- a/spec/policies/merge_request_policy_spec.rb +++ b/spec/policies/merge_request_policy_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe MergeRequestPolicy do + include ExternalAuthorizationServiceHelpers + let(:guest) { create(:user) } let(:author) { create(:user) } let(:developer) { create(:user) } @@ -47,4 +49,21 @@ describe MergeRequestPolicy do expect(permissions(guest, merge_request_locked)).to be_disallowed(:reopen_merge_request) end end + + context 'with external authorization enabled' do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:policies) { described_class.new(user, merge_request) } + + before do + enable_external_authorization_service_check + end + + it 'can read the issue iid without accessing the external service' do + expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?) + + expect(policies).to be_allowed(:read_merge_request_iid) + end + end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 125ed818bc6..42f8bf3137b 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe ProjectPolicy do + include ExternalAuthorizationServiceHelpers include_context 'ProjectPolicy context' set(:guest) { create(:user) } set(:reporter) { create(:user) } @@ -292,4 +293,56 @@ describe ProjectPolicy do projects: [clusterable]) end end + + context 'reading a project' do + it 'allows access when a user has read access to the repo' do + expect(described_class.new(owner, project)).to be_allowed(:read_project) + expect(described_class.new(developer, project)).to be_allowed(:read_project) + expect(described_class.new(admin, project)).to be_allowed(:read_project) + end + + it 'never checks the external service' do + expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?) + + expect(described_class.new(owner, project)).to be_allowed(:read_project) + end + + context 'with an external authorization service' do + before do + enable_external_authorization_service_check + end + + it 'allows access when the external service allows it' do + external_service_allow_access(owner, project) + external_service_allow_access(developer, project) + + expect(described_class.new(owner, project)).to be_allowed(:read_project) + expect(described_class.new(developer, project)).to be_allowed(:read_project) + end + + it 'does not check the external service for admins and allows access' do + expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?) + + expect(described_class.new(admin, project)).to be_allowed(:read_project) + end + + it 'prevents all but seeing a public project in a list when access is denied' do + [developer, owner, build(:user), nil].each do |user| + external_service_deny_access(user, project) + policy = described_class.new(user, project) + + expect(policy).not_to be_allowed(:read_project) + expect(policy).not_to be_allowed(:owner_access) + expect(policy).not_to be_allowed(:change_namespace) + end + end + + it 'passes the full path to external authorization for logging purposes' do + expect(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(owner, 'default_label', project.full_path).and_call_original + + described_class.new(owner, project).allowed?(:read_project) + end + end + end end diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 493d3642255..8fc7fdc8632 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -32,6 +32,7 @@ describe API::Environments do expect(json_response.first['name']).to eq(environment.name) expect(json_response.first['external_url']).to eq(environment.external_url) expect(json_response.first['project'].keys).to contain_exactly(*project_data_keys) + expect(json_response.first).not_to have_key("last_deployment") end end @@ -188,4 +189,25 @@ describe API::Environments do end end end + + describe 'GET /projects/:id/environments/:environment_id' do + context 'as member of the project' do + it 'returns project environments' do + create(:deployment, :success, project: project, environment: environment) + + get api("/projects/#{project.id}/environments/#{environment.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/environment') + end + end + + context 'as non member' do + it 'returns a 404 status code' do + get api("/projects/#{project.id}/environments/#{environment.id}", non_member) + + expect(response).to have_gitlab_http_status(404) + end + end + end end diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb index 0ac23505de7..065b16c6221 100644 --- a/spec/requests/api/events_spec.rb +++ b/spec/requests/api/events_spec.rb @@ -270,8 +270,8 @@ describe API::Events do end context 'when exists some events' do - let(:merge_request1) { create(:merge_request, :closed, author: user, assignee: user, source_project: private_project, title: 'Test') } - let(:merge_request2) { create(:merge_request, :closed, author: user, assignee: user, source_project: private_project, title: 'Test') } + let(:merge_request1) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') } + let(:merge_request2) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') } before do create_event(merge_request1) diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb index 708a000532b..f95f460fd14 100644 --- a/spec/requests/api/graphql/gitlab_schema_spec.rb +++ b/spec/requests/api/graphql/gitlab_schema_spec.rb @@ -3,14 +3,24 @@ require 'spec_helper' describe 'GitlabSchema configurations' do include GraphqlHelpers - let(:project) { create(:project, :repository) } - let!(:query) { graphql_query_for('project', 'fullPath' => project.full_path) } + it 'shows an error if complexity is too high' do + project = create(:project, :repository) + query = graphql_query_for('project', { 'fullPath' => project.full_path }, "id\nname\ndescription") - it 'shows an error if complexity it too high' do allow(GitlabSchema).to receive(:max_query_complexity).and_return 1 post_graphql(query, current_user: nil) expect(graphql_errors.first['message']).to include('which exceeds max complexity of 1') end + + context 'when IntrospectionQuery' do + it 'is not too complex' do + query = File.read(Rails.root.join('spec/fixtures/api/graphql/introspection.graphql')) + + post_graphql(query, current_user: nil) + + expect(graphql_errors).to be_nil + end + end end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 0919540e4ba..1ce8f520962 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -888,8 +888,10 @@ describe API::Internal do } end + let(:branch_name) { 'feature' } + let(:changes) do - "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" + "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{branch_name}" end let(:push_options) do @@ -905,9 +907,9 @@ describe API::Internal do it 'enqueues a PostReceive worker job' do expect(PostReceive).to receive(:perform_async) - .with(gl_repository, identifier, changes, push_options) + .with(gl_repository, identifier, changes, { ci: { skip: true } }) - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params end it 'decreases the reference counter and returns the result' do @@ -915,17 +917,17 @@ describe API::Internal do .and_return(reference_counter) expect(reference_counter).to receive(:decrease).and_return(true) - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(json_response['reference_counter_decreased']).to be(true) end it 'returns link to create new merge request' do - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(json_response['merge_request_urls']).to match [{ - "branch_name" => "new_branch", - "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch", + "branch_name" => branch_name, + "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{branch_name}", "new_merge_request" => true }] end @@ -933,16 +935,87 @@ describe API::Internal do it 'returns empty array if printing_merge_request_link_enabled is false' do project.update!(printing_merge_request_link_enabled: false) - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(json_response['merge_request_urls']).to eq([]) end + it 'does not invoke MergeRequests::PushOptionsHandlerService' do + expect(MergeRequests::PushOptionsHandlerService).not_to receive(:new) + + post api('/internal/post_receive'), params: valid_params + end + + context 'when there are merge_request push options' do + before do + valid_params[:push_options] = ['merge_request.create'] + end + + it 'invokes MergeRequests::PushOptionsHandlerService' do + expect(MergeRequests::PushOptionsHandlerService).to receive(:new) + + post api('/internal/post_receive'), params: valid_params + end + + it 'creates a new merge request' do + expect do + post api('/internal/post_receive'), params: valid_params + end.to change { MergeRequest.count }.by(1) + end + + it 'links to the newly created merge request' do + post api('/internal/post_receive'), params: valid_params + + expect(json_response['merge_request_urls']).to match [{ + 'branch_name' => branch_name, + 'url' => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/1", + 'new_merge_request' => false + }] + end + + it 'adds errors on the service instance to warnings' do + expect_any_instance_of( + MergeRequests::PushOptionsHandlerService + ).to receive(:errors).at_least(:once).and_return(['my error']) + + post api('/internal/post_receive'), params: valid_params + + expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my error') + end + + it 'adds ActiveRecord errors on invalid MergeRequest records to warnings' do + invalid_merge_request = MergeRequest.new + invalid_merge_request.errors.add(:base, 'my error') + + expect_any_instance_of( + MergeRequests::CreateService + ).to receive(:execute).and_return(invalid_merge_request) + + post api('/internal/post_receive'), params: valid_params + + expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my error') + end + + context 'when the feature is disabled' do + it 'does not invoke MergeRequests::PushOptionsHandlerService' do + Feature.disable(:mr_push_options) + + expect(MergeRequests::PushOptionsHandlerService).to receive(:new) + + expect do + post api('/internal/post_receive'), params: valid_params + end.not_to change { MergeRequest.count } + + Feature.enable(:mr_push_options) + end + end + end + context 'broadcast message exists' do let!(:broadcast_message) { create(:broadcast_message, starts_at: 1.day.ago, ends_at: 1.day.from_now ) } it 'returns one broadcast message' do - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(response).to have_gitlab_http_status(200) expect(json_response['broadcast_message']).to eq(broadcast_message.message) @@ -951,7 +1024,7 @@ describe API::Internal do context 'broadcast message does not exist' do it 'returns empty string' do - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(response).to have_gitlab_http_status(200) expect(json_response['broadcast_message']).to eq(nil) @@ -962,7 +1035,7 @@ describe API::Internal do it 'returns empty string' do allow(BroadcastMessage).to receive(:current).and_return(nil) - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(response).to have_gitlab_http_status(200) expect(json_response['broadcast_message']).to eq(nil) @@ -974,7 +1047,7 @@ describe API::Internal do project_moved = Gitlab::Checks::ProjectMoved.new(project, user, 'http', 'foo/baz') project_moved.add_message - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(response).to have_gitlab_http_status(200) expect(json_response["redirected_message"]).to be_present @@ -987,7 +1060,7 @@ describe API::Internal do project_created = Gitlab::Checks::ProjectCreated.new(project, user, 'http') project_created.add_message - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(response).to have_gitlab_http_status(200) expect(json_response["project_created_message"]).to be_present @@ -999,7 +1072,7 @@ describe API::Internal do it 'does not try to notify that project moved' do allow_any_instance_of(Gitlab::Identifier).to receive(:identify).and_return(nil) - post api("/internal/post_receive"), params: valid_params + post api('/internal/post_receive'), params: valid_params expect(response).to have_gitlab_http_status(200) end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 7ffa365c651..45818edbf68 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -5,14 +5,15 @@ describe API::MergeRequests do let(:base_time) { Time.now } set(:user) { create(:user) } + set(:user2) { create(:user) } set(:admin) { create(:user, :admin) } let(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) } let(:milestone) { create(:milestone, title: '1.0.0', project: project) } let(:milestone1) { create(:milestone, title: '0.9', project: project) } - let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) } - let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) } - let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') } - let!(:merge_request_locked) { create(:merge_request, state: "locked", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Locked test", created_at: base_time + 1.second) } + let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Test", created_at: base_time) } + let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) } + let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignees: [user], source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') } + let!(:merge_request_locked) { create(:merge_request, state: "locked", milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Locked test", created_at: base_time + 1.second) } let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } let(:label) { create(:label, title: 'label', color: '#FFAABB', project: project) } @@ -20,6 +21,9 @@ describe API::MergeRequests do before do project.add_reporter(user) + project.add_reporter(user2) + + stub_licensed_features(multiple_merge_request_assignees: false) end shared_context 'with labels' do @@ -45,9 +49,9 @@ describe API::MergeRequests do get api(endpoint_path, user) end - create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time) + create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: 'Test', created_at: base_time) - merge_request = create(:merge_request, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time) + merge_request = create(:merge_request, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: 'Test', created_at: base_time) merge_request.metrics.update!(merged_by: user, latest_closed_by: user, @@ -333,7 +337,7 @@ describe API::MergeRequests do state: 'closed', milestone: milestone1, author: user, - assignee: user, + assignees: [user], source_project: project, target_project: project, title: "Test", @@ -451,7 +455,7 @@ describe API::MergeRequests do context 'when authenticated' do let!(:project2) { create(:project, :public, namespace: user.namespace) } - let!(:merge_request2) { create(:merge_request, :simple, author: user, assignee: user, source_project: project2, target_project: project2) } + let!(:merge_request2) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project2, target_project: project2) } let(:user2) { create(:user) } it 'returns an array of all merge requests except unauthorized ones' do @@ -494,7 +498,7 @@ describe API::MergeRequests do end it 'returns an array of merge requests created by current user if no scope is given' do - merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch') + merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch') get api('/merge_requests', user2) @@ -502,7 +506,7 @@ describe API::MergeRequests do end it 'returns an array of merge requests authored by the given user' do - merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch') + merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch') get api('/merge_requests', user), params: { author_id: user2.id, scope: :all } @@ -510,7 +514,7 @@ describe API::MergeRequests do end it 'returns an array of merge requests assigned to the given user' do - merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch') + merge_request3 = create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, source_branch: 'other-branch') get api('/merge_requests', user), params: { assignee_id: user2.id, scope: :all } @@ -535,7 +539,7 @@ describe API::MergeRequests do end it 'returns an array of merge requests assigned to me' do - merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch') + merge_request3 = create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, source_branch: 'other-branch') get api('/merge_requests', user2), params: { scope: 'assigned_to_me' } @@ -543,7 +547,7 @@ describe API::MergeRequests do end it 'returns an array of merge requests assigned to me (kebab-case)' do - merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch') + merge_request3 = create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, source_branch: 'other-branch') get api('/merge_requests', user2), params: { scope: 'assigned-to-me' } @@ -551,7 +555,7 @@ describe API::MergeRequests do end it 'returns an array of merge requests created by me' do - merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch') + merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch') get api('/merge_requests', user2), params: { scope: 'created_by_me' } @@ -559,7 +563,7 @@ describe API::MergeRequests do end it 'returns an array of merge requests created by me (kebab-case)' do - merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch') + merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch') get api('/merge_requests', user2), params: { scope: 'created-by-me' } @@ -567,7 +571,7 @@ describe API::MergeRequests do end it 'returns merge requests reacted by the authenticated user by the given emoji' do - merge_request3 = create(:merge_request, :simple, author: user, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch') + merge_request3 = create(:merge_request, :simple, author: user, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch') award_emoji = create(:award_emoji, awardable: merge_request3, user: user2, name: 'star') get api('/merge_requests', user2), params: { my_reaction_emoji: award_emoji.name, scope: 'all' } @@ -700,7 +704,7 @@ describe API::MergeRequests do get api("/projects/#{project.id}/merge_requests", user) end.count - create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, created_at: base_time) + create(:merge_request, author: user, assignees: [user], source_project: project, target_project: project, created_at: base_time) expect do get api("/projects/#{project.id}/merge_requests", user) @@ -730,7 +734,7 @@ describe API::MergeRequests do describe "GET /projects/:id/merge_requests/:merge_request_iid" do it 'matches json schema' do - merge_request = create(:merge_request, :with_test_reports, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) + merge_request = create(:merge_request, :with_test_reports, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Test", created_at: base_time) get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) expect(response).to have_gitlab_http_status(200) @@ -851,7 +855,7 @@ describe API::MergeRequests do end context 'Work in Progress' do - let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) } + let!(:merge_request_wip) { create(:merge_request, author: user, assignees: [user], source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) } it "returns merge request" do get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.iid}", user) @@ -867,7 +871,7 @@ describe API::MergeRequests do merge_request_overflow = create(:merge_request, :simple, author: user, - assignee: user, + assignees: [user], source_project: project, source_branch: 'expand-collapse-files', target_project: project, @@ -1005,6 +1009,71 @@ describe API::MergeRequests do end describe 'POST /projects/:id/merge_requests' do + context 'support for deprecated assignee_id' do + let(:params) do + { + title: 'Test merge request', + source_branch: 'feature_conflict', + target_branch: 'master', + author_id: user.id, + assignee_id: user2.id + } + end + + it 'creates a new merge request' do + post api("/projects/#{project.id}/merge_requests", user), params: params + + expect(response).to have_gitlab_http_status(201) + expect(json_response['title']).to eq('Test merge request') + expect(json_response['assignee']['name']).to eq(user2.name) + expect(json_response['assignees'].first['name']).to eq(user2.name) + end + + it 'creates a new merge request when assignee_id is empty' do + params[:assignee_id] = '' + + post api("/projects/#{project.id}/merge_requests", user), params: params + + expect(response).to have_gitlab_http_status(201) + expect(json_response['title']).to eq('Test merge request') + expect(json_response['assignee']).to be_nil + end + + it 'filters assignee_id of unauthorized user' do + private_project = create(:project, :private, :repository) + another_user = create(:user) + private_project.add_maintainer(user) + params[:assignee_id] = another_user.id + + post api("/projects/#{private_project.id}/merge_requests", user), params: params + + expect(response).to have_gitlab_http_status(201) + expect(json_response['assignee']).to be_nil + end + end + + context 'single assignee restrictions' do + let(:params) do + { + title: 'Test merge request', + source_branch: 'feature_conflict', + target_branch: 'master', + author_id: user.id, + assignee_ids: [user.id, user2.id] + } + end + + it 'creates a new project merge request with no more than one assignee' do + post api("/projects/#{project.id}/merge_requests", user), params: params + + expect(response).to have_gitlab_http_status(201) + expect(json_response['title']).to eq('Test merge request') + expect(json_response['assignees'].count).to eq(1) + expect(json_response['assignees'].first['name']).to eq(user.name) + expect(json_response.dig('assignee', 'name')).to eq(user.name) + end + end + context 'between branches projects' do context 'different labels' do let(:params) do @@ -1574,6 +1643,19 @@ describe API::MergeRequests do expect(json_response['force_remove_source_branch']).to be_truthy end + it 'filters assignee_id of unauthorized user' do + private_project = create(:project, :private, :repository) + mr = create(:merge_request, source_project: private_project, target_project: private_project) + another_user = create(:user) + private_project.add_maintainer(user) + params = { assignee_id: another_user.id } + + put api("/projects/#{private_project.id}/merge_requests/#{mr.iid}", user), params: params + + expect(response).to have_gitlab_http_status(200) + expect(json_response['assignee']).to be_nil + end + context 'when updating labels' do it 'allows special label names' do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), @@ -1728,7 +1810,7 @@ describe API::MergeRequests do issue = create(:issue, project: jira_project) description = "Closes #{ext_issue.to_reference(jira_project)}\ncloses #{issue.to_reference}" merge_request = create(:merge_request, - :simple, author: user, assignee: user, source_project: jira_project, description: description) + :simple, author: user, assignees: [user], source_project: jira_project, description: description) get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.iid}/closes_issues", user) diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index 9fed07cae82..0d46463312b 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -445,6 +445,72 @@ describe API::Pipelines do end end + describe 'GET /projects/:id/pipelines/:pipeline_id/variables' do + subject { get api("/projects/#{project.id}/pipelines/#{pipeline.id}/variables", api_user) } + + let(:api_user) { user } + + context 'user is a mantainer' do + it 'returns pipeline variables empty' do + subject + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to be_empty + end + + context 'with variables' do + let!(:variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'foo', value: 'bar') } + + it 'returns pipeline variables' do + subject + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to contain_exactly({ "key" => "foo", "value" => "bar" }) + end + end + end + + context 'user is a developer' do + let(:pipeline_owner_user) { create(:user) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, user: pipeline_owner_user) } + + before do + project.add_developer(api_user) + end + + context 'pipeline created by the developer user' do + let(:api_user) { pipeline_owner_user } + let!(:variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'foo', value: 'bar') } + + it 'returns pipeline variables' do + subject + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to contain_exactly({ "key" => "foo", "value" => "bar" }) + end + end + + context 'pipeline created is not created by the developer user' do + let(:api_user) { create(:user) } + + it 'should not return pipeline variables' do + subject + + expect(response).to have_gitlab_http_status(403) + end + end + end + + context 'user is not a project member' do + it 'should not return pipeline variables' do + get api("/projects/#{project.id}/pipelines/#{pipeline.id}/variables", non_member) + + expect(response).to have_gitlab_http_status(404) + expect(json_response['message']).to eq '404 Project Not Found' + end + end + end + describe 'DELETE /projects/:id/pipelines/:pipeline_id' do context 'authorized user' do let(:owner) { project.owner } diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 2bfb17d9c9a..352ea448c00 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -46,6 +46,8 @@ shared_examples 'languages and percentages JSON response' do end describe API::Projects do + include ExternalAuthorizationServiceHelpers + let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } @@ -1336,6 +1338,39 @@ describe API::Projects do end end end + + context 'with external authorization' do + let(:project) do + create(:project, + namespace: user.namespace, + external_authorization_classification_label: 'the-label') + end + + context 'when the user has access to the project' do + before do + external_service_allow_access(user, project) + end + + it 'includes the label in the response' do + get api("/projects/#{project.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['external_authorization_classification_label']).to eq('the-label') + end + end + + context 'when the external service denies access' do + before do + external_service_deny_access(user, project) + end + + it 'returns a 404' do + get api("/projects/#{project.id}", user) + + expect(response).to have_gitlab_http_status(404) + end + end + end end describe 'GET /projects/:id/users' do @@ -1890,6 +1925,20 @@ describe API::Projects do expect(response).to have_gitlab_http_status(403) end end + + context 'when updating external classification' do + before do + enable_external_authorization_service_check + end + + it 'updates the classification label' do + put(api("/projects/#{project.id}", user), params: { external_authorization_classification_label: 'new label' }) + + expect(response).to have_gitlab_http_status(200) + + expect(project.reload.external_authorization_classification_label).to eq('new label') + end + end end describe 'POST /projects/:id/archive' do diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 5fdc7c64030..3585a827838 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -1734,7 +1734,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do end it 'download artifacts' do - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response.headers.to_h).to include download_headers end end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index f869325e892..527ab1cfb66 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -116,6 +116,39 @@ describe API::Settings, 'Settings' do expect(json_response['performance_bar_allowed_group_id']).to be_nil end + context 'external policy classification settings' do + let(:settings) do + { + external_authorization_service_enabled: true, + external_authorization_service_url: 'https://custom.service/', + external_authorization_service_default_label: 'default', + external_authorization_service_timeout: 9.99, + external_auth_client_cert: File.read('spec/fixtures/passphrase_x509_certificate.crt'), + external_auth_client_key: File.read('spec/fixtures/passphrase_x509_certificate_pk.key'), + external_auth_client_key_pass: "5iveL!fe" + } + end + let(:attribute_names) { settings.keys.map(&:to_s) } + + it 'includes the attributes in the API' do + get api("/application/settings", admin) + + expect(response).to have_gitlab_http_status(200) + attribute_names.each do |attribute| + expect(json_response.keys).to include(attribute) + end + end + + it 'allows updating the settings' do + put api("/application/settings", admin), params: settings + + expect(response).to have_gitlab_http_status(200) + settings.each do |attribute, value| + expect(ApplicationSetting.current.public_send(attribute)).to eq(value) + end + end + end + context "missing plantuml_url value when plantuml_enabled is true" do it "returns a blank parameter error message" do put api("/application/settings", admin), params: { plantuml_enabled: true } diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb index d02b4c554b1..b58d95ccb43 100644 --- a/spec/serializers/group_child_entity_spec.rb +++ b/spec/serializers/group_child_entity_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe GroupChildEntity do + include ExternalAuthorizationServiceHelpers include Gitlab::Routing.url_helpers let(:user) { create(:user) } @@ -109,4 +110,22 @@ describe GroupChildEntity do it_behaves_like 'group child json' end + + describe 'for a project with external authorization enabled' do + let(:object) do + create(:project, :with_avatar, + description: 'Awesomeness') + end + + before do + enable_external_authorization_service_check + object.add_maintainer(user) + end + + it 'does not hit the external authorization service' do + expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?) + + expect(json[:can_edit]).to eq(false) + end + end end diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index 0fdd675aa01..d9023036534 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -157,7 +157,8 @@ describe PipelineSerializer do it 'verifies number of queries', :request_store do recorded = ActiveRecord::QueryRecorder.new { subject } - expect(recorded.count).to be_within(2).of(31) + expected_queries = Gitlab.ee? ? 38 : 31 + expect(recorded.count).to be_within(2).of(expected_queries) expect(recorded.cached_count).to eq(0) end end @@ -176,7 +177,8 @@ describe PipelineSerializer do # pipeline. With the same ref this check is cached but if refs are # different then there is an extra query per ref # https://gitlab.com/gitlab-org/gitlab-ce/issues/46368 - expect(recorded.count).to be_within(2).of(38) + expected_queries = Gitlab.ee? ? 44 : 38 + expect(recorded.count).to be_within(2).of(expected_queries) expect(recorded.cached_count).to eq(0) end end diff --git a/spec/services/after_branch_delete_service_spec.rb b/spec/services/after_branch_delete_service_spec.rb deleted file mode 100644 index bc9747d1413..00000000000 --- a/spec/services/after_branch_delete_service_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'spec_helper' - -describe AfterBranchDeleteService do - let(:project) { create(:project, :repository) } - let(:user) { create(:user) } - let(:service) { described_class.new(project, user) } - - describe '#execute' do - it 'stops environments attached to branch' do - expect(service).to receive(:stop_environments) - - service.execute('feature') - end - end -end diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb index a4a733eff77..258e5635113 100644 --- a/spec/services/application_settings/update_service_spec.rb +++ b/spec/services/application_settings/update_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe ApplicationSettings::UpdateService do + include ExternalAuthorizationServiceHelpers + let(:application_settings) { create(:application_setting) } let(:admin) { create(:user, :admin) } let(:params) { {} } @@ -143,4 +145,37 @@ describe ApplicationSettings::UpdateService do end end end + + context 'when external authorization is enabled' do + before do + enable_external_authorization_service_check + end + + it 'does not save the settings with an error if the service denies access' do + expect(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(admin, 'new-label') { false } + + described_class.new(application_settings, admin, { external_authorization_service_default_label: 'new-label' }).execute + + expect(application_settings.errors[:external_authorization_service_default_label]).to be_present + end + + it 'saves the setting when the user has access to the label' do + expect(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(admin, 'new-label') { true } + + described_class.new(application_settings, admin, { external_authorization_service_default_label: 'new-label' }).execute + + # Read the attribute directly to avoid the stub from + # `enable_external_authorization_service_check` + expect(application_settings[:external_authorization_service_default_label]).to eq('new-label') + end + + it 'does not validate the label if it was not passed' do + expect(::Gitlab::ExternalAuthorization) + .not_to receive(:access_allowed?) + + described_class.new(application_settings, admin, { home_page_url: 'http://foo.bar' }).execute + end + end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 866d709d446..101b91e9cd8 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -418,8 +418,7 @@ describe Ci::CreatePipelineService do context 'when push options contain ci.skip' do let(:push_options) do - ['ci.skip', - 'another push option'] + { 'ci' => { 'skip' => true } } end it 'creates a pipline in the skipped state' do diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index 87185891470..17e2b17a499 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -35,7 +35,7 @@ describe Ci::RetryBuildService do commit_id deployment erased_by_id project_id runner_id tag_taggings taggings tags trigger_request_id user_id auto_canceled_by_id retried failure_reason - artifacts_file_store artifacts_metadata_store + sourced_pipelines artifacts_file_store artifacts_metadata_store metadata runner_session trace_chunks].freeze shared_examples 'build duplication' do @@ -95,7 +95,8 @@ describe Ci::RetryBuildService do end it 'has correct number of known attributes' do - known_accessors = CLONE_ACCESSORS + REJECT_ACCESSORS + IGNORE_ACCESSORS + processed_accessors = CLONE_ACCESSORS + REJECT_ACCESSORS + known_accessors = processed_accessors + IGNORE_ACCESSORS # :tag_list is a special case, this accessor does not exist # in reflected associations, comes from `act_as_taggable` and @@ -108,7 +109,8 @@ describe Ci::RetryBuildService do current_accessors.uniq! - expect(known_accessors).to contain_exactly(*current_accessors) + expect(current_accessors).to include(*processed_accessors) + expect(known_accessors).to include(*current_accessors) end end diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb new file mode 100644 index 00000000000..bb87267db7d --- /dev/null +++ b/spec/services/git/branch_hooks_service_spec.rb @@ -0,0 +1,339 @@ +require 'spec_helper' + +describe Git::BranchHooksService do + include RepoHelpers + + let(:project) { create(:project, :repository) } + let(:user) { project.creator } + + let(:branch) { project.default_branch } + let(:ref) { "refs/heads/#{branch}" } + let(:commit) { project.commit(sample_commit.id) } + let(:oldrev) { commit.parent_id } + let(:newrev) { commit.id } + + let(:service) do + described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) + end + + describe "Git Push Data" do + subject(:push_data) { service.execute } + + it 'has expected push data attributes' do + is_expected.to match a_hash_including( + object_kind: 'push', + before: oldrev, + after: newrev, + ref: ref, + user_id: user.id, + user_name: user.name, + project_id: project.id + ) + end + + context "with repository data" do + subject { push_data[:repository] } + + it 'has expected attributes' do + is_expected.to match a_hash_including( + name: project.name, + url: project.url_to_repo, + description: project.description, + homepage: project.web_url + ) + end + end + + context "with commits" do + subject { push_data[:commits] } + + it { is_expected.to be_an(Array) } + + it 'has 1 element' do + expect(subject.size).to eq(1) + end + + context "the commit" do + subject { push_data[:commits].first } + + it { expect(subject[:timestamp].in_time_zone).to eq(commit.date.in_time_zone) } + + it 'includes expected commit data' do + is_expected.to match a_hash_including( + id: commit.id, + message: commit.safe_message, + url: [ + Gitlab.config.gitlab.url, + project.namespace.to_param, + project.to_param, + 'commit', + commit.id + ].join('/') + ) + end + + context "with a author" do + subject { push_data[:commits].first[:author] } + + it 'includes expected author data' do + is_expected.to match a_hash_including( + name: commit.author_name, + email: commit.author_email + ) + end + end + end + end + end + + describe 'Push Event' do + let(:event) { Event.find_by_action(Event::PUSHED) } + + before do + service.execute + end + + context "with an existing branch" do + it 'generates a push event with one commit' do + expect(event).to be_an_instance_of(PushEvent) + expect(event.project).to eq(project) + expect(event.action).to eq(Event::PUSHED) + expect(event.push_event_payload).to be_an_instance_of(PushEventPayload) + expect(event.push_event_payload.commit_from).to eq(oldrev) + expect(event.push_event_payload.commit_to).to eq(newrev) + expect(event.push_event_payload.ref).to eq('master') + expect(event.push_event_payload.commit_count).to eq(1) + end + end + + context "with a new branch" do + let(:oldrev) { Gitlab::Git::BLANK_SHA } + + it 'generates a push event with more than one commit' do + expect(event).to be_an_instance_of(PushEvent) + expect(event.project).to eq(project) + expect(event.action).to eq(Event::PUSHED) + expect(event.push_event_payload).to be_an_instance_of(PushEventPayload) + expect(event.push_event_payload.commit_from).to be_nil + expect(event.push_event_payload.commit_to).to eq(newrev) + expect(event.push_event_payload.ref).to eq('master') + expect(event.push_event_payload.commit_count).to be > 1 + end + end + + context 'removing a branch' do + let(:newrev) { Gitlab::Git::BLANK_SHA } + + it 'generates a push event with no commits' do + expect(event).to be_an_instance_of(PushEvent) + expect(event.project).to eq(project) + expect(event.action).to eq(Event::PUSHED) + expect(event.push_event_payload).to be_an_instance_of(PushEventPayload) + expect(event.push_event_payload.commit_from).to eq(oldrev) + expect(event.push_event_payload.commit_to).to be_nil + expect(event.push_event_payload.ref).to eq('master') + expect(event.push_event_payload.commit_count).to eq(0) + end + end + end + + describe 'Invalidating project cache' do + let(:commit_id) do + project.repository.update_file( + user, 'README.md', '', message: 'Update', branch_name: branch + ) + end + + let(:commit) { project.repository.commit(commit_id) } + let(:blank_sha) { Gitlab::Git::BLANK_SHA } + + def clears_cache(extended: []) + expect(ProjectCacheWorker) + .to receive(:perform_async) + .with(project.id, extended, %i[commit_count repository_size]) + + service.execute + end + + def clears_extended_cache + clears_cache(extended: %i[readme]) + end + + context 'on default branch' do + context 'create' do + # FIXME: When creating the default branch,the cache worker runs twice + before do + allow(ProjectCacheWorker).to receive(:perform_async) + end + + let(:oldrev) { blank_sha } + + it { clears_cache } + end + + context 'update' do + it { clears_extended_cache } + end + + context 'remove' do + let(:newrev) { blank_sha } + + # TODO: this case should pass, but we only take account of added files + it { clears_cache } + end + end + + context 'on ordinary branch' do + let(:branch) { 'fix' } + + context 'create' do + let(:oldrev) { blank_sha } + + it { clears_cache } + end + + context 'update' do + it { clears_cache } + end + + context 'remove' do + let(:newrev) { blank_sha } + + it { clears_cache } + end + end + end + + describe 'GPG signatures' do + context 'when the commit has a signature' do + context 'when the signature is already cached' do + before do + create(:gpg_signature, commit_sha: commit.id) + end + + it 'does not queue a CreateGpgSignatureWorker' do + expect(CreateGpgSignatureWorker).not_to receive(:perform_async) + + service.execute + end + end + + context 'when the signature is not yet cached' do + it 'queues a CreateGpgSignatureWorker' do + expect(CreateGpgSignatureWorker).to receive(:perform_async).with([commit.id], project.id) + + service.execute + end + + it 'can queue several commits to create the gpg signature' do + allow(Gitlab::Git::Commit) + .to receive(:shas_with_signatures) + .and_return([sample_commit.id, another_sample_commit.id]) + + expect(CreateGpgSignatureWorker) + .to receive(:perform_async) + .with([sample_commit.id, another_sample_commit.id], project.id) + + service.execute + end + end + end + + context 'when the commit does not have a signature' do + before do + allow(Gitlab::Git::Commit) + .to receive(:shas_with_signatures) + .with(project.repository, [sample_commit.id]) + .and_return([]) + end + + it 'does not queue a CreateGpgSignatureWorker' do + expect(CreateGpgSignatureWorker) + .not_to receive(:perform_async) + .with(sample_commit.id, project.id) + + service.execute + end + end + end + + describe 'Processing commit messages' do + # Create 4 commits, 2 of which have references. Limiting to 2 commits, we + # expect to see one commit message processor enqueued. + let(:commit_ids) do + Array.new(4) do |i| + message = "Issue #{'#' if i.even?}#{i}" + project.repository.update_file( + user, 'README.md', '', message: message, branch_name: branch + ) + end + end + + let(:oldrev) { commit_ids.first } + let(:newrev) { commit_ids.last } + + before do + stub_const("::Git::BaseHooksService::PROCESS_COMMIT_LIMIT", 2) + end + + context 'creating the default branch' do + let(:oldrev) { Gitlab::Git::BLANK_SHA } + + it 'does not process commit messages' do + expect(ProcessCommitWorker).not_to receive(:perform_async) + + service.execute + end + end + + context 'updating the default branch' do + it 'processes a limited number of commit messages' do + expect(ProcessCommitWorker).to receive(:perform_async).once + + service.execute + end + end + + context 'removing the default branch' do + let(:newrev) { Gitlab::Git::BLANK_SHA } + + it 'does not process commit messages' do + expect(ProcessCommitWorker).not_to receive(:perform_async) + + service.execute + end + end + + context 'creating a normal branch' do + let(:branch) { 'fix' } + let(:oldrev) { Gitlab::Git::BLANK_SHA } + + it 'processes a limited number of commit messages' do + expect(ProcessCommitWorker).to receive(:perform_async).once + + service.execute + end + end + + context 'updating a normal branch' do + let(:branch) { 'fix' } + + it 'processes a limited number of commit messages' do + expect(ProcessCommitWorker).to receive(:perform_async).once + + service.execute + end + end + + context 'removing a normal branch' do + let(:branch) { 'fix' } + let(:newrev) { Gitlab::Git::BLANK_SHA } + + it 'does not process commit messages' do + expect(ProcessCommitWorker).not_to receive(:perform_async) + + service.execute + end + end + end +end diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb index d0e2169b4a6..322e40a8112 100644 --- a/spec/services/git/branch_push_service_spec.rb +++ b/spec/services/git/branch_push_service_spec.rb @@ -8,7 +8,8 @@ describe Git::BranchPushService, services: true do let(:blankrev) { Gitlab::Git::BLANK_SHA } let(:oldrev) { sample_commit.parent_id } let(:newrev) { sample_commit.id } - let(:ref) { 'refs/heads/master' } + let(:branch) { 'master' } + let(:ref) { "refs/heads/#{branch}" } before do project.add_maintainer(user) @@ -132,64 +133,6 @@ describe Git::BranchPushService, services: true do end end - describe "Git Push Data" do - let(:commit) { project.commit(newrev) } - - subject { push_data_from_service(project, user, oldrev, newrev, ref) } - - it { is_expected.to include(object_kind: 'push') } - it { is_expected.to include(before: oldrev) } - it { is_expected.to include(after: newrev) } - it { is_expected.to include(ref: ref) } - it { is_expected.to include(user_id: user.id) } - it { is_expected.to include(user_name: user.name) } - it { is_expected.to include(project_id: project.id) } - - context "with repository data" do - subject { push_data_from_service(project, user, oldrev, newrev, ref)[:repository] } - - it { is_expected.to include(name: project.name) } - it { is_expected.to include(url: project.url_to_repo) } - it { is_expected.to include(description: project.description) } - it { is_expected.to include(homepage: project.web_url) } - end - - context "with commits" do - subject { push_data_from_service(project, user, oldrev, newrev, ref)[:commits] } - - it { is_expected.to be_an(Array) } - it 'has 1 element' do - expect(subject.size).to eq(1) - end - - context "the commit" do - subject { push_data_from_service(project, user, oldrev, newrev, ref)[:commits].first } - - it { is_expected.to include(id: commit.id) } - it { is_expected.to include(message: commit.safe_message) } - it { expect(subject[:timestamp].in_time_zone).to eq(commit.date.in_time_zone) } - it do - is_expected.to include( - url: [ - Gitlab.config.gitlab.url, - project.namespace.to_param, - project.to_param, - 'commit', - commit.id - ].join('/') - ) - end - - context "with a author" do - subject { push_data_from_service(project, user, oldrev, newrev, ref)[:commits].first[:author] } - - it { is_expected.to include(name: commit.author_name) } - it { is_expected.to include(email: commit.author_email) } - end - end - end - end - describe "Pipelines" do subject { execute_service(project, user, oldrev, newrev, ref) } @@ -203,59 +146,13 @@ describe Git::BranchPushService, services: true do end end - describe "Push Event" do - context "with an existing branch" do - let!(:push_data) { push_data_from_service(project, user, oldrev, newrev, ref) } - let(:event) { Event.find_by_action(Event::PUSHED) } - - it 'generates a push event with one commit' do - expect(event).to be_an_instance_of(PushEvent) - expect(event.project).to eq(project) - expect(event.action).to eq(Event::PUSHED) - expect(event.push_event_payload).to be_an_instance_of(PushEventPayload) - expect(event.push_event_payload.commit_from).to eq(oldrev) - expect(event.push_event_payload.commit_to).to eq(newrev) - expect(event.push_event_payload.ref).to eq('master') - expect(event.push_event_payload.commit_count).to eq(1) - end - end - - context "with a new branch" do - let!(:new_branch_data) { push_data_from_service(project, user, Gitlab::Git::BLANK_SHA, newrev, ref) } - let(:event) { Event.find_by_action(Event::PUSHED) } - - it 'generates a push event with more than one commit' do - expect(event).to be_an_instance_of(PushEvent) - expect(event.project).to eq(project) - expect(event.action).to eq(Event::PUSHED) - expect(event.push_event_payload).to be_an_instance_of(PushEventPayload) - expect(event.push_event_payload.commit_from).to be_nil - expect(event.push_event_payload.commit_to).to eq(newrev) - expect(event.push_event_payload.ref).to eq('master') - expect(event.push_event_payload.commit_count).to be > 1 - end - end - - context "Updates merge requests" do - it "when pushing a new branch for the first time" do - expect(UpdateMergeRequestsWorker).to receive(:perform_async) - .with(project.id, user.id, blankrev, 'newrev', ref) - execute_service(project, user, blankrev, 'newrev', ref ) - end - end - - describe 'system hooks' do - let!(:push_data) { push_data_from_service(project, user, oldrev, newrev, ref) } - let!(:system_hooks_service) { SystemHooksService.new } + describe "Updates merge requests" do + it "when pushing a new branch for the first time" do + expect(UpdateMergeRequestsWorker) + .to receive(:perform_async) + .with(project.id, user.id, blankrev, 'newrev', ref) - it "sends a system hook after pushing a branch" do - allow(SystemHooksService).to receive(:new).and_return(system_hooks_service) - allow(system_hooks_service).to receive(:execute_hooks) - - execute_service(project, user, oldrev, newrev, ref) - - expect(system_hooks_service).to have_received(:execute_hooks).with(push_data, :push_hooks) - end + execute_service(project, user, blankrev, 'newrev', ref ) end end @@ -700,125 +597,64 @@ describe Git::BranchPushService, services: true do end end - describe '#update_caches' do - let(:service) do - described_class.new(project, - user, - oldrev: oldrev, - newrev: newrev, - ref: ref) - end - - context 'on the default branch' do - before do - allow(service).to receive(:default_branch?).and_return(true) - end - - it 'flushes the caches of any special files that have been changed' do - commit = double(:commit) - diff = double(:diff, new_path: 'README.md') - - expect(commit).to receive(:raw_deltas) - .and_return([diff]) - - service.push_commits = [commit] + describe "CI environments" do + context 'create branch' do + let(:oldrev) { blankrev } - expect(ProjectCacheWorker).to receive(:perform_async) - .with(project.id, %i(readme), %i(commit_count repository_size)) + it 'does nothing' do + expect(::Ci::StopEnvironmentsService).not_to receive(:new) - service.update_caches + execute_service(project, user, oldrev, newrev, ref) end end - context 'on a non-default branch' do - before do - allow(service).to receive(:default_branch?).and_return(false) - end - - it 'does not flush any conditional caches' do - expect(ProjectCacheWorker).to receive(:perform_async) - .with(project.id, [], %i(commit_count repository_size)) - .and_call_original + context 'update branch' do + it 'does nothing' do + expect(::Ci::StopEnvironmentsService).not_to receive(:new) - service.update_caches + execute_service(project, user, oldrev, newrev, ref) end end - end - - describe '#process_commit_messages' do - let(:service) do - described_class.new(project, - user, - oldrev: oldrev, - newrev: newrev, - ref: ref) - end - it 'only schedules a limited number of commits' do - service.push_commits = Array.new(1000, double(:commit, to_hash: {}, matches_cross_reference_regex?: true)) - - expect(ProcessCommitWorker).to receive(:perform_async).exactly(100).times - - service.process_commit_messages - end - - it "skips commits which don't include cross-references" do - service.push_commits = [double(:commit, to_hash: {}, matches_cross_reference_regex?: false)] - - expect(ProcessCommitWorker).not_to receive(:perform_async) - - service.process_commit_messages - end - end - - describe '#update_signatures' do - let(:service) do - described_class.new( - project, - user, - oldrev: oldrev, - newrev: newrev, - ref: 'refs/heads/master' - ) - end + context 'delete branch' do + let(:newrev) { blankrev } - context 'when the commit has a signature' do - context 'when the signature is already cached' do - before do - create(:gpg_signature, commit_sha: sample_commit.id) + it 'stops environments' do + expect_next_instance_of(::Ci::StopEnvironmentsService) do |stop_service| + expect(stop_service.project).to eq(project) + expect(stop_service.current_user).to eq(user) + expect(stop_service).to receive(:execute).with(branch) end - it 'does not queue a CreateGpgSignatureWorker' do - expect(CreateGpgSignatureWorker).not_to receive(:perform_async) - - execute_service(project, user, oldrev, newrev, ref) - end + execute_service(project, user, oldrev, newrev, ref) end + end + end - context 'when the signature is not yet cached' do - it 'queues a CreateGpgSignatureWorker' do - expect(CreateGpgSignatureWorker).to receive(:perform_async).with([sample_commit.id], project.id) + describe 'Hooks' do + context 'run on a branch' do + it 'delegates to Git::BranchHooksService' do + expect_next_instance_of(::Git::BranchHooksService) do |hooks_service| + expect(hooks_service.project).to eq(project) + expect(hooks_service.current_user).to eq(user) + expect(hooks_service.params).to include( + oldrev: oldrev, + newrev: newrev, + ref: ref + ) - execute_service(project, user, oldrev, newrev, ref) + expect(hooks_service).to receive(:execute) end - it 'can queue several commits to create the gpg signature' do - allow(Gitlab::Git::Commit).to receive(:shas_with_signatures).and_return([sample_commit.id, another_sample_commit.id]) - - expect(CreateGpgSignatureWorker).to receive(:perform_async).with([sample_commit.id, another_sample_commit.id], project.id) - - execute_service(project, user, oldrev, newrev, ref) - end + execute_service(project, user, oldrev, newrev, ref) end end - context 'when the commit does not have a signature' do - before do - allow(Gitlab::Git::Commit).to receive(:shas_with_signatures).with(project.repository, [sample_commit.id]).and_return([]) - end + context 'run on a tag' do + let(:ref) { 'refs/tags/v1.1.0' } - it 'does not queue a CreateGpgSignatureWorker' do - expect(CreateGpgSignatureWorker).not_to receive(:perform_async).with(sample_commit.id, project.id) + it 'does nothing' do + expect(::Git::BranchHooksService).not_to receive(:new) execute_service(project, user, oldrev, newrev, ref) end @@ -830,8 +666,4 @@ describe Git::BranchPushService, services: true do service.execute service end - - def push_data_from_service(project, user, oldrev, newrev, ref) - execute_service(project, user, oldrev, newrev, ref).push_data - end end diff --git a/spec/services/git/tag_hooks_service_spec.rb b/spec/services/git/tag_hooks_service_spec.rb new file mode 100644 index 00000000000..8f91ce3b4c5 --- /dev/null +++ b/spec/services/git/tag_hooks_service_spec.rb @@ -0,0 +1,144 @@ +require 'spec_helper' + +describe Git::TagHooksService, :service do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + + let(:oldrev) { Gitlab::Git::BLANK_SHA } + let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0 + let(:ref) { "refs/tags/#{tag_name}" } + let(:tag_name) { 'v1.1.0' } + + let(:tag) { project.repository.find_tag(tag_name) } + let(:commit) { tag.dereferenced_target } + + let(:service) do + described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) + end + + describe 'System hooks' do + it 'Executes system hooks' do + push_data = service.execute + + expect_next_instance_of(SystemHooksService) do |system_hooks_service| + expect(system_hooks_service) + .to receive(:execute_hooks) + .with(push_data, :tag_push_hooks) + end + + service.execute + end + end + + describe "Webhooks" do + it "executes hooks on the project" do + expect(project).to receive(:execute_hooks) + + service.execute + end + end + + describe "Pipelines" do + before do + stub_ci_pipeline_to_return_yaml_file + project.add_developer(user) + end + + it "creates a new pipeline" do + expect { service.execute }.to change { Ci::Pipeline.count } + + expect(Ci::Pipeline.last).to be_push + end + end + + describe 'Push data' do + shared_examples_for 'tag push data expectations' do + subject(:push_data) { service.execute } + it 'has expected push data attributes' do + is_expected.to match a_hash_including( + object_kind: 'tag_push', + ref: ref, + before: oldrev, + after: newrev, + message: tag.message, + user_id: user.id, + user_name: user.name, + project_id: project.id + ) + end + + context "with repository data" do + subject { push_data[:repository] } + + it 'has expected repository attributes' do + is_expected.to match a_hash_including( + name: project.name, + url: project.url_to_repo, + description: project.description, + homepage: project.web_url + ) + end + end + + context "with commits" do + subject { push_data[:commits] } + + it { is_expected.to be_an(Array) } + + it 'has 1 element' do + expect(subject.size).to eq(1) + end + + context "the commit" do + subject { push_data[:commits].first } + + it { is_expected.to include(timestamp: commit.date.xmlschema) } + + it 'has expected commit attributes' do + is_expected.to match a_hash_including( + id: commit.id, + message: commit.safe_message, + url: [ + Gitlab.config.gitlab.url, + project.namespace.to_param, + project.to_param, + 'commit', + commit.id + ].join('/') + ) + end + + context "with an author" do + subject { push_data[:commits].first[:author] } + + it 'has expected author attributes' do + is_expected.to match a_hash_including( + name: commit.author_name, + email: commit.author_email + ) + end + end + end + end + end + + context 'annotated tag' do + include_examples 'tag push data expectations' + end + + context 'lightweight tag' do + let(:tag_name) { 'light-tag' } + let(:newrev) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' } + + before do + # Create the lightweight tag + rugged_repo(project.repository).tags.create(tag_name, newrev) + + # Clear tag list cache + project.repository.expire_tags_cache + end + + include_examples 'tag push data expectations' + end + end +end diff --git a/spec/services/git/tag_push_service_spec.rb b/spec/services/git/tag_push_service_spec.rb index 2d960fc9f08..5e89a912060 100644 --- a/spec/services/git/tag_push_service_spec.rb +++ b/spec/services/git/tag_push_service_spec.rb @@ -31,178 +31,27 @@ describe Git::TagPushService do end end - describe 'System Hooks' do - let!(:push_data) { service.tap(&:execute).push_data } - - it "executes system hooks after pushing a tag" do - expect_next_instance_of(SystemHooksService) do |system_hooks_service| - expect(system_hooks_service) - .to receive(:execute_hooks) - .with(push_data, :tag_push_hooks) - end - - service.execute - end - end - - describe "Pipelines" do - subject { service.execute } - - before do - stub_ci_pipeline_to_return_yaml_file - project.add_developer(user) - end - - it "creates a new pipeline" do - expect { subject }.to change { Ci::Pipeline.count } - expect(Ci::Pipeline.last).to be_push - end - end - - describe "Git Tag Push Data" do - subject { @push_data } - let(:tag) { project.repository.find_tag(tag_name) } - let(:commit) { tag.dereferenced_target } - - context 'annotated tag' do - let(:tag_name) { Gitlab::Git.ref_name(ref) } - - before do - service.execute - @push_data = service.push_data - end - - it { is_expected.to include(object_kind: 'tag_push') } - it { is_expected.to include(ref: ref) } - it { is_expected.to include(before: oldrev) } - it { is_expected.to include(after: newrev) } - it { is_expected.to include(message: tag.message) } - it { is_expected.to include(user_id: user.id) } - it { is_expected.to include(user_name: user.name) } - it { is_expected.to include(project_id: project.id) } - - context "with repository data" do - subject { @push_data[:repository] } - - it { is_expected.to include(name: project.name) } - it { is_expected.to include(url: project.url_to_repo) } - it { is_expected.to include(description: project.description) } - it { is_expected.to include(homepage: project.web_url) } - end - - context "with commits" do - subject { @push_data[:commits] } - - it { is_expected.to be_an(Array) } - it 'has 1 element' do - expect(subject.size).to eq(1) - end - - context "the commit" do - subject { @push_data[:commits].first } - - it { is_expected.to include(id: commit.id) } - it { is_expected.to include(message: commit.safe_message) } - it { is_expected.to include(timestamp: commit.date.xmlschema) } - it do - is_expected.to include( - url: [ - Gitlab.config.gitlab.url, - project.namespace.to_param, - project.to_param, - 'commit', - commit.id - ].join('/') - ) - end - - context "with a author" do - subject { @push_data[:commits].first[:author] } - - it { is_expected.to include(name: commit.author_name) } - it { is_expected.to include(email: commit.author_email) } - end + describe 'Hooks' do + context 'run on a tag' do + it 'delegates to Git::TagHooksService' do + expect_next_instance_of(::Git::TagHooksService) do |hooks_service| + expect(hooks_service.project).to eq(service.project) + expect(hooks_service.current_user).to eq(service.current_user) + expect(hooks_service.params).to eq(service.params) + + expect(hooks_service).to receive(:execute) end - end - end - - context 'lightweight tag' do - let(:tag_name) { 'light-tag' } - let(:newrev) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' } - let(:ref) { "refs/tags/light-tag" } - - before do - # Create the lightweight tag - rugged_repo(project.repository).tags.create(tag_name, newrev) - - # Clear tag list cache - project.repository.expire_tags_cache service.execute - @push_data = service.push_data - end - - it { is_expected.to include(object_kind: 'tag_push') } - it { is_expected.to include(ref: ref) } - it { is_expected.to include(before: oldrev) } - it { is_expected.to include(after: newrev) } - it { is_expected.to include(message: tag.message) } - it { is_expected.to include(user_id: user.id) } - it { is_expected.to include(user_name: user.name) } - it { is_expected.to include(project_id: project.id) } - - context "with repository data" do - subject { @push_data[:repository] } - - it { is_expected.to include(name: project.name) } - it { is_expected.to include(url: project.url_to_repo) } - it { is_expected.to include(description: project.description) } - it { is_expected.to include(homepage: project.web_url) } - end - - context "with commits" do - subject { @push_data[:commits] } - - it { is_expected.to be_an(Array) } - it 'has 1 element' do - expect(subject.size).to eq(1) - end - - context "the commit" do - subject { @push_data[:commits].first } - - it { is_expected.to include(id: commit.id) } - it { is_expected.to include(message: commit.safe_message) } - it { is_expected.to include(timestamp: commit.date.xmlschema) } - it do - is_expected.to include( - url: [ - Gitlab.config.gitlab.url, - project.namespace.to_param, - project.to_param, - 'commit', - commit.id - ].join('/') - ) - end - - context "with a author" do - subject { @push_data[:commits].first[:author] } - - it { is_expected.to include(name: commit.author_name) } - it { is_expected.to include(email: commit.author_email) } - end - end end end - end - describe "Webhooks" do - context "execute webhooks" do - let(:service) { described_class.new(project, user, oldrev: 'oldrev', newrev: 'newrev', ref: 'refs/tags/v1.0.0') } + context 'run on a branch' do + let(:ref) { 'refs/heads/master' } + + it 'does nothing' do + expect(::Git::BranchHooksService).not_to receive(:new) - it "when pushing tags" do - expect(project).to receive(:execute_hooks) service.execute end end diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index ca366cdf1df..363b7266940 100644 --- a/spec/services/issuable/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -76,14 +76,14 @@ describe Issuable::BulkUpdateService do end describe 'updating merge request assignee' do - let(:merge_request) { create(:merge_request, target_project: project, source_project: project, assignee: user) } + let(:merge_request) { create(:merge_request, target_project: project, source_project: project, assignees: [user]) } context 'when the new assignee ID is a valid user' do it 'succeeds' do new_assignee = create(:user) project.add_developer(new_assignee) - result = bulk_update(merge_request, assignee_id: new_assignee.id) + result = bulk_update(merge_request, assignee_ids: [user.id, new_assignee.id]) expect(result[:success]).to be_truthy expect(result[:count]).to eq(1) @@ -93,22 +93,22 @@ describe Issuable::BulkUpdateService do assignee = create(:user) project.add_developer(assignee) - expect { bulk_update(merge_request, assignee_id: assignee.id) } - .to change { merge_request.reload.assignee }.from(user).to(assignee) + expect { bulk_update(merge_request, assignee_ids: [assignee.id]) } + .to change { merge_request.reload.assignee_ids }.from([user.id]).to([assignee.id]) end end context "when the new assignee ID is #{IssuableFinder::NONE}" do it 'unassigns the issues' do - expect { bulk_update(merge_request, assignee_id: IssuableFinder::NONE) } - .to change { merge_request.reload.assignee }.to(nil) + expect { bulk_update(merge_request, assignee_ids: [IssuableFinder::NONE]) } + .to change { merge_request.reload.assignee_ids }.to([]) end end context 'when the new assignee ID is not present' do it 'does not unassign' do - expect { bulk_update(merge_request, assignee_id: nil) } - .not_to change { merge_request.reload.assignee } + expect { bulk_update(merge_request, assignee_ids: []) } + .not_to change { merge_request.reload.assignee_ids } end end end diff --git a/spec/services/issuable/destroy_service_spec.rb b/spec/services/issuable/destroy_service_spec.rb index 8ccbba7fa58..15d1bb73ca3 100644 --- a/spec/services/issuable/destroy_service_spec.rb +++ b/spec/services/issuable/destroy_service_spec.rb @@ -34,7 +34,7 @@ describe Issuable::DestroyService do end context 'when issuable is a merge request' do - let!(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: user, assignee: user) } + let!(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: user, assignees: [user]) } it 'destroys the merge request' do expect { service.execute(merge_request) }.to change { project.merge_requests.count }.by(-1) diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb index d37ca13ebd2..91bf4dccd77 100644 --- a/spec/services/members/destroy_service_spec.rb +++ b/spec/services/members/destroy_service_spec.rb @@ -43,9 +43,9 @@ describe Members::DestroyService do shared_examples 'a service destroying a member with access' do it_behaves_like 'a service destroying a member' - it 'invalidates cached counts for todos and assigned issues and merge requests', :aggregate_failures do + it 'invalidates cached counts for assigned issues and merge requests', :aggregate_failures do create(:issue, project: group_project, assignees: [member_user]) - create(:merge_request, source_project: group_project, assignee: member_user) + create(:merge_request, source_project: group_project, assignees: [member_user]) create(:todo, :pending, project: group_project, user: member_user) create(:todo, :done, project: group_project, user: member_user) diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index 433ffbd97f0..706bcea8199 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -4,7 +4,7 @@ describe MergeRequests::CloseService do let(:user) { create(:user) } let(:user2) { create(:user) } let(:guest) { create(:user) } - let(:merge_request) { create(:merge_request, assignee: user2, author: create(:user)) } + let(:merge_request) { create(:merge_request, assignees: [user2], author: create(:user)) } let(:project) { merge_request.project } let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) } diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb index 393299cce00..20bf1cbb8b6 100644 --- a/spec/services/merge_requests/create_from_issue_service_spec.rb +++ b/spec/services/merge_requests/create_from_issue_service_spec.rb @@ -118,7 +118,7 @@ describe MergeRequests::CreateFromIssueService do result = service.execute - expect(result[:merge_request].assignee).to eq(user) + expect(result[:merge_request].assignees).to eq([user]) end context 'when ref branch is set' do diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index dc5d1cf2f04..30271e04c8e 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -32,7 +32,7 @@ describe MergeRequests::CreateService do expect(merge_request).to be_valid expect(merge_request.work_in_progress?).to be(false) expect(merge_request.title).to eq('Awesome merge_request') - expect(merge_request.assignee).to be_nil + expect(merge_request.assignees).to be_empty expect(merge_request.merge_params['force_remove_source_branch']).to eq('1') end @@ -73,7 +73,7 @@ describe MergeRequests::CreateService do description: "well this is not done yet\n/wip", source_branch: 'feature', target_branch: 'master', - assignee: assignee + assignees: [assignee] } end @@ -89,7 +89,7 @@ describe MergeRequests::CreateService do description: "well this is not done yet\n/wip", source_branch: 'feature', target_branch: 'master', - assignee: assignee + assignees: [assignee] } end @@ -106,11 +106,11 @@ describe MergeRequests::CreateService do description: 'please fix', source_branch: 'feature', target_branch: 'master', - assignee: assignee + assignees: [assignee] } end - it { expect(merge_request.assignee).to eq assignee } + it { expect(merge_request.assignees).to eq([assignee]) } it 'creates a todo for new assignee' do attributes = { @@ -301,7 +301,7 @@ describe MergeRequests::CreateService do let(:opts) do { - assignee_id: create(:user).id, + assignee_ids: create(:user).id, milestone_id: 1, title: 'Title', description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}"), @@ -317,7 +317,7 @@ describe MergeRequests::CreateService do it 'assigns and sets milestone to issuable from command' do expect(merge_request).to be_persisted - expect(merge_request.assignee).to eq(assignee) + expect(merge_request.assignees).to eq([assignee]) expect(merge_request.milestone).to eq(milestone) end end @@ -332,28 +332,28 @@ describe MergeRequests::CreateService do end it 'removes assignee_id when user id is invalid' do - opts = { title: 'Title', description: 'Description', assignee_id: -1 } + opts = { title: 'Title', description: 'Description', assignee_ids: [-1] } merge_request = described_class.new(project, user, opts).execute - expect(merge_request.assignee_id).to be_nil + expect(merge_request.assignee_ids).to be_empty end it 'removes assignee_id when user id is 0' do - opts = { title: 'Title', description: 'Description', assignee_id: 0 } + opts = { title: 'Title', description: 'Description', assignee_ids: [0] } merge_request = described_class.new(project, user, opts).execute - expect(merge_request.assignee_id).to be_nil + expect(merge_request.assignee_ids).to be_empty end it 'saves assignee when user id is valid' do project.add_maintainer(assignee) - opts = { title: 'Title', description: 'Description', assignee_id: assignee.id } + opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] } merge_request = described_class.new(project, user, opts).execute - expect(merge_request.assignee).to eq(assignee) + expect(merge_request.assignees).to eq([assignee]) end context 'when assignee is set' do @@ -361,7 +361,7 @@ describe MergeRequests::CreateService do { title: 'Title', description: 'Description', - assignee_id: assignee.id, + assignee_ids: [assignee.id], source_branch: 'feature', target_branch: 'master' } @@ -387,7 +387,7 @@ describe MergeRequests::CreateService do levels.each do |level| it "removes not authorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do project.update(visibility_level: level) - opts = { title: 'Title', description: 'Description', assignee_id: assignee.id } + opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] } merge_request = described_class.new(project, user, opts).execute diff --git a/spec/services/merge_requests/ff_merge_service_spec.rb b/spec/services/merge_requests/ff_merge_service_spec.rb index 1430e12a07e..a87d8b8752c 100644 --- a/spec/services/merge_requests/ff_merge_service_spec.rb +++ b/spec/services/merge_requests/ff_merge_service_spec.rb @@ -7,7 +7,7 @@ describe MergeRequests::FfMergeService do create(:merge_request, source_branch: 'flatten-dir', target_branch: 'improve/awesome', - assignee: user2, + assignees: [user2], author: create(:user)) end let(:project) { merge_request.project } diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 887ec17171e..b0b3273e3dc 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe MergeRequests::MergeService do set(:user) { create(:user) } set(:user2) { create(:user) } - let(:merge_request) { create(:merge_request, :simple, author: user2, assignee: user2) } + let(:merge_request) { create(:merge_request, :simple, author: user2, assignees: [user2]) } let(:project) { merge_request.project } before do @@ -111,7 +111,7 @@ describe MergeRequests::MergeService do end context 'closes related todos' do - let(:merge_request) { create(:merge_request, assignee: user, author: user) } + let(:merge_request) { create(:merge_request, assignees: [user], author: user) } let(:project) { merge_request.project } let(:service) { described_class.new(project, user, commit_message: 'Awesome message') } let!(:todo) do diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb index a3b48abae26..24d09c1fd00 100644 --- a/spec/services/merge_requests/merge_to_ref_service_spec.rb +++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb @@ -149,7 +149,7 @@ describe MergeRequests::MergeToRefService do end context 'does not close related todos' do - let(:merge_request) { create(:merge_request, assignee: user, author: user) } + let(:merge_request) { create(:merge_request, assignees: [user], author: user) } let(:project) { merge_request.project } let!(:todo) do create(:todo, :assigned, diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb index 5ad6f5528f9..2cebefee5d6 100644 --- a/spec/services/merge_requests/post_merge_service_spec.rb +++ b/spec/services/merge_requests/post_merge_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe MergeRequests::PostMergeService do let(:user) { create(:user) } - let(:merge_request) { create(:merge_request, assignee: user) } + let(:merge_request) { create(:merge_request, assignees: [user]) } let(:project) { merge_request.project } before do diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb new file mode 100644 index 00000000000..f7a39bb42d5 --- /dev/null +++ b/spec/services/merge_requests/push_options_handler_service_spec.rb @@ -0,0 +1,404 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MergeRequests::PushOptionsHandlerService do + include ProjectForksHelper + + let(:user) { create(:user) } + let(:project) { create(:project, :public, :repository) } + let(:forked_project) { fork_project(project, user, repository: true) } + let(:service) { described_class.new(project, user, changes, push_options) } + let(:source_branch) { 'fix' } + let(:target_branch) { 'feature' } + let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } + let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } + let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" } + let(:default_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{project.default_branch}" } + + before do + project.add_developer(user) + end + + shared_examples_for 'a service that can create a merge request' do + subject(:last_mr) { MergeRequest.last } + + it 'creates a merge request' do + expect { service.execute }.to change { MergeRequest.count }.by(1) + end + + it 'sets the correct target branch' do + branch = push_options[:target] || project.default_branch + + service.execute + + expect(last_mr.target_branch).to eq(branch) + end + + it 'assigns the MR to the user' do + service.execute + + expect(last_mr.assignees).to contain_exactly(user) + end + + context 'when project has been forked' do + let(:forked_project) { fork_project(project, user, repository: true) } + let(:service) { described_class.new(forked_project, user, changes, push_options) } + + before do + allow(forked_project).to receive(:empty_repo?).and_return(false) + end + + it 'sets the correct source project' do + service.execute + + expect(last_mr.source_project).to eq(forked_project) + end + + it 'sets the correct target project' do + service.execute + + expect(last_mr.target_project).to eq(project) + end + end + end + + shared_examples_for 'a service that can set the target of a merge request' do + subject(:last_mr) { MergeRequest.last } + + it 'sets the target_branch' do + service.execute + + expect(last_mr.target_branch).to eq(target_branch) + end + end + + shared_examples_for 'a service that can set the merge request to merge when pipeline succeeds' do + subject(:last_mr) { MergeRequest.last } + + it 'sets merge_when_pipeline_succeeds' do + service.execute + + expect(last_mr.merge_when_pipeline_succeeds).to eq(true) + end + + it 'sets merge_user to the user' do + service.execute + + expect(last_mr.merge_user).to eq(user) + end + end + + shared_examples_for 'a service that does not create a merge request' do + it do + expect { service.execute }.not_to change { MergeRequest.count } + end + end + + shared_examples_for 'a service that does not update a merge request' do + it do + expect { service.execute }.not_to change { MergeRequest.maximum(:updated_at) } + end + end + + shared_examples_for 'a service that does nothing' do + include_examples 'a service that does not create a merge request' + include_examples 'a service that does not update a merge request' + end + + describe '`create` push option' do + let(:push_options) { { create: true } } + + context 'with a new branch' do + let(:changes) { new_branch_changes } + + it_behaves_like 'a service that can create a merge request' + end + + context 'with an existing branch but no open MR' do + let(:changes) { existing_branch_changes } + + it_behaves_like 'a service that can create a merge request' + end + + context 'with an existing branch that has a merge request open' do + let(:changes) { existing_branch_changes } + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)} + + it_behaves_like 'a service that does not create a merge request' + end + + context 'with a deleted branch' do + let(:changes) { deleted_branch_changes } + + it_behaves_like 'a service that does nothing' + end + + context 'with the project default branch' do + let(:changes) { default_branch_changes } + + it_behaves_like 'a service that does nothing' + end + end + + describe '`merge_when_pipeline_succeeds` push option' do + let(:push_options) { { merge_when_pipeline_succeeds: true } } + + context 'with a new branch' do + let(:changes) { new_branch_changes } + + it_behaves_like 'a service that does not create a merge request' + + it 'adds an error to the service' do + error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}" + + service.execute + + expect(service.errors).to include(error) + end + + context 'when coupled with the `create` push option' do + let(:push_options) { { create: true, merge_when_pipeline_succeeds: true } } + + it_behaves_like 'a service that can create a merge request' + it_behaves_like 'a service that can set the merge request to merge when pipeline succeeds' + end + end + + context 'with an existing branch but no open MR' do + let(:changes) { existing_branch_changes } + + it_behaves_like 'a service that does not create a merge request' + + it 'adds an error to the service' do + error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}" + + service.execute + + expect(service.errors).to include(error) + end + + context 'when coupled with the `create` push option' do + let(:push_options) { { create: true, merge_when_pipeline_succeeds: true } } + + it_behaves_like 'a service that can create a merge request' + it_behaves_like 'a service that can set the merge request to merge when pipeline succeeds' + end + end + + context 'with an existing branch that has a merge request open' do + let(:changes) { existing_branch_changes } + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)} + + it_behaves_like 'a service that does not create a merge request' + it_behaves_like 'a service that can set the merge request to merge when pipeline succeeds' + end + + context 'with a deleted branch' do + let(:changes) { deleted_branch_changes } + + it_behaves_like 'a service that does nothing' + end + + context 'with the project default branch' do + let(:changes) { default_branch_changes } + + it_behaves_like 'a service that does nothing' + end + end + + describe '`target` push option' do + let(:push_options) { { target: target_branch } } + + context 'with a new branch' do + let(:changes) { new_branch_changes } + + it_behaves_like 'a service that does not create a merge request' + + it 'adds an error to the service' do + error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}" + + service.execute + + expect(service.errors).to include(error) + end + + context 'when coupled with the `create` push option' do + let(:push_options) { { create: true, target: target_branch } } + + it_behaves_like 'a service that can create a merge request' + it_behaves_like 'a service that can set the target of a merge request' + end + end + + context 'with an existing branch but no open MR' do + let(:changes) { existing_branch_changes } + + it_behaves_like 'a service that does not create a merge request' + + it 'adds an error to the service' do + error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}" + + service.execute + + expect(service.errors).to include(error) + end + + context 'when coupled with the `create` push option' do + let(:push_options) { { create: true, target: target_branch } } + + it_behaves_like 'a service that can create a merge request' + it_behaves_like 'a service that can set the target of a merge request' + end + end + + context 'with an existing branch that has a merge request open' do + let(:changes) { existing_branch_changes } + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)} + + it_behaves_like 'a service that does not create a merge request' + it_behaves_like 'a service that can set the target of a merge request' + end + + context 'with a deleted branch' do + let(:changes) { deleted_branch_changes } + + it_behaves_like 'a service that does nothing' + end + + context 'with the project default branch' do + let(:changes) { default_branch_changes } + + it_behaves_like 'a service that does nothing' + end + end + + describe 'multiple pushed branches' do + let(:push_options) { { create: true } } + let(:changes) do + [ + new_branch_changes, + "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/feature_conflict" + ] + end + + it 'creates a merge request per branch' do + expect { service.execute }.to change { MergeRequest.count }.by(2) + end + + context 'when there are too many pushed branches' do + let(:limit) { MergeRequests::PushOptionsHandlerService::LIMIT } + let(:changes) do + TestEnv::BRANCH_SHA.to_a[0..limit].map do |x| + "#{Gitlab::Git::BLANK_SHA} #{x.first} refs/heads/#{x.last}" + end + end + + it 'records an error' do + service.execute + + expect(service.errors).to eq(["Too many branches pushed (#{limit + 1} were pushed, limit is #{limit})"]) + end + end + end + + describe 'no push options' do + let(:push_options) { {} } + let(:changes) { new_branch_changes } + + it_behaves_like 'a service that does nothing' + end + + describe 'no user' do + let(:user) { nil } + let(:push_options) { { create: true } } + let(:changes) { new_branch_changes } + + it 'records an error' do + service.execute + + expect(service.errors).to eq(['User is required']) + end + end + + describe 'unauthorized user' do + let(:push_options) { { create: true } } + let(:changes) { new_branch_changes } + + it 'records an error' do + Members::DestroyService.new(user).execute(ProjectMember.find_by!(user_id: user.id)) + + service.execute + + expect(service.errors).to eq(['User access was denied']) + end + end + + describe 'handling unexpected exceptions' do + let(:push_options) { { create: true } } + let(:changes) { new_branch_changes } + let(:exception) { StandardError.new('My standard error') } + + def run_service_with_exception + allow_any_instance_of( + MergeRequests::BuildService + ).to receive(:execute).and_raise(exception) + + service.execute + end + + it 'records an error' do + run_service_with_exception + + expect(service.errors).to eq(['An unknown error occurred']) + end + + it 'writes to Gitlab::AppLogger' do + expect(Gitlab::AppLogger).to receive(:error).with(exception) + + run_service_with_exception + end + end + + describe 'when target is not a valid branch name' do + let(:push_options) { { create: true, target: 'my-branch' } } + let(:changes) { new_branch_changes } + + it 'records an error' do + service.execute + + expect(service.errors).to eq(['Branch my-branch does not exist']) + end + end + + describe 'when MRs are not enabled' do + let(:push_options) { { create: true } } + let(:changes) { new_branch_changes } + + it 'records an error' do + expect(project).to receive(:merge_requests_enabled?).and_return(false) + + service.execute + + expect(service.errors).to eq(["Merge requests are not enabled for project #{project.full_path}"]) + end + end + + describe 'when MR has ActiveRecord errors' do + let(:push_options) { { create: true } } + let(:changes) { new_branch_changes } + + it 'adds the error to its errors property' do + invalid_merge_request = MergeRequest.new + invalid_merge_request.errors.add(:base, 'my error') + + expect_any_instance_of( + MergeRequests::CreateService + ).to receive(:execute).and_return(invalid_merge_request) + + service.execute + + expect(service.errors).to eq(['my error']) + end + end +end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index bd10523bc94..5ed06df7072 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -146,7 +146,10 @@ describe MergeRequests::RefreshService do stub_ci_pipeline_yaml_file(YAML.dump(config)) end - subject { service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master') } + subject { service.new(project, @user).execute(@oldrev, @newrev, ref) } + + let(:ref) { 'refs/heads/master' } + let(:project) { @project } context "when .gitlab-ci.yml has merge_requests keywords" do let(:config) do @@ -162,14 +165,17 @@ describe MergeRequests::RefreshService do it 'create detached merge request pipeline with commits' do expect { subject } .to change { @merge_request.merge_request_pipelines.count }.by(1) - .and change { @fork_merge_request.merge_request_pipelines.count }.by(1) .and change { @another_merge_request.merge_request_pipelines.count }.by(0) expect(@merge_request.has_commits?).to be_truthy - expect(@fork_merge_request.has_commits?).to be_truthy expect(@another_merge_request.has_commits?).to be_falsy end + it 'does not create detached merge request pipeline for forked project' do + expect { subject } + .not_to change { @fork_merge_request.merge_request_pipelines.count } + end + it 'create detached merge request pipeline for non-fork merge request' do subject @@ -177,11 +183,25 @@ describe MergeRequests::RefreshService do .to be_detached_merge_request_pipeline end - it 'create legacy detached merge request pipeline for fork merge request' do - subject + context 'when service is hooked by target branch' do + let(:ref) { 'refs/heads/feature' } - expect(@fork_merge_request.merge_request_pipelines.first) - .to be_legacy_detached_merge_request_pipeline + it 'does not create detached merge request pipeline' do + expect { subject } + .not_to change { @merge_request.merge_request_pipelines.count } + end + end + + context 'when service runs on forked project' do + let(:project) { @fork_project } + + it 'creates legacy detached merge request pipeline for fork merge request' do + expect { subject } + .to change { @fork_merge_request.merge_request_pipelines.count }.by(1) + + expect(@fork_merge_request.merge_request_pipelines.first) + .to be_legacy_detached_merge_request_pipeline + end end context 'when ci_use_merge_request_ref feature flag is false' do diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb index 21e71509ed6..8b6db1ce33e 100644 --- a/spec/services/merge_requests/reopen_service_spec.rb +++ b/spec/services/merge_requests/reopen_service_spec.rb @@ -4,7 +4,7 @@ describe MergeRequests::ReopenService do let(:user) { create(:user) } let(:user2) { create(:user) } let(:guest) { create(:user) } - let(:merge_request) { create(:merge_request, :closed, assignee: user2, author: create(:user)) } + let(:merge_request) { create(:merge_request, :closed, assignees: [user2], author: create(:user)) } let(:project) { merge_request.project } before do diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 8e367db031c..0525899ebfa 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -13,7 +13,7 @@ describe MergeRequests::UpdateService, :mailer do let(:merge_request) do create(:merge_request, :simple, title: 'Old title', description: "FYI #{user2.to_reference}", - assignee_id: user3.id, + assignee_ids: [user3.id], source_project: project, author: create(:user)) end @@ -48,7 +48,7 @@ describe MergeRequests::UpdateService, :mailer do { title: 'New title', description: 'Also please fix', - assignee_id: user2.id, + assignee_ids: [user.id], state_event: 'close', label_ids: [label.id], target_branch: 'target', @@ -71,7 +71,7 @@ describe MergeRequests::UpdateService, :mailer do it 'matches base expectations' do expect(@merge_request).to be_valid expect(@merge_request.title).to eq('New title') - expect(@merge_request.assignee).to eq(user2) + expect(@merge_request.assignees).to match_array([user]) expect(@merge_request).to be_closed expect(@merge_request.labels.count).to eq(1) expect(@merge_request.labels.first.title).to eq(label.name) @@ -106,7 +106,7 @@ describe MergeRequests::UpdateService, :mailer do note = find_note('assigned to') expect(note).not_to be_nil - expect(note.note).to include "assigned to #{user2.to_reference}" + expect(note.note).to include "assigned to #{user.to_reference} and unassigned #{user3.to_reference}" end it 'creates a resource label event' do @@ -293,7 +293,7 @@ describe MergeRequests::UpdateService, :mailer do context 'when is reassigned' do before do - update_merge_request({ assignee: user2 }) + update_merge_request({ assignee_ids: [user2.id] }) end it 'marks previous assignee pending todos as done' do @@ -387,7 +387,7 @@ describe MergeRequests::UpdateService, :mailer do context 'when the assignee changes' do it 'updates open merge request counter for assignees when merge request is reassigned' do - update_merge_request(assignee_id: user2.id) + update_merge_request(assignee_ids: [user2.id]) expect(user3.assigned_open_merge_requests_count).to eq 0 expect(user2.assigned_open_merge_requests_count).to eq 1 @@ -541,36 +541,36 @@ describe MergeRequests::UpdateService, :mailer do end end - context 'updating asssignee_id' do + context 'updating asssignee_ids' do it 'does not update assignee when assignee_id is invalid' do - merge_request.update(assignee_id: user.id) + merge_request.update(assignee_ids: [user.id]) - update_merge_request(assignee_id: -1) + update_merge_request(assignee_ids: [-1]) - expect(merge_request.reload.assignee).to eq(user) + expect(merge_request.reload.assignees).to eq([user]) end it 'unassigns assignee when user id is 0' do - merge_request.update(assignee_id: user.id) + merge_request.update(assignee_ids: [user.id]) - update_merge_request(assignee_id: 0) + update_merge_request(assignee_ids: [0]) - expect(merge_request.assignee_id).to be_nil + expect(merge_request.assignee_ids).to be_empty end it 'saves assignee when user id is valid' do - update_merge_request(assignee_id: user.id) + update_merge_request(assignee_ids: [user.id]) - expect(merge_request.assignee_id).to eq(user.id) + expect(merge_request.assignee_ids).to eq([user.id]) end it 'does not update assignee_id when user cannot read issue' do - non_member = create(:user) - original_assignee = merge_request.assignee + non_member = create(:user) + original_assignees = merge_request.assignees - update_merge_request(assignee_id: non_member.id) + update_merge_request(assignee_ids: [non_member.id]) - expect(merge_request.assignee_id).to eq(original_assignee.id) + expect(merge_request.reload.assignees).to eq(original_assignees) end context "when issuable feature is private" do @@ -583,7 +583,7 @@ describe MergeRequests::UpdateService, :mailer do feature_visibility_attr = :"#{merge_request.model_name.plural}_access_level" project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE) - expect { update_merge_request(assignee_id: assignee) }.not_to change { merge_request.assignee } + expect { update_merge_request(assignee_ids: [assignee]) }.not_to change { merge_request.reload.assignees } end end end @@ -619,7 +619,7 @@ describe MergeRequests::UpdateService, :mailer do end it 'is allowed by a user that can push to the source and can update the merge request' do - merge_request.update!(assignee: user) + merge_request.update!(assignees: [user]) source_project.add_developer(user) update_merge_request(allow_collaboration: true, title: 'Updated title') diff --git a/spec/services/note_summary_spec.rb b/spec/services/note_summary_spec.rb index a6cc2251e48..f6ee15f750c 100644 --- a/spec/services/note_summary_spec.rb +++ b/spec/services/note_summary_spec.rb @@ -21,16 +21,20 @@ describe NoteSummary do describe '#note' do it 'returns note hash' do - expect(create_note_summary.note).to eq(noteable: noteable, project: project, author: user, note: 'note') + Timecop.freeze do + expect(create_note_summary.note).to eq(noteable: noteable, project: project, author: user, note: 'note', + created_at: Time.now) + end end context 'when noteable is a commit' do - let(:noteable) { build(:commit) } + let(:noteable) { build(:commit, system_note_timestamp: Time.at(43)) } it 'returns note hash specific to commit' do expect(create_note_summary.note).to eq( noteable: nil, project: project, author: user, note: 'note', - noteable_type: 'Commit', commit_id: noteable.id + noteable_type: 'Commit', commit_id: noteable.id, + created_at: Time.at(43) ) end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 9ba4a11104a..ac4aabf3fbd 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe NotificationService, :mailer do include EmailSpec::Matchers + include ExternalAuthorizationServiceHelpers include NotificationHelpers let(:notification) { described_class.new } @@ -125,11 +126,7 @@ describe NotificationService, :mailer do shared_examples 'participating by assignee notification' do it 'emails the participant' do - if issuable.is_a?(Issue) - issuable.assignees << participant - else - issuable.update_attribute(:assignee, participant) - end + issuable.assignees << participant notification_trigger @@ -620,13 +617,13 @@ describe NotificationService, :mailer do context "merge request diff note" do let(:project) { create(:project, :repository) } let(:user) { create(:user) } - let(:merge_request) { create(:merge_request, source_project: project, assignee: user, author: create(:user)) } + let(:merge_request) { create(:merge_request, source_project: project, assignees: [user], author: create(:user)) } let(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) } before do build_team(note.project) project.add_maintainer(merge_request.author) - project.add_maintainer(merge_request.assignee) + merge_request.assignees.each { |assignee| project.add_maintainer(assignee) } end describe '#new_note' do @@ -637,7 +634,7 @@ describe NotificationService, :mailer do notification.new_note(note) expect(SentNotification.last(3).map(&:recipient).map(&:id)) - .to contain_exactly(merge_request.assignee.id, merge_request.author.id, @u_watcher.id) + .to contain_exactly(*merge_request.assignees.pluck(:id), merge_request.author.id, @u_watcher.id) expect(SentNotification.last.in_reply_to_discussion_id).to eq(note.discussion_id) end end @@ -1223,11 +1220,12 @@ describe NotificationService, :mailer do let(:group) { create(:group) } let(:project) { create(:project, :public, :repository, namespace: group) } let(:another_project) { create(:project, :public, namespace: group) } - let(:merge_request) { create :merge_request, source_project: project, assignee: create(:user), description: 'cc @participant' } + let(:assignee) { create(:user) } + let(:merge_request) { create :merge_request, source_project: project, assignees: [assignee], description: 'cc @participant' } before do project.add_maintainer(merge_request.author) - project.add_maintainer(merge_request.assignee) + merge_request.assignees.each { |assignee| project.add_maintainer(assignee) } build_team(merge_request.target_project) add_users_with_subscription(merge_request.target_project, merge_request) update_custom_notification(:new_merge_request, @u_guest_custom, resource: project) @@ -1239,7 +1237,7 @@ describe NotificationService, :mailer do it do notification.new_merge_request(merge_request, @u_disabled) - should_email(merge_request.assignee) + merge_request.assignees.each { |assignee| should_email(assignee) } should_email(@u_watcher) should_email(@watcher_and_subscriber) should_email(@u_participant_mentioned) @@ -1254,9 +1252,11 @@ describe NotificationService, :mailer do it 'adds "assigned" reason for assignee, if any' do notification.new_merge_request(merge_request, @u_disabled) - email = find_email_for(merge_request.assignee) + merge_request.assignees.each do |assignee| + email = find_email_for(assignee) - expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED) + expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED) + end end it "emails any mentioned users with the mention level" do @@ -1347,9 +1347,9 @@ describe NotificationService, :mailer do end it do - notification.reassigned_merge_request(merge_request, current_user, merge_request.author) + notification.reassigned_merge_request(merge_request, current_user, [assignee]) - should_email(merge_request.assignee) + merge_request.assignees.each { |assignee| should_email(assignee) } should_email(merge_request.author) should_email(@u_watcher) should_email(@u_participant_mentioned) @@ -1365,17 +1365,19 @@ describe NotificationService, :mailer do end it 'adds "assigned" reason for new assignee' do - notification.reassigned_merge_request(merge_request, current_user, merge_request.author) + notification.reassigned_merge_request(merge_request, current_user, [assignee]) - email = find_email_for(merge_request.assignee) + merge_request.assignees.each do |assignee| + email = find_email_for(assignee) - expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED) + expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED) + end end it_behaves_like 'participating notifications' do let(:participant) { create(:user, username: 'user-participant') } let(:issuable) { merge_request } - let(:notification_trigger) { notification.reassigned_merge_request(merge_request, current_user, merge_request.author) } + let(:notification_trigger) { notification.reassigned_merge_request(merge_request, current_user, [assignee]) } end end @@ -1388,7 +1390,7 @@ describe NotificationService, :mailer do it do notification.push_to_merge_request(merge_request, @u_disabled) - should_email(merge_request.assignee) + merge_request.assignees.each { |assignee| should_email(assignee) } should_email(@u_guest_custom) should_email(@u_custom_global) should_email(@u_participant_mentioned) @@ -1430,7 +1432,7 @@ describe NotificationService, :mailer do should_email(subscriber_1_to_group_label_2) should_email(subscriber_2_to_group_label_2) should_email(subscriber_to_label_2) - should_not_email(merge_request.assignee) + merge_request.assignees.each { |assignee| should_not_email(assignee) } should_not_email(merge_request.author) should_not_email(@u_watcher) should_not_email(@u_participant_mentioned) @@ -1499,7 +1501,7 @@ describe NotificationService, :mailer do it do notification.close_mr(merge_request, @u_disabled) - should_email(merge_request.assignee) + merge_request.assignees.each { |assignee| should_email(assignee) } should_email(@u_watcher) should_email(@u_guest_watcher) should_email(@u_guest_custom) @@ -1529,7 +1531,7 @@ describe NotificationService, :mailer do it do notification.merge_mr(merge_request, @u_disabled) - should_email(merge_request.assignee) + merge_request.assignees.each { |assignee| should_email(assignee) } should_email(@u_watcher) should_email(@u_guest_watcher) should_email(@u_guest_custom) @@ -1581,7 +1583,7 @@ describe NotificationService, :mailer do it do notification.reopen_mr(merge_request, @u_disabled) - should_email(merge_request.assignee) + merge_request.assignees.each { |assignee| should_email(assignee) } should_email(@u_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) @@ -1606,7 +1608,7 @@ describe NotificationService, :mailer do it do notification.resolve_all_discussions(merge_request, @u_disabled) - should_email(merge_request.assignee) + merge_request.assignees.each { |assignee| should_email(assignee) } should_email(@u_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) @@ -1850,8 +1852,8 @@ describe NotificationService, :mailer do let(:guest) { create(:user) } let(:developer) { create(:user) } let(:assignee) { create(:user) } - let(:merge_request) { create(:merge_request, source_project: private_project, assignee: assignee) } - let(:merge_request1) { create(:merge_request, source_project: private_project, assignee: assignee, description: "cc @#{guest.username}") } + let(:merge_request) { create(:merge_request, source_project: private_project, assignees: [assignee]) } + let(:merge_request1) { create(:merge_request, source_project: private_project, assignees: [assignee], description: "cc @#{guest.username}") } let(:note) { create(:note, noteable: merge_request, project: private_project) } before do @@ -2217,6 +2219,46 @@ describe NotificationService, :mailer do end end + context 'with external authorization service' do + let(:issue) { create(:issue) } + let(:project) { issue.project } + let(:note) { create(:note, noteable: issue, project: project) } + let(:member) { create(:user) } + + subject { NotificationService.new } + + before do + project.add_maintainer(member) + member.global_notification_setting.update!(level: :watch) + end + + it 'sends email when the service is not enabled' do + expect(Notify).to receive(:new_issue_email).at_least(:once).with(member.id, issue.id, nil).and_call_original + + subject.new_issue(issue, member) + end + + context 'when the service is enabled' do + before do + enable_external_authorization_service_check + end + + it 'does not send an email' do + expect(Notify).not_to receive(:new_issue_email) + + subject.new_issue(issue, member) + end + + it 'still delivers email to admins' do + member.update!(admin: true) + + expect(Notify).to receive(:new_issue_email).at_least(:once).with(member.id, issue.id, nil).and_call_original + + subject.new_issue(issue, member) + end + end + end + def build_team(project) @u_watcher = create_global_setting_for(create(:user), :watch) @u_participating = create_global_setting_for(create(:user), :participating) diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index e8418b09dc2..e1ec932918e 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe Projects::CreateService, '#execute' do + include ExternalAuthorizationServiceHelpers include GitHelpers let(:gitlab_shell) { Gitlab::Shell.new } @@ -344,6 +345,42 @@ describe Projects::CreateService, '#execute' do expect(rugged.config['gitlab.fullpath']).to eq project.full_path end + context 'with external authorization enabled' do + before do + enable_external_authorization_service_check + end + + it 'does not save the project with an error if the service denies access' do + expect(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(user, 'new-label', any_args) { false } + + project = create_project(user, opts.merge({ external_authorization_classification_label: 'new-label' })) + + expect(project.errors[:external_authorization_classification_label]).to be_present + expect(project).not_to be_persisted + end + + it 'saves the project when the user has access to the label' do + expect(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(user, 'new-label', any_args) { true } + + project = create_project(user, opts.merge({ external_authorization_classification_label: 'new-label' })) + + expect(project).to be_persisted + expect(project.external_authorization_classification_label).to eq('new-label') + end + + it 'does not save the project when the user has no access to the default label and no label is provided' do + expect(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(user, 'default_label', any_args) { false } + + project = create_project(user, opts) + + expect(project.errors[:external_authorization_classification_label]).to be_present + expect(project).not_to be_persisted + end + end + def create_project(user, opts) Projects::CreateService.new(user, opts).execute end diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 90eaea9c872..95eb17b5e3a 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe Projects::UpdateService do + include ExternalAuthorizationServiceHelpers include ProjectForksHelper let(:user) { create(:user) } @@ -361,6 +362,46 @@ describe Projects::UpdateService do call_service end end + + context 'with external authorization enabled' do + before do + enable_external_authorization_service_check + end + + it 'does not save the project with an error if the service denies access' do + expect(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(user, 'new-label') { false } + + result = update_project(project, user, { external_authorization_classification_label: 'new-label' }) + + expect(result[:message]).to be_present + expect(result[:status]).to eq(:error) + end + + it 'saves the new label if the service allows access' do + expect(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(user, 'new-label') { true } + + result = update_project(project, user, { external_authorization_classification_label: 'new-label' }) + + expect(result[:status]).to eq(:success) + expect(project.reload.external_authorization_classification_label).to eq('new-label') + end + + it 'checks the default label when the classification label was cleared' do + expect(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(user, 'default_label') { true } + + update_project(project, user, { external_authorization_classification_label: '' }) + end + + it 'does not check the label when it does not change' do + expect(::Gitlab::ExternalAuthorization) + .not_to receive(:access_allowed?) + + update_project(project, user, { name: 'New name' }) + end + end end describe '#run_auto_devops_pipeline?' do diff --git a/spec/services/projects/update_statistics_service_spec.rb b/spec/services/projects/update_statistics_service_spec.rb new file mode 100644 index 00000000000..7e351c9ce54 --- /dev/null +++ b/spec/services/projects/update_statistics_service_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Projects::UpdateStatisticsService do + let(:service) { described_class.new(project, nil, statistics: statistics)} + let(:statistics) { %w(repository_size) } + + describe '#execute' do + context 'with a non-existing project' do + let(:project) { nil } + + it 'does nothing' do + expect_any_instance_of(ProjectStatistics).not_to receive(:refresh!) + + service.execute + end + end + + context 'with an existing project without a repository' do + let(:project) { create(:project) } + + it 'does nothing' do + expect_any_instance_of(ProjectStatistics).not_to receive(:refresh!) + + service.execute + end + end + + context 'with an existing project with a repository' do + let(:project) { create(:project, :repository) } + + it 'refreshes the project statistics' do + expect_any_instance_of(ProjectStatistics).to receive(:refresh!) + .with(only: statistics.map(&:to_sym)) + .and_call_original + + service.execute + end + end + end +end diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index c7e5cca324f..95a131e8c86 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -16,7 +16,9 @@ describe QuickActions::InterpretService do let(:service) { described_class.new(project, developer) } before do - stub_licensed_features(multiple_issue_assignees: false) + stub_licensed_features(multiple_issue_assignees: false, + multiple_merge_request_assignees: false) + project.add_developer(developer) end @@ -527,7 +529,7 @@ describe QuickActions::InterpretService do let(:issuable) { issue } end - it_behaves_like 'assign command' do + it_behaves_like 'assign command', :quarantine do let(:content) { "/assign @#{developer.username} @#{developer2.username}" } let(:issuable) { merge_request } end diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb index 612e9f152e7..0efe37f1167 100644 --- a/spec/services/releases/create_service_spec.rb +++ b/spec/services/releases/create_service_spec.rb @@ -19,6 +19,8 @@ describe Releases::CreateService do shared_examples 'a successful release creation' do it 'creates a new release' do result = service.execute + + expect(project.releases.count).to eq(1) expect(result[:status]).to eq(:success) expect(result[:tag]).not_to be_nil expect(result[:release]).not_to be_nil @@ -69,4 +71,12 @@ describe Releases::CreateService do end end end + + describe '#find_or_build_release' do + it 'does not save the built release' do + service.find_or_build_release + + expect(project.releases.count).to eq(0) + end + end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index b917de14b2e..51c5a803dbd 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -1,6 +1,9 @@ +# frozen_string_literal: true + require 'spec_helper' describe SystemNoteService do + include ProjectForksHelper include Gitlab::Routing include RepoHelpers include AssetsHelpers @@ -11,6 +14,14 @@ describe SystemNoteService do let(:noteable) { create(:issue, project: project) } let(:issue) { noteable } + shared_examples_for 'a note with overridable created_at' do + let(:noteable) { create(:issue, project: project, system_note_timestamp: Time.at(42)) } + + it 'the note has the correct time' do + expect(subject.created_at).to eq Time.at(42) + end + end + shared_examples_for 'a system note' do let(:expected_noteable) { noteable } let(:commit_count) { nil } @@ -137,6 +148,8 @@ describe SystemNoteService do end context 'when assignee added' do + it_behaves_like 'a note with overridable created_at' + it 'sets the note text' do expect(subject.note).to eq "assigned to @#{assignee.username}" end @@ -145,14 +158,16 @@ describe SystemNoteService do context 'when assignee removed' do let(:assignee) { nil } + it_behaves_like 'a note with overridable created_at' + it 'sets the note text' do expect(subject.note).to eq 'removed assignee' end end end - describe '.change_issue_assignees' do - subject { described_class.change_issue_assignees(noteable, project, author, [assignee]) } + describe '.change_issuable_assignees' do + subject { described_class.change_issuable_assignees(noteable, project, author, [assignee]) } let(:assignee) { create(:user) } let(:assignee1) { create(:user) } @@ -165,9 +180,11 @@ describe SystemNoteService do def build_note(old_assignees, new_assignees) issue.assignees = new_assignees - described_class.change_issue_assignees(issue, project, author, old_assignees).note + described_class.change_issuable_assignees(issue, project, author, old_assignees).note end + it_behaves_like 'a note with overridable created_at' + it 'builds a correct phrase when an assignee is added to a non-assigned issue' do expect(build_note([], [assignee1])).to eq "assigned to @#{assignee1.username}" end @@ -213,6 +230,8 @@ describe SystemNoteService do expect(subject.note).to eq "changed milestone to #{reference}" end + + it_behaves_like 'a note with overridable created_at' end context 'when milestone removed' do @@ -221,6 +240,8 @@ describe SystemNoteService do it 'sets the note text' do expect(subject.note).to eq 'removed milestone' end + + it_behaves_like 'a note with overridable created_at' end end @@ -237,6 +258,8 @@ describe SystemNoteService do it 'sets the note text to use the milestone name' do expect(subject.note).to eq "changed milestone to #{milestone.to_reference(format: :name)}" end + + it_behaves_like 'a note with overridable created_at' end context 'when milestone removed' do @@ -245,6 +268,8 @@ describe SystemNoteService do it 'sets the note text' do expect(subject.note).to eq 'removed milestone' end + + it_behaves_like 'a note with overridable created_at' end end end @@ -254,6 +279,8 @@ describe SystemNoteService do let(:due_date) { Date.today } + it_behaves_like 'a note with overridable created_at' + it_behaves_like 'a system note' do let(:action) { 'due_date' } end @@ -280,6 +307,8 @@ describe SystemNoteService do let(:status) { 'reopened' } let(:source) { nil } + it_behaves_like 'a note with overridable created_at' + it_behaves_like 'a system note' do let(:action) { 'opened' } end @@ -289,6 +318,8 @@ describe SystemNoteService do let(:status) { 'opened' } let(:source) { double('commit', gfm_reference: 'commit 123456') } + it_behaves_like 'a note with overridable created_at' + it 'sets the note text' do expect(subject.note).to eq "#{status} via commit 123456" end @@ -338,6 +369,8 @@ describe SystemNoteService do let(:action) { 'title' } end + it_behaves_like 'a note with overridable created_at' + it 'sets the note text' do expect(subject.note) .to eq "changed title from **{-Old title-}** to **{+Lorem ipsum+}**" @@ -353,6 +386,8 @@ describe SystemNoteService do let(:action) { 'description' } end + it_behaves_like 'a note with overridable created_at' + it 'sets the note text' do expect(subject.note).to eq('changed the description') end @@ -478,6 +513,8 @@ describe SystemNoteService do let(:action) { 'cross_reference' } end + it_behaves_like 'a note with overridable created_at' + describe 'note_body' do context 'cross-project' do let(:project2) { create(:project, :repository) } @@ -619,7 +656,7 @@ describe SystemNoteService do context 'commit with cross-reference from fork' do let(:author2) { create(:project_member, :reporter, user: create(:user), project: project).user } - let(:forked_project) { Projects::ForkService.new(project, author2).execute } + let(:forked_project) { fork_project(project, author2, repository: true) } let(:commit2) { forked_project.commit } before do @@ -896,6 +933,28 @@ describe SystemNoteService do end end + describe '.change_time_estimate' do + subject { described_class.change_time_estimate(noteable, project, author) } + + it_behaves_like 'a system note' do + let(:action) { 'time_tracking' } + end + + context 'with a time estimate' do + it 'sets the note text' do + noteable.update_attribute(:time_estimate, 277200) + + expect(subject.note).to eq "changed time estimate to 1w 4d 5h" + end + end + + context 'without a time estimate' do + it 'sets the note text' do + expect(subject.note).to eq "removed time estimate" + end + end + end + describe '.discussion_continued_in_issue' do let(:discussion) { create(:diff_note_on_merge_request, project: project).to_discussion } let(:merge_request) { discussion.noteable } @@ -922,28 +981,6 @@ describe SystemNoteService do end end - describe '.change_time_estimate' do - subject { described_class.change_time_estimate(noteable, project, author) } - - it_behaves_like 'a system note' do - let(:action) { 'time_tracking' } - end - - context 'with a time estimate' do - it 'sets the note text' do - noteable.update_attribute(:time_estimate, 277200) - - expect(subject.note).to eq "changed time estimate to 1w 4d 5h" - end - end - - context 'without a time estimate' do - it 'sets the note text' do - expect(subject.note).to eq "removed time estimate" - end - end - end - describe '.change_time_spent' do # We need a custom noteable in order to the shared examples to be green. let(:noteable) do diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 8631f3f9a33..89411b2e908 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -272,28 +272,6 @@ describe TodoService do end end - describe '#reassigned_issue' do - it 'creates a pending todo for new assignee' do - unassigned_issue.assignees << john_doe - service.reassigned_issue(unassigned_issue, author) - - should_create_todo(user: john_doe, target: unassigned_issue, action: Todo::ASSIGNED) - end - - it 'does not create a todo if unassigned' do - issue.assignees.destroy_all # rubocop: disable DestroyAll - - should_not_create_any_todo { service.reassigned_issue(issue, author) } - end - - it 'creates a todo if new assignee is the current user' do - unassigned_issue.assignees << john_doe - service.reassigned_issue(unassigned_issue, john_doe) - - should_create_todo(user: john_doe, target: unassigned_issue, author: john_doe, action: Todo::ASSIGNED) - end - end - describe '#mark_pending_todos_as_done' do it 'marks related pending todos to the target for the user as done' do first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) @@ -504,10 +482,60 @@ describe TodoService do end end + describe '#reassigned_issuable' do + shared_examples 'reassigned issuable' do + it 'creates a pending todo for new assignee' do + issuable_unassigned.assignees = [john_doe] + service.reassigned_issuable(issuable_unassigned, author) + + should_create_todo(user: john_doe, target: issuable_unassigned, action: Todo::ASSIGNED) + end + + it 'does not create a todo if unassigned' do + issuable_assigned.assignees = [] + + should_not_create_any_todo { service.reassigned_issuable(issuable_assigned, author) } + end + + it 'creates a todo if new assignee is the current user' do + issuable_assigned.assignees = [john_doe] + service.reassigned_issuable(issuable_assigned, john_doe) + + should_create_todo(user: john_doe, target: issuable_assigned, author: john_doe, action: Todo::ASSIGNED) + end + + it 'does not create a todo for guests' do + service.reassigned_issuable(issuable_assigned, author) + should_not_create_todo(user: guest, target: issuable_assigned, action: Todo::MENTIONED) + end + + it 'does not create a directly addressed todo for guests' do + service.reassigned_issuable(addressed_issuable_assigned, author) + should_not_create_todo(user: guest, target: addressed_issuable_assigned, action: Todo::DIRECTLY_ADDRESSED) + end + end + + context 'issuable is a merge request' do + it_behaves_like 'reassigned issuable' do + let(:issuable_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") } + let(:addressed_issuable_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") } + let(:issuable_unassigned) { create(:merge_request, source_project: project, author: author, assignees: []) } + end + end + + context 'issuable is an issue' do + it_behaves_like 'reassigned issuable' do + let(:issuable_assigned) { create(:issue, project: project, author: author, assignees: [john_doe], description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") } + let(:addressed_issuable_assigned) { create(:issue, project: project, author: author, assignees: [john_doe], description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") } + let(:issuable_unassigned) { create(:issue, project: project, author: author, assignees: []) } + end + end + end + describe 'Merge Requests' do - let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") } - let(:addressed_mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") } - let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignee: nil) } + let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") } + let(:addressed_mr_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") } + let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignees: []) } describe '#new_merge_request' do it 'creates a pending todo if assigned' do @@ -659,38 +687,6 @@ describe TodoService do end end - describe '#reassigned_merge_request' do - it 'creates a pending todo for new assignee' do - mr_unassigned.update_attribute(:assignee, john_doe) - service.reassigned_merge_request(mr_unassigned, author) - - should_create_todo(user: john_doe, target: mr_unassigned, action: Todo::ASSIGNED) - end - - it 'does not create a todo if unassigned' do - mr_assigned.update_attribute(:assignee, nil) - - should_not_create_any_todo { service.reassigned_merge_request(mr_assigned, author) } - end - - it 'creates a todo if new assignee is the current user' do - mr_assigned.update_attribute(:assignee, john_doe) - service.reassigned_merge_request(mr_assigned, john_doe) - - should_create_todo(user: john_doe, target: mr_assigned, author: john_doe, action: Todo::ASSIGNED) - end - - it 'does not create a todo for guests' do - service.reassigned_merge_request(mr_assigned, author) - should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED) - end - - it 'does not create a directly addressed todo for guests' do - service.reassigned_merge_request(addressed_mr_assigned, author) - should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED) - end - end - describe '#merge_merge_request' do it 'marks related pending todos to the target for the user as done' do first_todo = create(:todo, :assigned, user: john_doe, project: project, target: mr_assigned, author: author) diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb index 83f1495a1c6..450e76d5f58 100644 --- a/spec/services/users/destroy_service_spec.rb +++ b/spec/services/users/destroy_service_spec.rb @@ -78,7 +78,7 @@ describe Users::DestroyService do end context "for an merge request the user was assigned to" do - let!(:merge_request) { create(:merge_request, source_project: project, assignee: user) } + let!(:merge_request) { create(:merge_request, source_project: project, assignees: [user]) } before do service.execute(user) @@ -91,7 +91,7 @@ describe Users::DestroyService do it 'migrates the merge request so that it is "Unassigned"' do migrated_merge_request = MergeRequest.find_by_id(merge_request.id) - expect(migrated_merge_request.assignee).to be_nil + expect(migrated_merge_request.assignees).to be_empty end end end diff --git a/spec/services/verify_pages_domain_service_spec.rb b/spec/services/verify_pages_domain_service_spec.rb index ddf9d2b4917..b60b13bb3fc 100644 --- a/spec/services/verify_pages_domain_service_spec.rb +++ b/spec/services/verify_pages_domain_service_spec.rb @@ -27,6 +27,7 @@ describe VerifyPagesDomainService do expect(domain).to be_verified expect(domain).to be_enabled + expect(domain.remove_at).to be_nil end end @@ -48,18 +49,32 @@ describe VerifyPagesDomainService do end end + shared_examples 'unverifies and disables domain' do + it 'unverifies domain' do + expect(service.execute).to eq(error_status) + expect(domain).not_to be_verified + end + + it 'disables domain and shedules it for removal' do + Timecop.freeze do + service.execute + expect(domain).not_to be_enabled + expect(domain.remove_at).to be_within(1.second).of(1.week.from_now) + end + end + end + context 'when domain is disabled(or new)' do let(:domain) { create(:pages_domain, :disabled) } include_examples 'successful enablement and verification' - shared_examples 'unverifies and disables domain' do - it 'unverifies and disables domain' do - expect(service.execute).to eq(error_status) - - expect(domain).not_to be_verified - expect(domain).not_to be_enabled + context 'when txt record does not contain verification code' do + before do + stub_resolver(domain_name => 'something else') end + + include_examples 'unverifies and disables domain' end context 'when txt record does not contain verification code' do @@ -84,16 +99,25 @@ describe VerifyPagesDomainService do include_examples 'successful enablement and verification' - context 'when txt record does not contain verification code' do - before do - stub_resolver(domain_name => 'something else') - end - + shared_examples 'unverifing domain' do it 'unverifies but does not disable domain' do expect(service.execute).to eq(error_status) expect(domain).not_to be_verified expect(domain).to be_enabled end + + it 'does not schedule domain for removal' do + service.execute + expect(domain.remove_at).to be_nil + end + end + + context 'when txt record does not contain verification code' do + before do + stub_resolver(domain_name => 'something else') + end + + include_examples 'unverifing domain' end context 'when no txt records are present' do @@ -101,11 +125,7 @@ describe VerifyPagesDomainService do stub_resolver end - it 'unverifies but does not disable domain' do - expect(service.execute).to eq(error_status) - expect(domain).not_to be_verified - expect(domain).to be_enabled - end + include_examples 'unverifing domain' end end @@ -125,13 +145,40 @@ describe VerifyPagesDomainService do stub_resolver end - it 'disables domain' do - error_status[:message] += '. It is now disabled.' + let(:error_status) { { status: :error, message: "Couldn't verify #{domain.domain}. It is now disabled." } } - expect(service.execute).to eq(error_status) + include_examples 'unverifies and disables domain' + end + end - expect(domain).not_to be_verified - expect(domain).not_to be_enabled + context 'when domain is disabled and scheduled for removal' do + let(:domain) { create(:pages_domain, :disabled, :scheduled_for_removal) } + + context 'when the right code is present' do + before do + stub_resolver(domain.domain => domain.keyed_verification_code) + end + + it 'verifies and enables domain' do + expect(service.execute).to eq(status: :success) + + expect(domain).to be_verified + expect(domain).to be_enabled + end + + it 'prevent domain from being removed' do + expect { service.execute }.to change { domain.remove_at }.to(nil) + end + end + + context 'when the right code is not present' do + before do + stub_resolver + end + + it 'keeps domain scheduled for removal but does not change removal time' do + expect { service.execute }.not_to change { domain.remove_at } + expect(domain.remove_at).to be_present end end end diff --git a/spec/support/external_authorization_service_helpers.rb b/spec/support/external_authorization_service_helpers.rb new file mode 100644 index 00000000000..79dd9a3d58e --- /dev/null +++ b/spec/support/external_authorization_service_helpers.rb @@ -0,0 +1,33 @@ +module ExternalAuthorizationServiceHelpers + def enable_external_authorization_service_check + stub_application_setting(external_authorization_service_enabled: true) + + stub_application_setting(external_authorization_service_url: 'https://authorize.me') + stub_application_setting(external_authorization_service_default_label: 'default_label') + stub_request(:post, "https://authorize.me").to_return(status: 200) + end + + def external_service_set_access(allowed, user, project) + enable_external_authorization_service_check + classification_label = ::Gitlab::CurrentSettings.current_application_settings + .external_authorization_service_default_label + + # Reload the project so cached licensed features are reloaded + if project + classification_label = Project.find(project.id).external_authorization_classification_label + end + + allow(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?) + .with(user, classification_label, any_args) + .and_return(allowed) + end + + def external_service_allow_access(user, project = nil) + external_service_set_access(true, user, project) + end + + def external_service_deny_access(user, project = nil) + external_service_set_access(false, user, project) + end +end diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb index 5b79c40f27b..542f533d590 100644 --- a/spec/support/features/discussion_comments_shared_example.rb +++ b/spec/support/features/discussion_comments_shared_example.rb @@ -7,7 +7,7 @@ shared_examples 'discussion comments' do |resource_name| let(:close_selector) { "#{form_selector} .btn-comment-and-close" } let(:comments_selector) { '.timeline > .note.timeline-entry' } - it 'clicking "Comment" will post a comment' do + it 'clicking "Comment" will post a comment', :quarantine do expect(page).to have_selector toggle_selector find("#{form_selector} .note-textarea").send_keys('a') diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb index 6569feec39b..03057a102c5 100644 --- a/spec/support/helpers/filtered_search_helpers.rb +++ b/spec/support/helpers/filtered_search_helpers.rb @@ -149,4 +149,10 @@ module FilteredSearchHelpers loop until find('.filtered-search').value.strip == text end end + + def close_dropdown_menu_if_visible + find('.dropdown-menu-toggle', visible: :all).tap do |toggle| + toggle.click if toggle.visible? + end + end end diff --git a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb index 4df80b4168a..ab6687f1d07 100644 --- a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb +++ b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb @@ -46,9 +46,9 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests allow_gitaly_n_plus_1 { create(:project, group: subgroup) } end - let!(:merge_request1) { create(:merge_request, author: user, source_project: project2, target_project: project1, target_branch: 'merged-target') } - let!(:merge_request2) { create(:merge_request, :conflict, author: user, source_project: project2, target_project: project1, state: 'closed') } - let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') } + let!(:merge_request1) { create(:merge_request, assignees: [user], author: user, source_project: project2, target_project: project1, target_branch: 'merged-target') } + let!(:merge_request2) { create(:merge_request, :conflict, assignees: [user], author: user, source_project: project2, target_project: project1, state: 'closed') } + let!(:merge_request3) { create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') } let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3, title: 'WIP thing') } let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4, title: '[WIP]') } diff --git a/spec/support/shared_contexts/merge_request_create.rb b/spec/support/shared_contexts/merge_request_create.rb new file mode 100644 index 00000000000..529f481c2b6 --- /dev/null +++ b/spec/support/shared_contexts/merge_request_create.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +shared_context 'merge request create context' do + let(:user) { create(:user) } + let(:user2) { create(:user) } + let(:target_project) { create(:project, :public, :repository) } + let(:source_project) { target_project } + let!(:milestone) { create(:milestone, project: target_project) } + let!(:label) { create(:label, project: target_project) } + let!(:label2) { create(:label, project: target_project) } + + before do + source_project.add_maintainer(user) + target_project.add_maintainer(user) + target_project.add_maintainer(user2) + + sign_in(user) + visit project_new_merge_request_path(target_project, + merge_request: { + source_project_id: source_project.id, + target_project_id: target_project.id, + source_branch: 'fix', + target_branch: 'master' + }) + end +end diff --git a/spec/support/shared_contexts/merge_request_edit.rb b/spec/support/shared_contexts/merge_request_edit.rb new file mode 100644 index 00000000000..c84510ff47d --- /dev/null +++ b/spec/support/shared_contexts/merge_request_edit.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' +shared_context 'merge request edit context' do + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:milestone) { create(:milestone, project: target_project) } + let!(:label) { create(:label, project: target_project) } + let!(:label2) { create(:label, project: target_project) } + let(:target_project) { create(:project, :public, :repository) } + let(:source_project) { target_project } + let(:merge_request) do + create(:merge_request, + source_project: source_project, + target_project: target_project, + source_branch: 'fix', + target_branch: 'master') + end + + before do + source_project.add_maintainer(user) + target_project.add_maintainer(user) + target_project.add_maintainer(user2) + + sign_in(user) + visit edit_project_merge_request_path(target_project, merge_request) + end +end diff --git a/spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb b/spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb new file mode 100644 index 00000000000..8dd78fd0a25 --- /dev/null +++ b/spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +shared_examples 'disabled when using an external authorization service' do + include ExternalAuthorizationServiceHelpers + + it 'works when the feature is not enabled' do + subject + + expect(response).to be_success + end + + it 'renders a 404 with a message when the feature is enabled' do + enable_external_authorization_service_check + + subject + + expect(response).to have_gitlab_http_status(403) + end +end + +shared_examples 'unauthorized when external service denies access' do + include ExternalAuthorizationServiceHelpers + + it 'allows access when the authorization service allows it' do + external_service_allow_access(user, project) + + subject + + # Account for redirects after updates + expect(response.status).to be_between(200, 302) + end + + it 'allows access when the authorization service denies it' do + external_service_deny_access(user, project) + + subject + + expect(response).to have_gitlab_http_status(403) + end +end diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb index 7038a366144..ec1b1754cf0 100644 --- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb @@ -1,42 +1,17 @@ RSpec.shared_examples 'a creatable merge request' do include WaitForRequests - let(:user) { create(:user) } - let(:user2) { create(:user) } - let(:target_project) { create(:project, :public, :repository) } - let(:source_project) { target_project } - let!(:milestone) { create(:milestone, project: target_project) } - let!(:label) { create(:label, project: target_project) } - let!(:label2) { create(:label, project: target_project) } - - before do - source_project.add_maintainer(user) - target_project.add_maintainer(user) - target_project.add_maintainer(user2) - - sign_in(user) - visit project_new_merge_request_path( - target_project, - merge_request: { - source_project_id: source_project.id, - target_project_id: target_project.id, - source_branch: 'fix', - target_branch: 'master' - }) - end - it 'creates new merge request', :js do - click_button 'Assignee' + find('.js-assignee-search').click page.within '.dropdown-menu-user' do click_link user2.name end - expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user2.id.to_s) + expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user2.id.to_s) page.within '.js-assignee-search' do expect(page).to have_content user2.name end - click_link 'Assign to me' - expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s) + expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user.id.to_s) page.within '.js-assignee-search' do expect(page).to have_content user.name end diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb index eef0327c9a6..a6121fcc50a 100644 --- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb @@ -1,34 +1,10 @@ RSpec.shared_examples 'an editable merge request' do - let(:user) { create(:user) } - let(:user2) { create(:user) } - let!(:milestone) { create(:milestone, project: target_project) } - let!(:label) { create(:label, project: target_project) } - let!(:label2) { create(:label, project: target_project) } - let(:target_project) { create(:project, :public, :repository) } - let(:source_project) { target_project } - let(:merge_request) do - create(:merge_request, - source_project: source_project, - target_project: target_project, - source_branch: 'fix', - target_branch: 'master') - end - - before do - source_project.add_maintainer(user) - target_project.add_maintainer(user) - target_project.add_maintainer(user2) - - sign_in(user) - visit edit_project_merge_request_path(target_project, merge_request) - end - it 'updates merge request', :js do - click_button 'Assignee' + find('.js-assignee-search').click page.within '.dropdown-menu-user' do click_link user.name end - expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s) + expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user.id.to_s) page.within '.js-assignee-search' do expect(page).to have_content user.name end diff --git a/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb new file mode 100644 index 00000000000..bab7963f06f --- /dev/null +++ b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +shared_examples 'multiple assignees merge request' do |action, save_button_title| + it "#{action} a MR with multiple assignees", :js do + find('.js-assignee-search').click + page.within '.dropdown-menu-user' do + click_link user.name + click_link user2.name + end + + # Extra click needed in order to toggle the dropdown + find('.js-assignee-search').click + + expect(all('input[name="merge_request[assignee_ids][]"]', visible: false).map(&:value)) + .to match_array([user.id.to_s, user2.id.to_s]) + + page.within '.js-assignee-search' do + expect(page).to have_content "#{user2.name} + 1 more" + end + + click_button save_button_title + + page.within '.issuable-sidebar' do + page.within '.assignee' do + expect(page).to have_content '2 Assignees' + + click_link 'Edit' + + expect(page).to have_content user.name + expect(page).to have_content user2.name + end + end + + page.within '.dropdown-menu-user' do + click_link user.name + end + + page.within '.issuable-sidebar' do + page.within '.assignee' do + # Closing dropdown to persist + click_link 'Edit' + + expect(page).to have_content user2.name + end + end + end +end diff --git a/spec/support/shared_examples/finders/assignees_filter_spec.rb b/spec/support/shared_examples/finders/assignees_filter_spec.rb new file mode 100644 index 00000000000..782a2d97746 --- /dev/null +++ b/spec/support/shared_examples/finders/assignees_filter_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +shared_examples 'assignee ID filter' do + it 'returns issuables assigned to that user' do + expect(issuables).to contain_exactly(*expected_issuables) + end +end + +shared_examples 'assignee username filter' do + it 'returns issuables assigned to those users' do + expect(issuables).to contain_exactly(*expected_issuables) + end +end + +shared_examples 'no assignee filter' do + let(:params) { { assignee_id: 'None' } } + + it 'returns issuables not assigned to any assignee' do + expect(issuables).to contain_exactly(*expected_issuables) + end + + it 'returns issuables not assigned to any assignee' do + params[:assignee_id] = 0 + + expect(issuables).to contain_exactly(*expected_issuables) + end + + it 'returns issuables not assigned to any assignee' do + params[:assignee_id] = 'none' + + expect(issuables).to contain_exactly(*expected_issuables) + end +end + +shared_examples 'any assignee filter' do + context '' do + let(:params) { { assignee_id: 'Any' } } + + it 'returns issuables assigned to any assignee' do + expect(issuables).to contain_exactly(*expected_issuables) + end + + it 'returns issuables assigned to any assignee' do + params[:assignee_id] = 'any' + + expect(issuables).to contain_exactly(*expected_issuables) + end + end +end diff --git a/spec/support/shared_examples/finders/finder_with_external_authorization_enabled.rb b/spec/support/shared_examples/finders/finder_with_external_authorization_enabled.rb new file mode 100644 index 00000000000..d7e17cc0b70 --- /dev/null +++ b/spec/support/shared_examples/finders/finder_with_external_authorization_enabled.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +shared_examples 'a finder with external authorization service' do + include ExternalAuthorizationServiceHelpers + + let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + project.add_maintainer(user) + end + + it 'finds the subject' do + expect(described_class.new(user).execute).to include(subject) + end + + context 'with an external authorization service' do + before do + enable_external_authorization_service_check + end + + it 'does not include the subject when no project was given' do + expect(described_class.new(user).execute).not_to include(subject) + end + + it 'includes the subject when a project id was given' do + expect(described_class.new(user, project_params).execute).to include(subject) + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb index db3ecccc339..ae78cd86cd5 100644 --- a/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb @@ -1,25 +1,35 @@ # frozen_string_literal: true -shared_examples 'due quick action not available' do - it 'does not set the due date' do - add_note('/due 2016-08-28') +shared_examples 'due quick action' do + context 'due quick action available and date can be added' do + it 'sets the due date accordingly' do + add_note('/due 2016-08-28') - expect(page).not_to have_content 'Commands applied' - expect(page).not_to have_content '/due 2016-08-28' - end -end + expect(page).not_to have_content '/due 2016-08-28' + expect(page).to have_content 'Commands applied' + + visit project_issue_path(project, issue) -shared_examples 'due quick action available and date can be added' do - it 'sets the due date accordingly' do - add_note('/due 2016-08-28') + page.within '.due_date' do + expect(page).to have_content 'Aug 28, 2016' + end + end + end - expect(page).not_to have_content '/due 2016-08-28' - expect(page).to have_content 'Commands applied' + context 'due quick action not available' do + let(:guest) { create(:user) } + before do + project.add_guest(guest) + gitlab_sign_out + sign_in(guest) + visit project_issue_path(project, issue) + end - visit project_issue_path(project, issue) + it 'does not set the due date' do + add_note('/due 2016-08-28') - page.within '.due_date' do - expect(page).to have_content 'Aug 28, 2016' + expect(page).not_to have_content 'Commands applied' + expect(page).not_to have_content '/due 2016-08-28' end end end diff --git a/spec/support/shared_examples/services/base_helm_service_shared_examples.rb b/spec/support/shared_examples/services/base_helm_service_shared_examples.rb index 78a8e49fd76..fa76b95f768 100644 --- a/spec/support/shared_examples/services/base_helm_service_shared_examples.rb +++ b/spec/support/shared_examples/services/base_helm_service_shared_examples.rb @@ -20,7 +20,7 @@ shared_examples 'logs kubernetes errors' do end it 'logs into kubernetes.log and Sentry' do - expect(service.send(:logger)).to receive(:error).with(logger_hash) + expect(service.send(:logger)).to receive(:error).with(hash_including(logger_hash)) expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with( error, diff --git a/spec/support/shared_examples/wiki_file_attachments_examples.rb b/spec/support/shared_examples/wiki_file_attachments_examples.rb index b6fb2a66b0e..22fbfb48928 100644 --- a/spec/support/shared_examples/wiki_file_attachments_examples.rb +++ b/spec/support/shared_examples/wiki_file_attachments_examples.rb @@ -42,7 +42,7 @@ shared_examples 'wiki file attachments' do end end - context 'uploading is complete' do + context 'uploading is complete', :quarantine do it 'shows "Attach a file" button on uploading complete' do attach_with_dropzone wait_for_requests diff --git a/spec/validators/x509_certificate_credentials_validator_spec.rb b/spec/validators/x509_certificate_credentials_validator_spec.rb new file mode 100644 index 00000000000..24ef68c1fab --- /dev/null +++ b/spec/validators/x509_certificate_credentials_validator_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe X509CertificateCredentialsValidator do + let(:certificate_data) { File.read('spec/fixtures/x509_certificate.crt') } + let(:pkey_data) { File.read('spec/fixtures/x509_certificate_pk.key') } + + let(:validatable) do + Class.new do + include ActiveModel::Validations + + attr_accessor :certificate, :private_key, :passphrase + + def initialize(certificate, private_key, passphrase = nil) + @certificate, @private_key, @passphrase = certificate, private_key, passphrase + end + end + end + + subject(:validator) do + described_class.new(certificate: :certificate, pkey: :private_key) + end + + it 'is not valid when the certificate is not valid' do + record = validatable.new('not a certificate', nil) + + validator.validate(record) + + expect(record.errors[:certificate]).to include('is not a valid X509 certificate.') + end + + it 'is not valid without a certificate' do + record = validatable.new(nil, nil) + + validator.validate(record) + + expect(record.errors[:certificate]).not_to be_empty + end + + context 'when a valid certificate is passed' do + let(:record) { validatable.new(certificate_data, nil) } + + it 'does not track an error for the certificate' do + validator.validate(record) + + expect(record.errors[:certificate]).to be_empty + end + + it 'adds an error when not passing a correct private key' do + validator.validate(record) + + expect(record.errors[:private_key]).to include('could not read private key, is the passphrase correct?') + end + + it 'has no error when the private key is correct' do + record.private_key = pkey_data + + validator.validate(record) + + expect(record.errors).to be_empty + end + end + + context 'when using a passphrase' do + let(:passphrase_certificate_data) { File.read('spec/fixtures/passphrase_x509_certificate.crt') } + let(:passphrase_pkey_data) { File.read('spec/fixtures/passphrase_x509_certificate_pk.key') } + + let(:record) { validatable.new(passphrase_certificate_data, passphrase_pkey_data, '5iveL!fe') } + + subject(:validator) do + described_class.new(certificate: :certificate, pkey: :private_key, pass: :passphrase) + end + + it 'is valid with the correct data' do + validator.validate(record) + + expect(record.errors).to be_empty + end + + it 'adds an error when the passphrase is wrong' do + record.passphrase = 'wrong' + + validator.validate(record) + + expect(record.errors[:private_key]).not_to be_empty + end + end +end diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb index c13eab30054..529afa03f9c 100644 --- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb @@ -17,7 +17,7 @@ describe 'projects/merge_requests/edit.html.haml' do source_project: forked_project, target_project: project, author: user, - assignee: user, + assignees: [user], milestone: milestone) end @@ -40,7 +40,7 @@ describe 'projects/merge_requests/edit.html.haml' do expect(rendered).to have_field('merge_request[title]') expect(rendered).to have_field('merge_request[description]') - expect(rendered).to have_selector('#merge_request_assignee_id', visible: false) + expect(rendered).to have_selector('input[name="merge_request[label_ids][]"]', visible: false) expect(rendered).to have_selector('#merge_request_milestone_id', visible: false) expect(rendered).not_to have_selector('#merge_request_target_branch', visible: false) end @@ -52,7 +52,7 @@ describe 'projects/merge_requests/edit.html.haml' do expect(rendered).to have_field('merge_request[title]') expect(rendered).to have_field('merge_request[description]') - expect(rendered).to have_selector('#merge_request_assignee_id', visible: false) + expect(rendered).to have_selector('input[name="merge_request[label_ids][]"]', visible: false) expect(rendered).to have_selector('#merge_request_milestone_id', visible: false) expect(rendered).to have_selector('#merge_request_target_branch', visible: false) end diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb index d9bda1a3414..23cb319a202 100644 --- a/spec/views/projects/merge_requests/show.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb @@ -53,19 +53,6 @@ describe 'projects/merge_requests/show.html.haml' do expect(rendered).not_to have_css('.cannot-be-merged') end end - - context 'when assignee is not allowed to merge' do - it 'shows a warning icon' do - reporter = create(:user) - project.add_reporter(reporter) - closed_merge_request.update(assignee_id: reporter.id) - assign(:issuable_sidebar, serialize_issuable_sidebar(user, project, closed_merge_request)) - - render - - expect(rendered).to have_css('.cannot-be-merged') - end - end end context 'when the merge request is closed' do diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 66958a4c116..a3fe8fa4501 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -63,8 +63,12 @@ describe PostReceive do let(:changes) { "123456 789012 refs/heads/tést" } it "calls Git::BranchPushService" do - expect_any_instance_of(Git::BranchPushService).to receive(:execute).and_return(true) - expect_any_instance_of(Git::TagPushService).not_to receive(:execute) + expect_next_instance_of(Git::BranchPushService) do |service| + expect(service).to receive(:execute).and_return(true) + end + + expect(Git::TagPushService).not_to receive(:new) + described_class.new.perform(gl_repository, key_id, base64_changes) end end @@ -73,8 +77,12 @@ describe PostReceive do let(:changes) { "123456 789012 refs/tags/tag" } it "calls Git::TagPushService" do - expect_any_instance_of(Git::BranchPushService).not_to receive(:execute) - expect_any_instance_of(Git::TagPushService).to receive(:execute).and_return(true) + expect(Git::BranchPushService).not_to receive(:execute) + + expect_next_instance_of(Git::TagPushService) do |service| + expect(service).to receive(:execute).and_return(true) + end + described_class.new.perform(gl_repository, key_id, base64_changes) end end @@ -83,8 +91,9 @@ describe PostReceive do let(:changes) { "123456 789012 refs/merge-requests/123" } it "does not call any of the services" do - expect_any_instance_of(Git::BranchPushService).not_to receive(:execute) - expect_any_instance_of(Git::TagPushService).not_to receive(:execute) + expect(Git::BranchPushService).not_to receive(:new) + expect(Git::TagPushService).not_to receive(:new) + described_class.new.perform(gl_repository, key_id, base64_changes) end end @@ -127,7 +136,9 @@ describe PostReceive do allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data) # silence hooks so we can isolate allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true) - allow_any_instance_of(Git::BranchPushService).to receive(:execute).and_return(true) + expect_next_instance_of(Git::BranchPushService) do |service| + expect(service).to receive(:execute).and_return(true) + end end it 'calls SystemHooksService' do diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb index a7353227043..3c40269adc7 100644 --- a/spec/workers/project_cache_worker_spec.rb +++ b/spec/workers/project_cache_worker_spec.rb @@ -7,9 +7,9 @@ describe ProjectCacheWorker do let(:worker) { described_class.new } let(:project) { create(:project, :repository) } - let(:statistics) { project.statistics } - let(:lease_key) { "project_cache_worker:#{project.id}:update_statistics" } + let(:lease_key) { ["project_cache_worker", project.id, *statistics.sort].join(":") } let(:lease_timeout) { ProjectCacheWorker::LEASE_TIMEOUT } + let(:statistics) { [] } describe '#perform' do before do @@ -35,14 +35,6 @@ describe ProjectCacheWorker do end context 'with an existing project' do - it 'updates the project statistics' do - expect(worker).to receive(:update_statistics) - .with(kind_of(Project), %i(repository_size)) - .and_call_original - - worker.perform(project.id, [], %w(repository_size)) - end - it 'refreshes the method caches' do expect_any_instance_of(Repository).to receive(:refresh_method_caches) .with(%i(readme)) @@ -51,6 +43,18 @@ describe ProjectCacheWorker do worker.perform(project.id, %w(readme)) end + context 'with statistics' do + let(:statistics) { %w(repository_size) } + + it 'updates the project statistics' do + expect(worker).to receive(:update_statistics) + .with(kind_of(Project), statistics) + .and_call_original + + worker.perform(project.id, [], statistics) + end + end + context 'with plain readme' do it 'refreshes the method caches' do allow(MarkupHelper).to receive(:gitlab_markdown?).and_return(false) @@ -66,25 +70,34 @@ describe ProjectCacheWorker do end describe '#update_statistics' do + let(:statistics) { %w(repository_size) } + context 'when a lease could not be obtained' do - it 'does not update the repository size' do + it 'does not update the project statistics' do stub_exclusive_lease_taken(lease_key, timeout: lease_timeout) - expect(statistics).not_to receive(:refresh!) + expect(Projects::UpdateStatisticsService).not_to receive(:new) - worker.update_statistics(project) + expect(UpdateProjectStatisticsWorker).not_to receive(:perform_in) + + worker.update_statistics(project, statistics) end end context 'when a lease could be obtained' do - it 'updates the project statistics' do + it 'updates the project statistics twice' do stub_exclusive_lease(lease_key, timeout: lease_timeout) - expect(statistics).to receive(:refresh!) - .with(only: %i(repository_size)) + expect(Projects::UpdateStatisticsService).to receive(:new) + .with(project, nil, statistics: statistics) + .and_call_original + .twice + + expect(UpdateProjectStatisticsWorker).to receive(:perform_in) + .with(lease_timeout, project.id, statistics) .and_call_original - worker.update_statistics(project, %i(repository_size)) + worker.update_statistics(project, statistics) end end end diff --git a/spec/workers/update_project_statistics_worker_spec.rb b/spec/workers/update_project_statistics_worker_spec.rb new file mode 100644 index 00000000000..a268fd2e4ba --- /dev/null +++ b/spec/workers/update_project_statistics_worker_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe UpdateProjectStatisticsWorker do + let(:worker) { described_class.new } + let(:project) { create(:project, :repository) } + let(:statistics) { %w(repository_size) } + + describe '#perform' do + it 'updates the project statistics' do + expect(Projects::UpdateStatisticsService).to receive(:new) + .with(project, nil, statistics: statistics) + .and_call_original + + worker.perform(project.id, statistics) + end + end +end diff --git a/yarn.lock b/yarn.lock index 571ac952e90..41758181610 100644 --- a/yarn.lock +++ b/yarn.lock @@ -658,10 +658,10 @@ eslint-plugin-promise "^4.1.1" eslint-plugin-vue "^5.0.0" -"@gitlab/svgs@^1.58.0": - version "1.58.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.58.0.tgz#bb05263ff2eb7ca09a25cd14d0b1a932d2ea9c2f" - integrity sha512-RlWSjjBT4lMIFuNC1ziCO1nws9zqZtxCjhrqK2DxDDTgp2W0At9M/BFkHp8RHyMCrO3g1fHTrLPUgzr5oR3Epg== +"@gitlab/svgs@^1.59.0": + version "1.59.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.59.0.tgz#affcf9596d736836d37469bb4aea2226ac03e087" + integrity sha512-dokGyyLRRsoBKO70KP1g+ZsDGyTK/RIHWDmvWI6Bx5AxQ3UqAzVXn2OIb3owjJAexyRG1uBmJrriiVVyHznQ4g== "@gitlab/ui@^3.2.0": version "3.2.0" |