Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-02-04 12:09:25 +00:00
parent 72241c5e0a
commit f1500a385a
93 changed files with 1762 additions and 1002 deletions

View File

@ -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'

View File

@ -1 +1 @@
2c7c204731f6e4f1c8cdb3d8a705caf7acf6689d
c73d7cae656b0bedfa40a4865c8d886516eda78b

View File

@ -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 },

View File

@ -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);

View File

@ -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);

View File

@ -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];

View File

@ -47,6 +47,7 @@ export default () => ({
unresolvedDiscussionsCount: 0,
descriptionVersions: {},
isTimelineEnabled: false,
isFetching: false,
},
actions,
getters,

View File

@ -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';

View File

@ -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);

View File

@ -0,0 +1,3 @@
import initCompareSelector from '~/projects/compare';
initCompareSelector();

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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,
},
});
},
});
}

View File

@ -159,6 +159,7 @@ export default {
.then((data) => {
this.mr.setApprovals(data);
eventHub.$emit('MRWidgetUpdateRequested');
eventHub.$emit('ApprovalUpdated');
this.$emit('updated');
})
.catch(errFn)

View File

@ -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>

View File

@ -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.'));

View File

@ -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';

View File

@ -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"

View File

@ -35,5 +35,8 @@ export default {
shouldRenderMergeTrainHelperText() {
return false;
},
pipelineId() {
return this.pipeline.id;
},
},
};

View File

@ -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 }) => {

View File

@ -18,6 +18,7 @@ query getState($projectPath: ID!, $iid: String!) {
}
shouldBeRebased
sourceBranchExists
state
targetBranchExists
userPermissions {
canMerge

View File

@ -1,6 +1,7 @@
fragment autoMergeEnabled on MergeRequest {
autoMergeStrategy
mergeUser {
id
name
username
webUrl

View File

@ -4,7 +4,6 @@ query autoMergeEnabledQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
...autoMergeEnabled
mergeTrainsCount
}
}
}

View File

@ -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;

View File

@ -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 {

View File

@ -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

View File

@ -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)

View File

@ -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?

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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?) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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 : '' } }

View File

@ -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|

View File

@ -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',

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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)

View File

@ -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'

View File

@ -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...

View File

@ -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.

View File

@ -0,0 +1,5 @@
---
title: Fixes broken password reset link in account deletion message
merge_request: 53274
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Update question mark icon while checking pipeline status
merge_request: 52760
author: Yogi (@yo)
type: other

View File

@ -0,0 +1,5 @@
---
title: Fix opensearch for anonymous users
merge_request: 53056
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Apply new GitLab UI for badges in the project branch list
merge_request: 52868
author: Yogi (@yo)
type: other

View File

@ -0,0 +1,5 @@
---
title: Apply new GitLab UI for buttons in tags page
merge_request: 52862
author: Yogi (@yo)
type: other

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Edited UI copy wording to comply with GitLab style
merge_request: 50676
author:
type: other

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -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)

View File

