Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop_manual_todo.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js24
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue23
-rw-r--r--app/assets/javascripts/notes/stores/actions.js16
-rw-r--r--app/assets/javascripts/notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js18
-rw-r--r--app/assets/javascripts/pages/projects/compare/index/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue50
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue68
-rw-r--r--app/assets/javascripts/projects/compare/components/app.vue89
-rw-r--r--app/assets/javascripts/projects/compare/components/revision_dropdown.vue145
-rw-r--r--app/assets/javascripts/projects/compare/index.js33
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue20
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue23
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js14
-rw-r--r--app/assets/stylesheets/pages/projects.scss14
-rw-r--r--app/controllers/concerns/notes_actions.rb6
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/controllers/search_controller.rb2
-rw-r--r--app/graphql/types/merge_request_state_enum.rb2
-rw-r--r--app/helpers/notes_helper.rb6
-rw-r--r--app/helpers/tree_helper.rb43
-rw-r--r--app/serializers/base_discussion_entity.rb1
-rw-r--r--app/services/git/branch_hooks_service.rb36
-rw-r--r--app/views/profiles/accounts/show.html.haml3
-rw-r--r--app/views/projects/branches/_branch.html.haml6
-rw-r--r--app/views/projects/buttons/_remove_tag.html.haml2
-rw-r--r--app/views/projects/compare/index.html.haml6
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml7
-rw-r--r--app/views/projects/network/show.html.haml4
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/tags/index.html.haml4
-rw-r--r--app/views/projects/tree/_readme.html.haml10
-rw-r--r--app/views/projects/tree/_tree_content.html.haml24
-rw-r--r--app/views/projects/tree/_tree_row.html.haml27
-rw-r--r--app/views/projects/tree/_truncated_notice_tree_row.html.haml7
-rw-r--r--changelogs/unreleased/300847-fix-link.yml5
-rw-r--r--changelogs/unreleased/consistent-question-mark-mr.yml5
-rw-r--r--changelogs/unreleased/fix-opensearch.yml5
-rw-r--r--changelogs/unreleased/gl-badge-branch-list.yml5
-rw-r--r--changelogs/unreleased/gl-button-tags.yml5
-rw-r--r--changelogs/unreleased/gl-ui-graph-page.yml5
-rw-r--r--changelogs/unreleased/russell-update-project-visibility-settings-ui-text.yml5
-rw-r--r--config/feature_flags/development/usage_data_unique_users_committing_ciconfigfile.yml8
-rw-r--r--doc/administration/img/time_zone_settings.pngbin0 -> 42339 bytes
-rw-r--r--doc/administration/timezone.md15
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql202
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json268
-rw-r--r--doc/api/graphql/reference/index.md177
-rw-r--r--doc/development/database/strings_and_the_text_data_type.md6
-rw-r--r--doc/user/admin_area/settings/account_and_limit_settings.md25
-rw-r--r--lib/bulk_imports/pipeline/runner.rb9
-rw-r--r--lib/gitlab/sidekiq_logging/structured_logger.rb6
-rw-r--r--lib/gitlab/usage_data_counters/known_events/common.yml6
-rw-r--r--locale/gitlab.pot86
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/jira_issue_import_spec.rb27
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/jira/jira_basic_integration_spec.rb25
-rwxr-xr-xscripts/trigger-build2
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb4
-rw-r--r--spec/controllers/search_controller_spec.rb400
-rw-r--r--spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb2
-rw-r--r--spec/features/projects/commits/user_browses_commits_spec.rb9
-rw-r--r--spec/features/projects/compare_spec.rb23
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb16
-rw-r--r--spec/frontend/notes/stores/actions_spec.js49
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js8
-rw-r--r--spec/frontend/pipelines/pipelines_actions_spec.js64
-rw-r--r--spec/frontend/projects/compare/components/app_spec.js116
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_spec.js92
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js6
-rw-r--r--spec/helpers/notes_helper_spec.rb11
-rw-r--r--spec/helpers/tree_helper_spec.rb88
-rw-r--r--spec/lib/bulk_imports/pipeline/runner_spec.rb23
-rw-r--r--spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb3
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb3
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb4
-rw-r--r--spec/requests/api/projects_spec.rb14
-rw-r--r--spec/requests/projects/noteable_notes_spec.rb11
-rw-r--r--spec/services/git/branch_hooks_service_spec.rb83
-rw-r--r--spec/spec_helper.rb2
-rw-r--r--spec/views/projects/tree/_tree_row.html.haml_spec.rb43
93 files changed, 1748 insertions, 988 deletions
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]"
/>
<input v-model="requestAccessEnabled" type="checkbox" />
- {{ s__('ProjectSettings|Allow users to request access') }}
+ {{ s__('ProjectSettings|Users can request access') }}
</label>
</project-setting-row>
</div>
@@ -411,7 +411,7 @@ export default {
<project-setting-row
ref="issues-settings"
:label="s__('ProjectSettings|Issues')"
- :help-text="s__('ProjectSettings|Lightweight issue tracking system for this project')"
+ :help-text="s__('ProjectSettings|Lightweight issue tracking system.')"
>
<project-feature-setting
v-model="issuesAccessLevel"
@@ -434,7 +434,7 @@ export default {
<project-setting-row
ref="merge-request-settings"
:label="s__('ProjectSettings|Merge requests')"
- :help-text="s__('ProjectSettings|Submit changes to be merged upstream')"
+ :help-text="s__('ProjectSettings|Submit changes to be merged upstream.')"
>
<project-feature-setting
v-model="mergeRequestsAccessLevel"
@@ -446,9 +446,7 @@ export default {
<project-setting-row
ref="fork-settings"
:label="s__('ProjectSettings|Forks')"
- :help-text="
- s__('ProjectSettings|Allow users to make copies of your repository to a new project')
- "
+ :help-text="s__('ProjectSettings|Users can copy the repository to a new project.')"
>
<project-feature-setting
v-model="forkingAccessLevel"
@@ -460,7 +458,7 @@ export default {
<project-setting-row
ref="pipeline-settings"
:label="s__('ProjectSettings|Pipelines')"
- :help-text="s__('ProjectSettings|Build, test, and deploy your changes')"
+ :help-text="s__('ProjectSettings|Build, test, and deploy your changes.')"
>
<project-feature-setting
v-model="buildsAccessLevel"
@@ -497,7 +495,7 @@ export default {
:help-path="lfsHelpPath"
:label="s__('ProjectSettings|Git Large File Storage (LFS)')"
:help-text="
- s__('ProjectSettings|Manages large files such as audio, video, and graphics files')
+ s__('ProjectSettings|Manages large files such as audio, video, and graphics files.')
"
>
<project-feature-toggle
@@ -509,7 +507,7 @@ export default {
<gl-sprintf
:message="
s__(
- 'ProjectSettings|LFS objects from this repository are still available to forks. %{linkStart}How do I remove them?%{linkEnd}',
+ 'ProjectSettings|LFS objects from this repository are available to forks. %{linkStart}How do I remove them?%{linkEnd}',
)
"
>
@@ -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.')
"
>
<project-feature-toggle
@@ -542,7 +540,7 @@ export default {
<project-setting-row
ref="analytics-settings"
:label="s__('ProjectSettings|Analytics')"
- :help-text="s__('ProjectSettings|View project analytics')"
+ :help-text="s__('ProjectSettings|View project analytics.')"
>
<project-feature-setting
v-model="analyticsAccessLevel"
@@ -554,7 +552,7 @@ export default {
v-if="requirementsAvailable"
ref="requirements-settings"
:label="s__('ProjectSettings|Requirements')"
- :help-text="s__('ProjectSettings|Requirements management system for this project')"
+ :help-text="s__('ProjectSettings|Requirements management system.')"
>
<project-feature-setting
v-model="requirementsAccessLevel"
@@ -576,7 +574,7 @@ export default {
<project-setting-row
ref="wiki-settings"
:label="s__('ProjectSettings|Wiki')"
- :help-text="s__('ProjectSettings|Pages for project documentation')"
+ :help-text="s__('ProjectSettings|Pages for project documentation.')"
>
<project-feature-setting
v-model="wikiAccessLevel"
@@ -587,7 +585,7 @@ export default {
<project-setting-row
ref="snippet-settings"
:label="s__('ProjectSettings|Snippets')"
- :help-text="s__('ProjectSettings|Share code pastes with others out of Git repository')"
+ :help-text="s__('ProjectSettings|Share code with others outside the project.')"
>
<project-feature-setting
v-model="snippetsAccessLevel"
@@ -601,7 +599,7 @@ export default {
:help-path="pagesHelpPath"
:label="s__('ProjectSettings|Pages')"
:help-text="
- s__('ProjectSettings|With GitLab Pages you can host your static websites on GitLab')
+ s__('ProjectSettings|With GitLab Pages you can host your static websites on GitLab.')
"
>
<project-feature-setting
@@ -613,7 +611,7 @@ export default {
<project-setting-row
ref="operations-settings"
:label="s__('ProjectSettings|Operations')"
- :help-text="s__('ProjectSettings|Environments, logs, cluster management, and more')"
+ :help-text="s__('ProjectSettings|Environments, logs, cluster management, and more.')"
>
<project-feature-setting
v-model="operationsAccessLevel"
@@ -625,11 +623,7 @@ export default {
<project-setting-row
ref="metrics-visibility-settings"
:label="__('Metrics Dashboard')"
- :help-text="
- s__(
- 'ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics',
- )
- "
+ :help-text="s__('ProjectSettings|Visualize the project\'s performance metrics.')"
>
<project-feature-setting
v-model="metricsDashboardAccessLevel"
@@ -647,9 +641,7 @@ export default {
{{ s__('ProjectSettings|Disable email notifications') }}
</label>
<span class="form-text text-muted">{{
- s__(
- 'ProjectSettings|This setting will override user notification preferences for all project members.',
- )
+ s__('ProjectSettings|Override user notification preferences for all project members.')
}}</span>
</project-setting-row>
<project-setting-row class="mb-3">
@@ -665,7 +657,7 @@ export default {
{{ s__('ProjectSettings|Show default award emojis') }}
<template #help>{{
s__(
- 'ProjectSettings|When enabled, issues, merge requests, and snippets will always show thumbs-up and thumbs-down award emoji buttons.',
+ 'ProjectSettings|Always show thumbs-up and thumbs-down award emoji buttons on issues, merge requests, and snippets.',
)
}}</template>
</gl-form-checkbox>
@@ -683,9 +675,7 @@ export default {
<gl-form-checkbox v-model="allowEditingCommitMessages">
{{ s__('ProjectSettings|Allow editing commit messages') }}
<template #help>{{
- s__(
- 'ProjectSettings|When enabled, commit authors will be able to edit commit messages on unprotected branches.',
- )
+ s__('ProjectSettings|Commit authors can edit commit messages on unprotected branches.')
}}</template>
</gl-form-checkbox>
</project-setting-row>
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 @@
<script>
-import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import { s__, __, sprintf } from '~/locale';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import eventHub from '../../event_hub';
@@ -11,10 +11,10 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- GlIcon,
GlCountdown,
- GlButton,
- GlLoadingIcon,
+ GlDropdown,
+ GlDropdownItem,
+ GlIcon,
},
props: {
actions: {
@@ -61,7 +61,7 @@ export default {
})
.catch(() => {
this.isLoading = false;
- flash(__('An error occurred while making the request.'));
+ createFlash({ message: __('An error occurred while making the request.') });
});
},
@@ -76,39 +76,27 @@ export default {
};
</script>
<template>
- <div class="btn-group">
- <button
- v-gl-tooltip
- type="button"
- :disabled="isLoading"
- class="dropdown-new gl-button btn btn-default js-pipeline-dropdown-manual-actions"
- :title="__('Run manual or delayed jobs')"
- data-toggle="dropdown"
- :aria-label="__('Run manual or delayed jobs')"
+ <gl-dropdown
+ v-gl-tooltip
+ :title="__('Run manual or delayed jobs')"
+ :loading="isLoading"
+ data-testid="pipelines-manual-actions-dropdown"
+ right
+ icon="play"
+ >
+ <gl-dropdown-item
+ v-for="action in actions"
+ :key="action.path"
+ :disabled="isActionDisabled(action)"
+ @click="onClickAction(action)"
>
- <gl-icon name="play" class="icon-play" />
- <gl-icon name="chevron-down" />
- <gl-loading-icon v-if="isLoading" />
- </button>
-
- <ul class="dropdown-menu dropdown-menu-right">
- <li v-for="action in actions" :key="action.path">
- <gl-button
- category="tertiary"
- :class="{ disabled: isActionDisabled(action) }"
- :disabled="isActionDisabled(action)"
- class="js-pipeline-action-link"
- @click="onClickAction(action)"
- >
- <div class="d-flex justify-content-between flex-wrap">
- {{ action.name }}
- <span v-if="action.scheduled_at">
- <gl-icon name="clock" />
- <gl-countdown :end-date-string="action.scheduled_at" />
- </span>
- </div>
- </gl-button>
- </li>
- </ul>
- </div>
+ <div class="gl-display-flex gl-justify-content-space-between gl-flex-wrap">
+ {{ action.name }}
+ <span v-if="action.scheduled_at">
+ <gl-icon name="clock" />
+ <gl-countdown :end-date-string="action.scheduled_at" />
+ </span>
+ </div>
+ </gl-dropdown-item>
+ </gl-dropdown>
</template>
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 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import csrf from '~/lib/utils/csrf';
+import RevisionDropdown from './revision_dropdown.vue';
+
+export default {
+ csrf,
+ components: {
+ RevisionDropdown,
+ GlButton,
+ },
+ props: {
+ projectCompareIndexPath: {
+ type: String,
+ required: true,
+ },
+ refsProjectPath: {
+ type: String,
+ required: true,
+ },
+ paramsFrom: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ paramsTo: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ projectMergeRequestPath: {
+ type: String,
+ required: true,
+ },
+ createMrPath: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ onSubmit() {
+ this.$refs.form.submit();
+ },
+ },
+};
+</script>
+
+<template>
+ <form
+ ref="form"
+ class="form-inline js-requires-input js-signature-container"
+ method="POST"
+ :action="projectCompareIndexPath"
+ >
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+ <revision-dropdown
+ :refs-project-path="refsProjectPath"
+ revision-text="Source"
+ params-name="to"
+ :params-branch="paramsTo"
+ />
+ <div class="compare-ellipsis gl-display-inline" data-testid="ellipsis">...</div>
+ <revision-dropdown
+ :refs-project-path="refsProjectPath"
+ revision-text="Target"
+ params-name="from"
+ :params-branch="paramsFrom"
+ />
+ <gl-button category="primary" variant="success" class="gl-ml-3" @click="onSubmit">
+ {{ s__('CompareRevisions|Compare') }}
+ </gl-button>
+ <a
+ v-if="projectMergeRequestPath"
+ :href="projectMergeRequestPath"
+ data-testid="projectMrButton"
+ class="btn btn-default gl-button gl-ml-3"
+ >
+ {{ s__('CompareRevisions|View open merge request') }}
+ </a>
+ <a
+ v-else-if="createMrPath"
+ :href="createMrPath"
+ data-testid="createMrButton"
+ class="btn btn-default gl-button gl-ml-3"
+ >
+ {{ s__('CompareRevisions|Create merge request') }}
+ </a>
+ </form>
+</template>
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 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlDropdownSectionHeader } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlSearchBoxByType,
+ },
+ props: {
+ refsProjectPath: {
+ type: String,
+ required: true,
+ },
+ revisionText: {
+ type: String,
+ required: true,
+ },
+ paramsName: {
+ type: String,
+ required: true,
+ },
+ paramsBranch: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ branches: [],
+ tags: [],
+ loading: true,
+ searchTerm: '',
+ selectedRevision: this.getDefaultBranch(),
+ };
+ },
+ computed: {
+ filteredBranches() {
+ return this.branches.filter((branch) =>
+ branch.toLowerCase().includes(this.searchTerm.toLowerCase()),
+ );
+ },
+ hasFilteredBranches() {
+ return this.filteredBranches.length;
+ },
+ filteredTags() {
+ return this.tags.filter((tag) => tag.toLowerCase().includes(this.searchTerm.toLowerCase()));
+ },
+ hasFilteredTags() {
+ return this.filteredTags.length;
+ },
+ },
+ mounted() {
+ this.fetchBranchesAndTags();
+ },
+ methods: {
+ fetchBranchesAndTags() {
+ const endpoint = this.refsProjectPath;
+
+ return axios
+ .get(endpoint)
+ .then(({ data }) => {
+ this.branches = data.Branches;
+ this.tags = data.Tags;
+ })
+ .catch(() => {
+ createFlash({
+ message: `${s__(
+ 'CompareRevisions|There was an error while updating the branch/tag list. Please try again.',
+ )}`,
+ });
+ })
+ .finally(() => {
+ this.loading = false;
+ });
+ },
+ getDefaultBranch() {
+ return this.paramsBranch || s__('CompareRevisions|Select branch/tag');
+ },
+ onClick(revision) {
+ this.selectedRevision = revision;
+ },
+ onSearchEnter() {
+ this.selectedRevision = this.searchTerm;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="form-group compare-form-group" :class="`js-compare-${paramsName}-dropdown`">
+ <div class="input-group inline-input-group">
+ <span class="input-group-prepend">
+ <div class="input-group-text">
+ {{ revisionText }}
+ </div>
+ </span>
+ <input type="hidden" :name="paramsName" :value="selectedRevision" />
+ <gl-dropdown
+ class="gl-flex-grow-1 gl-flex-basis-0 gl-min-w-0 gl-font-monospace"
+ toggle-class="form-control compare-dropdown-toggle js-compare-dropdown gl-min-w-0 gl-rounded-top-left-none! gl-rounded-bottom-left-none!"
+ :text="selectedRevision"
+ header-text="Select Git revision"
+ :loading="loading"
+ >
+ <template #header>
+ <gl-search-box-by-type
+ v-model.trim="searchTerm"
+ :placeholder="s__('CompareRevisions|Filter by Git revision')"
+ @keyup.enter="onSearchEnter"
+ />
+ </template>
+ <gl-dropdown-section-header v-if="hasFilteredBranches">
+ {{ s__('CompareRevisions|Branches') }}
+ </gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="(branch, index) in filteredBranches"
+ :key="`branch${index}`"
+ is-check-item
+ :is-checked="selectedRevision === branch"
+ @click="onClick(branch)"
+ >
+ {{ branch }}
+ </gl-dropdown-item>
+ <gl-dropdown-section-header v-if="hasFilteredTags">
+ {{ s__('CompareRevisions|Tags') }}
+ </gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="(tag, index) in filteredTags"
+ :key="`tag${index}`"
+ is-check-item
+ :is-checked="selectedRevision === tag"
+ @click="onClick(tag)"
+ >
+ {{ tag }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+ </div>
+</template>
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 }}
<gl-link
+ v-gl-tooltip
:href="ciTroubleshootingDocsPath"
target="_blank"
+ :title="__('About this feature')"
class="gl-display-flex gl-align-items-center gl-ml-2"
>
<gl-icon
name="question"
- :size="12"
:aria-label="__('Link to go to GitLab pipeline documentation')"
/>
</gl-link>
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 {
</div>
<merge-train-helper-text
v-if="shouldRenderMergeTrainHelperText"
- :pipeline-id="pipeline.id"
+ :pipeline-id="pipelineId"
:pipeline-link="pipeline.path"
:merge-train-length="stateData.mergeTrainsCount"
:merge-train-when-pipeline-succeeds-docs-path="mr.mergeTrainWhenPipelineSucceedsDocsPath"
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
index fe512d68ea2..23215982e6e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
@@ -35,5 +35,8 @@ export default {
shouldRenderMergeTrainHelperText() {
return false;
},
+ pipelineId() {
+ return this.pipeline.id;
+ },
},
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 097dee52800..83370901cc3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -94,7 +94,6 @@ export default {
state: {
query: getStateQuery,
manual: true,
- pollInterval: 10 * 1000,
skip() {
return !this.mr || !window.gon?.features?.mergeRequestWidgetGraphql;
},
@@ -286,6 +285,10 @@ export default {
return new MRWidgetService(this.getServiceEndpoints(store));
},
checkStatus(cb, isRebased) {
+ if (window.gon?.features?.mergeRequestWidgetGraphql) {
+ this.$apollo.queries.state.refetch();
+ }
+
return this.service
.checkStatus()
.then(({ data }) => {
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: "<a href='#{reset_link}'>".html_safe, closingTag: '</a>'.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: "<a href='#{reset_profile_password_path}' rel=\"nofollow\" data-method=\"put\">".html_safe, closingTag: '</a>'.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: '<b>'.html_safe, b_close: '</b>'.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
--- /dev/null
+++ b/doc/administration/img/time_zone_settings.png
Binary files 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
@@ -743,6 +743,26 @@ enum AlertManagementIntegrationType {
}
"""
+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
"""
input AlertManagementPayloadAlertFieldInput {
@@ -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
}
@@ -18309,6 +18333,16 @@ type Project {
): 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
@@ -1910,6 +1910,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",
"description": "Field that are available while modifying the custom mapping attributes for an HTTP integration",
@@ -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
}
@@ -53969,6 +54032,41 @@
"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",
"args": [
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,52 +23069,49 @@ 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 only push commits to this repository that were committed with one of their own verified emails."
+msgid "ProjectSettings|Users can copy the repository to a new project."
msgstr ""
-msgid "ProjectSettings|View and edit files in this project"
+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. Non-project members will only have read access"
+msgid "ProjectSettings|Users can request access"
msgstr ""
-msgid "ProjectSettings|View project analytics"
+msgid "ProjectSettings|View and edit files in this project."
msgstr ""
-msgid "ProjectSettings|Visibility options for this fork are limited by the current visibility of the source project."
+msgid "ProjectSettings|View and edit files in this project. Non-project members will only have read access."
msgstr ""
-msgid "ProjectSettings|What are badges?"
+msgid "ProjectSettings|View project analytics."
msgstr ""
-msgid "ProjectSettings|When approved for merge, merge requests are queued and pipelines validate the combined results of the source and target branches before merge."
+msgid "ProjectSettings|Visibility options for this fork are limited by the current visibility of the source project."
msgstr ""
-msgid "ProjectSettings|When conflicts arise the user is given the option to rebase"
+msgid "ProjectSettings|Visualize the project's performance metrics."
msgstr ""
-msgid "ProjectSettings|When enabled, commit authors will be able to edit commit messages on unprotected branches."
+msgid "ProjectSettings|What are badges?"
msgstr ""
-msgid "ProjectSettings|When enabled, issues, merge requests, and snippets will always show thumbs-up and thumbs-down award emoji buttons."
+msgid "ProjectSettings|When approved for merge, merge requests are queued and pipelines validate the combined results of the source and target branches before merge."
msgstr ""
-msgid "ProjectSettings|Wiki"
+msgid "ProjectSettings|When conflicts arise the user is given the option to rebase"
msgstr ""
-msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
+msgid "ProjectSettings|Wiki"
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) }
+ context 'authorized user' do
+ 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 }
+ sign_in(user)
end
- it 'blocks access without a project_id' do
- get action, params: params
-
- expect(response).to have_gitlab_http_status(:forbidden)
- 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 'allows access with a project_id' do
- get action, params: params.merge(project_id: create(:project, :public).id)
+ it 'blocks access without a project_id' do
+ get action, params: params
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
+ expect(response).to have_gitlab_http_status(:forbidden)
+ 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) }
+ it 'allows access with a project_id' do
+ get action, params: params.merge(project_id: create(:project, :public).id)
- before do
- enable_external_authorization_service_check
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
- it 'renders a 403 when no project is given' do
- get action, params: params
+ 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) }
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ before do
+ enable_external_authorization_service_check
+ end
- it 'renders a 200 when a project was set' do
- get action, params: params.merge(project_id: project.id)
+ it 'renders a 403 when no project is given' do
+ get action, params: params
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
+ expect(response).to have_gitlab_http_status(:forbidden)
+ 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 '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(:ok)
end
end
- it_behaves_like 'with external authorization service enabled', :show, { search: 'hello' }
+ 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
- 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) }
-
- before do
- expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
- subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) }
+ it_behaves_like 'with external authorization service enabled', :show, { search: 'hello' }
- where(:partial, :scope) do
- '_blob' | :blobs
- '_wiki_blob' | :wiki_blobs
- '_commit' | :commits
- end
+ context 'uses the right partials depending on scope' do
+ using RSpec::Parameterized::TableSyntax
+ render_views
- with_them do
- it do
- project_wiki = create(:project_wiki, project: project, user: user)
- create(:wiki_page, wiki: project_wiki, title: 'merge', content: 'merge')
+ let_it_be(:project) { create(:project, :public, :repository, :wiki_repo) }
- expect(subject).to render_template("search/results/#{partial}")
+ before do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
end
- end
- end
- context 'global search' do
- using RSpec::Parameterized::TableSyntax
- render_views
+ subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) }
- context 'when block_anonymous_global_searches is disabled' do
- before do
- stub_feature_flags(block_anonymous_global_searches: false)
+ where(:partial, :scope) do
+ '_blob' | :blobs
+ '_wiki_blob' | :wiki_blobs
+ '_commit' | :commits
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)
+ with_them do
+ it do
+ project_wiki = create(:project_wiki, project: project, user: user)
+ create(:wiki_page, wiki: project_wiki, title: 'merge', content: 'merge')
- get :show, params: { scope: 'projects', search: project.name }
-
- expect(assigns[:search_objects].first).to eq project
+ expect(subject).to render_template("search/results/#{partial}")
+ end
end
+ 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))
- }
+ 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] }
-
- 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/)
+ 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
+
+ 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
- end
- context 'when block_anonymous_global_searches is enabled' do
- context 'for unauthenticated user' do
- before do
- sign_out(user)
- 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: '*' }
+ it 'redirects to login page' do
+ get :show, params: { scope: 'projects', search: '*' }
- expect(response).to redirect_to new_user_session_path
+ expect(response).to redirect_to new_user_session_path
+ end
end
- end
- context 'for authenticated user' do
- it 'succeeds' do
- get :show, params: { scope: 'projects', search: '*' }
+ context 'for authenticated user' do
+ it 'succeeds' do
+ get :show, params: { scope: 'projects', search: '*' }
- expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
end
end
- end
- it 'finds issue comments' do
- project = create(:project, :public)
- note = create(:note_on_issue, project: project)
+ 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
+ get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
- context 'unique users tracking' do
- before do
- allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
+ expect(assigns[:search_objects].first).to eq note
end
- it_behaves_like 'tracking unique hll events', :search_track_unique_users do
- subject(:request) { get :show, params: { scope: 'projects', search: 'term' } }
+ context 'unique users tracking' do
+ before do
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
+ end
- let(:target_id) { 'i_search_total' }
- let(:expected_type) { instance_of(String) }
- end
- end
+ it_behaves_like 'tracking unique hll events', :search_track_unique_users do
+ subject(:request) { get :show, params: { scope: 'projects', search: 'term' } }
- context 'on restricted projects' do
- context 'when signed out' do
- before do
- sign_out(user)
+ let(:target_id) { 'i_search_total' }
+ let(:expected_type) { instance_of(String) }
end
+ end
- it "doesn't expose comments on issues" do
- project = create(:project, :public, :issues_private)
- note = create(:note_on_issue, project: project)
+ context 'on restricted projects' do
+ context 'when signed out' do
+ before do
+ sign_out(user)
+ end
- get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
+ it "doesn't expose comments on issues" do
+ project = create(:project, :public, :issues_private)
+ note = create(:note_on_issue, project: project)
- expect(assigns[:search_objects].count).to eq(0)
+ 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)
+ it "doesn't expose comments on merge_requests" do
+ project = create(:project, :public, :merge_requests_private)
+ note = create(:note_on_merge_request, project: project)
- get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
+ get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
- expect(assigns[:search_objects].count).to eq(0)
- end
+ expect(assigns[:search_objects].count).to eq(0)
+ end
- it "doesn't expose comments on snippets" do
- project = create(:project, :public, :snippets_private)
- note = create(:note_on_project_snippet, 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 }
+ get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
- expect(assigns[:search_objects].count).to eq(0)
+ expect(assigns[:search_objects].count).to eq(0)
+ end
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' }
+ 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' }
- 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 'returns the result count for the given term and scope' do
+ create(:project, :public, name: 'hello world')
+ create(:project, :public, name: 'foo bar')
- get :count, params: { search: 'hello', scope: 'projects' }
+ get :count, params: { search: 'hello', scope: 'projects' }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq({ 'count' => '1' })
- end
+ 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 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)
+ 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
- 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 #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
+ 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)
- it 'renders xml' do
- get :opensearch, format: :xml
+ expect(controller).to receive(:append_info_to_payload) do |payload|
+ original_append_info_to_payload.call(payload)
+ last_payload = payload
+ end
- doc = Nokogiri::XML.parse(response.body)
+ get :show, params: { scope: 'issues', search: 'hello world', group_id: '123', project_id: '456', confidential: true, state: true, force_search_results: true }
- 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])
+ 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 '#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)
+ context 'unauthorized user' do
+ describe 'GET #opensearch' do
+ render_views
- expect(controller).to receive(:append_info_to_payload) do |payload|
- original_append_info_to_payload.call(payload)
- last_payload = payload
- end
+ it 'renders xml' do
+ get :opensearch, format: :xml
- get :show, params: { scope: 'issues', search: 'hello world', group_id: '123', project_id: '456', confidential: true, state: true, force_search_results: true }
+ doc = Nokogiri::XML.parse(response.body)
- 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')
+ 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
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');
+
+ await wrapper.vm.$nextTick();
+ expect(findDropdown().props('loading')).toBe(true);
+
+ 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');
- 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);
+ 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',
+ pipeline_class: 'BulkImports::MyPipeline'
+ )
+ expect(logger).to receive(:info)
+ .with(
bulk_import_entity_id: entity.id,
- bulk_import_entity_type: 'group_entity'
+ bulk_import_entity_type: 'group_entity',
+ pipeline_class: 'BulkImports::MyPipeline',
+ 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