diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml index 1b391bb163a..15a61e68c06 100644 --- a/.rubocop_manual_todo.yml +++ b/.rubocop_manual_todo.yml @@ -804,7 +804,6 @@ Graphql/Descriptions: - 'ee/app/graphql/types/epic_health_status_type.rb' - 'ee/app/graphql/types/epic_issue_type.rb' - 'ee/app/graphql/types/epic_tree/epic_tree_node_input_type.rb' - - 'ee/app/graphql/types/epic_type.rb' - 'ee/app/graphql/types/external_issue_type.rb' - 'ee/app/graphql/types/geo/geo_node_type.rb' - 'ee/app/graphql/types/geo/merge_request_diff_registry_type.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 8455685fcc5..b56015c9de1 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -2c7c204731f6e4f1c8cdb3d8a705caf7acf6689d +c73d7cae656b0bedfa40a4865c8d886516eda78b diff --git a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js index 6ce5ebda241..bd7909bfa76 100644 --- a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js +++ b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js @@ -67,13 +67,23 @@ export const publishReview = ({ commit, dispatch, getters }) => { .catch(() => commit(types.RECEIVE_PUBLISH_REVIEW_ERROR)); }; -export const updateDiscussionsAfterPublish = ({ dispatch, getters, rootGetters }) => - dispatch('fetchDiscussions', { path: getters.getNotesData.discussionsPath }, { root: true }).then( - () => - dispatch('diffs/assignDiscussionsToDiff', rootGetters.discussionsStructuredByLineCode, { - root: true, - }), - ); +export const updateDiscussionsAfterPublish = async ({ dispatch, getters, rootGetters }) => { + if (window.gon?.features?.paginatedNotes) { + await dispatch('stopPolling', null, { root: true }); + await dispatch('fetchData', null, { root: true }); + await dispatch('restartPolling', null, { root: true }); + } else { + await dispatch( + 'fetchDiscussions', + { path: getters.getNotesData.discussionsPath }, + { root: true }, + ); + } + + dispatch('diffs/assignDiscussionsToDiff', rootGetters.discussionsStructuredByLineCode, { + root: true, + }); +}; export const updateDraft = ( { commit, getters }, diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 62454483ca1..c0468e5df0f 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -4,6 +4,7 @@ import OrderedLayout from '~/vue_shared/components/ordered_layout.vue'; import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user'; import { __ } from '~/locale'; import initUserPopovers from '~/user_popovers'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility'; import { deprecatedCreateFlash as Flash } from '../../flash'; import * as constants from '../constants'; @@ -30,6 +31,7 @@ export default { discussionFilterNote, OrderedLayout, }, + mixins: [glFeatureFlagsMixin()], props: { noteableData: { type: Object, @@ -57,7 +59,6 @@ export default { }, data() { return { - isFetching: false, currentFilter: null, }; }, @@ -68,6 +69,7 @@ export default { 'convertedDisscussionIds', 'getNotesDataByProp', 'isLoading', + 'isFetching', 'commentsDisabled', 'getNoteableData', 'userCanReply', @@ -103,6 +105,13 @@ export default { }, }, watch: { + async isFetching() { + if (!this.isFetching) { + await this.$nextTick(); + await this.startTaskList(); + await this.checkLocationHash(); + } + }, shouldShow() { if (!this.isNotesFetched) { this.fetchNotes(); @@ -153,6 +162,7 @@ export default { }, methods: { ...mapActions([ + 'setFetchingState', 'setLoadingState', 'fetchDiscussions', 'poll', @@ -183,7 +193,11 @@ export default { fetchNotes() { if (this.isFetching) return null; - this.isFetching = true; + this.setFetchingState(true); + + if (this.glFeatures.paginatedNotes) { + return this.initPolling(); + } return this.fetchDiscussions(this.getFetchDiscussionsConfig()) .then(this.initPolling) @@ -191,11 +205,8 @@ export default { this.setLoadingState(false); this.setNotesFetchedState(true); eventHub.$emit('fetchedNotesData'); - this.isFetching = false; + this.setFetchingState(false); }) - .then(this.$nextTick) - .then(this.startTaskList) - .then(this.checkLocationHash) .catch(() => { this.setLoadingState(false); this.setNotesFetchedState(true); diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 057c97d3d1e..ddc6c44a4e5 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -15,6 +15,7 @@ import loadAwardsHandler from '../../awards_handler'; import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils'; import { mergeUrlParams } from '../../lib/utils/url_utility'; +import eventHub from '../event_hub'; import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub'; import * as utils from './utils'; import * as types from './mutation_types'; @@ -420,14 +421,25 @@ export const saveNote = ({ commit, dispatch }, noteData) => { .catch(processErrors); }; -const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => { +export const setFetchingState = ({ commit }, fetchingState) => + commit(types.SET_NOTES_FETCHING_STATE, fetchingState); + +const pollSuccessCallBack = async (resp, commit, state, getters, dispatch) => { if (state.isResolvingDiscussion) { return null; } + if (window.gon?.features?.paginatedNotes && !resp.more && state.isFetching) { + eventHub.$emit('fetchedNotesData'); + dispatch('setFetchingState', false); + dispatch('setNotesFetchedState', true); + dispatch('setLoadingState', false); + } + if (resp.notes?.length) { - dispatch('updateOrCreateNotes', resp.notes); + await dispatch('updateOrCreateNotes', resp.notes); dispatch('startTaskList'); + dispatch('updateResolvableDiscussionsCounts'); } commit(types.SET_LAST_FETCHED_AT, resp.last_fetched_at); diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 5891a2e63e3..43d99937b8d 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -48,6 +48,8 @@ export const persistSortOrder = (state) => state.persistSortOrder; export const timelineEnabled = (state) => state.isTimelineEnabled; +export const isFetching = (state) => state.isFetching; + export const isLoading = (state) => state.isLoading; export const getNotesDataByProp = (state) => (prop) => state.notesData[prop]; diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index 144a3d7ba90..c1738eb20da 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -47,6 +47,7 @@ export default () => ({ unresolvedDiscussionsCount: 0, descriptionVersions: {}, isTimelineEnabled: false, + isFetching: false, }, actions, getters, diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index 5c4f62f4575..2e8b728e013 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -14,6 +14,7 @@ export const UPDATE_NOTE = 'UPDATE_NOTE'; export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION'; export const UPDATE_DISCUSSION_POSITION = 'UPDATE_DISCUSSION_POSITION'; export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES'; +export const SET_NOTES_FETCHING_STATE = 'SET_NOTES_FETCHING_STATE'; export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE'; export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE'; export const DISABLE_COMMENTS = 'DISABLE_COMMENTS'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 4c8392f7220..536b47667c2 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -32,6 +32,20 @@ export default { } } + if (window.gon?.features?.paginatedNotes && note.base_discussion) { + if (discussion.diff_file) { + discussion.file_hash = discussion.diff_file.file_hash; + + discussion.truncated_diff_lines = utils.prepareDiffLines( + discussion.truncated_diff_lines || [], + ); + } + + discussion.resolvable = note.resolvable; + discussion.expanded = note.base_discussion.expanded; + discussion.resolved = note.resolved; + } + // note.base_discussion = undefined; // No point keeping a reference to this delete note.base_discussion; discussion.notes = [note]; @@ -323,6 +337,10 @@ export default { state.isLoading = value; }, + [types.SET_NOTES_FETCHING_STATE](state, value) { + state.isFetching = value; + }, + [types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) { const discussion = utils.findNoteObjectById(state.discussions, discussionId); diff --git a/app/assets/javascripts/pages/projects/compare/index/index.js b/app/assets/javascripts/pages/projects/compare/index/index.js new file mode 100644 index 00000000000..b86c9ec442f --- /dev/null +++ b/app/assets/javascripts/pages/projects/compare/index/index.js @@ -0,0 +1,3 @@ +import initCompareSelector from '~/projects/compare'; + +initCompareSelector(); diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index 8a7dbf890ab..d81c11ddbaf 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -224,11 +224,11 @@ export default { repositoryHelpText() { if (this.visibilityLevel === visibilityOptions.PRIVATE) { - return s__('ProjectSettings|View and edit files in this project'); + return s__('ProjectSettings|View and edit files in this project.'); } return s__( - 'ProjectSettings|View and edit files in this project. Non-project members will only have read access', + 'ProjectSettings|View and edit files in this project. Non-project members will only have read access.', ); }, }, @@ -400,7 +400,7 @@ export default { name="project[request_access_enabled]" /> - {{ s__('ProjectSettings|Allow users to request access') }} + {{ s__('ProjectSettings|Users can request access') }} @@ -411,7 +411,7 @@ export default { @@ -529,7 +527,7 @@ export default { :help-path="packagesHelpPath" :label="s__('ProjectSettings|Packages')" :help-text=" - s__('ProjectSettings|Every project can have its own space to store its packages') + s__('ProjectSettings|Every project can have its own space to store its packages.') " > {{ - s__( - 'ProjectSettings|This setting will override user notification preferences for all project members.', - ) + s__('ProjectSettings|Override user notification preferences for all project members.') }} @@ -665,7 +657,7 @@ export default { {{ s__('ProjectSettings|Show default award emojis') }} @@ -683,9 +675,7 @@ export default { {{ s__('ProjectSettings|Allow editing commit messages') }} diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue index 8021ff5a585..13f314a3a45 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue @@ -1,7 +1,7 @@ diff --git a/app/assets/javascripts/projects/compare/components/app.vue b/app/assets/javascripts/projects/compare/components/app.vue new file mode 100644 index 00000000000..05bd0f1370b --- /dev/null +++ b/app/assets/javascripts/projects/compare/components/app.vue @@ -0,0 +1,89 @@ + + + diff --git a/app/assets/javascripts/projects/compare/components/revision_dropdown.vue b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue new file mode 100644 index 00000000000..f657f36322d --- /dev/null +++ b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue @@ -0,0 +1,145 @@ + + + diff --git a/app/assets/javascripts/projects/compare/index.js b/app/assets/javascripts/projects/compare/index.js new file mode 100644 index 00000000000..4337eecb667 --- /dev/null +++ b/app/assets/javascripts/projects/compare/index.js @@ -0,0 +1,33 @@ +import Vue from 'vue'; +import CompareApp from './components/app.vue'; + +export default function init() { + const el = document.getElementById('js-compare-selector'); + const { + refsProjectPath, + paramsFrom, + paramsTo, + projectCompareIndexPath, + projectMergeRequestPath, + createMrPath, + } = el.dataset; + + return new Vue({ + el, + components: { + CompareApp, + }, + render(createElement) { + return createElement(CompareApp, { + props: { + refsProjectPath, + paramsFrom, + paramsTo, + projectCompareIndexPath, + projectMergeRequestPath, + createMrPath, + }, + }); + }, + }); +} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue index f46470418f8..951dc108c51 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue @@ -159,6 +159,7 @@ export default { .then((data) => { this.mr.setApprovals(data); eventHub.$emit('MRWidgetUpdateRequested'); + eventHub.$emit('ApprovalUpdated'); this.$emit('updated'); }) .catch(errFn) diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index 3ff7d436c97..8084ad59f42 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -155,13 +155,14 @@ export default { > {{ $options.monitoringPipelineText }} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue index 3e361ce487b..9c16bf78c93 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue @@ -4,6 +4,7 @@ import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merg import autoMergeEnabledQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { __ } from '~/locale'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { deprecatedCreateFlash as Flash } from '../../../flash'; import statusIcon from '../mr_widget_status_icon.vue'; import MrWidgetAuthor from '../mr_widget_author.vue'; @@ -53,7 +54,11 @@ export default { }, computed: { loading() { - return this.glFeatures.mergeRequestWidgetGraphql && this.$apollo.queries.state.loading; + return ( + this.glFeatures.mergeRequestWidgetGraphql && + this.$apollo.queries.state.loading && + Object.keys(this.state).length === 0 + ); }, mergeUser() { if (this.glFeatures.mergeRequestWidgetGraphql) { @@ -78,7 +83,7 @@ export default { canRemoveSourceBranch() { const { currentUserId } = this.mr; const mergeUserId = this.glFeatures.mergeRequestWidgetGraphql - ? this.state.mergeUser?.id + ? getIdFromGraphQLId(this.state.mergeUser?.id) : this.mr.mergeUserId; const canRemoveSourceBranch = this.glFeatures.mergeRequestWidgetGraphql ? this.state.userPermissions.removeSourceBranch @@ -96,7 +101,11 @@ export default { .cancelAutomaticMerge() .then((res) => res.data) .then((data) => { - eventHub.$emit('UpdateWidgetData', data); + if (this.glFeatures.mergeRequestWidgetGraphql) { + eventHub.$emit('MRWidgetUpdateRequested'); + } else { + eventHub.$emit('UpdateWidgetData', data); + } }) .catch(() => { this.isCancellingAutoMerge = false; @@ -119,6 +128,11 @@ export default { eventHub.$emit('MRWidgetUpdateRequested'); } }) + .then(() => { + if (this.glFeatures.mergeRequestWidgetGraphql) { + this.$apollo.queries.state.refetch(); + } + }) .catch(() => { this.isRemovingSourceBranch = false; Flash(__('Something went wrong. Please try again.')); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index bf86e0d8b07..5127ab3d400 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -7,7 +7,7 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import simplePoll from '../../../lib/utils/simple_poll'; import eventHub from '../../event_hub'; import statusIcon from '../mr_widget_status_icon.vue'; -import rebaseQuery from '../../queries/states/ready_to_merge.query.graphql'; +import rebaseQuery from '../../queries/states/rebase.query.graphql'; import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; import { deprecatedCreateFlash as Flash } from '../../../flash'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index 89862dc09e6..58337ea8f67 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -53,8 +53,8 @@ export default { result({ data }) { this.state = { ...data.project.mergeRequest, - mergeRequestsFfOnlyEnabled: data.mergeRequestsFfOnlyEnabled, - onlyAllowMergeIfPipelineSucceeds: data.onlyAllowMergeIfPipelineSucceeds, + mergeRequestsFfOnlyEnabled: data.project.mergeRequestsFfOnlyEnabled, + onlyAllowMergeIfPipelineSucceeds: data.project.onlyAllowMergeIfPipelineSucceeds, }; this.removeSourceBranch = data.project.mergeRequest.shouldRemoveSourceBranch; this.commitMessage = data.project.mergeRequest.defaultMergeCommitMessage; @@ -277,7 +277,20 @@ export default { return this.mr.mergeRequestDiffsPath; }, }, + mounted() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + eventHub.$on('ApprovalUpdated', this.updateGraphqlState); + } + }, + beforeDestroy() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + eventHub.$off('ApprovalUpdated', this.updateGraphqlState); + } + }, methods: { + updateGraphqlState() { + return this.$apollo.queries.state.refetch(); + }, updateMergeCommitMessage(includeDescription) { const commitMessage = this.glFeatures.mergeRequestWidgetGraphql ? this.state.defaultMergeCommitMessage @@ -326,6 +339,10 @@ export default { } else if (hasError) { eventHub.$emit('FailedToMerge', data.merge_error); } + + if (this.glFeatures.mergeRequestWidgetGraphql) { + this.updateGraphqlState(); + } }) .catch(() => { this.isMakingRequest = false; @@ -532,7 +549,7 @@ export default { { diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql index 44fc1cc7f23..b284bb23969 100644 --- a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql +++ b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql @@ -18,6 +18,7 @@ query getState($projectPath: ID!, $iid: String!) { } shouldBeRebased sourceBranchExists + state targetBranchExists userPermissions { canMerge diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql index 64cd70fcf42..ad715599eb1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql +++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql @@ -1,6 +1,7 @@ fragment autoMergeEnabled on MergeRequest { autoMergeStrategy mergeUser { + id name username webUrl diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql index bdcb7a8206b..daf21e75b3b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql +++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql @@ -4,7 +4,6 @@ query autoMergeEnabledQuery($projectPath: ID!, $iid: String!) { project(fullPath: $projectPath) { mergeRequest(iid: $iid) { ...autoMergeEnabled - mergeTrainsCount } } } diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index a68989fdfed..78a17493d31 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -156,9 +156,9 @@ export default class MergeRequestStore { this.setState(); - mrEventHub.$emit('mr.state.updated', { - state: this.mergeRequestState, - }); + if (!window.gon?.features?.mergeRequestWidgetGraphql) { + this.emitUpdatedState(); + } } setGraphqlData(project) { @@ -182,7 +182,9 @@ export default class MergeRequestStore { this.isSHAMismatch = this.sha !== mergeRequest.diffHeadSha; this.shouldBeRebased = mergeRequest.shouldBeRebased; this.workInProgress = mergeRequest.workInProgress; + this.mergeRequestState = mergeRequest.state; + this.emitUpdatedState(); this.setState(); } @@ -208,6 +210,12 @@ export default class MergeRequestStore { } } + emitUpdatedState() { + mrEventHub.$emit('mr.state.updated', { + state: this.mergeRequestState, + }); + } + setPaths(data) { // Paths are set on the first load of the page and not auto-refreshed this.squashBeforeMergeHelpPath = data.squash_before_merge_help_path; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 1648c5e0a42..8251cdb9bbb 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -992,6 +992,20 @@ pre.light-well { width: auto; } } + + // Remove once gitlab/ui solution is implemented: + // https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1157 + // https://gitlab.com/gitlab-org/gitlab/-/issues/300405 + .gl-search-box-by-type-input { + width: 100%; + } + + // Remove once gitlab/ui solution is implemented + // https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1158 + // https://gitlab.com/gitlab-org/gitlab/-/issues/300405 + .gl-new-dropdown-button-text { + @include str-truncated; + } } .clearable-input { diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index bfa7a30bc65..2cef43f19ab 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -31,9 +31,9 @@ module NotesActions # We know there's more data, so tell the frontend to poll again after 1ms set_polling_interval_header(interval: 1) if meta[:more] - # Only present an ETag for the empty response to ensure pagination works - # as expected - ::Gitlab::EtagCaching::Middleware.skip!(response) if notes.present? + # We might still want to investigate further adjusting ETag caching with paginated notes, but + # let's avoid ETag caching for now until we confirm the viability of paginated notes. + ::Gitlab::EtagCaching::Middleware.skip!(response) render json: meta.merge(notes: notes) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 65d9b475d2a..88f59484cdd 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -44,6 +44,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo push_frontend_feature_flag(:codequality_mr_diff, @project) push_frontend_feature_flag(:suggestions_custom_commit, @project) push_frontend_feature_flag(:local_file_reviews, default_enabled: :yaml) + push_frontend_feature_flag(:paginated_notes, @project, default_enabled: :yaml) record_experiment_user(:invite_members_version_a) record_experiment_user(:invite_members_version_b) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 5b7c040bcd7..40e6590d85c 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -9,7 +9,7 @@ class SearchController < ApplicationController around_action :allow_gitaly_ref_name_caching - before_action :block_anonymous_global_searches + before_action :block_anonymous_global_searches, except: :opensearch skip_before_action :authenticate_user! requires_cross_project_access if: -> do search_term_present = params[:search].present? || params[:term].present? diff --git a/app/graphql/types/merge_request_state_enum.rb b/app/graphql/types/merge_request_state_enum.rb index 92f52726ab3..c14b9f80a53 100644 --- a/app/graphql/types/merge_request_state_enum.rb +++ b/app/graphql/types/merge_request_state_enum.rb @@ -5,6 +5,6 @@ module Types graphql_name 'MergeRequestState' description 'State of a GitLab merge request' - value 'merged' + value 'merged', description: "Merge Request has been merged" end end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 871d19c6a8c..62580124c0f 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -175,7 +175,9 @@ module NotesHelper end end - def notes_data(issuable) + def notes_data(issuable, start_at_zero = false) + initial_last_fetched_at = start_at_zero ? 0 : Time.current.to_i * ::Gitlab::UpdatedNotesPaginator::MICROSECOND + data = { discussionsPath: discussions_path(issuable), registerPath: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'), @@ -186,7 +188,7 @@ module NotesHelper reopenPath: reopen_issuable_path(issuable), notesPath: notes_url, prerenderedNotesCount: issuable.capped_notes_count(MAX_PRERENDERED_NOTES), - lastFetchedAt: Time.now.to_i * ::Gitlab::UpdatedNotesPaginator::MICROSECOND + lastFetchedAt: initial_last_fetched_at } if issuable.is_a?(MergeRequest) diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index f24aa5d3bcb..c44a67d3e66 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -6,28 +6,6 @@ module TreeHelper FILE_LIMIT = 1_000 - # Sorts a repository's tree so that folders are before files and renders - # their corresponding partials - # - # tree - A `Tree` object for the current tree - # rubocop: disable CodeReuse/ActiveRecord - def render_tree(tree) - # Sort submodules and folders together by name ahead of files - folders, files, submodules = tree.trees, tree.blobs, tree.submodules - tree = [] - items = (folders + submodules).sort_by(&:name) + files - - if items.size > FILE_LIMIT - tree << render(partial: 'projects/tree/truncated_notice_tree_row', - locals: { limit: FILE_LIMIT, total: items.size }) - items = items.take(FILE_LIMIT) - end - - tree << render(partial: 'projects/tree/tree_row', collection: items) if items.present? - tree.join.html_safe - end - # rubocop: enable CodeReuse/ActiveRecord - # Return an image icon depending on the file type and mode # # type - String type of the tree item; either 'folder' or 'file' @@ -37,20 +15,6 @@ module TreeHelper sprite_icon(file_type_icon_class(type, mode, name)) end - # Using Rails `*_path` methods can be slow, especially when generating - # many paths, as with a repository tree that has thousands of items. - def fast_project_blob_path(project, blob_path) - ActionDispatch::Journey::Router::Utils.escape_path( - File.join(relative_url_root, project.path_with_namespace, '-', 'blob', blob_path) - ) - end - - def fast_project_tree_path(project, tree_path) - ActionDispatch::Journey::Router::Utils.escape_path( - File.join(relative_url_root, project.path_with_namespace, '-', 'tree', tree_path) - ) - end - # Simple shortcut to File.join def tree_join(*args) File.join(*args) @@ -167,13 +131,6 @@ module TreeHelper Gitlab.config.gitlab.relative_url_root.presence || '/' end - # project and path are used on the EE version - def tree_content_data(logs_path, project, path) - { - "logs-path" => logs_path - } - end - def breadcrumb_data_attributes attrs = { can_collaborate: can_collaborate_with_project?(@project).to_s, diff --git a/app/serializers/base_discussion_entity.rb b/app/serializers/base_discussion_entity.rb index 5ca4d1d6cc9..8d4c3906847 100644 --- a/app/serializers/base_discussion_entity.rb +++ b/app/serializers/base_discussion_entity.rb @@ -15,6 +15,7 @@ class BaseDiscussionEntity < Grape::Entity expose :for_commit?, as: :for_commit expose :individual_note?, as: :individual_note expose :resolvable?, as: :resolvable + expose :resolved_by_push?, as: :resolved_by_push expose :truncated_diff_lines, using: DiffLineEntity, if: -> (d, _) { d.diff_discussion? && d.on_text? && (d.expanded? || render_truncated_diff_lines?) } diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb index 4edcff0e3d0..19b9b439fed 100644 --- a/app/services/git/branch_hooks_service.rb +++ b/app/services/git/branch_hooks_service.rb @@ -44,11 +44,7 @@ module Git 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 + paths = commit_paths.values.reduce(&:merge) || Set.new Gitlab::FileDetector.types_in_paths(paths) end @@ -77,6 +73,7 @@ module Git enqueue_process_commit_messages enqueue_jira_connect_sync_messages enqueue_metrics_dashboard_sync + track_ci_config_change_event end def branch_remove_hooks @@ -89,6 +86,18 @@ module Git ::Metrics::Dashboard::SyncDashboardsWorker.perform_async(project.id) end + def track_ci_config_change_event + return unless Gitlab::CurrentSettings.usage_ping_enabled? + return unless ::Feature.enabled?(:usage_data_unique_users_committing_ciconfigfile, project, default_enabled: :yaml) + return unless default_branch? + + commits_changing_ci_config.each do |commit| + Gitlab::UsageDataCounters::HLLRedisCounter.track_event( + 'o_pipeline_authoring_unique_users_committing_ciconfigfile', values: commit.author&.id + ) + end + end + # Schedules processing of commit messages def enqueue_process_commit_messages referencing_commits = limited_commits.select(&:matches_cross_reference_regex?) @@ -190,6 +199,23 @@ module Git set end + + def commits_changing_ci_config + commit_paths.select do |commit, paths| + next if commit.merge_commit? + + paths.include?(project.ci_config_path_or_default) + end.keys + end + + def commit_paths + strong_memoize(:commit_paths) do + limited_commits.map do |commit| + paths = Set.new(commit.raw_deltas.map(&:new_path)) + [commit, paths] + end.to_h + end + end end end diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 505ee40182e..e8b2c5db4e6 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -84,8 +84,7 @@ = s_('Profiles|You must transfer ownership or delete these groups before you can delete your account.') - elsif !current_user.can_remove_self? %p - - reset_link = reset_profile_password_path - = s_('Profiles|GitLab is unable to verify your identity automatically. For security purposes, you must set a password by %{openingTag}resetting your password%{closingTag} to delete your account.').html_safe % { openingTag: "".html_safe, closingTag: ''.html_safe} + = s_('Profiles|GitLab is unable to verify your identity automatically. For security purposes, you must set a password by %{openingTag}resetting your password%{closingTag} to delete your account.').html_safe % { openingTag: "".html_safe, closingTag: ''.html_safe} %p = s_('Profiles|If after setting a password, the option to delete your account is still not available, please email %{data_request} to begin the account deletion process.').html_safe % { data_request: mail_to('personal-data-request@gitlab.com') } - else diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 9ea4d3df631..fcf073e1e09 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -8,13 +8,13 @@ = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name gl-ml-3 qa-branch-name' do = branch.name - if branch.name == @repository.root_ref - %span.badge.badge-primary.gl-ml-2 default + %span.badge.gl-badge.sm.badge-pill.badge-primary.gl-ml-2 default - elsif merged - %span.badge.badge-info.has-tooltip.gl-ml-2{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } } + %span.badge.gl-badge.sm.badge-pill.badge-info.has-tooltip.gl-ml-2{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } } = s_('Branches|merged') - if protected_branch?(@project, branch) - %span.badge.badge-success.gl-ml-2 + %span.badge.gl-badge.sm.badge-pill.badge-success.gl-ml-2 = s_('Branches|protected') = render_if_exists 'projects/branches/diverged_from_upstream', branch: branch diff --git a/app/views/projects/buttons/_remove_tag.html.haml b/app/views/projects/buttons/_remove_tag.html.haml index ae776e93203..68a9d715674 100644 --- a/app/views/projects/buttons/_remove_tag.html.haml +++ b/app/views/projects/buttons/_remove_tag.html.haml @@ -2,5 +2,5 @@ - tag = local_assigns.fetch(:tag, nil) - return unless project && tag -%button{ type: "button", class: "js-remove-tag js-confirm-modal-button gl-button btn btn-danger remove-row has-tooltip gl-ml-3 #{protected_tag?(project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), data: { container: 'body', path: project_tag_path(@project, tag.name), modal_attributes: delete_tag_modal_attributes(tag.name) } } +%button{ type: "button", class: "js-remove-tag js-confirm-modal-button gl-button btn btn-danger btn-icon remove-row has-tooltip gl-ml-3 #{protected_tag?(project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), data: { container: 'body', path: project_tag_path(@project, tag.name), modal_attributes: delete_tag_modal_attributes(tag.name) } } = sprite_icon("remove") diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index e45ea209e8c..3f9aa24a569 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -13,4 +13,8 @@ = html_escape(_("Changes are shown as if the %{b_open}source%{b_close} revision was being merged into the %{b_open}target%{b_close} revision.")) % { b_open: ''.html_safe, b_close: ''.html_safe } .prepend-top-20 - = render "form" + #js-compare-selector{ data: { project_compare_index_path: project_compare_index_path(@project), + refs_project_path: refs_project_path(@project), + params_from: params[:from], params_to: params[:to], + project_merge_request_path: @merge_request.present? ? project_merge_request_path(@project, @merge_request) : '', + create_mr_path: create_mr_button? ? create_mr_path : '' } } diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 9e441eac602..962e1158118 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -16,7 +16,7 @@ .settings-header %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Visibility, project features, permissions') %button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand') - %p= _('Choose visibility level, enable/disable project features (issues, repository, wiki, snippets) and set permissions.') + %p= _('Choose visibility level, enable/disable project features and their permissions, disable email notifications, and show default award emoji.') .settings-content = form_for @project, remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 879365bffb4..f20a4094f8f 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -56,10 +56,13 @@ = render "projects/merge_requests/widget" = render "projects/merge_requests/awards_block" - if mr_action === "show" - - add_page_startup_api_call discussions_path(@merge_request) + - if Feature.enabled?(:paginated_notes, @project) + - add_page_startup_api_call notes_url + - else + - add_page_startup_api_call discussions_path(@merge_request) - add_page_startup_api_call widget_project_json_merge_request_path(@project, @merge_request, format: :json) - add_page_startup_api_call cached_widget_project_json_merge_request_path(@project, @merge_request, format: :json) - #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request).to_json, + #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request, Feature.enabled?(:paginated_notes, @project)).to_json, noteable_data: serialize_issuable(@merge_request, serializer: 'noteable'), noteable_type: 'MergeRequest', target_type: 'merge_request', diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 30ba22ba53c..99672ded6db 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -5,8 +5,8 @@ .project-network.gl-border-1.gl-border-solid.gl-border-gray-300 .controls.gl-bg-gray-50.gl-p-2.gl-font-base.gl-text-gray-400.gl-border-b-1.gl-border-b-solid.gl-border-b-gray-300 = form_tag project_network_path(@project, @id), method: :get, class: 'form-inline network-form' do |f| - = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: _("Git revision"), class: 'search-input form-control input-mx-250 search-sha' - = button_tag class: 'btn btn-success' do + = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: _("Git revision"), class: 'search-input form-control gl-form-input input-mx-250 search-sha gl-mr-2' + = button_tag class: 'btn gl-button btn-success btn-icon' do = sprite_icon('search') .inline.gl-ml-5 .form-check.light diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 9d4e5d629f4..61b357831fd 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -41,6 +41,6 @@ = render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name] - if can?(current_user, :admin_tag, @project) - = link_to edit_project_tag_release_path(@project, tag.name), class: 'btn btn-edit has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do + = link_to edit_project_tag_release_path(@project, tag.name), class: 'btn gl-button btn-default btn-icon btn-edit has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do = sprite_icon("pencil") = render 'projects/buttons/remove_tag', project: @project, tag: tag diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 2fe5c5888f5..04d8c1f42bc 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -24,9 +24,9 @@ %li = link_to title, filter_tags_path(sort: value), class: ("is-active" if @sort == value) - if can?(current_user, :admin_tag, @project) - = link_to new_project_tag_path(@project), class: 'btn btn-success new-tag-btn', data: { qa_selector: "new_tag_button" } do + = link_to new_project_tag_path(@project), class: 'btn gl-button btn-success', data: { qa_selector: "new_tag_button" } do = s_('TagsPage|New tag') - = link_to project_tags_path(@project, rss_url_options), title: _("Tags feed"), class: 'btn btn-svg d-none d-sm-inline-block has-tooltip' do + = link_to project_tags_path(@project, rss_url_options), title: _("Tags feed"), class: 'btn gl-button btn-default btn-icon d-none d-sm-inline-block has-tooltip' do = sprite_icon('rss', css_class: 'qa-rss-icon') = render_if_exists 'projects/commits/mirror_status' diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml deleted file mode 100644 index 6d2bdda8254..00000000000 --- a/app/views/projects/tree/_readme.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -- if readme.rich_viewer - %article.file-holder.readme-holder{ id: 'readme', class: [("limited-width-container" unless fluid_layout)] } - .js-file-title.file-title-flex-parent - .file-header-content - = blob_icon readme.mode, readme.name - = link_to project_blob_path(@project, tree_join(@ref, readme.path)) do - %strong - = readme.name - - = render 'projects/blob/viewer', viewer: readme.rich_viewer, viewer_url: project_blob_path(@project, tree_join(@ref, readme.path), viewer: :rich, format: :json) diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml deleted file mode 100644 index a4427c6eedb..00000000000 --- a/app/views/projects/tree/_tree_content.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -.tree-content-holder.js-tree-content{ data: tree_content_data(@logs_path, @project, @path) } - .table-holder.bordered-box - %table.table#tree-slider{ class: "table_#{@hex_path} tree-table" } - %thead - %tr - %th= s_('ProjectFileTree|Name') - %th.d-none.d-sm-table-cell - .float-left= _('Last commit') - %th.text-right= _('Last update') - - if @path.present? - %tr.tree-item - %td.tree-item-file-name - = link_to "..", project_tree_path(@project, up_dir_path), class: 'gl-ml-3' - %td - %td.d-none.d-sm-table-cell - - = render_tree(tree) - - - if tree.readme - = render "projects/tree/readme", readme: tree.readme - -- if can_edit_tree? - = render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, @id), method: :post - = render 'projects/blob/new_dir' diff --git a/app/views/projects/tree/_tree_row.html.haml b/app/views/projects/tree/_tree_row.html.haml deleted file mode 100644 index 04496914c02..00000000000 --- a/app/views/projects/tree/_tree_row.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -- tree_row_name = tree_row.name -- tree_row_type = tree_row.type - -%tr{ class: "tree-item file_#{hexdigest(tree_row_name)}" } - %td.tree-item-file-name - - if tree_row_type == :tree - = tree_icon('folder', tree_row.mode, tree_row.name) - - path = flatten_tree(@path, tree_row) - %a.str-truncated{ href: fast_project_tree_path(@project, tree_join(@id || @commit.id, path)), title: path } - %span= path - - - elsif tree_row_type == :blob - = tree_icon('file', tree_row.mode, tree_row_name) - %a.str-truncated{ href: fast_project_blob_path(@project, tree_join(@id || @commit.id, tree_row_name)), title: tree_row_name } - %span= tree_row_name - - if @lfs_blob_ids.include?(tree_row.id) - %span.badge.label-lfs.gl-ml-2 LFS - - - elsif tree_row_type == :commit - = tree_icon('archive', tree_row.mode, tree_row.name) - = submodule_link(tree_row, @ref) - - %td.d-none.d-sm-table-cell.tree-commit - %td.tree-time-ago.text-right - %span.log_loading.hide - = loading_icon - Loading commit data... diff --git a/app/views/projects/tree/_truncated_notice_tree_row.html.haml b/app/views/projects/tree/_truncated_notice_tree_row.html.haml deleted file mode 100644 index a03e0a549ee..00000000000 --- a/app/views/projects/tree/_truncated_notice_tree_row.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -%tr.tree-truncated-warning - %td{ colspan: '3' } - = sprite_icon('warning-solid') - %span - Too many items to show. To preserve performance only - %strong #{number_with_delimiter(limit)} of #{number_with_delimiter(total)} - items are displayed. diff --git a/changelogs/unreleased/300847-fix-link.yml b/changelogs/unreleased/300847-fix-link.yml new file mode 100644 index 00000000000..adb25997c00 --- /dev/null +++ b/changelogs/unreleased/300847-fix-link.yml @@ -0,0 +1,5 @@ +--- +title: Fixes broken password reset link in account deletion message +merge_request: 53274 +author: +type: fixed diff --git a/changelogs/unreleased/consistent-question-mark-mr.yml b/changelogs/unreleased/consistent-question-mark-mr.yml new file mode 100644 index 00000000000..ba8f1ea2c29 --- /dev/null +++ b/changelogs/unreleased/consistent-question-mark-mr.yml @@ -0,0 +1,5 @@ +--- +title: Update question mark icon while checking pipeline status +merge_request: 52760 +author: Yogi (@yo) +type: other diff --git a/changelogs/unreleased/fix-opensearch.yml b/changelogs/unreleased/fix-opensearch.yml new file mode 100644 index 00000000000..2ded63e6fdd --- /dev/null +++ b/changelogs/unreleased/fix-opensearch.yml @@ -0,0 +1,5 @@ +--- +title: Fix opensearch for anonymous users +merge_request: 53056 +author: +type: fixed diff --git a/changelogs/unreleased/gl-badge-branch-list.yml b/changelogs/unreleased/gl-badge-branch-list.yml new file mode 100644 index 00000000000..0b5f89c6eda --- /dev/null +++ b/changelogs/unreleased/gl-badge-branch-list.yml @@ -0,0 +1,5 @@ +--- +title: Apply new GitLab UI for badges in the project branch list +merge_request: 52868 +author: Yogi (@yo) +type: other diff --git a/changelogs/unreleased/gl-button-tags.yml b/changelogs/unreleased/gl-button-tags.yml new file mode 100644 index 00000000000..5415c3a6502 --- /dev/null +++ b/changelogs/unreleased/gl-button-tags.yml @@ -0,0 +1,5 @@ +--- +title: Apply new GitLab UI for buttons in tags page +merge_request: 52862 +author: Yogi (@yo) +type: other diff --git a/changelogs/unreleased/gl-ui-graph-page.yml b/changelogs/unreleased/gl-ui-graph-page.yml new file mode 100644 index 00000000000..b98c6053f99 --- /dev/null +++ b/changelogs/unreleased/gl-ui-graph-page.yml @@ -0,0 +1,5 @@ +--- +title: Apply new GitLab UI for buttons and input in the project graph page +merge_request: 52864 +author: Yogi (@yo) +type: other diff --git a/changelogs/unreleased/russell-update-project-visibility-settings-ui-text.yml b/changelogs/unreleased/russell-update-project-visibility-settings-ui-text.yml new file mode 100644 index 00000000000..a1d09aec758 --- /dev/null +++ b/changelogs/unreleased/russell-update-project-visibility-settings-ui-text.yml @@ -0,0 +1,5 @@ +--- +title: Edited UI copy wording to comply with GitLab style +merge_request: 50676 +author: +type: other diff --git a/config/feature_flags/development/usage_data_unique_users_committing_ciconfigfile.yml b/config/feature_flags/development/usage_data_unique_users_committing_ciconfigfile.yml new file mode 100644 index 00000000000..ebe8125aa65 --- /dev/null +++ b/config/feature_flags/development/usage_data_unique_users_committing_ciconfigfile.yml @@ -0,0 +1,8 @@ +--- +name: usage_data_unique_users_committing_ciconfigfile +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52172 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/299403 +milestone: '13.9' +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/doc/administration/img/time_zone_settings.png b/doc/administration/img/time_zone_settings.png new file mode 100644 index 00000000000..73961b1090c Binary files /dev/null and b/doc/administration/img/time_zone_settings.png differ diff --git a/doc/administration/timezone.md b/doc/administration/timezone.md index 798e7f5050c..6f460ed0ea8 100644 --- a/doc/administration/timezone.md +++ b/doc/administration/timezone.md @@ -41,3 +41,18 @@ After adding the configuration parameter, reconfigure and restart your GitLab in gitlab-ctl reconfigure gitlab-ctl restart ``` + +## Changing time zone per user + +To allow users to change the time zone in their profile, the feature flag `user_time_settings` should be enabled: + +1. [Start a Rails console session](operations/rails_console.md). +1. Enable the feature flag: + + ```ruby + Feature.enable(:user_time_settings) + ``` + +1. You should now be able to see the timezone dropdown in the users' **Settings > Profile** page. + + ![User Time Zone Settings](img/time_zone_settings.png) diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 6b92b4b17d0..49f0586de45 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -742,6 +742,26 @@ enum AlertManagementIntegrationType { PROMETHEUS } +""" +Parsed field from an alert used for custom mappings +""" +type AlertManagementPayloadAlertField { + """ + Human-readable label of the payload path. + """ + label: String + + """ + Path to value inside payload JSON. + """ + path: [String!] + + """ + Type of the parsed value. + """ + type: AlertManagementPayloadAlertFieldType +} + """ Field that are available while modifying the custom mapping attributes for an HTTP integration """ @@ -1571,12 +1591,12 @@ Represents an epic on an issue board """ type BoardEpic implements CurrentUserTodos & Noteable { """ - Author of the epic + Author of the epic. """ author: User! """ - A list of award emojis associated with the epic + A list of award emojis associated with the epic. """ awardEmoji( """ @@ -1601,7 +1621,7 @@ type BoardEpic implements CurrentUserTodos & Noteable { ): AwardEmojiConnection """ - Children (sub-epics) of the epic + Children (sub-epics) of the epic. """ children( """ @@ -1699,17 +1719,17 @@ type BoardEpic implements CurrentUserTodos & Noteable { ): EpicConnection """ - Timestamp of when the epic was closed + Timestamp of when the epic was closed. """ closedAt: Time """ - Indicates if the epic is confidential + Indicates if the epic is confidential. """ confidential: Boolean """ - Timestamp of when the epic was created + Timestamp of when the epic was created. """ createdAt: Time @@ -1744,17 +1764,17 @@ type BoardEpic implements CurrentUserTodos & Noteable { ): TodoConnection! """ - Number of open and closed descendant epics and issues + Number of open and closed descendant epics and issues. """ descendantCounts: EpicDescendantCount """ - Total weight of open and closed issues in the epic and its descendants + Total weight of open and closed issues in the epic and its descendants. """ descendantWeightSum: EpicDescendantWeights """ - Description of the epic + Description of the epic. """ description: String @@ -1784,67 +1804,67 @@ type BoardEpic implements CurrentUserTodos & Noteable { ): DiscussionConnection! """ - Number of downvotes the epic has received + Number of downvotes the epic has received. """ downvotes: Int! """ - Due date of the epic + Due date of the epic. """ dueDate: Time """ - Fixed due date of the epic + Fixed due date of the epic. """ dueDateFixed: Time """ - Inherited due date of the epic from milestones + Inherited due date of the epic from milestones. """ dueDateFromMilestones: Time """ - Indicates if the due date has been manually set + Indicates if the due date has been manually set. """ dueDateIsFixed: Boolean """ - Group to which the epic belongs + Group to which the epic belongs. """ group: Group! """ - Indicates if the epic has children + Indicates if the epic has children. """ hasChildren: Boolean! """ - Indicates if the epic has direct issues + Indicates if the epic has direct issues. """ hasIssues: Boolean! """ - Indicates if the epic has a parent epic + Indicates if the epic has a parent epic. """ hasParent: Boolean! """ - Current health status of the epic + Current health status of the epic. """ healthStatus: EpicHealthStatus """ - ID of the epic + ID of the epic. """ id: ID! """ - Internal ID of the epic + Internal ID of the epic. """ iid: ID! """ - A list of issues associated with the epic + A list of issues associated with the epic. """ issues( """ @@ -1869,7 +1889,7 @@ type BoardEpic implements CurrentUserTodos & Noteable { ): EpicIssueConnection """ - Labels assigned to the epic + Labels assigned to the epic. """ labels( """ @@ -1919,12 +1939,12 @@ type BoardEpic implements CurrentUserTodos & Noteable { ): NoteConnection! """ - Parent epic of the epic + Parent epic of the epic. """ parent: Epic """ - List of participants for the epic + List of participants for the epic. """ participants( """ @@ -1949,77 +1969,77 @@ type BoardEpic implements CurrentUserTodos & Noteable { ): UserConnection """ - Internal reference of the epic. Returned in shortened format by default + Internal reference of the epic. Returned in shortened format by default. """ reference( """ - Indicates if the reference should be returned in full + Indicates if the reference should be returned in full. """ full: Boolean = false ): String! """ - URI path of the epic-issue relationship + URI path of the epic-issue relationship. """ relationPath: String """ - The relative position of the epic in the epic tree + The relative position of the epic in the epic tree. """ relativePosition: Int """ - Start date of the epic + Start date of the epic. """ startDate: Time """ - Fixed start date of the epic + Fixed start date of the epic. """ startDateFixed: Time """ - Inherited start date of the epic from milestones + Inherited start date of the epic from milestones. """ startDateFromMilestones: Time """ - Indicates if the start date has been manually set + Indicates if the start date has been manually set. """ startDateIsFixed: Boolean """ - State of the epic + State of the epic. """ state: EpicState! """ - Indicates the currently logged in user is subscribed to the epic + Indicates the currently logged in user is subscribed to the epic. """ subscribed: Boolean! """ - Title of the epic + Title of the epic. """ title: String """ - Timestamp of when the epic was updated + Timestamp of when the epic was updated. """ updatedAt: Time """ - Number of upvotes the epic has received + Number of upvotes the epic has received. """ upvotes: Int! """ - Number of user discussions in the epic + Number of user discussions in the epic. """ userDiscussionsCount: Int! """ - Number of user notes of the epic + Number of user notes of the epic. """ userNotesCount: Int! @@ -2034,12 +2054,12 @@ type BoardEpic implements CurrentUserTodos & Noteable { userPreferences: BoardEpicUserPreferences """ - Web path of the epic + Web path of the epic. """ webPath: String! """ - Web URL of the epic + Web URL of the epic. """ webUrl: String! } @@ -8417,12 +8437,12 @@ Represents an epic """ type Epic implements CurrentUserTodos & Noteable { """ - Author of the epic + Author of the epic. """ author: User! """ - A list of award emojis associated with the epic + A list of award emojis associated with the epic. """ awardEmoji( """ @@ -8447,7 +8467,7 @@ type Epic implements CurrentUserTodos & Noteable { ): AwardEmojiConnection """ - Children (sub-epics) of the epic + Children (sub-epics) of the epic. """ children( """ @@ -8545,17 +8565,17 @@ type Epic implements CurrentUserTodos & Noteable { ): EpicConnection """ - Timestamp of when the epic was closed + Timestamp of when the epic was closed. """ closedAt: Time """ - Indicates if the epic is confidential + Indicates if the epic is confidential. """ confidential: Boolean """ - Timestamp of when the epic was created + Timestamp of when the epic was created. """ createdAt: Time @@ -8590,17 +8610,17 @@ type Epic implements CurrentUserTodos & Noteable { ): TodoConnection! """ - Number of open and closed descendant epics and issues + Number of open and closed descendant epics and issues. """ descendantCounts: EpicDescendantCount """ - Total weight of open and closed issues in the epic and its descendants + Total weight of open and closed issues in the epic and its descendants. """ descendantWeightSum: EpicDescendantWeights """ - Description of the epic + Description of the epic. """ description: String @@ -8630,67 +8650,67 @@ type Epic implements CurrentUserTodos & Noteable { ): DiscussionConnection! """ - Number of downvotes the epic has received + Number of downvotes the epic has received. """ downvotes: Int! """ - Due date of the epic + Due date of the epic. """ dueDate: Time """ - Fixed due date of the epic + Fixed due date of the epic. """ dueDateFixed: Time """ - Inherited due date of the epic from milestones + Inherited due date of the epic from milestones. """ dueDateFromMilestones: Time """ - Indicates if the due date has been manually set + Indicates if the due date has been manually set. """ dueDateIsFixed: Boolean """ - Group to which the epic belongs + Group to which the epic belongs. """ group: Group! """ - Indicates if the epic has children + Indicates if the epic has children. """ hasChildren: Boolean! """ - Indicates if the epic has direct issues + Indicates if the epic has direct issues. """ hasIssues: Boolean! """ - Indicates if the epic has a parent epic + Indicates if the epic has a parent epic. """ hasParent: Boolean! """ - Current health status of the epic + Current health status of the epic. """ healthStatus: EpicHealthStatus """ - ID of the epic + ID of the epic. """ id: ID! """ - Internal ID of the epic + Internal ID of the epic. """ iid: ID! """ - A list of issues associated with the epic + A list of issues associated with the epic. """ issues( """ @@ -8715,7 +8735,7 @@ type Epic implements CurrentUserTodos & Noteable { ): EpicIssueConnection """ - Labels assigned to the epic + Labels assigned to the epic. """ labels( """ @@ -8765,12 +8785,12 @@ type Epic implements CurrentUserTodos & Noteable { ): NoteConnection! """ - Parent epic of the epic + Parent epic of the epic. """ parent: Epic """ - List of participants for the epic + List of participants for the epic. """ participants( """ @@ -8795,77 +8815,77 @@ type Epic implements CurrentUserTodos & Noteable { ): UserConnection """ - Internal reference of the epic. Returned in shortened format by default + Internal reference of the epic. Returned in shortened format by default. """ reference( """ - Indicates if the reference should be returned in full + Indicates if the reference should be returned in full. """ full: Boolean = false ): String! """ - URI path of the epic-issue relationship + URI path of the epic-issue relationship. """ relationPath: String """ - The relative position of the epic in the epic tree + The relative position of the epic in the epic tree. """ relativePosition: Int """ - Start date of the epic + Start date of the epic. """ startDate: Time """ - Fixed start date of the epic + Fixed start date of the epic. """ startDateFixed: Time """ - Inherited start date of the epic from milestones + Inherited start date of the epic from milestones. """ startDateFromMilestones: Time """ - Indicates if the start date has been manually set + Indicates if the start date has been manually set. """ startDateIsFixed: Boolean """ - State of the epic + State of the epic. """ state: EpicState! """ - Indicates the currently logged in user is subscribed to the epic + Indicates the currently logged in user is subscribed to the epic. """ subscribed: Boolean! """ - Title of the epic + Title of the epic. """ title: String """ - Timestamp of when the epic was updated + Timestamp of when the epic was updated. """ updatedAt: Time """ - Number of upvotes the epic has received + Number of upvotes the epic has received. """ upvotes: Int! """ - Number of user discussions in the epic + Number of user discussions in the epic. """ userDiscussionsCount: Int! """ - Number of user notes of the epic + Number of user notes of the epic. """ userNotesCount: Int! @@ -8875,12 +8895,12 @@ type Epic implements CurrentUserTodos & Noteable { userPermissions: EpicPermissions! """ - Web path of the epic + Web path of the epic. """ webPath: String! """ - Web URL of the epic + Web URL of the epic. """ webUrl: String! } @@ -15696,6 +15716,10 @@ enum MergeRequestState { all closed locked + + """ + Merge Request has been merged + """ merged opened } @@ -18308,6 +18332,16 @@ type Project { last: Int ): AlertManagementIntegrationConnection + """ + Extract alert fields from payload for custom mapping + """ + alertManagementPayloadFields( + """ + Sample payload for extracting alert fields for custom mappings. + """ + payloadExample: String! + ): [AlertManagementPayloadAlertField!] + """ If `only_allow_merge_if_pipeline_succeeds` is true, indicates if merge requests of the project can also be merged with skipped jobs diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index e5e9593dcf9..846d70917b6 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -1909,6 +1909,69 @@ ], "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "AlertManagementPayloadAlertField", + "description": "Parsed field from an alert used for custom mappings", + "fields": [ + { + "name": "label", + "description": "Human-readable label of the payload path.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "path", + "description": "Path to value inside payload JSON.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": "Type of the parsed value.", + "args": [ + + ], + "type": { + "kind": "ENUM", + "name": "AlertManagementPayloadAlertFieldType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "AlertManagementPayloadAlertFieldInput", @@ -4097,7 +4160,7 @@ "fields": [ { "name": "author", - "description": "Author of the epic", + "description": "Author of the epic.", "args": [ ], @@ -4115,7 +4178,7 @@ }, { "name": "awardEmoji", - "description": "A list of award emojis associated with the epic", + "description": "A list of award emojis associated with the epic.", "args": [ { "name": "after", @@ -4168,7 +4231,7 @@ }, { "name": "children", - "description": "Children (sub-epics) of the epic", + "description": "Children (sub-epics) of the epic.", "args": [ { "name": "startDate", @@ -4377,7 +4440,7 @@ }, { "name": "closedAt", - "description": "Timestamp of when the epic was closed", + "description": "Timestamp of when the epic was closed.", "args": [ ], @@ -4391,7 +4454,7 @@ }, { "name": "confidential", - "description": "Indicates if the epic is confidential", + "description": "Indicates if the epic is confidential.", "args": [ ], @@ -4405,7 +4468,7 @@ }, { "name": "createdAt", - "description": "Timestamp of when the epic was created", + "description": "Timestamp of when the epic was created.", "args": [ ], @@ -4486,7 +4549,7 @@ }, { "name": "descendantCounts", - "description": "Number of open and closed descendant epics and issues", + "description": "Number of open and closed descendant epics and issues.", "args": [ ], @@ -4500,7 +4563,7 @@ }, { "name": "descendantWeightSum", - "description": "Total weight of open and closed issues in the epic and its descendants", + "description": "Total weight of open and closed issues in the epic and its descendants.", "args": [ ], @@ -4514,7 +4577,7 @@ }, { "name": "description", - "description": "Description of the epic", + "description": "Description of the epic.", "args": [ ], @@ -4585,7 +4648,7 @@ }, { "name": "downvotes", - "description": "Number of downvotes the epic has received", + "description": "Number of downvotes the epic has received.", "args": [ ], @@ -4603,7 +4666,7 @@ }, { "name": "dueDate", - "description": "Due date of the epic", + "description": "Due date of the epic.", "args": [ ], @@ -4617,7 +4680,7 @@ }, { "name": "dueDateFixed", - "description": "Fixed due date of the epic", + "description": "Fixed due date of the epic.", "args": [ ], @@ -4631,7 +4694,7 @@ }, { "name": "dueDateFromMilestones", - "description": "Inherited due date of the epic from milestones", + "description": "Inherited due date of the epic from milestones.", "args": [ ], @@ -4645,7 +4708,7 @@ }, { "name": "dueDateIsFixed", - "description": "Indicates if the due date has been manually set", + "description": "Indicates if the due date has been manually set.", "args": [ ], @@ -4659,7 +4722,7 @@ }, { "name": "group", - "description": "Group to which the epic belongs", + "description": "Group to which the epic belongs.", "args": [ ], @@ -4677,7 +4740,7 @@ }, { "name": "hasChildren", - "description": "Indicates if the epic has children", + "description": "Indicates if the epic has children.", "args": [ ], @@ -4695,7 +4758,7 @@ }, { "name": "hasIssues", - "description": "Indicates if the epic has direct issues", + "description": "Indicates if the epic has direct issues.", "args": [ ], @@ -4713,7 +4776,7 @@ }, { "name": "hasParent", - "description": "Indicates if the epic has a parent epic", + "description": "Indicates if the epic has a parent epic.", "args": [ ], @@ -4731,7 +4794,7 @@ }, { "name": "healthStatus", - "description": "Current health status of the epic", + "description": "Current health status of the epic.", "args": [ ], @@ -4745,7 +4808,7 @@ }, { "name": "id", - "description": "ID of the epic", + "description": "ID of the epic.", "args": [ ], @@ -4763,7 +4826,7 @@ }, { "name": "iid", - "description": "Internal ID of the epic", + "description": "Internal ID of the epic.", "args": [ ], @@ -4781,7 +4844,7 @@ }, { "name": "issues", - "description": "A list of issues associated with the epic", + "description": "A list of issues associated with the epic.", "args": [ { "name": "after", @@ -4834,7 +4897,7 @@ }, { "name": "labels", - "description": "Labels assigned to the epic", + "description": "Labels assigned to the epic.", "args": [ { "name": "after", @@ -4944,7 +5007,7 @@ }, { "name": "parent", - "description": "Parent epic of the epic", + "description": "Parent epic of the epic.", "args": [ ], @@ -4958,7 +5021,7 @@ }, { "name": "participants", - "description": "List of participants for the epic", + "description": "List of participants for the epic.", "args": [ { "name": "after", @@ -5011,11 +5074,11 @@ }, { "name": "reference", - "description": "Internal reference of the epic. Returned in shortened format by default", + "description": "Internal reference of the epic. Returned in shortened format by default.", "args": [ { "name": "full", - "description": "Indicates if the reference should be returned in full", + "description": "Indicates if the reference should be returned in full.", "type": { "kind": "SCALAR", "name": "Boolean", @@ -5038,7 +5101,7 @@ }, { "name": "relationPath", - "description": "URI path of the epic-issue relationship", + "description": "URI path of the epic-issue relationship.", "args": [ ], @@ -5052,7 +5115,7 @@ }, { "name": "relativePosition", - "description": "The relative position of the epic in the epic tree", + "description": "The relative position of the epic in the epic tree.", "args": [ ], @@ -5066,7 +5129,7 @@ }, { "name": "startDate", - "description": "Start date of the epic", + "description": "Start date of the epic.", "args": [ ], @@ -5080,7 +5143,7 @@ }, { "name": "startDateFixed", - "description": "Fixed start date of the epic", + "description": "Fixed start date of the epic.", "args": [ ], @@ -5094,7 +5157,7 @@ }, { "name": "startDateFromMilestones", - "description": "Inherited start date of the epic from milestones", + "description": "Inherited start date of the epic from milestones.", "args": [ ], @@ -5108,7 +5171,7 @@ }, { "name": "startDateIsFixed", - "description": "Indicates if the start date has been manually set", + "description": "Indicates if the start date has been manually set.", "args": [ ], @@ -5122,7 +5185,7 @@ }, { "name": "state", - "description": "State of the epic", + "description": "State of the epic.", "args": [ ], @@ -5140,7 +5203,7 @@ }, { "name": "subscribed", - "description": "Indicates the currently logged in user is subscribed to the epic", + "description": "Indicates the currently logged in user is subscribed to the epic.", "args": [ ], @@ -5158,7 +5221,7 @@ }, { "name": "title", - "description": "Title of the epic", + "description": "Title of the epic.", "args": [ ], @@ -5172,7 +5235,7 @@ }, { "name": "updatedAt", - "description": "Timestamp of when the epic was updated", + "description": "Timestamp of when the epic was updated.", "args": [ ], @@ -5186,7 +5249,7 @@ }, { "name": "upvotes", - "description": "Number of upvotes the epic has received", + "description": "Number of upvotes the epic has received.", "args": [ ], @@ -5204,7 +5267,7 @@ }, { "name": "userDiscussionsCount", - "description": "Number of user discussions in the epic", + "description": "Number of user discussions in the epic.", "args": [ ], @@ -5222,7 +5285,7 @@ }, { "name": "userNotesCount", - "description": "Number of user notes of the epic", + "description": "Number of user notes of the epic.", "args": [ ], @@ -5272,7 +5335,7 @@ }, { "name": "webPath", - "description": "Web path of the epic", + "description": "Web path of the epic.", "args": [ ], @@ -5290,7 +5353,7 @@ }, { "name": "webUrl", - "description": "Web URL of the epic", + "description": "Web URL of the epic.", "args": [ ], @@ -23328,7 +23391,7 @@ "fields": [ { "name": "author", - "description": "Author of the epic", + "description": "Author of the epic.", "args": [ ], @@ -23346,7 +23409,7 @@ }, { "name": "awardEmoji", - "description": "A list of award emojis associated with the epic", + "description": "A list of award emojis associated with the epic.", "args": [ { "name": "after", @@ -23399,7 +23462,7 @@ }, { "name": "children", - "description": "Children (sub-epics) of the epic", + "description": "Children (sub-epics) of the epic.", "args": [ { "name": "startDate", @@ -23608,7 +23671,7 @@ }, { "name": "closedAt", - "description": "Timestamp of when the epic was closed", + "description": "Timestamp of when the epic was closed.", "args": [ ], @@ -23622,7 +23685,7 @@ }, { "name": "confidential", - "description": "Indicates if the epic is confidential", + "description": "Indicates if the epic is confidential.", "args": [ ], @@ -23636,7 +23699,7 @@ }, { "name": "createdAt", - "description": "Timestamp of when the epic was created", + "description": "Timestamp of when the epic was created.", "args": [ ], @@ -23717,7 +23780,7 @@ }, { "name": "descendantCounts", - "description": "Number of open and closed descendant epics and issues", + "description": "Number of open and closed descendant epics and issues.", "args": [ ], @@ -23731,7 +23794,7 @@ }, { "name": "descendantWeightSum", - "description": "Total weight of open and closed issues in the epic and its descendants", + "description": "Total weight of open and closed issues in the epic and its descendants.", "args": [ ], @@ -23745,7 +23808,7 @@ }, { "name": "description", - "description": "Description of the epic", + "description": "Description of the epic.", "args": [ ], @@ -23816,7 +23879,7 @@ }, { "name": "downvotes", - "description": "Number of downvotes the epic has received", + "description": "Number of downvotes the epic has received.", "args": [ ], @@ -23834,7 +23897,7 @@ }, { "name": "dueDate", - "description": "Due date of the epic", + "description": "Due date of the epic.", "args": [ ], @@ -23848,7 +23911,7 @@ }, { "name": "dueDateFixed", - "description": "Fixed due date of the epic", + "description": "Fixed due date of the epic.", "args": [ ], @@ -23862,7 +23925,7 @@ }, { "name": "dueDateFromMilestones", - "description": "Inherited due date of the epic from milestones", + "description": "Inherited due date of the epic from milestones.", "args": [ ], @@ -23876,7 +23939,7 @@ }, { "name": "dueDateIsFixed", - "description": "Indicates if the due date has been manually set", + "description": "Indicates if the due date has been manually set.", "args": [ ], @@ -23890,7 +23953,7 @@ }, { "name": "group", - "description": "Group to which the epic belongs", + "description": "Group to which the epic belongs.", "args": [ ], @@ -23908,7 +23971,7 @@ }, { "name": "hasChildren", - "description": "Indicates if the epic has children", + "description": "Indicates if the epic has children.", "args": [ ], @@ -23926,7 +23989,7 @@ }, { "name": "hasIssues", - "description": "Indicates if the epic has direct issues", + "description": "Indicates if the epic has direct issues.", "args": [ ], @@ -23944,7 +24007,7 @@ }, { "name": "hasParent", - "description": "Indicates if the epic has a parent epic", + "description": "Indicates if the epic has a parent epic.", "args": [ ], @@ -23962,7 +24025,7 @@ }, { "name": "healthStatus", - "description": "Current health status of the epic", + "description": "Current health status of the epic.", "args": [ ], @@ -23976,7 +24039,7 @@ }, { "name": "id", - "description": "ID of the epic", + "description": "ID of the epic.", "args": [ ], @@ -23994,7 +24057,7 @@ }, { "name": "iid", - "description": "Internal ID of the epic", + "description": "Internal ID of the epic.", "args": [ ], @@ -24012,7 +24075,7 @@ }, { "name": "issues", - "description": "A list of issues associated with the epic", + "description": "A list of issues associated with the epic.", "args": [ { "name": "after", @@ -24065,7 +24128,7 @@ }, { "name": "labels", - "description": "Labels assigned to the epic", + "description": "Labels assigned to the epic.", "args": [ { "name": "after", @@ -24175,7 +24238,7 @@ }, { "name": "parent", - "description": "Parent epic of the epic", + "description": "Parent epic of the epic.", "args": [ ], @@ -24189,7 +24252,7 @@ }, { "name": "participants", - "description": "List of participants for the epic", + "description": "List of participants for the epic.", "args": [ { "name": "after", @@ -24242,11 +24305,11 @@ }, { "name": "reference", - "description": "Internal reference of the epic. Returned in shortened format by default", + "description": "Internal reference of the epic. Returned in shortened format by default.", "args": [ { "name": "full", - "description": "Indicates if the reference should be returned in full", + "description": "Indicates if the reference should be returned in full.", "type": { "kind": "SCALAR", "name": "Boolean", @@ -24269,7 +24332,7 @@ }, { "name": "relationPath", - "description": "URI path of the epic-issue relationship", + "description": "URI path of the epic-issue relationship.", "args": [ ], @@ -24283,7 +24346,7 @@ }, { "name": "relativePosition", - "description": "The relative position of the epic in the epic tree", + "description": "The relative position of the epic in the epic tree.", "args": [ ], @@ -24297,7 +24360,7 @@ }, { "name": "startDate", - "description": "Start date of the epic", + "description": "Start date of the epic.", "args": [ ], @@ -24311,7 +24374,7 @@ }, { "name": "startDateFixed", - "description": "Fixed start date of the epic", + "description": "Fixed start date of the epic.", "args": [ ], @@ -24325,7 +24388,7 @@ }, { "name": "startDateFromMilestones", - "description": "Inherited start date of the epic from milestones", + "description": "Inherited start date of the epic from milestones.", "args": [ ], @@ -24339,7 +24402,7 @@ }, { "name": "startDateIsFixed", - "description": "Indicates if the start date has been manually set", + "description": "Indicates if the start date has been manually set.", "args": [ ], @@ -24353,7 +24416,7 @@ }, { "name": "state", - "description": "State of the epic", + "description": "State of the epic.", "args": [ ], @@ -24371,7 +24434,7 @@ }, { "name": "subscribed", - "description": "Indicates the currently logged in user is subscribed to the epic", + "description": "Indicates the currently logged in user is subscribed to the epic.", "args": [ ], @@ -24389,7 +24452,7 @@ }, { "name": "title", - "description": "Title of the epic", + "description": "Title of the epic.", "args": [ ], @@ -24403,7 +24466,7 @@ }, { "name": "updatedAt", - "description": "Timestamp of when the epic was updated", + "description": "Timestamp of when the epic was updated.", "args": [ ], @@ -24417,7 +24480,7 @@ }, { "name": "upvotes", - "description": "Number of upvotes the epic has received", + "description": "Number of upvotes the epic has received.", "args": [ ], @@ -24435,7 +24498,7 @@ }, { "name": "userDiscussionsCount", - "description": "Number of user discussions in the epic", + "description": "Number of user discussions in the epic.", "args": [ ], @@ -24453,7 +24516,7 @@ }, { "name": "userNotesCount", - "description": "Number of user notes of the epic", + "description": "Number of user notes of the epic.", "args": [ ], @@ -24489,7 +24552,7 @@ }, { "name": "webPath", - "description": "Web path of the epic", + "description": "Web path of the epic.", "args": [ ], @@ -24507,7 +24570,7 @@ }, { "name": "webUrl", - "description": "Web URL of the epic", + "description": "Web URL of the epic.", "args": [ ], @@ -43076,7 +43139,7 @@ }, { "name": "merged", - "description": null, + "description": "Merge Request has been merged", "isDeprecated": false, "deprecationReason": null } @@ -53968,6 +54031,41 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "alertManagementPayloadFields", + "description": "Extract alert fields from payload for custom mapping", + "args": [ + { + "name": "payloadExample", + "description": "Sample payload for extracting alert fields for custom mappings.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "AlertManagementPayloadAlertField", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "allowMergeOnSkippedPipeline", "description": "If `only_allow_merge_if_pipeline_succeeds` is true, indicates if merge requests of the project can also be merged with skipped jobs", diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index c879465121c..8d0b25a2093 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -132,6 +132,16 @@ An endpoint and credentials used to accept alerts for a project. | `type` | AlertManagementIntegrationType! | Type of integration. | | `url` | String | Endpoint which accepts alert notifications. | +### AlertManagementPayloadAlertField + +Parsed field from an alert used for custom mappings. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `label` | String | Human-readable label of the payload path. | +| `path` | String! => Array | Path to value inside payload JSON. | +| `type` | AlertManagementPayloadAlertFieldType | Type of the parsed value. | + ### AlertManagementPrometheusIntegration An endpoint and credentials used to accept Prometheus alerts for a project. @@ -262,52 +272,52 @@ Represents an epic on an issue board. | Field | Type | Description | | ----- | ---- | ----------- | -| `author` | User! | Author of the epic | -| `awardEmoji` | AwardEmojiConnection | A list of award emojis associated with the epic | -| `children` | EpicConnection | Children (sub-epics) of the epic | -| `closedAt` | Time | Timestamp of when the epic was closed | -| `confidential` | Boolean | Indicates if the epic is confidential | -| `createdAt` | Time | Timestamp of when the epic was created | +| `author` | User! | Author of the epic. | +| `awardEmoji` | AwardEmojiConnection | A list of award emojis associated with the epic. | +| `children` | EpicConnection | Children (sub-epics) of the epic. | +| `closedAt` | Time | Timestamp of when the epic was closed. | +| `confidential` | Boolean | Indicates if the epic is confidential. | +| `createdAt` | Time | Timestamp of when the epic was created. | | `currentUserTodos` | TodoConnection! | Todos for the current user. | -| `descendantCounts` | EpicDescendantCount | Number of open and closed descendant epics and issues | -| `descendantWeightSum` | EpicDescendantWeights | Total weight of open and closed issues in the epic and its descendants | -| `description` | String | Description of the epic | +| `descendantCounts` | EpicDescendantCount | Number of open and closed descendant epics and issues. | +| `descendantWeightSum` | EpicDescendantWeights | Total weight of open and closed issues in the epic and its descendants. | +| `description` | String | Description of the epic. | | `discussions` | DiscussionConnection! | All discussions on this noteable | -| `downvotes` | Int! | Number of downvotes the epic has received | -| `dueDate` | Time | Due date of the epic | -| `dueDateFixed` | Time | Fixed due date of the epic | -| `dueDateFromMilestones` | Time | Inherited due date of the epic from milestones | -| `dueDateIsFixed` | Boolean | Indicates if the due date has been manually set | -| `group` | Group! | Group to which the epic belongs | -| `hasChildren` | Boolean! | Indicates if the epic has children | -| `hasIssues` | Boolean! | Indicates if the epic has direct issues | -| `hasParent` | Boolean! | Indicates if the epic has a parent epic | -| `healthStatus` | EpicHealthStatus | Current health status of the epic | -| `id` | ID! | ID of the epic | -| `iid` | ID! | Internal ID of the epic | -| `issues` | EpicIssueConnection | A list of issues associated with the epic | -| `labels` | LabelConnection | Labels assigned to the epic | +| `downvotes` | Int! | Number of downvotes the epic has received. | +| `dueDate` | Time | Due date of the epic. | +| `dueDateFixed` | Time | Fixed due date of the epic. | +| `dueDateFromMilestones` | Time | Inherited due date of the epic from milestones. | +| `dueDateIsFixed` | Boolean | Indicates if the due date has been manually set. | +| `group` | Group! | Group to which the epic belongs. | +| `hasChildren` | Boolean! | Indicates if the epic has children. | +| `hasIssues` | Boolean! | Indicates if the epic has direct issues. | +| `hasParent` | Boolean! | Indicates if the epic has a parent epic. | +| `healthStatus` | EpicHealthStatus | Current health status of the epic. | +| `id` | ID! | ID of the epic. | +| `iid` | ID! | Internal ID of the epic. | +| `issues` | EpicIssueConnection | A list of issues associated with the epic. | +| `labels` | LabelConnection | Labels assigned to the epic. | | `notes` | NoteConnection! | All notes on this noteable | -| `parent` | Epic | Parent epic of the epic | -| `participants` | UserConnection | List of participants for the epic | -| `reference` | String! | Internal reference of the epic. Returned in shortened format by default | -| `relationPath` | String | URI path of the epic-issue relationship | -| `relativePosition` | Int | The relative position of the epic in the epic tree | -| `startDate` | Time | Start date of the epic | -| `startDateFixed` | Time | Fixed start date of the epic | -| `startDateFromMilestones` | Time | Inherited start date of the epic from milestones | -| `startDateIsFixed` | Boolean | Indicates if the start date has been manually set | -| `state` | EpicState! | State of the epic | -| `subscribed` | Boolean! | Indicates the currently logged in user is subscribed to the epic | -| `title` | String | Title of the epic | -| `updatedAt` | Time | Timestamp of when the epic was updated | -| `upvotes` | Int! | Number of upvotes the epic has received | -| `userDiscussionsCount` | Int! | Number of user discussions in the epic | -| `userNotesCount` | Int! | Number of user notes of the epic | +| `parent` | Epic | Parent epic of the epic. | +| `participants` | UserConnection | List of participants for the epic. | +| `reference` | String! | Internal reference of the epic. Returned in shortened format by default. | +| `relationPath` | String | URI path of the epic-issue relationship. | +| `relativePosition` | Int | The relative position of the epic in the epic tree. | +| `startDate` | Time | Start date of the epic. | +| `startDateFixed` | Time | Fixed start date of the epic. | +| `startDateFromMilestones` | Time | Inherited start date of the epic from milestones. | +| `startDateIsFixed` | Boolean | Indicates if the start date has been manually set. | +| `state` | EpicState! | State of the epic. | +| `subscribed` | Boolean! | Indicates the currently logged in user is subscribed to the epic. | +| `title` | String | Title of the epic. | +| `updatedAt` | Time | Timestamp of when the epic was updated. | +| `upvotes` | Int! | Number of upvotes the epic has received. | +| `userDiscussionsCount` | Int! | Number of user discussions in the epic. | +| `userNotesCount` | Int! | Number of user notes of the epic. | | `userPermissions` | EpicPermissions! | Permissions for the current user on the resource | | `userPreferences` | BoardEpicUserPreferences | User preferences for the epic on the issue board | -| `webPath` | String! | Web path of the epic | -| `webUrl` | String! | Web URL of the epic | +| `webPath` | String! | Web path of the epic. | +| `webUrl` | String! | Web URL of the epic. | ### BoardEpicUserPreferences @@ -1384,51 +1394,51 @@ Represents an epic. | Field | Type | Description | | ----- | ---- | ----------- | -| `author` | User! | Author of the epic | -| `awardEmoji` | AwardEmojiConnection | A list of award emojis associated with the epic | -| `children` | EpicConnection | Children (sub-epics) of the epic | -| `closedAt` | Time | Timestamp of when the epic was closed | -| `confidential` | Boolean | Indicates if the epic is confidential | -| `createdAt` | Time | Timestamp of when the epic was created | +| `author` | User! | Author of the epic. | +| `awardEmoji` | AwardEmojiConnection | A list of award emojis associated with the epic. | +| `children` | EpicConnection | Children (sub-epics) of the epic. | +| `closedAt` | Time | Timestamp of when the epic was closed. | +| `confidential` | Boolean | Indicates if the epic is confidential. | +| `createdAt` | Time | Timestamp of when the epic was created. | | `currentUserTodos` | TodoConnection! | Todos for the current user. | -| `descendantCounts` | EpicDescendantCount | Number of open and closed descendant epics and issues | -| `descendantWeightSum` | EpicDescendantWeights | Total weight of open and closed issues in the epic and its descendants | -| `description` | String | Description of the epic | +| `descendantCounts` | EpicDescendantCount | Number of open and closed descendant epics and issues. | +| `descendantWeightSum` | EpicDescendantWeights | Total weight of open and closed issues in the epic and its descendants. | +| `description` | String | Description of the epic. | | `discussions` | DiscussionConnection! | All discussions on this noteable | -| `downvotes` | Int! | Number of downvotes the epic has received | -| `dueDate` | Time | Due date of the epic | -| `dueDateFixed` | Time | Fixed due date of the epic | -| `dueDateFromMilestones` | Time | Inherited due date of the epic from milestones | -| `dueDateIsFixed` | Boolean | Indicates if the due date has been manually set | -| `group` | Group! | Group to which the epic belongs | -| `hasChildren` | Boolean! | Indicates if the epic has children | -| `hasIssues` | Boolean! | Indicates if the epic has direct issues | -| `hasParent` | Boolean! | Indicates if the epic has a parent epic | -| `healthStatus` | EpicHealthStatus | Current health status of the epic | -| `id` | ID! | ID of the epic | -| `iid` | ID! | Internal ID of the epic | -| `issues` | EpicIssueConnection | A list of issues associated with the epic | -| `labels` | LabelConnection | Labels assigned to the epic | +| `downvotes` | Int! | Number of downvotes the epic has received. | +| `dueDate` | Time | Due date of the epic. | +| `dueDateFixed` | Time | Fixed due date of the epic. | +| `dueDateFromMilestones` | Time | Inherited due date of the epic from milestones. | +| `dueDateIsFixed` | Boolean | Indicates if the due date has been manually set. | +| `group` | Group! | Group to which the epic belongs. | +| `hasChildren` | Boolean! | Indicates if the epic has children. | +| `hasIssues` | Boolean! | Indicates if the epic has direct issues. | +| `hasParent` | Boolean! | Indicates if the epic has a parent epic. | +| `healthStatus` | EpicHealthStatus | Current health status of the epic. | +| `id` | ID! | ID of the epic. | +| `iid` | ID! | Internal ID of the epic. | +| `issues` | EpicIssueConnection | A list of issues associated with the epic. | +| `labels` | LabelConnection | Labels assigned to the epic. | | `notes` | NoteConnection! | All notes on this noteable | -| `parent` | Epic | Parent epic of the epic | -| `participants` | UserConnection | List of participants for the epic | -| `reference` | String! | Internal reference of the epic. Returned in shortened format by default | -| `relationPath` | String | URI path of the epic-issue relationship | -| `relativePosition` | Int | The relative position of the epic in the epic tree | -| `startDate` | Time | Start date of the epic | -| `startDateFixed` | Time | Fixed start date of the epic | -| `startDateFromMilestones` | Time | Inherited start date of the epic from milestones | -| `startDateIsFixed` | Boolean | Indicates if the start date has been manually set | -| `state` | EpicState! | State of the epic | -| `subscribed` | Boolean! | Indicates the currently logged in user is subscribed to the epic | -| `title` | String | Title of the epic | -| `updatedAt` | Time | Timestamp of when the epic was updated | -| `upvotes` | Int! | Number of upvotes the epic has received | -| `userDiscussionsCount` | Int! | Number of user discussions in the epic | -| `userNotesCount` | Int! | Number of user notes of the epic | +| `parent` | Epic | Parent epic of the epic. | +| `participants` | UserConnection | List of participants for the epic. | +| `reference` | String! | Internal reference of the epic. Returned in shortened format by default. | +| `relationPath` | String | URI path of the epic-issue relationship. | +| `relativePosition` | Int | The relative position of the epic in the epic tree. | +| `startDate` | Time | Start date of the epic. | +| `startDateFixed` | Time | Fixed start date of the epic. | +| `startDateFromMilestones` | Time | Inherited start date of the epic from milestones. | +| `startDateIsFixed` | Boolean | Indicates if the start date has been manually set. | +| `state` | EpicState! | State of the epic. | +| `subscribed` | Boolean! | Indicates the currently logged in user is subscribed to the epic. | +| `title` | String | Title of the epic. | +| `updatedAt` | Time | Timestamp of when the epic was updated. | +| `upvotes` | Int! | Number of upvotes the epic has received. | +| `userDiscussionsCount` | Int! | Number of user discussions in the epic. | +| `userNotesCount` | Int! | Number of user notes of the epic. | | `userPermissions` | EpicPermissions! | Permissions for the current user on the resource | -| `webPath` | String! | Web path of the epic | -| `webUrl` | String! | Web URL of the epic | +| `webPath` | String! | Web path of the epic. | +| `webUrl` | String! | Web URL of the epic. | ### EpicAddIssuePayload @@ -2767,6 +2777,7 @@ Autogenerated return type of PipelineRetry. | `alertManagementAlertStatusCounts` | AlertManagementAlertStatusCountsType | Counts of alerts by status for the project | | `alertManagementAlerts` | AlertManagementAlertConnection | Alert Management alerts of the project | | `alertManagementIntegrations` | AlertManagementIntegrationConnection | Integrations which can receive alerts for the project | +| `alertManagementPayloadFields` | AlertManagementPayloadAlertField! => Array | Extract alert fields from payload for custom mapping | | `allowMergeOnSkippedPipeline` | Boolean | If `only_allow_merge_if_pipeline_succeeds` is true, indicates if merge requests of the project can also be merged with skipped jobs | | `archived` | Boolean | Indicates the archived status of the project | | `autocloseReferencedIssues` | Boolean | Indicates if issues referenced by merge requests and commits within the default branch are closed automatically | @@ -4991,7 +5002,7 @@ State of a GitLab merge request. | `all` | | | `closed` | | | `locked` | | -| `merged` | | +| `merged` | Merge Request has been merged | | `opened` | | ### MilestoneStateEnum diff --git a/doc/development/database/strings_and_the_text_data_type.md b/doc/development/database/strings_and_the_text_data_type.md index 33a0fd2ebb7..703118b23d2 100644 --- a/doc/development/database/strings_and_the_text_data_type.md +++ b/doc/development/database/strings_and_the_text_data_type.md @@ -34,6 +34,12 @@ but only for updating the declaration of the columns. We can then validate it at `VALIDATE CONSTRAINT`, which requires only a `SHARE UPDATE EXCLUSIVE LOCK` (only conflicts with other validations and index creation while it allows reads and writes). +### Exceptions + +Text columns used by `attr_encrypted` are not required to have a limit, becuase the length of the +text after encryption may be longer than the text itself. Instead, you can use an Active Record +length validation on the attribute. + ## Create a new table with text columns When adding a new table, the limits for all text columns should be added in the same migration as diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md index 9dd6fcbe246..e497c006f24 100644 --- a/doc/user/admin_area/settings/account_and_limit_settings.md +++ b/doc/user/admin_area/settings/account_and_limit_settings.md @@ -163,13 +163,7 @@ Once a lifetime for personal access tokens is set, GitLab will: allowed lifetime. Three hours is given to allow administrators to change the allowed lifetime, or remove it, before revocation takes place. -## Enforcement of SSH key expiration **(ULTIMATE ONLY)** - -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/276221) in GitLab Ultimate 13.9. -> - It is deployed behind a feature flag, disabled by default. -> - It is disabled on GitLab.com. -> - It is not recommended for production use. -> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-enforcement-of-ssh-key-expiration-feature). **(CORE ONLY)** +## Enforcement of SSH key expiration **(ULTIMATE SELF)** GitLab administrators can choose to enforce the expiration of SSH keys after their expiration dates. If you enable this feature, this disables all _expired_ SSH keys. @@ -180,23 +174,6 @@ To do this: 1. Expand the **Account and limit** section. 1. Select the **Enforce SSH key expiration** checkbox. -### Enable or disable enforcement of SSH key expiration Feature **(CORE ONLY)** - -Enforcement of SSH key expiry is deployed behind a feature flag and is **disabled by default**. -[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) can enable it for your instance from the [rails console](../../../administration/feature_flags.md#start-the-gitlab-rails-console). - -To enable it: - -```ruby -Feature.enable(:ff_enforce_ssh_key_expiration) -``` - -To disable it: - -```ruby -Feature.disable(:ff_enforce_ssh_key_expiration) -``` - ## Optional enforcement of Personal Access Token expiry **(ULTIMATE SELF)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214723) in GitLab Ultimate 13.1. diff --git a/lib/bulk_imports/pipeline/runner.rb b/lib/bulk_imports/pipeline/runner.rb index 946d8d45622..1c4ee154874 100644 --- a/lib/bulk_imports/pipeline/runner.rb +++ b/lib/bulk_imports/pipeline/runner.rb @@ -10,7 +10,7 @@ module BulkImports def run(context) raise MarkedAsFailedError if marked_as_failed?(context) - info(context, message: 'Pipeline started', pipeline_class: pipeline) + info(context, message: 'Pipeline started') extracted_data = extracted_data_from(context) @@ -27,6 +27,8 @@ module BulkImports end after_run(context, extracted_data) if respond_to?(:after_run) + + info(context, message: 'Pipeline finished') rescue MarkedAsFailedError log_skip(context) end @@ -36,7 +38,7 @@ module BulkImports def run_pipeline_step(step, class_name, context) raise MarkedAsFailedError if marked_as_failed?(context) - info(context, step => class_name) + info(context, pipeline_step: step, step_class: class_name) yield rescue MarkedAsFailedError @@ -100,7 +102,8 @@ module BulkImports def log_base_params(context) { bulk_import_entity_id: context.entity.id, - bulk_import_entity_type: context.entity.source_type + bulk_import_entity_type: context.entity.source_type, + pipeline_class: pipeline } end diff --git a/lib/gitlab/sidekiq_logging/structured_logger.rb b/lib/gitlab/sidekiq_logging/structured_logger.rb index eb845c5ff8d..f7b826ba648 100644 --- a/lib/gitlab/sidekiq_logging/structured_logger.rb +++ b/lib/gitlab/sidekiq_logging/structured_logger.rb @@ -13,7 +13,7 @@ module Gitlab base_payload = parse_job(job) ActiveRecord::LogSubscriber.reset_runtime - Sidekiq.logger.info log_job_start(base_payload) + Sidekiq.logger.info log_job_start(job, base_payload) yield @@ -40,13 +40,15 @@ module Gitlab output_payload.merge!(job.slice(*::Gitlab::Metrics::Subscribers::ActiveRecord::DB_COUNTERS)) end - def log_job_start(payload) + def log_job_start(job, payload) payload['message'] = "#{base_message(payload)}: start" payload['job_status'] = 'start' scheduling_latency_s = ::Gitlab::InstrumentationHelper.queue_duration_for_job(payload) payload['scheduling_latency_s'] = scheduling_latency_s if scheduling_latency_s + payload['job_size_bytes'] = Sidekiq.dump_json(job).bytesize + payload end diff --git a/lib/gitlab/usage_data_counters/known_events/common.yml b/lib/gitlab/usage_data_counters/known_events/common.yml index 466c151a803..413b5076a20 100644 --- a/lib/gitlab/usage_data_counters/known_events/common.yml +++ b/lib/gitlab/usage_data_counters/known_events/common.yml @@ -608,3 +608,9 @@ redis_slot: ci_templates aggregation: weekly feature_flag: usage_data_track_ci_templates_unique_projects +# Pipeline Authoring +- name: o_pipeline_authoring_unique_users_committing_ciconfigfile + category: pipeline_authoring + redis_slot: pipeline_authoring + aggregation: weekly + feature_flag: usage_data_unique_users_committing_ciconfigfile diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 91300f92f80..aa9c306649a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5694,7 +5694,7 @@ msgstr "" msgid "Choose the top-level group for your repository imports." msgstr "" -msgid "Choose visibility level, enable/disable project features (issues, repository, wiki, snippets) and set permissions." +msgid "Choose visibility level, enable/disable project features and their permissions, disable email notifications, and show default award emoji." msgstr "" msgid "Choose what content you want to see on a groupโ€™s overview page." @@ -7343,6 +7343,30 @@ msgstr "" msgid "CompareBranches|There isn't anything to compare." msgstr "" +msgid "CompareRevisions|Branches" +msgstr "" + +msgid "CompareRevisions|Compare" +msgstr "" + +msgid "CompareRevisions|Create merge request" +msgstr "" + +msgid "CompareRevisions|Filter by Git revision" +msgstr "" + +msgid "CompareRevisions|Select branch/tag" +msgstr "" + +msgid "CompareRevisions|Tags" +msgstr "" + +msgid "CompareRevisions|There was an error while updating the branch/tag list. Please try again." +msgstr "" + +msgid "CompareRevisions|View open merge request" +msgstr "" + msgid "Complete" msgstr "" @@ -22799,10 +22823,7 @@ msgstr "" msgid "ProjectSettings|Allow editing commit messages" msgstr "" -msgid "ProjectSettings|Allow users to make copies of your repository to a new project" -msgstr "" - -msgid "ProjectSettings|Allow users to request access" +msgid "ProjectSettings|Always show thumbs-up and thumbs-down award emoji buttons on issues, merge requests, and snippets." msgstr "" msgid "ProjectSettings|Analytics" @@ -22814,7 +22835,7 @@ msgstr "" msgid "ProjectSettings|Badges" msgstr "" -msgid "ProjectSettings|Build, test, and deploy your changes" +msgid "ProjectSettings|Build, test, and deploy your changes." msgstr "" msgid "ProjectSettings|Checkbox is visible and selected by default." @@ -22829,6 +22850,9 @@ msgstr "" msgid "ProjectSettings|Choose your merge method, merge options, merge checks, merge suggestions, and set up a default description template for merge requests." msgstr "" +msgid "ProjectSettings|Commit authors can edit commit messages on unprotected branches." +msgstr "" + msgid "ProjectSettings|Contact an admin to change this setting." msgstr "" @@ -22856,7 +22880,7 @@ msgstr "" msgid "ProjectSettings|Encourage" msgstr "" -msgid "ProjectSettings|Environments, logs, cluster management, and more" +msgid "ProjectSettings|Environments, logs, cluster management, and more." msgstr "" msgid "ProjectSettings|Every merge creates a merge commit" @@ -22865,7 +22889,7 @@ msgstr "" msgid "ProjectSettings|Every project can have its own space to store its Docker images" msgstr "" -msgid "ProjectSettings|Every project can have its own space to store its packages" +msgid "ProjectSettings|Every project can have its own space to store its packages." msgstr "" msgid "ProjectSettings|Everyone" @@ -22904,13 +22928,13 @@ msgstr "" msgid "ProjectSettings|Issues" msgstr "" -msgid "ProjectSettings|LFS objects from this repository are still available to forks. %{linkStart}How do I remove them?%{linkEnd}" +msgid "ProjectSettings|LFS objects from this repository are available to forks. %{linkStart}How do I remove them?%{linkEnd}" msgstr "" -msgid "ProjectSettings|Lightweight issue tracking system for this project" +msgid "ProjectSettings|Lightweight issue tracking system." msgstr "" -msgid "ProjectSettings|Manages large files such as audio, video, and graphics files" +msgid "ProjectSettings|Manages large files such as audio, video, and graphics files." msgstr "" msgid "ProjectSettings|Merge checks" @@ -22946,13 +22970,16 @@ msgstr "" msgid "ProjectSettings|Operations" msgstr "" +msgid "ProjectSettings|Override user notification preferences for all project members." +msgstr "" + msgid "ProjectSettings|Packages" msgstr "" msgid "ProjectSettings|Pages" msgstr "" -msgid "ProjectSettings|Pages for project documentation" +msgid "ProjectSettings|Pages for project documentation." msgstr "" msgid "ProjectSettings|Pipelines" @@ -22982,7 +23009,7 @@ msgstr "" msgid "ProjectSettings|Requirements" msgstr "" -msgid "ProjectSettings|Requirements management system for this project" +msgid "ProjectSettings|Requirements management system." msgstr "" msgid "ProjectSettings|Security & Compliance" @@ -22994,7 +23021,7 @@ msgstr "" msgid "ProjectSettings|Set the default behavior and availability of this option in merge requests. Changes made are also applied to existing merge requests." msgstr "" -msgid "ProjectSettings|Share code pastes with others out of Git repository" +msgid "ProjectSettings|Share code with others outside the project." msgstr "" msgid "ProjectSettings|Show default award emojis" @@ -23018,7 +23045,7 @@ msgstr "" msgid "ProjectSettings|Squashing is never performed and the checkbox is hidden." msgstr "" -msgid "ProjectSettings|Submit changes to be merged upstream" +msgid "ProjectSettings|Submit changes to be merged upstream." msgstr "" msgid "ProjectSettings|The commit message used to apply merge request suggestions" @@ -23042,30 +23069,36 @@ msgstr "" msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin." msgstr "" -msgid "ProjectSettings|This setting will override user notification preferences for all project members." -msgstr "" - msgid "ProjectSettings|This will dictate the commit history when you merge a merge request" msgstr "" msgid "ProjectSettings|Transfer project" msgstr "" +msgid "ProjectSettings|Users can copy the repository to a new project." +msgstr "" + msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails." msgstr "" -msgid "ProjectSettings|View and edit files in this project" +msgid "ProjectSettings|Users can request access" msgstr "" -msgid "ProjectSettings|View and edit files in this project. Non-project members will only have read access" +msgid "ProjectSettings|View and edit files in this project." msgstr "" -msgid "ProjectSettings|View project analytics" +msgid "ProjectSettings|View and edit files in this project. Non-project members will only have read access." +msgstr "" + +msgid "ProjectSettings|View project analytics." msgstr "" msgid "ProjectSettings|Visibility options for this fork are limited by the current visibility of the source project." msgstr "" +msgid "ProjectSettings|Visualize the project's performance metrics." +msgstr "" + msgid "ProjectSettings|What are badges?" msgstr "" @@ -23075,19 +23108,10 @@ msgstr "" msgid "ProjectSettings|When conflicts arise the user is given the option to rebase" msgstr "" -msgid "ProjectSettings|When enabled, commit authors will be able to edit commit messages on unprotected branches." -msgstr "" - -msgid "ProjectSettings|When enabled, issues, merge requests, and snippets will always show thumbs-up and thumbs-down award emoji buttons." -msgstr "" - msgid "ProjectSettings|Wiki" msgstr "" -msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab" -msgstr "" - -msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics" +msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab." msgstr "" msgid "ProjectTemplates|.NET Core" diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/jira_issue_import_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/jira_issue_import_spec.rb index 42b3e5364b7..1455847277e 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/jira_issue_import_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/jira_issue_import_spec.rb @@ -39,27 +39,22 @@ module QA private def set_up_jira_integration - # Retry is required because allow_local_requests_from_web_hooks_and_services - # takes some time to get enabled. - # Bug issue: https://gitlab.com/gitlab-org/gitlab/-/issues/217010 - QA::Support::Retrier.retry_on_exception(max_attempts: 5, sleep_interval: 3) do - Runtime::ApplicationSettings.set_application_settings(allow_local_requests_from_web_hooks_and_services: true) + Runtime::ApplicationSettings.set_application_settings(allow_local_requests_from_web_hooks_and_services: true) - page.visit Runtime::Scenario.gitlab_address - Flow::Login.sign_in_unless_signed_in + page.visit Runtime::Scenario.gitlab_address + Flow::Login.sign_in_unless_signed_in - project.visit! + project.visit! - Page::Project::Menu.perform(&:go_to_integrations_settings) - QA::Page::Project::Settings::Integrations.perform(&:click_jira_link) + Page::Project::Menu.perform(&:go_to_integrations_settings) + QA::Page::Project::Settings::Integrations.perform(&:click_jira_link) - QA::Page::Project::Settings::Services::Jira.perform do |jira| - jira.setup_service_with(url: Vendor::Jira::JiraAPI.perform(&:base_url)) - end - - expect(page).not_to have_text("Url is blocked") - expect(page).to have_text("Jira settings saved and active.") + QA::Page::Project::Settings::Services::Jira.perform do |jira| + jira.setup_service_with(url: Vendor::Jira::JiraAPI.perform(&:base_url)) end + + expect(page).not_to have_text("Url is blocked") + expect(page).to have_text("Jira settings saved and active.") end def import_jira_issues diff --git a/qa/qa/specs/features/browser_ui/3_create/jira/jira_basic_integration_spec.rb b/qa/qa/specs/features/browser_ui/3_create/jira/jira_basic_integration_spec.rb index d53e7fcf69a..449795f9707 100644 --- a/qa/qa/specs/features/browser_ui/3_create/jira/jira_basic_integration_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/jira/jira_basic_integration_spec.rb @@ -19,26 +19,21 @@ module QA page.has_text? 'Welcome to Jira' end - # Retry is required because allow_local_requests_from_web_hooks_and_services - # takes some time to get enabled. - # Bug issue: https://gitlab.com/gitlab-org/gitlab/-/issues/217010 - QA::Support::Retrier.retry_on_exception(max_attempts: 5, sleep_interval: 3) do - Runtime::ApplicationSettings.set_application_settings(allow_local_requests_from_web_hooks_and_services: true) + Runtime::ApplicationSettings.set_application_settings(allow_local_requests_from_web_hooks_and_services: true) - page.visit Runtime::Scenario.gitlab_address - Flow::Login.sign_in_unless_signed_in + page.visit Runtime::Scenario.gitlab_address + Flow::Login.sign_in_unless_signed_in - project.visit! + project.visit! - Page::Project::Menu.perform(&:go_to_integrations_settings) - QA::Page::Project::Settings::Integrations.perform(&:click_jira_link) + Page::Project::Menu.perform(&:go_to_integrations_settings) + QA::Page::Project::Settings::Integrations.perform(&:click_jira_link) - QA::Page::Project::Settings::Services::Jira.perform do |jira| - jira.setup_service_with(url: Vendor::Jira::JiraAPI.perform(&:base_url)) - end - - expect(page).not_to have_text("Requests to the local network are not allowed") + QA::Page::Project::Settings::Services::Jira.perform do |jira| + jira.setup_service_with(url: Vendor::Jira::JiraAPI.perform(&:base_url)) end + + expect(page).not_to have_text("Requests to the local network are not allowed") end it 'closes an issue via pushing a commit', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/827' do diff --git a/scripts/trigger-build b/scripts/trigger-build index 84a970860b0..29d53609026 100755 --- a/scripts/trigger-build +++ b/scripts/trigger-build @@ -200,7 +200,7 @@ module Trigger class Docs < Base def self.access_token - ENV['DOCS_API_TOKEN'] + ENV['DOCS_PROJECT_API_TOKEN'] end SUCCESS_MESSAGE = <<~MSG diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index e96113c0133..6b77794c66d 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -150,7 +150,7 @@ RSpec.describe Projects::NotesController do end it 'returns an empty page of notes' do - expect(Gitlab::EtagCaching::Middleware).not_to receive(:skip!) + expect(Gitlab::EtagCaching::Middleware).to receive(:skip!) request.headers['X-Last-Fetched-At'] = microseconds(Time.zone.now) @@ -169,6 +169,8 @@ RSpec.describe Projects::NotesController do end it 'returns all notes' do + expect(Gitlab::EtagCaching::Middleware).to receive(:skip!) + get :index, params: request_params expect(json_response['notes'].count).to eq((page_1 + page_2 + page_3).size + 1) diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index a24363f29f2..c531c699e98 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -5,292 +5,296 @@ require 'spec_helper' RSpec.describe SearchController do include ExternalAuthorizationServiceHelpers - let(:user) { create(:user) } - - before do - sign_in(user) - end - - shared_examples_for 'when the user cannot read cross project' do |action, params| - before do - allow(Ability).to receive(:allowed?).and_call_original - allow(Ability).to receive(:allowed?) - .with(user, :read_cross_project, :global) { false } - end - - it 'blocks access without a project_id' do - get action, params: params - - expect(response).to have_gitlab_http_status(:forbidden) - end - - it 'allows access with a project_id' do - get action, params: params.merge(project_id: create(:project, :public).id) - - expect(response).to have_gitlab_http_status(:ok) - end - end - - shared_examples_for 'with external authorization service enabled' do |action, params| - let(:project) { create(:project, namespace: user.namespace) } - let(:note) { create(:note_on_issue, project: project) } + context 'authorized user' do + let(:user) { create(:user) } before do - enable_external_authorization_service_check + sign_in(user) end - it 'renders a 403 when no project is given' do - get action, params: params + shared_examples_for 'when the user cannot read cross project' do |action, params| + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(user, :read_cross_project, :global) { false } + end - expect(response).to have_gitlab_http_status(:forbidden) - end + it 'blocks access without a project_id' do + get action, params: params - it 'renders a 200 when a project was set' do - get action, params: params.merge(project_id: project.id) + expect(response).to have_gitlab_http_status(:forbidden) + end - expect(response).to have_gitlab_http_status(:ok) - end - end - - describe 'GET #show' do - it_behaves_like 'when the user cannot read cross project', :show, { search: 'hello' } do - it 'still allows accessing the search page' do - get :show + it 'allows access with a project_id' do + get action, params: params.merge(project_id: create(:project, :public).id) expect(response).to have_gitlab_http_status(:ok) end end - it_behaves_like 'with external authorization service enabled', :show, { search: 'hello' } - - context 'uses the right partials depending on scope' do - using RSpec::Parameterized::TableSyntax - render_views - - let_it_be(:project) { create(:project, :public, :repository, :wiki_repo) } + shared_examples_for 'with external authorization service enabled' do |action, params| + let(:project) { create(:project, namespace: user.namespace) } + let(:note) { create(:note_on_issue, project: project) } before do - expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original + enable_external_authorization_service_check end - subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) } + it 'renders a 403 when no project is given' do + get action, params: params - where(:partial, :scope) do - '_blob' | :blobs - '_wiki_blob' | :wiki_blobs - '_commit' | :commits + expect(response).to have_gitlab_http_status(:forbidden) end - with_them do - it do - project_wiki = create(:project_wiki, project: project, user: user) - create(:wiki_page, wiki: project_wiki, title: 'merge', content: 'merge') + it 'renders a 200 when a project was set' do + get action, params: params.merge(project_id: project.id) - expect(subject).to render_template("search/results/#{partial}") - end + expect(response).to have_gitlab_http_status(:ok) end end - context 'global search' do - using RSpec::Parameterized::TableSyntax - render_views + describe 'GET #show' do + it_behaves_like 'when the user cannot read cross project', :show, { search: 'hello' } do + it 'still allows accessing the search page' do + get :show + + expect(response).to have_gitlab_http_status(:ok) + end + end + + it_behaves_like 'with external authorization service enabled', :show, { search: 'hello' } + + context 'uses the right partials depending on scope' do + using RSpec::Parameterized::TableSyntax + render_views + + let_it_be(:project) { create(:project, :public, :repository, :wiki_repo) } - context 'when block_anonymous_global_searches is disabled' do before do - stub_feature_flags(block_anonymous_global_searches: false) + expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original end - it 'omits pipeline status from load' do - project = create(:project, :public) - expect(Gitlab::Cache::Ci::ProjectPipelineStatus).not_to receive(:load_in_batch_for_projects) + subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) } - get :show, params: { scope: 'projects', search: project.name } - - expect(assigns[:search_objects].first).to eq project + where(:partial, :scope) do + '_blob' | :blobs + '_wiki_blob' | :wiki_blobs + '_commit' | :commits end - context 'check search term length' do - let(:search_queries) do - char_limit = SearchService::SEARCH_CHAR_LIMIT - term_limit = SearchService::SEARCH_TERM_LIMIT - { - chars_under_limit: ('a' * (char_limit - 1)), - chars_over_limit: ('a' * (char_limit + 1)), - terms_under_limit: ('abc ' * (term_limit - 1)), - terms_over_limit: ('abc ' * (term_limit + 1)) - } + with_them do + it do + project_wiki = create(:project_wiki, project: project, user: user) + create(:wiki_page, wiki: project_wiki, title: 'merge', content: 'merge') + + expect(subject).to render_template("search/results/#{partial}") + end + end + end + + context 'global search' do + using RSpec::Parameterized::TableSyntax + render_views + + context 'when block_anonymous_global_searches is disabled' do + before do + stub_feature_flags(block_anonymous_global_searches: false) end - where(:string_name, :expectation) do - :chars_under_limit | :not_to_set_flash - :chars_over_limit | :set_chars_flash - :terms_under_limit | :not_to_set_flash - :terms_over_limit | :set_terms_flash + it 'omits pipeline status from load' do + project = create(:project, :public) + expect(Gitlab::Cache::Ci::ProjectPipelineStatus).not_to receive(:load_in_batch_for_projects) + + get :show, params: { scope: 'projects', search: project.name } + + expect(assigns[:search_objects].first).to eq project end - with_them do - it do - get :show, params: { scope: 'projects', search: search_queries[string_name] } + context 'check search term length' do + let(:search_queries) do + char_limit = SearchService::SEARCH_CHAR_LIMIT + term_limit = SearchService::SEARCH_TERM_LIMIT + { + chars_under_limit: ('a' * (char_limit - 1)), + chars_over_limit: ('a' * (char_limit + 1)), + terms_under_limit: ('abc ' * (term_limit - 1)), + terms_over_limit: ('abc ' * (term_limit + 1)) + } + end - case expectation - when :not_to_set_flash - expect(controller).not_to set_flash[:alert] - when :set_chars_flash - expect(controller).to set_flash[:alert].to(/characters/) - when :set_terms_flash - expect(controller).to set_flash[:alert].to(/terms/) + where(:string_name, :expectation) do + :chars_under_limit | :not_to_set_flash + :chars_over_limit | :set_chars_flash + :terms_under_limit | :not_to_set_flash + :terms_over_limit | :set_terms_flash + end + + with_them do + it do + get :show, params: { scope: 'projects', search: search_queries[string_name] } + + case expectation + when :not_to_set_flash + expect(controller).not_to set_flash[:alert] + when :set_chars_flash + expect(controller).to set_flash[:alert].to(/characters/) + when :set_terms_flash + expect(controller).to set_flash[:alert].to(/terms/) + end end end end end + + context 'when block_anonymous_global_searches is enabled' do + context 'for unauthenticated user' do + before do + sign_out(user) + end + + it 'redirects to login page' do + get :show, params: { scope: 'projects', search: '*' } + + expect(response).to redirect_to new_user_session_path + end + end + + context 'for authenticated user' do + it 'succeeds' do + get :show, params: { scope: 'projects', search: '*' } + + expect(response).to have_gitlab_http_status(:ok) + end + end + end end - context 'when block_anonymous_global_searches is enabled' do - context 'for unauthenticated user' do + it 'finds issue comments' do + project = create(:project, :public) + note = create(:note_on_issue, project: project) + + get :show, params: { project_id: project.id, scope: 'notes', search: note.note } + + expect(assigns[:search_objects].first).to eq note + end + + context 'unique users tracking' do + before do + allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event) + end + + it_behaves_like 'tracking unique hll events', :search_track_unique_users do + subject(:request) { get :show, params: { scope: 'projects', search: 'term' } } + + let(:target_id) { 'i_search_total' } + let(:expected_type) { instance_of(String) } + end + end + + context 'on restricted projects' do + context 'when signed out' do before do sign_out(user) end - it 'redirects to login page' do - get :show, params: { scope: 'projects', search: '*' } + it "doesn't expose comments on issues" do + project = create(:project, :public, :issues_private) + note = create(:note_on_issue, project: project) - expect(response).to redirect_to new_user_session_path + get :show, params: { project_id: project.id, scope: 'notes', search: note.note } + + expect(assigns[:search_objects].count).to eq(0) end end - context 'for authenticated user' do - it 'succeeds' do - get :show, params: { scope: 'projects', search: '*' } + it "doesn't expose comments on merge_requests" do + project = create(:project, :public, :merge_requests_private) + note = create(:note_on_merge_request, project: project) - expect(response).to have_gitlab_http_status(:ok) - end - end - end - end + get :show, params: { project_id: project.id, scope: 'notes', search: note.note } - it 'finds issue comments' do - project = create(:project, :public) - note = create(:note_on_issue, project: project) - - get :show, params: { project_id: project.id, scope: 'notes', search: note.note } - - expect(assigns[:search_objects].first).to eq note - end - - context 'unique users tracking' do - before do - allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event) - end - - it_behaves_like 'tracking unique hll events', :search_track_unique_users do - subject(:request) { get :show, params: { scope: 'projects', search: 'term' } } - - let(:target_id) { 'i_search_total' } - let(:expected_type) { instance_of(String) } - end - end - - context 'on restricted projects' do - context 'when signed out' do - before do - sign_out(user) + expect(assigns[:search_objects].count).to eq(0) end - it "doesn't expose comments on issues" do - project = create(:project, :public, :issues_private) - note = create(:note_on_issue, project: project) + it "doesn't expose comments on snippets" do + project = create(:project, :public, :snippets_private) + note = create(:note_on_project_snippet, project: project) get :show, params: { project_id: project.id, scope: 'notes', search: note.note } expect(assigns[:search_objects].count).to eq(0) end end + end - it "doesn't expose comments on merge_requests" do - project = create(:project, :public, :merge_requests_private) - note = create(:note_on_merge_request, project: project) + describe 'GET #count' do + it_behaves_like 'when the user cannot read cross project', :count, { search: 'hello', scope: 'projects' } + it_behaves_like 'with external authorization service enabled', :count, { search: 'hello', scope: 'projects' } - get :show, params: { project_id: project.id, scope: 'notes', search: note.note } + it 'returns the result count for the given term and scope' do + create(:project, :public, name: 'hello world') + create(:project, :public, name: 'foo bar') - expect(assigns[:search_objects].count).to eq(0) + get :count, params: { search: 'hello', scope: 'projects' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'count' => '1' }) end - it "doesn't expose comments on snippets" do - project = create(:project, :public, :snippets_private) - note = create(:note_on_project_snippet, project: project) + it 'raises an error if search term is missing' do + expect do + get :count, params: { scope: 'projects' } + end.to raise_error(ActionController::ParameterMissing) + end - get :show, params: { project_id: project.id, scope: 'notes', search: note.note } + it 'raises an error if search scope is missing' do + expect do + get :count, params: { search: 'hello' } + end.to raise_error(ActionController::ParameterMissing) + end + end - expect(assigns[:search_objects].count).to eq(0) + describe 'GET #autocomplete' do + it_behaves_like 'when the user cannot read cross project', :autocomplete, { term: 'hello' } + it_behaves_like 'with external authorization service enabled', :autocomplete, { term: 'hello' } + end + + describe '#append_info_to_payload' do + it 'appends search metadata for logging' do + last_payload = nil + original_append_info_to_payload = controller.method(:append_info_to_payload) + + expect(controller).to receive(:append_info_to_payload) do |payload| + original_append_info_to_payload.call(payload) + last_payload = payload + end + + get :show, params: { scope: 'issues', search: 'hello world', group_id: '123', project_id: '456', confidential: true, state: true, force_search_results: true } + + expect(last_payload[:metadata]['meta.search.group_id']).to eq('123') + expect(last_payload[:metadata]['meta.search.project_id']).to eq('456') + expect(last_payload[:metadata]).not_to have_key('meta.search.search') + expect(last_payload[:metadata]['meta.search.scope']).to eq('issues') + expect(last_payload[:metadata]['meta.search.force_search_results']).to eq('true') + expect(last_payload[:metadata]['meta.search.filters.confidential']).to eq('true') + expect(last_payload[:metadata]['meta.search.filters.state']).to eq('true') end end end - describe 'GET #count' do - it_behaves_like 'when the user cannot read cross project', :count, { search: 'hello', scope: 'projects' } - it_behaves_like 'with external authorization service enabled', :count, { search: 'hello', scope: 'projects' } + context 'unauthorized user' do + describe 'GET #opensearch' do + render_views - it 'returns the result count for the given term and scope' do - create(:project, :public, name: 'hello world') - create(:project, :public, name: 'foo bar') + it 'renders xml' do + get :opensearch, format: :xml - get :count, params: { search: 'hello', scope: 'projects' } + doc = Nokogiri::XML.parse(response.body) - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'count' => '1' }) - end - - it 'raises an error if search term is missing' do - expect do - get :count, params: { scope: 'projects' } - end.to raise_error(ActionController::ParameterMissing) - end - - it 'raises an error if search scope is missing' do - expect do - get :count, params: { search: 'hello' } - end.to raise_error(ActionController::ParameterMissing) - end - end - - describe 'GET #autocomplete' do - it_behaves_like 'when the user cannot read cross project', :autocomplete, { term: 'hello' } - it_behaves_like 'with external authorization service enabled', :autocomplete, { term: 'hello' } - end - - describe 'GET #opensearch' do - render_views - - it 'renders xml' do - get :opensearch, format: :xml - - doc = Nokogiri::XML.parse(response.body) - - expect(response).to have_gitlab_http_status(:ok) - expect(doc.css('OpenSearchDescription ShortName').text).to eq('GitLab') - expect(doc.css('OpenSearchDescription *').map(&:name)).to eq(%w[ShortName Description InputEncoding Image Url SearchForm]) - end - end - - describe '#append_info_to_payload' do - it 'appends search metadata for logging' do - last_payload = nil - original_append_info_to_payload = controller.method(:append_info_to_payload) - - expect(controller).to receive(:append_info_to_payload) do |payload| - original_append_info_to_payload.call(payload) - last_payload = payload + expect(response).to have_gitlab_http_status(:ok) + expect(doc.css('OpenSearchDescription ShortName').text).to eq('GitLab') + expect(doc.css('OpenSearchDescription *').map(&:name)).to eq(%w[ShortName Description InputEncoding Image Url SearchForm]) end - - get :show, params: { scope: 'issues', search: 'hello world', group_id: '123', project_id: '456', confidential: true, state: true, force_search_results: true } - - expect(last_payload[:metadata]['meta.search.group_id']).to eq('123') - expect(last_payload[:metadata]['meta.search.project_id']).to eq('456') - expect(last_payload[:metadata]).not_to have_key('meta.search.search') - expect(last_payload[:metadata]['meta.search.scope']).to eq('issues') - expect(last_payload[:metadata]['meta.search.force_search_results']).to eq('true') - expect(last_payload[:metadata]['meta.search.filters.confidential']).to eq('true') - expect(last_payload[:metadata]['meta.search.filters.state']).to eq('true') end end end diff --git a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb index 885b41c3227..63b463a2c5f 100644 --- a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb +++ b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb @@ -68,7 +68,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do wait_for_requests - expect(page).to have_content 'Merge when pipeline succeeds', wait: 0 + expect(page).to have_content 'Merge when pipeline succeeds' end it_behaves_like 'Merge when pipeline succeeds activator' diff --git a/spec/features/projects/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb index 596b4773716..4894e2b7f3e 100644 --- a/spec/features/projects/commits/user_browses_commits_spec.rb +++ b/spec/features/projects/commits/user_browses_commits_spec.rb @@ -203,10 +203,11 @@ RSpec.describe 'User browses commits' do context 'when click the compare tab' do before do + wait_for_requests click_link('Compare') end - it 'does not render create merge request button' do + it 'does not render create merge request button', :js do expect(page).not_to have_link 'Create merge request' end end @@ -236,10 +237,11 @@ RSpec.describe 'User browses commits' do context 'when click the compare tab' do before do + wait_for_requests click_link('Compare') end - it 'renders create merge request button' do + it 'renders create merge request button', :js do expect(page).to have_link 'Create merge request' end end @@ -276,10 +278,11 @@ RSpec.describe 'User browses commits' do context 'when click the compare tab' do before do + wait_for_requests click_link('Compare') end - it 'renders button to the merge request' do + it 'renders button to the merge request', :js do expect(page).not_to have_link 'Create merge request' expect(page).to have_link 'View open merge request', href: project_merge_request_path(project, merge_request) end diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb index e387ea4d473..64e9968061c 100644 --- a/spec/features/projects/compare_spec.rb +++ b/spec/features/projects/compare_spec.rb @@ -17,10 +17,10 @@ RSpec.describe "Compare", :js do visit project_compare_index_path(project, from: 'master', to: 'master') select_using_dropdown 'from', 'feature' - expect(find('.js-compare-from-dropdown .dropdown-toggle-text')).to have_content('feature') + expect(find('.js-compare-from-dropdown .gl-new-dropdown-button-text')).to have_content('feature') select_using_dropdown 'to', 'binary-encoding' - expect(find('.js-compare-to-dropdown .dropdown-toggle-text')).to have_content('binary-encoding') + expect(find('.js-compare-to-dropdown .gl-new-dropdown-button-text')).to have_content('binary-encoding') click_button 'Compare' @@ -32,8 +32,8 @@ RSpec.describe "Compare", :js do it "pre-populates fields" do visit project_compare_index_path(project, from: "master", to: "master") - expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("master") - expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("master") + expect(find(".js-compare-from-dropdown .gl-new-dropdown-button-text")).to have_content("master") + expect(find(".js-compare-to-dropdown .gl-new-dropdown-button-text")).to have_content("master") end it_behaves_like 'compares branches' @@ -99,7 +99,7 @@ RSpec.describe "Compare", :js do find(".js-compare-from-dropdown .compare-dropdown-toggle").click - expect(find(".js-compare-from-dropdown .dropdown-content")).to have_selector("li", count: 3) + expect(find(".js-compare-from-dropdown .gl-new-dropdown-contents")).to have_selector('li.gl-new-dropdown-item', count: 1) end context 'when commit has overflow', :js do @@ -125,10 +125,10 @@ RSpec.describe "Compare", :js do visit project_compare_index_path(project, from: "master", to: "master") select_using_dropdown "from", "v1.0.0" - expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("v1.0.0") + expect(find(".js-compare-from-dropdown .gl-new-dropdown-button-text")).to have_content("v1.0.0") select_using_dropdown "to", "v1.1.0" - expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("v1.1.0") + expect(find(".js-compare-to-dropdown .gl-new-dropdown-button-text")).to have_content("v1.1.0") click_button "Compare" expect(page).to have_content "Commits" @@ -136,19 +136,22 @@ RSpec.describe "Compare", :js do end def select_using_dropdown(dropdown_type, selection, commit: false) + wait_for_requests + dropdown = find(".js-compare-#{dropdown_type}-dropdown") dropdown.find(".compare-dropdown-toggle").click # find input before using to wait for the inputs visibility dropdown.find('.dropdown-menu') dropdown.fill_in("Filter by Git revision", with: selection) + wait_for_requests if commit - dropdown.find('input[type="search"]').send_keys(:return) + dropdown.find('.gl-search-box-by-type-input').send_keys(:return) else # find before all to wait for the items visibility - dropdown.find("a[data-ref=\"#{selection}\"]", match: :first) - dropdown.all("a[data-ref=\"#{selection}\"]").last.click + dropdown.find(".js-compare-#{dropdown_type}-dropdown .dropdown-item", text: selection, match: :first) + dropdown.all(".js-compare-#{dropdown_type}-dropdown .dropdown-item", text: selection).first.click end end end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 5986a453dd5..d9a23edc03c 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -288,23 +288,23 @@ RSpec.describe 'Pipelines', :js do end it 'has a dropdown with play button' do - expect(page).to have_selector('.dropdown-new.btn.btn-default .icon-play') + expect(page).to have_selector('[data-testid="pipelines-manual-actions-dropdown"] [data-testid="play-icon"]') end it 'has link to the manual action' do - find('.js-pipeline-dropdown-manual-actions').click + find('[data-testid="pipelines-manual-actions-dropdown"]').click expect(page).to have_button('manual build') end context 'when manual action was played' do before do - find('.js-pipeline-dropdown-manual-actions').click + find('[data-testid="pipelines-manual-actions-dropdown"]').click click_button('manual build') end it 'enqueues manual action job' do - expect(page).to have_selector('.js-pipeline-dropdown-manual-actions:disabled') + expect(page).to have_selector('[data-testid="pipelines-manual-actions-dropdown"] .gl-dropdown-toggle:disabled') end end end @@ -322,11 +322,11 @@ RSpec.describe 'Pipelines', :js do end it 'has a dropdown for actionable jobs' do - expect(page).to have_selector('.dropdown-new.btn.btn-default .icon-play') + expect(page).to have_selector('[data-testid="pipelines-manual-actions-dropdown"] [data-testid="play-icon"]') end it "has link to the delayed job's action" do - find('.js-pipeline-dropdown-manual-actions').click + find('[data-testid="pipelines-manual-actions-dropdown"]').click time_diff = [0, delayed_job.scheduled_at - Time.now].max expect(page).to have_button('delayed job 1') @@ -342,7 +342,7 @@ RSpec.describe 'Pipelines', :js do end it "shows 00:00:00 as the remaining time" do - find('.js-pipeline-dropdown-manual-actions').click + find('[data-testid="pipelines-manual-actions-dropdown"]').click expect(page).to have_content("00:00:00") end @@ -350,7 +350,7 @@ RSpec.describe 'Pipelines', :js do context 'when user played a delayed job immediately' do before do - find('.js-pipeline-dropdown-manual-actions').click + find('[data-testid="pipelines-manual-actions-dropdown"]').click page.accept_confirm { click_button('delayed job 1') } wait_for_requests end diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js index 5cd8464bf77..468e5af6f1a 100644 --- a/spec/frontend/notes/stores/actions_spec.js +++ b/spec/frontend/notes/stores/actions_spec.js @@ -291,9 +291,45 @@ describe('Actions Notes Store', () => { [ { type: 'updateOrCreateNotes', payload: discussionMock.notes }, { type: 'startTaskList' }, + { type: 'updateResolvableDiscussionsCounts' }, ], )); }); + + describe('paginated notes feature flag enabled', () => { + const lastFetchedAt = '12358'; + + beforeEach(() => { + window.gon = { features: { paginatedNotes: true } }; + + axiosMock.onGet(notesDataMock.notesPath).replyOnce(200, { + notes: discussionMock.notes, + more: false, + last_fetched_at: lastFetchedAt, + }); + }); + + afterEach(() => { + window.gon = null; + }); + + it('should dispatch setFetchingState, setNotesFetchedState, setLoadingState, updateOrCreateNotes, startTaskList and commit SET_LAST_FETCHED_AT', () => { + return testAction( + actions.fetchData, + null, + { notesData: notesDataMock, isFetching: true }, + [{ type: 'SET_LAST_FETCHED_AT', payload: lastFetchedAt }], + [ + { type: 'setFetchingState', payload: false }, + { type: 'setNotesFetchedState', payload: true }, + { type: 'setLoadingState', payload: false }, + { type: 'updateOrCreateNotes', payload: discussionMock.notes }, + { type: 'startTaskList' }, + { type: 'updateResolvableDiscussionsCounts' }, + ], + ); + }); + }); }); describe('poll', () => { @@ -1355,4 +1391,17 @@ describe('Actions Notes Store', () => { ); }); }); + + describe('setFetchingState', () => { + it('commits SET_NOTES_FETCHING_STATE', (done) => { + testAction( + actions.setFetchingState, + true, + null, + [{ type: mutationTypes.SET_NOTES_FETCHING_STATE, payload: true }], + [], + done, + ); + }); + }); }); diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js index 9aee6ec7ace..689649063c0 100644 --- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js +++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js @@ -175,7 +175,7 @@ describe('Settings Panel', () => { wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PRIVATE }); expect(findRepositoryFeatureProjectRow().props().helpText).toBe( - 'View and edit files in this project', + 'View and edit files in this project.', ); }); @@ -183,7 +183,7 @@ describe('Settings Panel', () => { wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PUBLIC }); expect(findRepositoryFeatureProjectRow().props().helpText).toBe( - 'View and edit files in this project. Non-project members will only have read access', + 'View and edit files in this project. Non-project members will only have read access.', ); }); }); @@ -400,7 +400,7 @@ describe('Settings Panel', () => { const link = message.find('a'); expect(message.text()).toContain( - 'LFS objects from this repository are still available to forks', + 'LFS objects from this repository are available to forks.', ); expect(link.text()).toBe('How do I remove them?'); expect(link.attributes('href')).toBe( @@ -530,7 +530,7 @@ describe('Settings Panel', () => { it('should contain help text', () => { expect(wrapper.find({ ref: 'metrics-visibility-settings' }).props().helpText).toBe( - 'With Metrics Dashboard you can visualize this project performance metrics', + "Visualize the project's performance metrics.", ); }); diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js index 4e21ade2175..ec01fe7c324 100644 --- a/spec/frontend/pipelines/pipelines_actions_spec.js +++ b/spec/frontend/pipelines/pipelines_actions_spec.js @@ -1,25 +1,29 @@ -import { shallowMount } from '@vue/test-utils'; +import { shallowMount, mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; -import { GlButton } from '@gitlab/ui'; +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { TEST_HOST } from 'spec/test_constants'; import waitForPromises from 'helpers/wait_for_promises'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import PipelinesActions from '~/pipelines/components/pipelines_list/pipelines_actions.vue'; import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; +jest.mock('~/flash'); + describe('Pipelines Actions dropdown', () => { let wrapper; let mock; - const createComponent = (actions = []) => { - wrapper = shallowMount(PipelinesActions, { + const createComponent = (props, mountFn = shallowMount) => { + wrapper = mountFn(PipelinesActions, { propsData: { - actions, + ...props, }, }); }; - const findAllDropdownItems = () => wrapper.findAll(GlButton); + const findDropdown = () => wrapper.find(GlDropdown); + const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem); const findAllCountdowns = () => wrapper.findAll(GlCountdown); beforeEach(() => { @@ -47,7 +51,7 @@ describe('Pipelines Actions dropdown', () => { ]; beforeEach(() => { - createComponent(mockActions); + createComponent({ actions: mockActions }); }); it('renders a dropdown with the provided actions', () => { @@ -59,16 +63,33 @@ describe('Pipelines Actions dropdown', () => { }); describe('on click', () => { - it('makes a request and toggles the loading state', () => { + beforeEach(() => { + createComponent({ actions: mockActions }, mount); + }); + + it('makes a request and toggles the loading state', async () => { mock.onPost(mockActions.path).reply(200); - wrapper.find(GlButton).vm.$emit('click'); + findAllDropdownItems().at(0).vm.$emit('click'); - expect(wrapper.vm.isLoading).toBe(true); + await wrapper.vm.$nextTick(); + expect(findDropdown().props('loading')).toBe(true); - return waitForPromises().then(() => { - expect(wrapper.vm.isLoading).toBe(false); - }); + await waitForPromises(); + expect(findDropdown().props('loading')).toBe(false); + }); + + it('makes a failed request and toggles the loading state', async () => { + mock.onPost(mockActions.path).reply(500); + + findAllDropdownItems().at(0).vm.$emit('click'); + + await wrapper.vm.$nextTick(); + expect(findDropdown().props('loading')).toBe(true); + + await waitForPromises(); + expect(findDropdown().props('loading')).toBe(false); + expect(createFlash).toHaveBeenCalledTimes(1); }); }); }); @@ -89,10 +110,10 @@ describe('Pipelines Actions dropdown', () => { beforeEach(() => { jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime()); - createComponent([scheduledJobAction, expiredJobAction]); + createComponent({ actions: [scheduledJobAction, expiredJobAction] }); }); - it('makes post request after confirming', () => { + it('makes post request after confirming', async () => { mock.onPost(scheduledJobAction.path).reply(200); jest.spyOn(window, 'confirm').mockReturnValue(true); @@ -100,19 +121,22 @@ describe('Pipelines Actions dropdown', () => { expect(window.confirm).toHaveBeenCalled(); - return waitForPromises().then(() => { - expect(mock.history.post.length).toBe(1); - }); + await waitForPromises(); + + expect(mock.history.post).toHaveLength(1); }); - it('does not make post request if confirmation is cancelled', () => { + it('does not make post request if confirmation is cancelled', async () => { mock.onPost(scheduledJobAction.path).reply(200); jest.spyOn(window, 'confirm').mockReturnValue(false); findAllDropdownItems().at(0).vm.$emit('click'); expect(window.confirm).toHaveBeenCalled(); - expect(mock.history.post.length).toBe(0); + + await waitForPromises(); + + expect(mock.history.post).toHaveLength(0); }); it('displays the remaining time in the dropdown', () => { diff --git a/spec/frontend/projects/compare/components/app_spec.js b/spec/frontend/projects/compare/components/app_spec.js new file mode 100644 index 00000000000..e265055ef1b --- /dev/null +++ b/spec/frontend/projects/compare/components/app_spec.js @@ -0,0 +1,116 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlButton } from '@gitlab/ui'; +import CompareApp from '~/projects/compare/components/app.vue'; +import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vue'; + +jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' })); + +const projectCompareIndexPath = 'some/path'; +const refsProjectPath = 'some/refs/path'; +const paramsFrom = 'master'; +const paramsTo = 'master'; + +describe('CompareApp component', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMount(CompareApp, { + propsData: { + projectCompareIndexPath, + refsProjectPath, + paramsFrom, + paramsTo, + projectMergeRequestPath: '', + createMrPath: '', + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + beforeEach(() => { + createComponent(); + }); + + it('renders component with prop', () => { + expect(wrapper.props()).toEqual( + expect.objectContaining({ + projectCompareIndexPath, + refsProjectPath, + paramsFrom, + paramsTo, + }), + ); + }); + + it('contains the correct form attributes', () => { + expect(wrapper.attributes('action')).toBe(projectCompareIndexPath); + expect(wrapper.attributes('method')).toBe('POST'); + }); + + it('has input with csrf token', () => { + expect(wrapper.find('input[name="authenticity_token"]').attributes('value')).toBe( + 'mock-csrf-token', + ); + }); + + it('has ellipsis', () => { + expect(wrapper.find('[data-testid="ellipsis"]').exists()).toBe(true); + }); + + it('render Source and Target BranchDropdown components', () => { + const branchDropdowns = wrapper.findAll(RevisionDropdown); + + expect(branchDropdowns.length).toBe(2); + expect(branchDropdowns.at(0).props('revisionText')).toBe('Source'); + expect(branchDropdowns.at(1).props('revisionText')).toBe('Target'); + }); + + describe('compare button', () => { + const findCompareButton = () => wrapper.find(GlButton); + + it('renders button', () => { + expect(findCompareButton().exists()).toBe(true); + }); + + it('submits form', () => { + findCompareButton().vm.$emit('click'); + expect(wrapper.find('form').element.submit).toHaveBeenCalled(); + }); + + it('has compare text', () => { + expect(findCompareButton().text()).toBe('Compare'); + }); + }); + + describe('merge request buttons', () => { + const findProjectMrButton = () => wrapper.find('[data-testid="projectMrButton"]'); + const findCreateMrButton = () => wrapper.find('[data-testid="createMrButton"]'); + + it('does not have merge request buttons', () => { + createComponent(); + expect(findProjectMrButton().exists()).toBe(false); + expect(findCreateMrButton().exists()).toBe(false); + }); + + it('has "View open merge request" button', () => { + createComponent({ + projectMergeRequestPath: 'some/project/merge/request/path', + }); + expect(findProjectMrButton().exists()).toBe(true); + expect(findCreateMrButton().exists()).toBe(false); + }); + + it('has "Create merge request" button', () => { + createComponent({ + createMrPath: 'some/create/create/mr/path', + }); + expect(findProjectMrButton().exists()).toBe(false); + expect(findCreateMrButton().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/projects/compare/components/revision_dropdown_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_spec.js new file mode 100644 index 00000000000..c9e87fb904d --- /dev/null +++ b/spec/frontend/projects/compare/components/revision_dropdown_spec.js @@ -0,0 +1,92 @@ +import { shallowMount } from '@vue/test-utils'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import { GlDropdown } from '@gitlab/ui'; +import axios from '~/lib/utils/axios_utils'; +import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vue'; +import createFlash from '~/flash'; + +const defaultProps = { + refsProjectPath: 'some/refs/path', + revisionText: 'Target', + paramsName: 'from', + paramsBranch: 'master', +}; + +jest.mock('~/flash'); + +describe('RevisionDropdown component', () => { + let wrapper; + let axiosMock; + + const createComponent = (props = {}) => { + wrapper = shallowMount(RevisionDropdown, { + propsData: { + ...defaultProps, + ...props, + }, + }); + }; + + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + wrapper.destroy(); + axiosMock.restore(); + }); + + const findGlDropdown = () => wrapper.find(GlDropdown); + + it('sets hidden input', () => { + createComponent(); + expect(wrapper.find('input[type="hidden"]').attributes('value')).toBe( + defaultProps.paramsBranch, + ); + }); + + it('update the branches on success', async () => { + const Branches = ['branch-1', 'branch-2']; + const Tags = ['tag-1', 'tag-2', 'tag-3']; + + axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(200, { + Branches, + Tags, + }); + + createComponent(); + + await axios.waitForAll(); + + expect(wrapper.vm.branches).toEqual(Branches); + expect(wrapper.vm.tags).toEqual(Tags); + }); + + it('shows flash message on error', async () => { + axiosMock.onGet('some/invalid/path').replyOnce(404); + + createComponent(); + + await wrapper.vm.fetchBranchesAndTags(); + expect(createFlash).toHaveBeenCalled(); + }); + + describe('GlDropdown component', () => { + it('renders props', () => { + createComponent(); + expect(wrapper.props()).toEqual(expect.objectContaining(defaultProps)); + }); + + it('display default text', () => { + createComponent({ + paramsBranch: null, + }); + expect(findGlDropdown().props('text')).toBe('Select branch/tag'); + }); + + it('display params branch text', () => { + createComponent(); + expect(findGlDropdown().props('text')).toBe(defaultProps.paramsBranch); + }); + }); +}); diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js index 850bbd93df5..d309aa905e8 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js @@ -202,7 +202,11 @@ describe('MRWidgetAutoMergeEnabled', () => { wrapper.vm.cancelAutomaticMerge(); setImmediate(() => { expect(wrapper.vm.isCancellingAutoMerge).toBeTruthy(); - expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj); + if (mergeRequestWidgetGraphql) { + expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); + } else { + expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj); + } done(); }); }); diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb index f9b3b535334..b8502cdf25e 100644 --- a/spec/helpers/notes_helper_spec.rb +++ b/spec/helpers/notes_helper_spec.rb @@ -316,4 +316,15 @@ RSpec.describe NotesHelper do end end end + + describe '#notes_data' do + let(:issue) { create(:issue, project: project) } + + it 'sets last_fetched_at to 0 when start_at_zero is true' do + @project = project + @noteable = issue + + expect(helper.notes_data(issue, true)[:lastFetchedAt]).to eq(0) + end + end end diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb index 6cb9894e306..bc25a2fcdfc 100644 --- a/spec/helpers/tree_helper_spec.rb +++ b/spec/helpers/tree_helper_spec.rb @@ -19,94 +19,6 @@ RSpec.describe TreeHelper do ) end - describe '.render_tree' do - before do - @id = sha - @path = "" - @project = project - @lfs_blob_ids = [] - end - - it 'displays all entries without a warning' do - tree = repository.tree(sha, 'files') - - html = render_tree(tree) - - expect(html).not_to have_selector('.tree-truncated-warning') - end - - it 'truncates entries and adds a warning' do - stub_const('TreeHelper::FILE_LIMIT', 1) - tree = repository.tree(sha, 'files') - - html = render_tree(tree) - - expect(html).to have_selector('.tree-truncated-warning', count: 1) - expect(html).to have_selector('.tree-item-file-name', count: 1) - end - end - - describe '.fast_project_blob_path' do - it 'generates the same path as project_blob_path' do - blob_path = repository.tree(sha, 'with space').entries.first.path - fast_path = fast_project_blob_path(project, blob_path) - std_path = project_blob_path(project, blob_path) - - expect(fast_path).to eq(std_path) - end - - it 'generates the same path with encoded file names' do - tree = repository.tree(sha, 'encoding') - blob_path = tree.entries.find { |entry| entry.path == 'encoding/ใƒ†ใ‚นใƒˆ.txt' }.path - fast_path = fast_project_blob_path(project, blob_path) - std_path = project_blob_path(project, blob_path) - - expect(fast_path).to eq(std_path) - end - - it 'respects a configured relative URL' do - allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root') - blob_path = repository.tree(sha, '').entries.first.path - fast_path = fast_project_blob_path(project, blob_path) - - expect(fast_path).to start_with('/gitlab/root') - end - - it 'encodes files starting with #' do - filename = '#test-file' - create_file(filename) - - fast_path = fast_project_blob_path(project, filename) - - expect(fast_path).to end_with('%23test-file') - end - end - - describe '.fast_project_tree_path' do - let(:tree_path) { repository.tree(sha, 'with space').path } - let(:fast_path) { fast_project_tree_path(project, tree_path) } - let(:std_path) { project_tree_path(project, tree_path) } - - it 'generates the same path as project_tree_path' do - expect(fast_path).to eq(std_path) - end - - it 'respects a configured relative URL' do - allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root') - - expect(fast_path).to start_with('/gitlab/root') - end - - it 'encodes files starting with #' do - filename = '#test-file' - create_file(filename) - - fast_path = fast_project_tree_path(project, filename) - - expect(fast_path).to end_with('%23test-file') - end - end - describe 'flatten_tree' do let(:tree) { repository.tree(sha, 'files') } let(:root_path) { 'files' } diff --git a/spec/lib/bulk_imports/pipeline/runner_spec.rb b/spec/lib/bulk_imports/pipeline/runner_spec.rb index 7373588db53..3f6a172beee 100644 --- a/spec/lib/bulk_imports/pipeline/runner_spec.rb +++ b/spec/lib/bulk_imports/pipeline/runner_spec.rb @@ -74,28 +74,41 @@ RSpec.describe BulkImports::Pipeline::Runner do expect_next_instance_of(Gitlab::Import::Logger) do |logger| expect(logger).to receive(:info) .with( + bulk_import_entity_id: entity.id, + bulk_import_entity_type: 'group_entity', message: 'Pipeline started', + pipeline_class: 'BulkImports::MyPipeline' + ) + expect(logger).to receive(:info) + .with( + bulk_import_entity_id: entity.id, + bulk_import_entity_type: 'group_entity', pipeline_class: 'BulkImports::MyPipeline', - bulk_import_entity_id: entity.id, - bulk_import_entity_type: 'group_entity' + pipeline_step: :extractor, + step_class: 'BulkImports::Extractor' ) expect(logger).to receive(:info) .with( bulk_import_entity_id: entity.id, bulk_import_entity_type: 'group_entity', - extractor: 'BulkImports::Extractor' + pipeline_class: 'BulkImports::MyPipeline', + pipeline_step: :transformer, + step_class: 'BulkImports::Transformer' ) expect(logger).to receive(:info) .with( bulk_import_entity_id: entity.id, bulk_import_entity_type: 'group_entity', - transformer: 'BulkImports::Transformer' + pipeline_class: 'BulkImports::MyPipeline', + pipeline_step: :loader, + step_class: 'BulkImports::Loader' ) expect(logger).to receive(:info) .with( bulk_import_entity_id: entity.id, bulk_import_entity_type: 'group_entity', - loader: 'BulkImports::Loader' + message: 'Pipeline finished', + pipeline_class: 'BulkImports::MyPipeline' ) end diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb index b99a5352717..856ae87c5bf 100644 --- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb +++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb @@ -38,7 +38,8 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do 'pid' => Process.pid, 'created_at' => created_at.to_f, 'enqueued_at' => created_at.to_f, - 'scheduling_latency_s' => scheduling_latency_s + 'scheduling_latency_s' => scheduling_latency_s, + 'job_size_bytes' => be > 0 ) end diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb index e6bfc45cfff..f0b8ce6c2fb 100644 --- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb @@ -40,7 +40,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s 'code_review', 'terraform', 'ci_templates', - 'quickactions' + 'quickactions', + 'pipeline_authoring' ) end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index b3632522b57..602f6640d72 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -1324,7 +1324,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do subject { described_class.redis_hll_counters } let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories } - let(:ineligible_total_categories) { %w[source_code ci_secrets_management incident_management_alerts snippets terraform] } + let(:ineligible_total_categories) do + %w[source_code ci_secrets_management incident_management_alerts snippets terraform pipeline_authoring] + end it 'has all known_events' do expect(subject).to have_key(:redis_hll_counters) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 351a9436628..6d8cdde2c4f 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1139,7 +1139,7 @@ RSpec.describe API::Projects do let!(:public_project) { create(:project, :public, name: 'public_project', creator_id: user4.id, namespace: user4.namespace) } it 'returns error when user not found' do - get api('/users/0/projects/') + get api("/users/#{non_existing_record_id}/projects/") expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 User Not Found') @@ -2154,7 +2154,7 @@ RSpec.describe API::Projects do end it 'fails if forked_from project which does not exist' do - post api("/projects/#{project_fork_target.id}/fork/0", admin) + post api("/projects/#{project_fork_target.id}/fork/#{non_existing_record_id}", admin) expect(response).to have_gitlab_http_status(:not_found) end @@ -2398,7 +2398,7 @@ RSpec.describe API::Projects do end it 'returns a 404 error when project does not exist' do - delete api("/projects/123/share/#{non_existing_record_id}", user) + delete api("/projects/#{non_existing_record_id}/share/#{non_existing_record_id}", user) expect(response).to have_gitlab_http_status(:not_found) end @@ -2955,7 +2955,7 @@ RSpec.describe API::Projects do end it 'returns the proper security headers' do - get api('/projects/1/starrers', current_user) + get api("/projects/#{public_project.id}/starrers", current_user) expect(response).to include_security_headers end @@ -3028,7 +3028,7 @@ RSpec.describe API::Projects do end it 'returns not_found(404) for not existing project' do - get api("/projects/0/languages", user) + get api("/projects/#{non_existing_record_id}/languages", user) expect(response).to have_gitlab_http_status(:not_found) end @@ -3079,7 +3079,7 @@ RSpec.describe API::Projects do end it 'does not remove a non existing project' do - delete api('/projects/1328', user) + delete api("/projects/#{non_existing_record_id}", user) expect(response).to have_gitlab_http_status(:not_found) end @@ -3098,7 +3098,7 @@ RSpec.describe API::Projects do end it 'does not remove a non existing project' do - delete api('/projects/1328', admin) + delete api("/projects/#{non_existing_record_id}", admin) expect(response).to have_gitlab_http_status(:not_found) end diff --git a/spec/requests/projects/noteable_notes_spec.rb b/spec/requests/projects/noteable_notes_spec.rb index 2bf1ffb2edc..5ae2aadaa84 100644 --- a/spec/requests/projects/noteable_notes_spec.rb +++ b/spec/requests/projects/noteable_notes_spec.rb @@ -18,9 +18,7 @@ RSpec.describe 'Project noteable notes' do login_as(user) end - it 'does not set a Gitlab::EtagCaching ETag if there is a note' do - create(:note_on_merge_request, noteable: merge_request, project: merge_request.project) - + it 'does not set a Gitlab::EtagCaching ETag' do get notes_path expect(response).to have_gitlab_http_status(:ok) @@ -29,12 +27,5 @@ RSpec.describe 'Project noteable notes' do # interfere with notes pagination expect(response_etag).not_to eq(stored_etag) end - - it 'sets a Gitlab::EtagCaching ETag if there is no note' do - get notes_path - - expect(response).to have_gitlab_http_status(:ok) - expect(response_etag).to eq(stored_etag) - end end end diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb index a5290f0be68..bf2a7390258 100644 --- a/spec/services/git/branch_hooks_service_spec.rb +++ b/spec/services/git/branch_hooks_service_spec.rb @@ -93,12 +93,12 @@ RSpec.describe Git::BranchHooksService do describe 'Push Event' do let(:event) { Event.pushed_action.first } - before do - service.execute - end + subject(:execute_service) { service.execute } context "with an existing branch" do it 'generates a push event with one commit' do + execute_service + expect(event).to be_an_instance_of(PushEvent) expect(event.project).to eq(project) expect(event).to be_pushed_action @@ -109,12 +109,87 @@ RSpec.describe Git::BranchHooksService do expect(event.push_event_payload.ref).to eq('master') expect(event.push_event_payload.commit_count).to eq(1) end + + context 'with changing CI config' do + before do + allow_next_instance_of(Gitlab::Git::Diff) do |diff| + allow(diff).to receive(:new_path).and_return('.gitlab-ci.yml') + end + + allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event) + end + + let!(:commit_author) { create(:user, email: sample_commit.author_email) } + + let(:tracking_params) do + ['o_pipeline_authoring_unique_users_committing_ciconfigfile', values: commit_author.id] + end + + it 'tracks the event' do + execute_service + + expect(Gitlab::UsageDataCounters::HLLRedisCounter) + .to have_received(:track_event).with(*tracking_params) + end + + context 'when the FF usage_data_unique_users_committing_ciconfigfile is disabled' do + before do + stub_feature_flags(usage_data_unique_users_committing_ciconfigfile: false) + end + + it 'does not track the event' do + execute_service + + expect(Gitlab::UsageDataCounters::HLLRedisCounter) + .not_to have_received(:track_event).with(*tracking_params) + end + end + + context 'when usage ping is disabled' do + before do + stub_application_setting(usage_ping_enabled: false) + end + + it 'does not track the event' do + execute_service + + expect(Gitlab::UsageDataCounters::HLLRedisCounter) + .not_to have_received(:track_event).with(*tracking_params) + end + end + + context 'when the branch is not the main branch' do + let(:branch) { 'feature' } + + it 'does not track the event' do + execute_service + + expect(Gitlab::UsageDataCounters::HLLRedisCounter) + .not_to have_received(:track_event).with(*tracking_params) + end + end + + context 'when the CI config is a different path' do + before do + project.ci_config_path = 'config/ci.yml' + end + + it 'does not track the event' do + execute_service + + expect(Gitlab::UsageDataCounters::HLLRedisCounter) + .not_to have_received(:track_event).with(*tracking_params) + end + end + 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 + execute_service + expect(event).to be_an_instance_of(PushEvent) expect(event.project).to eq(project) expect(event).to be_pushed_action @@ -131,6 +206,8 @@ RSpec.describe Git::BranchHooksService do let(:newrev) { Gitlab::Git::BLANK_SHA } it 'generates a push event with no commits' do + execute_service + expect(event).to be_an_instance_of(PushEvent) expect(event.project).to eq(project) expect(event).to be_pushed_action diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b1b106f58ff..cd3818b256d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -220,7 +220,7 @@ RSpec.configure do |config| # Merge request widget GraphQL requests are disabled in the tests # for now whilst we migrate as much as we can over the GraphQL - stub_feature_flags(merge_request_widget_graphql: false) + # stub_feature_flags(merge_request_widget_graphql: false) # Using FortiAuthenticator as OTP provider is disabled by default in # tests, until we introduce it in user settings diff --git a/spec/views/projects/tree/_tree_row.html.haml_spec.rb b/spec/views/projects/tree/_tree_row.html.haml_spec.rb deleted file mode 100644 index 43a37934afd..00000000000 --- a/spec/views/projects/tree/_tree_row.html.haml_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'projects/tree/_tree_row' do - let(:project) { create(:project, :repository) } - let(:repository) { project.repository } - - # rubocop: disable Rails/FindBy - # This is not ActiveRecord where..first - let(:blob_item) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files/ruby').first } - # rubocop: enable Rails/FindBy - - before do - assign(:project, project) - assign(:repository, repository) - assign(:id, File.join('master', '')) - assign(:lfs_blob_ids, []) - end - - it 'renders blob item' do - render_partial(blob_item) - - expect(rendered).to have_content(blob_item.name) - expect(rendered).not_to have_selector('.label-lfs', text: 'LFS') - end - - describe 'LFS blob' do - before do - assign(:lfs_blob_ids, [blob_item].map(&:id)) - - render_partial(blob_item) - end - - it 'renders LFS badge' do - expect(rendered).to have_selector('.label-lfs', text: 'LFS') - end - end - - def render_partial(items) - render partial: 'projects/tree/tree_row', collection: [items].flatten - end -end