@ -742,6 +742,26 @@ enum AlertManagementIntegrationType {
PROMETHEUS
}
"""
Parsed field from an alert used for custom mappings
"""
type AlertManagementPayloadAlertField {
"""
Human-readable label of the payload path.
"""
label: String
"""
Path to value inside payload JSON.
"""
path: [String!]
"""
Type of the parsed value.
"""
type: AlertManagementPayloadAlertFieldType
}
"""
Field that are available while modifying the custom mapping attributes for an HTTP integration
"""
@ -1571,12 +1591,12 @@ Represents an epic on an issue board
"""
type BoardEpic implements CurrentUserTodos & Noteable {
"""
Author of the epic
Author of the epic.
"""
author: User!
"""
A list of award emojis associated with the epic
A list of award emojis associated with the epic.
"""
awardEmoji(
"""
@ -1601,7 +1621,7 @@ type BoardEpic implements CurrentUserTodos & Noteable {
): AwardEmojiConnection
"""
Children (sub-epics) of the epic
Children (sub-epics) of the epic.
"""
children(
"""
@ -1699,17 +1719,17 @@ type BoardEpic implements CurrentUserTodos & Noteable {
): EpicConnection
"""
Timestamp of when the epic was closed
Timestamp of when the epic was closed.
"""
closedAt: Time
"""
Indicates if the epic is confidential
Indicates if the epic is confidential.
"""
confidential: Boolean
"""
Timestamp of when the epic was created
Timestamp of when the epic was created.
"""
createdAt: Time
@ -1744,17 +1764,17 @@ type BoardEpic implements CurrentUserTodos & Noteable {
): TodoConnection!
"""
Number of open and closed descendant epics and issues
Number of open and closed descendant epics and issues.
"""
descendantCounts: EpicDescendantCount
"""
Total weight of open and closed issues in the epic and its descendants
Total weight of open and closed issues in the epic and its descendants.
"""
descendantWeightSum: EpicDescendantWeights
"""
Description of the epic
Description of the epic.
"""
description: String
@ -1784,67 +1804,67 @@ type BoardEpic implements CurrentUserTodos & Noteable {
): DiscussionConnection!
"""
Number of downvotes the epic has received
Number of downvotes the epic has received.
"""
downvotes: Int!
"""
Due date of the epic
Due date of the epic.
"""
dueDate: Time
"""
Fixed due date of the epic
Fixed due date of the epic.
"""
dueDateFixed: Time
"""
Inherited due date of the epic from milestones
Inherited due date of the epic from milestones.
"""
dueDateFromMilestones: Time
"""
Indicates if the due date has been manually set
Indicates if the due date has been manually set.
"""
dueDateIsFixed: Boolean
"""
Group to which the epic belongs
Group to which the epic belongs.
"""
group: Group!
"""
Indicates if the epic has children
Indicates if the epic has children.
"""
hasChildren: Boolean!
"""
Indicates if the epic has direct issues
Indicates if the epic has direct issues.
"""
hasIssues: Boolean!
"""
Indicates if the epic has a parent epic
Indicates if the epic has a parent epic.
"""
hasParent: Boolean!
"""
Current health status of the epic
Current health status of the epic.
"""
healthStatus: EpicHealthStatus
"""
ID of the epic
ID of the epic.
"""
id: ID!
"""
Internal ID of the epic
Internal ID of the epic.
"""
iid: ID!
"""
A list of issues associated with the epic
A list of issues associated with the epic.
"""
issues(
"""
@ -1869,7 +1889,7 @@ type BoardEpic implements CurrentUserTodos & Noteable {
): EpicIssueConnection
"""
Labels assigned to the epic
Labels assigned to the epic.
"""
labels(
"""
@ -1919,12 +1939,12 @@ type BoardEpic implements CurrentUserTodos & Noteable {
): NoteConnection!
"""
Parent epic of the epic
Parent epic of the epic.
"""
parent: Epic
"""
List of participants for the epic
List of participants for the epic.
"""
participants(
"""
@ -1949,77 +1969,77 @@ type BoardEpic implements CurrentUserTodos & Noteable {
): UserConnection
"""
Internal reference of the epic. Returned in shortened format by default
Internal reference of the epic. Returned in shortened format by default.
"""
reference(
"""
Indicates if the reference should be returned in full
Indicates if the reference should be returned in full.
"""
full: Boolean = false
): String!
"""
URI path of the epic-issue relationship
URI path of the epic-issue relationship.
"""
relationPath: String
"""
The relative position of the epic in the epic tree
The relative position of the epic in the epic tree.
"""
relativePosition: Int
"""
Start date of the epic
Start date of the epic.
"""
startDate: Time
"""
Fixed start date of the epic
Fixed start date of the epic.
"""
startDateFixed: Time
"""
Inherited start date of the epic from milestones
Inherited start date of the epic from milestones.
"""
startDateFromMilestones: Time
"""
Indicates if the start date has been manually set
Indicates if the start date has been manually set.
"""
startDateIsFixed: Boolean
"""
State of the epic
State of the epic.
"""
state: EpicState!
"""
Indicates the currently logged in user is subscribed to the epic
Indicates the currently logged in user is subscribed to the epic.
"""
subscribed: Boolean!
"""
Title of the epic
Title of the epic.
"""
title: String
"""
Timestamp of when the epic was updated
Timestamp of when the epic was updated.
"""
updatedAt: Time
"""
Number of upvotes the epic has received
Number of upvotes the epic has received.
"""
upvotes: Int!
"""
Number of user discussions in the epic
Number of user discussions in the epic.
"""
userDiscussionsCount: Int!
"""
Number of user notes of the epic
Number of user notes of the epic.
"""
userNotesCount: Int!
@ -2034,12 +2054,12 @@ type BoardEpic implements CurrentUserTodos & Noteable {
userPreferences: BoardEpicUserPreferences
"""
Web path of the epic
Web path of the epic.
"""
webPath: String!
"""
Web URL of the epic
Web URL of the epic.
"""
webUrl: String!
}
@ -8417,12 +8437,12 @@ Represents an epic
"""
type Epic implements CurrentUserTodos & Noteable {
"""
Author of the epic
Author of the epic.
"""
author: User!
"""
A list of award emojis associated with the epic
A list of award emojis associated with the epic.
"""
awardEmoji(
"""
@ -8447,7 +8467,7 @@ type Epic implements CurrentUserTodos & Noteable {
): AwardEmojiConnection
"""
Children (sub-epics) of the epic
Children (sub-epics) of the epic.
"""
children(
"""
@ -8545,17 +8565,17 @@ type Epic implements CurrentUserTodos & Noteable {
): EpicConnection
"""
Timestamp of when the epic was closed
Timestamp of when the epic was closed.
"""
closedAt: Time
"""
Indicates if the epic is confidential
Indicates if the epic is confidential.
"""
confidential: Boolean
"""
Timestamp of when the epic was created
Timestamp of when the epic was created.
"""
createdAt: Time
@ -8590,17 +8610,17 @@ type Epic implements CurrentUserTodos & Noteable {
): TodoConnection!
"""
Number of open and closed descendant epics and issues
Number of open and closed descendant epics and issues.
"""
descendantCounts: EpicDescendantCount
"""
Total weight of open and closed issues in the epic and its descendants
Total weight of open and closed issues in the epic and its descendants.
"""
descendantWeightSum: EpicDescendantWeights
"""
Description of the epic
Description of the epic.
"""
description: String
@ -8630,67 +8650,67 @@ type Epic implements CurrentUserTodos & Noteable {
): DiscussionConnection!
"""
Number of downvotes the epic has received
Number of downvotes the epic has received.
"""
downvotes: Int!
"""
Due date of the epic
Due date of the epic.
"""
dueDate: Time
"""
Fixed due date of the epic
Fixed due date of the epic.
"""
dueDateFixed: Time
"""
Inherited due date of the epic from milestones
Inherited due date of the epic from milestones.
"""
dueDateFromMilestones: Time
"""
Indicates if the due date has been manually set
Indicates if the due date has been manually set.
"""
dueDateIsFixed: Boolean
"""
Group to which the epic belongs
Group to which the epic belongs.
"""
group: Group!
"""
Indicates if the epic has children
Indicates if the epic has children.
"""
hasChildren: Boolean!
"""
Indicates if the epic has direct issues
Indicates if the epic has direct issues.
"""
hasIssues: Boolean!
"""
Indicates if the epic has a parent epic
Indicates if the epic has a parent epic.
"""
hasParent: Boolean!
"""
Current health status of the epic
Current health status of the epic.
"""
healthStatus: EpicHealthStatus
"""
ID of the epic
ID of the epic.
"""
id: ID!
"""
Internal ID of the epic
Internal ID of the epic.
"""
iid: ID!
"""
A list of issues associated with the epic
A list of issues associated with the epic.
"""
issues(
"""
@ -8715,7 +8735,7 @@ type Epic implements CurrentUserTodos & Noteable {
): EpicIssueConnection
"""
Labels assigned to the epic
Labels assigned to the epic.
"""
labels(
"""
@ -8765,12 +8785,12 @@ type Epic implements CurrentUserTodos & Noteable {
): NoteConnection!
"""
Parent epic of the epic
Parent epic of the epic.
"""
parent: Epic
"""
List of participants for the epic
List of participants for the epic.
"""
participants(
"""
@ -8795,77 +8815,77 @@ type Epic implements CurrentUserTodos & Noteable {
): UserConnection
"""
Internal reference of the epic. Returned in shortened format by default
Internal reference of the epic. Returned in shortened format by default.
"""
reference(
"""
Indicates if the reference should be returned in full
Indicates if the reference should be returned in full.
"""
full: Boolean = false
): String!
"""
URI path of the epic-issue relationship
URI path of the epic-issue relationship.
"""
relationPath: String
"""
The relative position of the epic in the epic tree
The relative position of the epic in the epic tree.
"""
relativePosition: Int
"""
Start date of the epic
Start date of the epic.
"""
startDate: Time
"""
Fixed start date of the epic
Fixed start date of the epic.
"""
startDateFixed: Time
"""
Inherited start date of the epic from milestones
Inherited start date of the epic from milestones.
"""
startDateFromMilestones: Time
"""
Indicates if the start date has been manually set
Indicates if the start date has been manually set.
"""
startDateIsFixed: Boolean
"""
State of the epic
State of the epic.
"""
state: EpicState!
"""
Indicates the currently logged in user is subscribed to the epic
Indicates the currently logged in user is subscribed to the epic.
"""
subscribed: Boolean!
"""
Title of the epic
Title of the epic.
"""
title: String
"""
Timestamp of when the epic was updated
Timestamp of when the epic was updated.
"""
updatedAt: Time
"""
Number of upvotes the epic has received
Number of upvotes the epic has received.
"""
upvotes: Int!
"""
Number of user discussions in the epic
Number of user discussions in the epic.
"""
userDiscussionsCount: Int!
"""
Number of user notes of the epic
Number of user notes of the epic.
"""
userNotesCount: Int!
@ -8875,12 +8895,12 @@ type Epic implements CurrentUserTodos & Noteable {
userPermissions: EpicPermissions!
"""
Web path of the epic
Web path of the epic.
"""
webPath: String!
"""
Web URL of the epic
Web URL of the epic.
"""
webUrl: String!
}
@ -15696,6 +15716,10 @@ enum MergeRequestState {
all
closed
locked
"""
Merge Request has been merged
"""
merged
opened
}
@ -18308,6 +18332,16 @@ type Project {
last: Int
): AlertManagementIntegrationConnection
"""
Extract alert fields from payload for custom mapping
"""
alertManagementPayloadFields(
"""
Sample payload for extracting alert fields for custom mappings.
"""
payloadExample: String!
): [AlertManagementPayloadAlertField!]
"""
If `only_allow_merge_if_pipeline_succeeds` is true, indicates if merge
requests of the project can also be merged with skipped jobs

View File

@ -1909,6 +1909,69 @@
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AlertManagementPayloadAlertField",
"description": "Parsed field from an alert used for custom mappings",
"fields": [
{
"name": "label",
"description": "Human-readable label of the payload path.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "path",
"description": "Path to value inside payload JSON.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": "Type of the parsed value.",
"args": [
],
"type": {
"kind": "ENUM",
"name": "AlertManagementPayloadAlertFieldType",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "AlertManagementPayloadAlertFieldInput",
@ -4097,7 +4160,7 @@
"fields": [
{
"name": "author",
"description": "Author of the epic",
"description": "Author of the epic.",
"args": [
],
@ -4115,7 +4178,7 @@
},
{
"name": "awardEmoji",
"description": "A list of award emojis associated with the epic",
"description": "A list of award emojis associated with the epic.",
"args": [
{
"name": "after",
@ -4168,7 +4231,7 @@
},
{
"name": "children",
"description": "Children (sub-epics) of the epic",
"description": "Children (sub-epics) of the epic.",
"args": [
{
"name": "startDate",
@ -4377,7 +4440,7 @@
},
{
"name": "closedAt",
"description": "Timestamp of when the epic was closed",
"description": "Timestamp of when the epic was closed.",
"args": [
],
@ -4391,7 +4454,7 @@
},
{
"name": "confidential",
"description": "Indicates if the epic is confidential",
"description": "Indicates if the epic is confidential.",
"args": [
],
@ -4405,7 +4468,7 @@
},
{
"name": "createdAt",
"description": "Timestamp of when the epic was created",
"description": "Timestamp of when the epic was created.",
"args": [
],
@ -4486,7 +4549,7 @@
},
{
"name": "descendantCounts",
"description": "Number of open and closed descendant epics and issues",
"description": "Number of open and closed descendant epics and issues.",
"args": [
],
@ -4500,7 +4563,7 @@
},
{
"name": "descendantWeightSum",
"description": "Total weight of open and closed issues in the epic and its descendants",
"description": "Total weight of open and closed issues in the epic and its descendants.",
"args": [
],
@ -4514,7 +4577,7 @@
},
{
"name": "description",
"description": "Description of the epic",
"description": "Description of the epic.",
"args": [
],
@ -4585,7 +4648,7 @@
},
{
"name": "downvotes",
"description": "Number of downvotes the epic has received",
"description": "Number of downvotes the epic has received.",
"args": [
],
@ -4603,7 +4666,7 @@
},
{
"name": "dueDate",
"description": "Due date of the epic",
"description": "Due date of the epic.",
"args": [
],
@ -4617,7 +4680,7 @@
},
{
"name": "dueDateFixed",
"description": "Fixed due date of the epic",
"description": "Fixed due date of the epic.",
"args": [
],
@ -4631,7 +4694,7 @@
},
{
"name": "dueDateFromMilestones",
"description": "Inherited due date of the epic from milestones",
"description": "Inherited due date of the epic from milestones.",
"args": [
],
@ -4645,7 +4708,7 @@
},
{
"name": "dueDateIsFixed",
"description": "Indicates if the due date has been manually set",
"description": "Indicates if the due date has been manually set.",
"args": [
],
@ -4659,7 +4722,7 @@
},
{
"name": "group",
"description": "Group to which the epic belongs",
"description": "Group to which the epic belongs.",
"args": [
],
@ -4677,7 +4740,7 @@
},
{
"name": "hasChildren",
"description": "Indicates if the epic has children",
"description": "Indicates if the epic has children.",
"args": [
],
@ -4695,7 +4758,7 @@
},
{
"name": "hasIssues",
"description": "Indicates if the epic has direct issues",
"description": "Indicates if the epic has direct issues.",
"args": [
],
@ -4713,7 +4776,7 @@
},
{
"name": "hasParent",
"description": "Indicates if the epic has a parent epic",
"description": "Indicates if the epic has a parent epic.",
"args": [
],
@ -4731,7 +4794,7 @@
},
{
"name": "healthStatus",
"description": "Current health status of the epic",
"description": "Current health status of the epic.",
"args": [
],
@ -4745,7 +4808,7 @@
},
{
"name": "id",
"description": "ID of the epic",
"description": "ID of the epic.",
"args": [
],
@ -4763,7 +4826,7 @@
},
{
"name": "iid",
"description": "Internal ID of the epic",
"description": "Internal ID of the epic.",
"args": [
],
@ -4781,7 +4844,7 @@
},
{
"name": "issues",
"description": "A list of issues associated with the epic",
"description": "A list of issues associated with the epic.",
"args": [
{
"name": "after",
@ -4834,7 +4897,7 @@
},
{
"name": "labels",
"description": "Labels assigned to the epic",
"description": "Labels assigned to the epic.",
"args": [
{
"name": "after",
@ -4944,7 +5007,7 @@
},
{
"name": "parent",
"description": "Parent epic of the epic",
"description": "Parent epic of the epic.",
"args": [
],
@ -4958,7 +5021,7 @@
},
{
"name": "participants",
"description": "List of participants for the epic",
"description": "List of participants for the epic.",
"args": [
{
"name": "after",
@ -5011,11 +5074,11 @@
},
{
"name": "reference",
"description": "Internal reference of the epic. Returned in shortened format by default",
"description": "Internal reference of the epic. Returned in shortened format by default.",
"args": [
{
"name": "full",
"description": "Indicates if the reference should be returned in full",
"description": "Indicates if the reference should be returned in full.",
"type": {
"kind": "SCALAR",
"name": "Boolean",
@ -5038,7 +5101,7 @@
},
{
"name": "relationPath",
"description": "URI path of the epic-issue relationship",
"description": "URI path of the epic-issue relationship.",
"args": [
],
@ -5052,7 +5115,7 @@
},
{
"name": "relativePosition",
"description": "The relative position of the epic in the epic tree",
"description": "The relative position of the epic in the epic tree.",
"args": [
],
@ -5066,7 +5129,7 @@
},
{
"name": "startDate",
"description": "Start date of the epic",
"description": "Start date of the epic.",
"args": [
],
@ -5080,7 +5143,7 @@
},
{
"name": "startDateFixed",
"description": "Fixed start date of the epic",
"description": "Fixed start date of the epic.",
"args": [
],
@ -5094,7 +5157,7 @@
},
{
"name": "startDateFromMilestones",
"description": "Inherited start date of the epic from milestones",
"description": "Inherited start date of the epic from milestones.",
"args": [
],
@ -5108,7 +5171,7 @@
},
{
"name": "startDateIsFixed",
"description": "Indicates if the start date has been manually set",
"description": "Indicates if the start date has been manually set.",
"args": [
],
@ -5122,7 +5185,7 @@
},
{
"name": "state",
"description": "State of the epic",
"description": "State of the epic.",
"args": [
],
@ -5140,7 +5203,7 @@
},
{
"name": "subscribed",
"description": "Indicates the currently logged in user is subscribed to the epic",
"description": "Indicates the currently logged in user is subscribed to the epic.",
"args": [
],
@ -5158,7 +5221,7 @@
},
{
"name": "title",
"description": "Title of the epic",
"description": "Title of the epic.",
"args": [
],
@ -5172,7 +5235,7 @@
},
{
"name": "updatedAt",
"description": "Timestamp of when the epic was updated",
"description": "Timestamp of when the epic was updated.",
"args": [
],
@ -5186,7 +5249,7 @@
},
{
"name": "upvotes",
"description": "Number of upvotes the epic has received",
"description": "Number of upvotes the epic has received.",
"args": [
],
@ -5204,7 +5267,7 @@
},
{
"name": "userDiscussionsCount",
"description": "Number of user discussions in the epic",
"description": "Number of user discussions in the epic.",
"args": [
],
@ -5222,7 +5285,7 @@
},
{
"name": "userNotesCount",
"description": "Number of user notes of the epic",
"description": "Number of user notes of the epic.",
"args": [
],
@ -5272,7 +5335,7 @@
},
{
"name": "webPath",
"description": "Web path of the epic",
"description": "Web path of the epic.",
"args": [
],
@ -5290,7 +5353,7 @@
},
{
"name": "webUrl",
"description": "Web URL of the epic",
"description": "Web URL of the epic.",
"args": [
],
@ -23328,7 +23391,7 @@
"fields": [
{
"name": "author",
"description": "Author of the epic",
"description": "Author of the epic.",
"args": [
],
@ -23346,7 +23409,7 @@
},
{
"name": "awardEmoji",
"description": "A list of award emojis associated with the epic",
"description": "A list of award emojis associated with the epic.",
"args": [
{
"name": "after",
@ -23399,7 +23462,7 @@
},
{
"name": "children",
"description": "Children (sub-epics) of the epic",
"description": "Children (sub-epics) of the epic.",
"args": [
{
"name": "startDate",
@ -23608,7 +23671,7 @@
},
{
"name": "closedAt",
"description": "Timestamp of when the epic was closed",
"description": "Timestamp of when the epic was closed.",
"args": [
],
@ -23622,7 +23685,7 @@
},
{
"name": "confidential",
"description": "Indicates if the epic is confidential",
"description": "Indicates if the epic is confidential.",
"args": [
],
@ -23636,7 +23699,7 @@
},
{
"name": "createdAt",
"description": "Timestamp of when the epic was created",
"description": "Timestamp of when the epic was created.",
"args": [
],
@ -23717,7 +23780,7 @@
},
{
"name": "descendantCounts",
"description": "Number of open and closed descendant epics and issues",
"description": "Number of open and closed descendant epics and issues.",
"args": [
],
@ -23731,7 +23794,7 @@
},
{
"name": "descendantWeightSum",
"description": "Total weight of open and closed issues in the epic and its descendants",
"description": "Total weight of open and closed issues in the epic and its descendants.",
"args": [
],
@ -23745,7 +23808,7 @@
},
{
"name": "description",
"description": "Description of the epic",
"description": "Description of the epic.",
"args": [
],
@ -23816,7 +23879,7 @@
},
{
"name": "downvotes",
"description": "Number of downvotes the epic has received",
"description": "Number of downvotes the epic has received.",
"args": [
],
@ -23834,7 +23897,7 @@
},
{
"name": "dueDate",
"description": "Due date of the epic",
"description": "Due date of the epic.",
"args": [
],
@ -23848,7 +23911,7 @@
},
{
"name": "dueDateFixed",
"description": "Fixed due date of the epic",
"description": "Fixed due date of the epic.",
"args": [
],
@ -23862,7 +23925,7 @@
},
{
"name": "dueDateFromMilestones",
"description": "Inherited due date of the epic from milestones",
"description": "Inherited due date of the epic from milestones.",
"args": [
],
@ -23876,7 +23939,7 @@
},
{
"name": "dueDateIsFixed",
"description": "Indicates if the due date has been manually set",
"description": "Indicates if the due date has been manually set.",
"args": [
],
@ -23890,7 +23953,7 @@
},
{
"name": "group",
"description": "Group to which the epic belongs",
"description": "Group to which the epic belongs.",
"args": [
],
@ -23908,7 +23971,7 @@
},
{
"name": "hasChildren",
"description": "Indicates if the epic has children",
"description": "Indicates if the epic has children.",
"args": [
],
@ -23926,7 +23989,7 @@
},
{
"name": "hasIssues",
"description": "Indicates if the epic has direct issues",
"description": "Indicates if the epic has direct issues.",
"args": [
],
@ -23944,7 +24007,7 @@
},
{
"name": "hasParent",
"description": "Indicates if the epic has a parent epic",
"description": "Indicates if the epic has a parent epic.",
"args": [
],
@ -23962,7 +24025,7 @@
},
{
"name": "healthStatus",
"description": "Current health status of the epic",
"description": "Current health status of the epic.",
"args": [
],
@ -23976,7 +24039,7 @@
},
{
"name": "id",
"description": "ID of the epic",
"description": "ID of the epic.",
"args": [
],
@ -23994,7 +24057,7 @@
},
{
"name": "iid",
"description": "Internal ID of the epic",
"description": "Internal ID of the epic.",
"args": [
],
@ -24012,7 +24075,7 @@
},
{
"name": "issues",
"description": "A list of issues associated with the epic",
"description": "A list of issues associated with the epic.",
"args": [
{
"name": "after",
@ -24065,7 +24128,7 @@
},
{
"name": "labels",
"description": "Labels assigned to the epic",
"description": "Labels assigned to the epic.",
"args": [
{
"name": "after",
@ -24175,7 +24238,7 @@
},
{
"name": "parent",
"description": "Parent epic of the epic",
"description": "Parent epic of the epic.",
"args": [
],
@ -24189,7 +24252,7 @@
},
{
"name": "participants",
"description": "List of participants for the epic",
"description": "List of participants for the epic.",
"args": [
{
"name": "after",
@ -24242,11 +24305,11 @@
},
{
"name": "reference",
"description": "Internal reference of the epic. Returned in shortened format by default",
"description": "Internal reference of the epic. Returned in shortened format by default.",
"args": [
{
"name": "full",
"description": "Indicates if the reference should be returned in full",
"description": "Indicates if the reference should be returned in full.",
"type": {
"kind": "SCALAR",
"name": "Boolean",
@ -24269,7 +24332,7 @@
},
{
"name": "relationPath",
"description": "URI path of the epic-issue relationship",
"description": "URI path of the epic-issue relationship.",
"args": [
],
@ -24283,7 +24346,7 @@
},
{
"name": "relativePosition",
"description": "The relative position of the epic in the epic tree",
"description": "The relative position of the epic in the epic tree.",
"args": [
],
@ -24297,7 +24360,7 @@
},
{
"name": "startDate",
"description": "Start date of the epic",
"description": "Start date of the epic.",
"args": [
],
@ -24311,7 +24374,7 @@
},
{
"name": "startDateFixed",
"description": "Fixed start date of the epic",
"description": "Fixed start date of the epic.",
"args": [
],
@ -24325,7 +24388,7 @@
},
{
"name": "startDateFromMilestones",
"description": "Inherited start date of the epic from milestones",
"description": "Inherited start date of the epic from milestones.",
"args": [
],
@ -24339,7 +24402,7 @@
},
{
"name": "startDateIsFixed",
"description": "Indicates if the start date has been manually set",
"description": "Indicates if the start date has been manually set.",
"args": [
],
@ -24353,7 +24416,7 @@
},
{
"name": "state",
"description": "State of the epic",
"description": "State of the epic.",
"args": [
],
@ -24371,7 +24434,7 @@
},
{
"name": "subscribed",
"description": "Indicates the currently logged in user is subscribed to the epic",
"description": "Indicates the currently logged in user is subscribed to the epic.",
"args": [
],
@ -24389,7 +24452,7 @@
},
{
"name": "title",
"description": "Title of the epic",
"description": "Title of the epic.",
"args": [
],
@ -24403,7 +24466,7 @@
},
{
"name": "updatedAt",
"description": "Timestamp of when the epic was updated",
"description": "Timestamp of when the epic was updated.",
"args": [
],
@ -24417,7 +24480,7 @@
},
{
"name": "upvotes",
"description": "Number of upvotes the epic has received",
"description": "Number of upvotes the epic has received.",
"args": [
],
@ -24435,7 +24498,7 @@
},
{
"name": "userDiscussionsCount",
"description": "Number of user discussions in the epic",
"description": "Number of user discussions in the epic.",
"args": [
],
@ -24453,7 +24516,7 @@
},
{
"name": "userNotesCount",
"description": "Number of user notes of the epic",
"description": "Number of user notes of the epic.",
"args": [
],
@ -24489,7 +24552,7 @@
},
{
"name": "webPath",
"description": "Web path of the epic",
"description": "Web path of the epic.",
"args": [
],
@ -24507,7 +24570,7 @@
},
{
"name": "webUrl",
"description": "Web URL of the epic",
"description": "Web URL of the epic.",
"args": [
],
@ -43076,7 +43139,7 @@
},
{
"name": "merged",
"description": null,
"description": "Merge Request has been merged",
"isDeprecated": false,
"deprecationReason": null
}
@ -53968,6 +54031,41 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "alertManagementPayloadFields",
"description": "Extract alert fields from payload for custom mapping",
"args": [
{
"name": "payloadExample",
"description": "Sample payload for extracting alert fields for custom mappings.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "AlertManagementPayloadAlertField",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "allowMergeOnSkippedPipeline",
"description": "If `only_allow_merge_if_pipeline_succeeds` is true, indicates if merge requests of the project can also be merged with skipped jobs",

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 groups overview page."
@ -7343,6 +7343,30 @@ msgstr ""
msgid "CompareBranches|There isn't anything to compare."
msgstr ""
msgid "CompareRevisions|Branches"
msgstr ""
msgid "CompareRevisions|Compare"
msgstr ""
msgid "CompareRevisions|Create merge request"
msgstr ""
msgid "CompareRevisions|Filter by Git revision"
msgstr ""
msgid "CompareRevisions|Select branch/tag"
msgstr ""
msgid "CompareRevisions|Tags"
msgstr ""
msgid "CompareRevisions|There was an error while updating the branch/tag list. Please try again."
msgstr ""
msgid "CompareRevisions|View open merge request"
msgstr ""
msgid "Complete"
msgstr ""
@ -22799,10 +22823,7 @@ msgstr ""
msgid "ProjectSettings|Allow editing commit messages"
msgstr ""
msgid "ProjectSettings|Allow users to make copies of your repository to a new project"
msgstr ""
msgid "ProjectSettings|Allow users to request access"
msgid "ProjectSettings|Always show thumbs-up and thumbs-down award emoji buttons on issues, merge requests, and snippets."
msgstr ""
msgid "ProjectSettings|Analytics"
@ -22814,7 +22835,7 @@ msgstr ""
msgid "ProjectSettings|Badges"
msgstr ""
msgid "ProjectSettings|Build, test, and deploy your changes"
msgid "ProjectSettings|Build, test, and deploy your changes."
msgstr ""
msgid "ProjectSettings|Checkbox is visible and selected by default."
@ -22829,6 +22850,9 @@ msgstr ""
msgid "ProjectSettings|Choose your merge method, merge options, merge checks, merge suggestions, and set up a default description template for merge requests."
msgstr ""
msgid "ProjectSettings|Commit authors can edit commit messages on unprotected branches."
msgstr ""
msgid "ProjectSettings|Contact an admin to change this setting."
msgstr ""
@ -22856,7 +22880,7 @@ msgstr ""
msgid "ProjectSettings|Encourage"
msgstr ""
msgid "ProjectSettings|Environments, logs, cluster management, and more"
msgid "ProjectSettings|Environments, logs, cluster management, and more."
msgstr ""
msgid "ProjectSettings|Every merge creates a merge commit"
@ -22865,7 +22889,7 @@ msgstr ""
msgid "ProjectSettings|Every project can have its own space to store its Docker images"
msgstr ""
msgid "ProjectSettings|Every project can have its own space to store its packages"
msgid "ProjectSettings|Every project can have its own space to store its packages."
msgstr ""
msgid "ProjectSettings|Everyone"
@ -22904,13 +22928,13 @@ msgstr ""
msgid "ProjectSettings|Issues"
msgstr ""
msgid "ProjectSettings|LFS objects from this repository are still available to forks. %{linkStart}How do I remove them?%{linkEnd}"
msgid "ProjectSettings|LFS objects from this repository are available to forks. %{linkStart}How do I remove them?%{linkEnd}"
msgstr ""
msgid "ProjectSettings|Lightweight issue tracking system for this project"
msgid "ProjectSettings|Lightweight issue tracking system."
msgstr ""
msgid "ProjectSettings|Manages large files such as audio, video, and graphics files"
msgid "ProjectSettings|Manages large files such as audio, video, and graphics files."
msgstr ""
msgid "ProjectSettings|Merge checks"
@ -22946,13 +22970,16 @@ msgstr ""
msgid "ProjectSettings|Operations"
msgstr ""
msgid "ProjectSettings|Override user notification preferences for all project members."
msgstr ""
msgid "ProjectSettings|Packages"
msgstr ""
msgid "ProjectSettings|Pages"
msgstr ""
msgid "ProjectSettings|Pages for project documentation"
msgid "ProjectSettings|Pages for project documentation."
msgstr ""
msgid "ProjectSettings|Pipelines"
@ -22982,7 +23009,7 @@ msgstr ""
msgid "ProjectSettings|Requirements"
msgstr ""
msgid "ProjectSettings|Requirements management system for this project"
msgid "ProjectSettings|Requirements management system."
msgstr ""
msgid "ProjectSettings|Security & Compliance"
@ -22994,7 +23021,7 @@ msgstr ""
msgid "ProjectSettings|Set the default behavior and availability of this option in merge requests. Changes made are also applied to existing merge requests."
msgstr ""
msgid "ProjectSettings|Share code pastes with others out of Git repository"
msgid "ProjectSettings|Share code with others outside the project."
msgstr ""
msgid "ProjectSettings|Show default award emojis"
@ -23018,7 +23045,7 @@ msgstr ""
msgid "ProjectSettings|Squashing is never performed and the checkbox is hidden."
msgstr ""
msgid "ProjectSettings|Submit changes to be merged upstream"
msgid "ProjectSettings|Submit changes to be merged upstream."
msgstr ""
msgid "ProjectSettings|The commit message used to apply merge request suggestions"
@ -23042,30 +23069,36 @@ msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
msgstr ""
msgid "ProjectSettings|This setting will override user notification preferences for all project members."
msgstr ""
msgid "ProjectSettings|This will dictate the commit history when you merge a merge request"
msgstr ""
msgid "ProjectSettings|Transfer project"
msgstr ""
msgid "ProjectSettings|Users can copy the repository to a new project."
msgstr ""
msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
msgstr ""
msgid "ProjectSettings|View and edit files in this project"
msgid "ProjectSettings|Users can request access"
msgstr ""
msgid "ProjectSettings|View and edit files in this project. Non-project members will only have read access"
msgid "ProjectSettings|View and edit files in this project."
msgstr ""
msgid "ProjectSettings|View project analytics"
msgid "ProjectSettings|View and edit files in this project. Non-project members will only have read access."
msgstr ""
msgid "ProjectSettings|View project analytics."
msgstr ""
msgid "ProjectSettings|Visibility options for this fork are limited by the current visibility of the source project."
msgstr ""
msgid "ProjectSettings|Visualize the project's performance metrics."
msgstr ""
msgid "ProjectSettings|What are badges?"
msgstr ""
@ -23075,19 +23108,10 @@ msgstr ""
msgid "ProjectSettings|When conflicts arise the user is given the option to rebase"
msgstr ""
msgid "ProjectSettings|When enabled, commit authors will be able to edit commit messages on unprotected branches."
msgstr ""
msgid "ProjectSettings|When enabled, issues, merge requests, and snippets will always show thumbs-up and thumbs-down award emoji buttons."
msgstr ""
msgid "ProjectSettings|Wiki"
msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab."
msgstr ""
msgid "ProjectTemplates|.NET Core"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -5,292 +5,296 @@ require 'spec_helper'
RSpec.describe SearchController do
include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) }
before do
sign_in(user)
end
shared_examples_for 'when the user cannot read cross project' do |action, params|
before do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?)
.with(user, :read_cross_project, :global) { false }
end
it 'blocks access without a project_id' do
get action, params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'allows access with a project_id' do
get action, params: params.merge(project_id: create(:project, :public).id)
expect(response).to have_gitlab_http_status(:ok)
end
end
shared_examples_for 'with external authorization service enabled' do |action, params|
let(:project) { create(:project, namespace: user.namespace) }
let(:note) { create(:note_on_issue, project: project) }
context 'authorized user' do
let(:user) { create(:user) }
before do
enable_external_authorization_service_check
sign_in(user)
end
it 'renders a 403 when no project is given' do
get action, params: params
shared_examples_for 'when the user cannot read cross project' do |action, params|
before do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?)
.with(user, :read_cross_project, :global) { false }
end
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'blocks access without a project_id' do
get action, params: params
it 'renders a 200 when a project was set' do
get action, params: params.merge(project_id: project.id)
expect(response).to have_gitlab_http_status(:forbidden)
end
expect(response).to have_gitlab_http_status(:ok)
end
end
describe 'GET #show' do
it_behaves_like 'when the user cannot read cross project', :show, { search: 'hello' } do
it 'still allows accessing the search page' do
get :show
it 'allows access with a project_id' do
get action, params: params.merge(project_id: create(:project, :public).id)
expect(response).to have_gitlab_http_status(:ok)
end
end
it_behaves_like 'with external authorization service enabled', :show, { search: 'hello' }
context 'uses the right partials depending on scope' do
using RSpec::Parameterized::TableSyntax
render_views
let_it_be(:project) { create(:project, :public, :repository, :wiki_repo) }
shared_examples_for 'with external authorization service enabled' do |action, params|
let(:project) { create(:project, namespace: user.namespace) }
let(:note) { create(:note_on_issue, project: project) }
before do
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
enable_external_authorization_service_check
end
subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) }
it 'renders a 403 when no project is given' do
get action, params: params
where(:partial, :scope) do
'_blob' | :blobs
'_wiki_blob' | :wiki_blobs
'_commit' | :commits
expect(response).to have_gitlab_http_status(:forbidden)
end
with_them do
it do
project_wiki = create(:project_wiki, project: project, user: user)
create(:wiki_page, wiki: project_wiki, title: 'merge', content: 'merge')
it 'renders a 200 when a project was set' do
get action, params: params.merge(project_id: project.id)
expect(subject).to render_template("search/results/#{partial}")
end
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'global search' do
using RSpec::Parameterized::TableSyntax
render_views
describe 'GET #show' do
it_behaves_like 'when the user cannot read cross project', :show, { search: 'hello' } do
it 'still allows accessing the search page' do
get :show
expect(response).to have_gitlab_http_status(:ok)
end
end
it_behaves_like 'with external authorization service enabled', :show, { search: 'hello' }
context 'uses the right partials depending on scope' do
using RSpec::Parameterized::TableSyntax
render_views
let_it_be(:project) { create(:project, :public, :repository, :wiki_repo) }
context 'when block_anonymous_global_searches is disabled' do
before do
stub_feature_flags(block_anonymous_global_searches: false)
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
end
it 'omits pipeline status from load' do
project = create(:project, :public)
expect(Gitlab::Cache::Ci::ProjectPipelineStatus).not_to receive(:load_in_batch_for_projects)
subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) }
get :show, params: { scope: 'projects', search: project.name }
expect(assigns[:search_objects].first).to eq project
where(:partial, :scope) do
'_blob' | :blobs
'_wiki_blob' | :wiki_blobs
'_commit' | :commits
end
context 'check search term length' do
let(:search_queries) do
char_limit = SearchService::SEARCH_CHAR_LIMIT
term_limit = SearchService::SEARCH_TERM_LIMIT
{
chars_under_limit: ('a' * (char_limit - 1)),
chars_over_limit: ('a' * (char_limit + 1)),
terms_under_limit: ('abc ' * (term_limit - 1)),
terms_over_limit: ('abc ' * (term_limit + 1))
}
with_them do
it do
project_wiki = create(:project_wiki, project: project, user: user)
create(:wiki_page, wiki: project_wiki, title: 'merge', content: 'merge')
expect(subject).to render_template("search/results/#{partial}")
end
end
end
context 'global search' do
using RSpec::Parameterized::TableSyntax
render_views
context 'when block_anonymous_global_searches is disabled' do
before do
stub_feature_flags(block_anonymous_global_searches: false)
end
where(:string_name, :expectation) do
:chars_under_limit | :not_to_set_flash
:chars_over_limit | :set_chars_flash
:terms_under_limit | :not_to_set_flash
:terms_over_limit | :set_terms_flash
it 'omits pipeline status from load' do
project = create(:project, :public)
expect(Gitlab::Cache::Ci::ProjectPipelineStatus).not_to receive(:load_in_batch_for_projects)
get :show, params: { scope: 'projects', search: project.name }
expect(assigns[:search_objects].first).to eq project
end
with_them do
it do
get :show, params: { scope: 'projects', search: search_queries[string_name] }
context 'check search term length' do
let(:search_queries) do
char_limit = SearchService::SEARCH_CHAR_LIMIT
term_limit = SearchService::SEARCH_TERM_LIMIT
{
chars_under_limit: ('a' * (char_limit - 1)),
chars_over_limit: ('a' * (char_limit + 1)),
terms_under_limit: ('abc ' * (term_limit - 1)),
terms_over_limit: ('abc ' * (term_limit + 1))
}
end
case expectation
when :not_to_set_flash
expect(controller).not_to set_flash[:alert]
when :set_chars_flash
expect(controller).to set_flash[:alert].to(/characters/)
when :set_terms_flash
expect(controller).to set_flash[:alert].to(/terms/)
where(:string_name, :expectation) do
:chars_under_limit | :not_to_set_flash
:chars_over_limit | :set_chars_flash
:terms_under_limit | :not_to_set_flash
:terms_over_limit | :set_terms_flash
end
with_them do
it do
get :show, params: { scope: 'projects', search: search_queries[string_name] }
case expectation
when :not_to_set_flash
expect(controller).not_to set_flash[:alert]
when :set_chars_flash
expect(controller).to set_flash[:alert].to(/characters/)
when :set_terms_flash
expect(controller).to set_flash[:alert].to(/terms/)
end
end
end
end
end
context 'when block_anonymous_global_searches is enabled' do
context 'for unauthenticated user' do
before do
sign_out(user)
end
it 'redirects to login page' do
get :show, params: { scope: 'projects', search: '*' }
expect(response).to redirect_to new_user_session_path
end
end
context 'for authenticated user' do
it 'succeeds' do
get :show, params: { scope: 'projects', search: '*' }
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end
context 'when block_anonymous_global_searches is enabled' do
context 'for unauthenticated user' do
it 'finds issue comments' do
project = create(:project, :public)
note = create(:note_on_issue, project: project)
get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
expect(assigns[:search_objects].first).to eq note
end
context 'unique users tracking' do
before do
allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
end
it_behaves_like 'tracking unique hll events', :search_track_unique_users do
subject(:request) { get :show, params: { scope: 'projects', search: 'term' } }
let(:target_id) { 'i_search_total' }
let(:expected_type) { instance_of(String) }
end
end
context 'on restricted projects' do
context 'when signed out' do
before do
sign_out(user)
end
it 'redirects to login page' do
get :show, params: { scope: 'projects', search: '*' }
it "doesn't expose comments on issues" do
project = create(:project, :public, :issues_private)
note = create(:note_on_issue, project: project)
expect(response).to redirect_to new_user_session_path
get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
expect(assigns[:search_objects].count).to eq(0)
end
end
context 'for authenticated user' do
it 'succeeds' do
get :show, params: { scope: 'projects', search: '*' }
it "doesn't expose comments on merge_requests" do
project = create(:project, :public, :merge_requests_private)
note = create(:note_on_merge_request, project: project)
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end
get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
it 'finds issue comments' do
project = create(:project, :public)
note = create(:note_on_issue, project: project)
get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
expect(assigns[:search_objects].first).to eq note
end
context 'unique users tracking' do
before do
allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
end
it_behaves_like 'tracking unique hll events', :search_track_unique_users do
subject(:request) { get :show, params: { scope: 'projects', search: 'term' } }
let(:target_id) { 'i_search_total' }
let(:expected_type) { instance_of(String) }
end
end
context 'on restricted projects' do
context 'when signed out' do
before do
sign_out(user)
expect(assigns[:search_objects].count).to eq(0)
end
it "doesn't expose comments on issues" do
project = create(:project, :public, :issues_private)
note = create(:note_on_issue, project: project)
it "doesn't expose comments on snippets" do
project = create(:project, :public, :snippets_private)
note = create(:note_on_project_snippet, project: project)
get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
expect(assigns[:search_objects].count).to eq(0)
end
end
end
it "doesn't expose comments on merge_requests" do
project = create(:project, :public, :merge_requests_private)
note = create(:note_on_merge_request, project: project)
describe 'GET #count' do
it_behaves_like 'when the user cannot read cross project', :count, { search: 'hello', scope: 'projects' }
it_behaves_like 'with external authorization service enabled', :count, { search: 'hello', scope: 'projects' }
get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
it 'returns the result count for the given term and scope' do
create(:project, :public, name: 'hello world')
create(:project, :public, name: 'foo bar')
expect(assigns[:search_objects].count).to eq(0)
get :count, params: { search: 'hello', scope: 'projects' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq({ 'count' => '1' })
end
it "doesn't expose comments on snippets" do
project = create(:project, :public, :snippets_private)
note = create(:note_on_project_snippet, project: project)
it 'raises an error if search term is missing' do
expect do
get :count, params: { scope: 'projects' }
end.to raise_error(ActionController::ParameterMissing)
end
get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
it 'raises an error if search scope is missing' do
expect do
get :count, params: { search: 'hello' }
end.to raise_error(ActionController::ParameterMissing)
end
end
expect(assigns[:search_objects].count).to eq(0)
describe 'GET #autocomplete' do
it_behaves_like 'when the user cannot read cross project', :autocomplete, { term: 'hello' }
it_behaves_like 'with external authorization service enabled', :autocomplete, { term: 'hello' }
end
describe '#append_info_to_payload' do
it 'appends search metadata for logging' do
last_payload = nil
original_append_info_to_payload = controller.method(:append_info_to_payload)
expect(controller).to receive(:append_info_to_payload) do |payload|
original_append_info_to_payload.call(payload)
last_payload = payload
end
get :show, params: { scope: 'issues', search: 'hello world', group_id: '123', project_id: '456', confidential: true, state: true, force_search_results: true }
expect(last_payload[:metadata]['meta.search.group_id']).to eq('123')
expect(last_payload[:metadata]['meta.search.project_id']).to eq('456')
expect(last_payload[:metadata]).not_to have_key('meta.search.search')
expect(last_payload[:metadata]['meta.search.scope']).to eq('issues')
expect(last_payload[:metadata]['meta.search.force_search_results']).to eq('true')
expect(last_payload[:metadata]['meta.search.filters.confidential']).to eq('true')
expect(last_payload[:metadata]['meta.search.filters.state']).to eq('true')
end
end
end
describe 'GET #count' do
it_behaves_like 'when the user cannot read cross project', :count, { search: 'hello', scope: 'projects' }
it_behaves_like 'with external authorization service enabled', :count, { search: 'hello', scope: 'projects' }
context 'unauthorized user' do
describe 'GET #opensearch' do
render_views
it 'returns the result count for the given term and scope' do
create(:project, :public, name: 'hello world')
create(:project, :public, name: 'foo bar')
it 'renders xml' do
get :opensearch, format: :xml
get :count, params: { search: 'hello', scope: 'projects' }
doc = Nokogiri::XML.parse(response.body)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq({ 'count' => '1' })
end
it 'raises an error if search term is missing' do
expect do
get :count, params: { scope: 'projects' }
end.to raise_error(ActionController::ParameterMissing)
end
it 'raises an error if search scope is missing' do
expect do
get :count, params: { search: 'hello' }
end.to raise_error(ActionController::ParameterMissing)
end
end
describe 'GET #autocomplete' do
it_behaves_like 'when the user cannot read cross project', :autocomplete, { term: 'hello' }
it_behaves_like 'with external authorization service enabled', :autocomplete, { term: 'hello' }
end
describe 'GET #opensearch' do
render_views
it 'renders xml' do
get :opensearch, format: :xml
doc = Nokogiri::XML.parse(response.body)
expect(response).to have_gitlab_http_status(:ok)
expect(doc.css('OpenSearchDescription ShortName').text).to eq('GitLab')
expect(doc.css('OpenSearchDescription *').map(&:name)).to eq(%w[ShortName Description InputEncoding Image Url SearchForm])
end
end
describe '#append_info_to_payload' do
it 'appends search metadata for logging' do
last_payload = nil
original_append_info_to_payload = controller.method(:append_info_to_payload)
expect(controller).to receive(:append_info_to_payload) do |payload|
original_append_info_to_payload.call(payload)
last_payload = payload
expect(response).to have_gitlab_http_status(:ok)
expect(doc.css('OpenSearchDescription ShortName').text).to eq('GitLab')
expect(doc.css('OpenSearchDescription *').map(&:name)).to eq(%w[ShortName Description InputEncoding Image Url SearchForm])
end
get :show, params: { scope: 'issues', search: 'hello world', group_id: '123', project_id: '456', confidential: true, state: true, force_search_results: true }
expect(last_payload[:metadata]['meta.search.group_id']).to eq('123')
expect(last_payload[:metadata]['meta.search.project_id']).to eq('456')
expect(last_payload[:metadata]).not_to have_key('meta.search.search')
expect(last_payload[:metadata]['meta.search.scope']).to eq('issues')
expect(last_payload[:metadata]['meta.search.force_search_results']).to eq('true')
expect(last_payload[:metadata]['meta.search.filters.confidential']).to eq('true')
expect(last_payload[:metadata]['meta.search.filters.state']).to eq('true')
end
end
end

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,
);
});
});
});

View File

@ -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.",
);
});

View File

@ -1,25 +1,29 @@
import { shallowMount } from '@vue/test-utils';
import { shallowMount, mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { GlButton } from '@gitlab/ui';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { TEST_HOST } from 'spec/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import PipelinesActions from '~/pipelines/components/pipelines_list/pipelines_actions.vue';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
jest.mock('~/flash');
describe('Pipelines Actions dropdown', () => {
let wrapper;
let mock;
const createComponent = (actions = []) => {
wrapper = shallowMount(PipelinesActions, {
const createComponent = (props, mountFn = shallowMount) => {
wrapper = mountFn(PipelinesActions, {
propsData: {
actions,
...props,
},
});
};
const findAllDropdownItems = () => wrapper.findAll(GlButton);
const findDropdown = () => wrapper.find(GlDropdown);
const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findAllCountdowns = () => wrapper.findAll(GlCountdown);
beforeEach(() => {
@ -47,7 +51,7 @@ describe('Pipelines Actions dropdown', () => {
];
beforeEach(() => {
createComponent(mockActions);
createComponent({ actions: mockActions });
});
it('renders a dropdown with the provided actions', () => {
@ -59,16 +63,33 @@ describe('Pipelines Actions dropdown', () => {
});
describe('on click', () => {
it('makes a request and toggles the loading state', () => {
beforeEach(() => {
createComponent({ actions: mockActions }, mount);
});
it('makes a request and toggles the loading state', async () => {
mock.onPost(mockActions.path).reply(200);
wrapper.find(GlButton).vm.$emit('click');
findAllDropdownItems().at(0).vm.$emit('click');
expect(wrapper.vm.isLoading).toBe(true);
await wrapper.vm.$nextTick();
expect(findDropdown().props('loading')).toBe(true);
return waitForPromises().then(() => {
expect(wrapper.vm.isLoading).toBe(false);
});
await waitForPromises();
expect(findDropdown().props('loading')).toBe(false);
});
it('makes a failed request and toggles the loading state', async () => {
mock.onPost(mockActions.path).reply(500);
findAllDropdownItems().at(0).vm.$emit('click');
await wrapper.vm.$nextTick();
expect(findDropdown().props('loading')).toBe(true);
await waitForPromises();
expect(findDropdown().props('loading')).toBe(false);
expect(createFlash).toHaveBeenCalledTimes(1);
});
});
});
@ -89,10 +110,10 @@ describe('Pipelines Actions dropdown', () => {
beforeEach(() => {
jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
createComponent([scheduledJobAction, expiredJobAction]);
createComponent({ actions: [scheduledJobAction, expiredJobAction] });
});
it('makes post request after confirming', () => {
it('makes post request after confirming', async () => {
mock.onPost(scheduledJobAction.path).reply(200);
jest.spyOn(window, 'confirm').mockReturnValue(true);
@ -100,19 +121,22 @@ describe('Pipelines Actions dropdown', () => {
expect(window.confirm).toHaveBeenCalled();
return waitForPromises().then(() => {
expect(mock.history.post.length).toBe(1);
});
await waitForPromises();
expect(mock.history.post).toHaveLength(1);
});
it('does not make post request if confirmation is cancelled', () => {
it('does not make post request if confirmation is cancelled', async () => {
mock.onPost(scheduledJobAction.path).reply(200);
jest.spyOn(window, 'confirm').mockReturnValue(false);
findAllDropdownItems().at(0).vm.$emit('click');
expect(window.confirm).toHaveBeenCalled();
expect(mock.history.post.length).toBe(0);
await waitForPromises();
expect(mock.history.post).toHaveLength(0);
});
it('displays the remaining time in the dropdown', () => {

View File

@ -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);
});
});
});

View File

@ -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);
});
});
});

View File

@ -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();
});
});

View File

@ -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

View File

@ -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' }

View File

@ -74,28 +74,41 @@ RSpec.describe BulkImports::Pipeline::Runner do
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
expect(logger).to receive(:info)
.with(
bulk_import_entity_id: entity.id,
bulk_import_entity_type: 'group_entity',
message: 'Pipeline started',
pipeline_class: 'BulkImports::MyPipeline'
)
expect(logger).to receive(:info)
.with(
bulk_import_entity_id: entity.id,
bulk_import_entity_type: 'group_entity',
pipeline_class: 'BulkImports::MyPipeline',
bulk_import_entity_id: entity.id,
bulk_import_entity_type: 'group_entity'
pipeline_step: :extractor,
step_class: 'BulkImports::Extractor'
)
expect(logger).to receive(:info)
.with(
bulk_import_entity_id: entity.id,
bulk_import_entity_type: 'group_entity',
extractor: 'BulkImports::Extractor'
pipeline_class: 'BulkImports::MyPipeline',
pipeline_step: :transformer,
step_class: 'BulkImports::Transformer'
)
expect(logger).to receive(:info)
.with(
bulk_import_entity_id: entity.id,
bulk_import_entity_type: 'group_entity',
transformer: 'BulkImports::Transformer'
pipeline_class: 'BulkImports::MyPipeline',
pipeline_step: :loader,
step_class: 'BulkImports::Loader'
)
expect(logger).to receive(:info)
.with(
bulk_import_entity_id: entity.id,
bulk_import_entity_type: 'group_entity',
loader: 'BulkImports::Loader'
message: 'Pipeline finished',
pipeline_class: 'BulkImports::MyPipeline'
)
end

View File

@ -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

View File

@ -40,7 +40,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
'code_review',
'terraform',
'ci_templates',
'quickactions'
'quickactions',
'pipeline_authoring'
)
end
end

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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