Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-05-27 15:10:39 +00:00
parent 0afd7f1817
commit f719944dee
76 changed files with 1039 additions and 1000 deletions

View File

@ -1 +1 @@
6e58da454633a53c86d14a59587ee7a6a9d11031
58261cb08bae1bae4fed28173ce9457bd9728c76

View File

@ -50,6 +50,9 @@ export default {
},
},
computed: {
issuableId() {
return getIdFromGraphQLId(this.issuable.id);
},
createdInPastDay() {
const createdSecondsAgo = differenceInSeconds(new Date(this.issuable.createdAt), new Date());
return createdSecondsAgo < SECONDS_IN_DAY;
@ -61,7 +64,7 @@ export default {
return this.issuable.gitlabWebUrl || this.issuable.webUrl;
},
authorId() {
return getIdFromGraphQLId(`${this.author.id}`);
return getIdFromGraphQLId(this.author.id);
},
isIssuableUrlExternal() {
return isExternal(this.webUrl);
@ -70,10 +73,10 @@ export default {
return this.issuable.labels?.nodes || this.issuable.labels || [];
},
labelIdsString() {
return JSON.stringify(this.labels.map((label) => label.id));
return JSON.stringify(this.labels.map((label) => getIdFromGraphQLId(label.id)));
},
assignees() {
return this.issuable.assignees || [];
return this.issuable.assignees?.nodes || this.issuable.assignees || [];
},
createdAt() {
return sprintf(__('created %{timeAgo}'), {
@ -81,6 +84,9 @@ export default {
});
},
updatedAt() {
if (!this.issuable.updatedAt) {
return '';
}
return sprintf(__('updated %{timeAgo}'), {
timeAgo: getTimeago().format(this.issuable.updatedAt),
});
@ -157,7 +163,7 @@ export default {
<template>
<li
:id="`issuable_${issuable.id}`"
:id="`issuable_${issuableId}`"
class="issue gl-px-5!"
:class="{ closed: issuable.closedAt, today: createdInPastDay }"
:data-labels="labelIdsString"
@ -167,7 +173,7 @@ export default {
<gl-form-checkbox
class="gl-mr-0"
:checked="checked"
:data-id="issuable.id"
:data-id="issuableId"
@input="$emit('checked-input', $event)"
>
<span class="gl-sr-only">{{ issuable.title }}</span>

View File

@ -2,6 +2,7 @@
import { GlSkeletonLoading, GlPagination } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
@ -211,7 +212,7 @@ export default {
},
methods: {
issuableId(issuable) {
return issuable.id || issuable.iid || uniqueId();
return getIdFromGraphQLId(issuable.id) || issuable.iid || uniqueId();
},
issuableChecked(issuable) {
return this.checkedIssuables[this.issuableId(issuable)]?.checked;

View File

@ -42,6 +42,9 @@ export default {
}
return __('Milestone');
},
milestoneLink() {
return this.issue.milestone.webPath || this.issue.milestone.webUrl;
},
dueDate() {
return this.issue.dueDate && dateInWords(new Date(this.issue.dueDate), true);
},
@ -49,7 +52,7 @@ export default {
return isInPast(new Date(this.issue.dueDate));
},
timeEstimate() {
return this.issue.timeStats?.humanTimeEstimate;
return this.issue.humanTimeEstimate || this.issue.timeStats?.humanTimeEstimate;
},
showHealthStatus() {
return this.hasIssuableHealthStatusFeature && this.issue.healthStatus;
@ -85,7 +88,7 @@ export default {
class="issuable-milestone gl-display-none gl-sm-display-inline-block! gl-mr-3"
data-testid="issuable-milestone"
>
<gl-link v-gl-tooltip :href="issue.milestone.webUrl" :title="milestoneDate">
<gl-link v-gl-tooltip :href="milestoneLink" :title="milestoneDate">
<gl-icon name="clock" />
{{ issue.milestone.title }}
</gl-link>

View File

@ -9,24 +9,21 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { toNumber } from 'lodash';
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
import createFlash from '~/flash';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
import {
API_PARAM,
apiSortParams,
CREATED_DESC,
i18n,
MAX_LIST_SIZE,
PAGE_SIZE,
PARAM_DUE_DATE,
PARAM_PAGE,
PARAM_SORT,
PARAM_STATE,
RELATIVE_POSITION_DESC,
RELATIVE_POSITION_ASC,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL,
@ -37,19 +34,19 @@ import {
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_WEIGHT,
UPDATED_DESC,
URL_PARAM,
urlSortParams,
} from '~/issues_list/constants';
import {
convertToParams,
convertToApiParams,
convertToSearchQuery,
convertToUrlParams,
getDueDateValue,
getFilterTokens,
getSortKey,
getSortOptions,
} from '~/issues_list/utils';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils';
import { getParameterByName } from '~/lib/utils/common_utils';
import {
DEFAULT_NONE_ANY,
OPERATOR_IS_ONLY,
@ -107,9 +104,6 @@ export default {
emptyStateSvgPath: {
default: '',
},
endpoint: {
default: '',
},
exportCsvPath: {
default: '',
},
@ -173,15 +167,53 @@ export default {
dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)),
exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
filterTokens: getFilterTokens(window.location.search),
isLoading: false,
issues: [],
page: toNumber(getParameterByName(PARAM_PAGE)) || 1,
page: 1,
pageInfo: {},
pageParams: {
firstPageSize: PAGE_SIZE,
},
showBulkEditSidebar: false,
sortKey: getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey,
state: state || IssuableStates.Opened,
totalIssues: 0,
};
},
apollo: {
issues: {
query: getIssuesQuery,
variables() {
const filterParams = {
...this.apiFilterParams,
};
if (filterParams.epicId) {
filterParams.epicId = filterParams.epicId.split('::&').pop();
} else if (filterParams.not?.epicId) {
filterParams.not.epicId = filterParams.not.epicId.split('::&').pop();
}
return {
projectPath: this.projectPath,
search: this.searchQuery,
sort: this.sortKey,
state: this.state,
...this.pageParams,
...filterParams,
};
},
update: ({ project }) => project.issues.nodes,
result({ data }) {
this.pageInfo = data.project.issues.pageInfo;
this.totalIssues = data.project.issues.count;
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
},
error() {
createFlash({ message: this.$options.i18n.errorFetchingIssues });
},
debounce: 200,
},
},
computed: {
hasSearch() {
return this.searchQuery || Object.keys(this.urlFilterParams).length;
@ -190,16 +222,22 @@ export default {
return this.showBulkEditSidebar || !this.issues.length;
},
isManualOrdering() {
return this.sortKey === RELATIVE_POSITION_DESC;
return this.sortKey === RELATIVE_POSITION_ASC;
},
isOpenTab() {
return this.state === IssuableStates.Opened;
},
nextPage() {
return Number(this.pageInfo.hasNextPage);
},
previousPage() {
return Number(this.pageInfo.hasPreviousPage);
},
apiFilterParams() {
return convertToParams(this.filterTokens, API_PARAM);
return convertToApiParams(this.filterTokens);
},
urlFilterParams() {
return convertToParams(this.filterTokens, URL_PARAM);
return convertToUrlParams(this.filterTokens);
},
searchQuery() {
return convertToSearchQuery(this.filterTokens) || undefined;
@ -214,6 +252,7 @@ export default {
dataType: 'user',
unique: true,
defaultAuthors: [],
operators: OPERATOR_IS_ONLY,
fetchAuthors: this.fetchUsers,
},
{
@ -240,7 +279,7 @@ export default {
title: TOKEN_TITLE_LABEL,
icon: 'labels',
token: LabelToken,
defaultLabels: [],
defaultLabels: DEFAULT_NONE_ANY,
fetchLabels: this.fetchLabels,
},
];
@ -333,10 +372,9 @@ export default {
return {
due_date: this.dueDateFilter,
page: this.page,
search: this.searchQuery,
sort: urlSortParams[this.sortKey],
state: this.state,
...urlSortParams[this.sortKey],
...filterParams,
};
},
@ -346,7 +384,6 @@ export default {
},
mounted() {
eventHub.$on('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
this.fetchIssues();
},
beforeDestroy() {
eventHub.$off('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
@ -386,59 +423,19 @@ export default {
return this.fetchWithCache(this.projectMilestonesPath, 'milestones', 'title', search, true);
},
fetchIterations(search) {
return axios.get(this.projectIterationsPath, { params: { search } });
const number = Number(search);
return !search || Number.isNaN(number)
? axios.get(this.projectIterationsPath, { params: { search } })
: axios.get(this.projectIterationsPath, { params: { id: number } });
},
fetchUsers(search) {
return axios.get(this.autocompleteUsersPath, { params: { search } });
},
fetchIssues() {
if (!this.hasProjectIssues) {
return undefined;
}
this.isLoading = true;
const filterParams = {
...this.apiFilterParams,
};
if (filterParams.epic_id) {
filterParams.epic_id = filterParams.epic_id.split('::&').pop();
} else if (filterParams['not[epic_id]']) {
filterParams['not[epic_id]'] = filterParams['not[epic_id]'].split('::&').pop();
}
return axios
.get(this.endpoint, {
params: {
due_date: this.dueDateFilter,
page: this.page,
per_page: PAGE_SIZE,
search: this.searchQuery,
state: this.state,
with_labels_details: true,
...apiSortParams[this.sortKey],
...filterParams,
},
})
.then(({ data, headers }) => {
this.page = Number(headers['x-page']);
this.totalIssues = Number(headers['x-total']);
this.issues = data.map((issue) => convertObjectPropsToCamelCase(issue, { deep: true }));
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
})
.catch(() => {
createFlash({ message: this.$options.i18n.errorFetchingIssues });
})
.finally(() => {
this.isLoading = false;
});
},
getExportCsvPathWithQuery() {
return `${this.exportCsvPath}${window.location.search}`;
},
getStatus(issue) {
if (issue.closedAt && issue.movedToId) {
if (issue.closedAt && issue.moved) {
return this.$options.i18n.closedMoved;
}
if (issue.closedAt) {
@ -469,18 +466,30 @@ export default {
},
handleClickTab(state) {
if (this.state !== state) {
this.pageParams = {
firstPageSize: PAGE_SIZE,
};
this.page = 1;
}
this.state = state;
this.fetchIssues();
},
handleFilter(filter) {
this.filterTokens = filter;
this.fetchIssues();
},
handlePageChange(page) {
if (page > this.page) {
this.pageParams = {
afterCursor: this.pageInfo.endCursor,
firstPageSize: PAGE_SIZE,
};
} else {
this.pageParams = {
beforeCursor: this.pageInfo.startCursor,
lastPageSize: PAGE_SIZE,
};
}
this.page = page;
this.fetchIssues();
},
handleReorder({ newIndex, oldIndex }) {
const issueToMove = this.issues[oldIndex];
@ -517,7 +526,6 @@ export default {
},
handleSort(value) {
this.sortKey = value;
this.fetchIssues();
},
toggleBulkEditSidebar(showBulkEditSidebar) {
this.showBulkEditSidebar = showBulkEditSidebar;
@ -541,14 +549,13 @@ export default {
:tabs="$options.IssuableListTabs"
:current-tab="state"
:tab-counts="tabCounts"
:issuables-loading="isLoading"
:issuables-loading="$apollo.loading"
:is-manual-ordering="isManualOrdering"
:show-bulk-edit-sidebar="showBulkEditSidebar"
:show-pagination-controls="showPaginationControls"
:total-items="totalIssues"
:current-page="page"
:previous-page="page - 1"
:next-page="page + 1"
:previous-page="previousPage"
:next-page="nextPage"
:url-params="urlParams"
@click-tab="handleClickTab"
@filter="handleFilter"
@ -631,7 +638,7 @@ export default {
</li>
<blocking-issues-count
class="gl-display-none gl-sm-display-block"
:blocking-issues-count="issuable.blockingIssuesCount"
:blocking-issues-count="issuable.blockedByCount"
:is-list-item="true"
/>
</template>

View File

@ -101,7 +101,6 @@ export const i18n = {
export const JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY = 'jira-import-success-alert-hide-map';
export const PARAM_DUE_DATE = 'due_date';
export const PARAM_PAGE = 'page';
export const PARAM_SORT = 'sort';
export const PARAM_STATE = 'state';
@ -125,21 +124,21 @@ export const CREATED_ASC = 'CREATED_ASC';
export const CREATED_DESC = 'CREATED_DESC';
export const DUE_DATE_ASC = 'DUE_DATE_ASC';
export const DUE_DATE_DESC = 'DUE_DATE_DESC';
export const LABEL_PRIORITY_ASC = 'LABEL_PRIORITY_ASC';
export const LABEL_PRIORITY_DESC = 'LABEL_PRIORITY_DESC';
export const MILESTONE_DUE_ASC = 'MILESTONE_DUE_ASC';
export const MILESTONE_DUE_DESC = 'MILESTONE_DUE_DESC';
export const POPULARITY_ASC = 'POPULARITY_ASC';
export const POPULARITY_DESC = 'POPULARITY_DESC';
export const PRIORITY_ASC = 'PRIORITY_ASC';
export const PRIORITY_DESC = 'PRIORITY_DESC';
export const RELATIVE_POSITION_DESC = 'RELATIVE_POSITION_DESC';
export const RELATIVE_POSITION_ASC = 'RELATIVE_POSITION_ASC';
export const UPDATED_ASC = 'UPDATED_ASC';
export const UPDATED_DESC = 'UPDATED_DESC';
export const WEIGHT_ASC = 'WEIGHT_ASC';
export const WEIGHT_DESC = 'WEIGHT_DESC';
const SORT_ASC = 'asc';
const SORT_DESC = 'desc';
const PRIORITY_ASC_SORT = 'priority_asc';
const CREATED_DATE_SORT = 'created_date';
const CREATED_ASC_SORT = 'created_asc';
const UPDATED_DESC_SORT = 'updated_desc';
@ -147,129 +146,30 @@ const UPDATED_ASC_SORT = 'updated_asc';
const MILESTONE_SORT = 'milestone';
const MILESTONE_DUE_DESC_SORT = 'milestone_due_desc';
const DUE_DATE_DESC_SORT = 'due_date_desc';
const LABEL_PRIORITY_ASC_SORT = 'label_priority_asc';
const POPULARITY_ASC_SORT = 'popularity_asc';
const WEIGHT_DESC_SORT = 'weight_desc';
const BLOCKING_ISSUES_DESC_SORT = 'blocking_issues_desc';
const BLOCKING_ISSUES = 'blocking_issues';
export const apiSortParams = {
[PRIORITY_DESC]: {
order_by: PRIORITY,
sort: SORT_DESC,
},
[CREATED_ASC]: {
order_by: CREATED_AT,
sort: SORT_ASC,
},
[CREATED_DESC]: {
order_by: CREATED_AT,
sort: SORT_DESC,
},
[UPDATED_ASC]: {
order_by: UPDATED_AT,
sort: SORT_ASC,
},
[UPDATED_DESC]: {
order_by: UPDATED_AT,
sort: SORT_DESC,
},
[MILESTONE_DUE_ASC]: {
order_by: MILESTONE_DUE,
sort: SORT_ASC,
},
[MILESTONE_DUE_DESC]: {
order_by: MILESTONE_DUE,
sort: SORT_DESC,
},
[DUE_DATE_ASC]: {
order_by: DUE_DATE,
sort: SORT_ASC,
},
[DUE_DATE_DESC]: {
order_by: DUE_DATE,
sort: SORT_DESC,
},
[POPULARITY_ASC]: {
order_by: POPULARITY,
sort: SORT_ASC,
},
[POPULARITY_DESC]: {
order_by: POPULARITY,
sort: SORT_DESC,
},
[LABEL_PRIORITY_DESC]: {
order_by: LABEL_PRIORITY,
sort: SORT_DESC,
},
[RELATIVE_POSITION_DESC]: {
order_by: RELATIVE_POSITION,
per_page: 100,
sort: SORT_ASC,
},
[WEIGHT_ASC]: {
order_by: WEIGHT,
sort: SORT_ASC,
},
[WEIGHT_DESC]: {
order_by: WEIGHT,
sort: SORT_DESC,
},
[BLOCKING_ISSUES_DESC]: {
order_by: BLOCKING_ISSUES,
sort: SORT_DESC,
},
};
export const urlSortParams = {
[PRIORITY_DESC]: {
sort: PRIORITY,
},
[CREATED_ASC]: {
sort: CREATED_ASC_SORT,
},
[CREATED_DESC]: {
sort: CREATED_DATE_SORT,
},
[UPDATED_ASC]: {
sort: UPDATED_ASC_SORT,
},
[UPDATED_DESC]: {
sort: UPDATED_DESC_SORT,
},
[MILESTONE_DUE_ASC]: {
sort: MILESTONE_SORT,
},
[MILESTONE_DUE_DESC]: {
sort: MILESTONE_DUE_DESC_SORT,
},
[DUE_DATE_ASC]: {
sort: DUE_DATE,
},
[DUE_DATE_DESC]: {
sort: DUE_DATE_DESC_SORT,
},
[POPULARITY_ASC]: {
sort: POPULARITY_ASC_SORT,
},
[POPULARITY_DESC]: {
sort: POPULARITY,
},
[LABEL_PRIORITY_DESC]: {
sort: LABEL_PRIORITY,
},
[RELATIVE_POSITION_DESC]: {
sort: RELATIVE_POSITION,
per_page: 100,
},
[WEIGHT_ASC]: {
sort: WEIGHT,
},
[WEIGHT_DESC]: {
sort: WEIGHT_DESC_SORT,
},
[BLOCKING_ISSUES_DESC]: {
sort: BLOCKING_ISSUES_DESC_SORT,
},
[PRIORITY_ASC]: PRIORITY_ASC_SORT,
[PRIORITY_DESC]: PRIORITY,
[CREATED_ASC]: CREATED_ASC_SORT,
[CREATED_DESC]: CREATED_DATE_SORT,
[UPDATED_ASC]: UPDATED_ASC_SORT,
[UPDATED_DESC]: UPDATED_DESC_SORT,
[MILESTONE_DUE_ASC]: MILESTONE_SORT,
[MILESTONE_DUE_DESC]: MILESTONE_DUE_DESC_SORT,
[DUE_DATE_ASC]: DUE_DATE,
[DUE_DATE_DESC]: DUE_DATE_DESC_SORT,
[POPULARITY_ASC]: POPULARITY_ASC_SORT,
[POPULARITY_DESC]: POPULARITY,
[LABEL_PRIORITY_ASC]: LABEL_PRIORITY_ASC_SORT,
[LABEL_PRIORITY_DESC]: LABEL_PRIORITY,
[RELATIVE_POSITION_ASC]: RELATIVE_POSITION,
[WEIGHT_ASC]: WEIGHT,
[WEIGHT_DESC]: WEIGHT_DESC_SORT,
[BLOCKING_ISSUES_DESC]: BLOCKING_ISSUES_DESC_SORT,
};
export const MAX_LIST_SIZE = 10;
@ -294,12 +194,7 @@ export const TOKEN_TYPE_WEIGHT = 'weight';
export const filters = {
[TOKEN_TYPE_AUTHOR]: {
[API_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'author_username',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[author_username]',
},
[NORMAL_FILTER]: 'authorUsername',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@ -312,13 +207,8 @@ export const filters = {
},
[TOKEN_TYPE_ASSIGNEE]: {
[API_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'assignee_username',
[SPECIAL_FILTER]: 'assignee_id',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[assignee_username]',
},
[NORMAL_FILTER]: 'assigneeUsernames',
[SPECIAL_FILTER]: 'assigneeId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@ -333,12 +223,7 @@ export const filters = {
},
[TOKEN_TYPE_MILESTONE]: {
[API_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'milestone',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[milestone]',
},
[NORMAL_FILTER]: 'milestoneTitle',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@ -351,16 +236,13 @@ export const filters = {
},
[TOKEN_TYPE_LABEL]: {
[API_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'labels',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[labels]',
},
[NORMAL_FILTER]: 'labelName',
[SPECIAL_FILTER]: 'labelName',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'label_name[]',
[SPECIAL_FILTER]: 'label_name[]',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[label_name][]',
@ -369,10 +251,8 @@ export const filters = {
},
[TOKEN_TYPE_MY_REACTION]: {
[API_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'my_reaction_emoji',
[SPECIAL_FILTER]: 'my_reaction_emoji',
},
[NORMAL_FILTER]: 'myReactionEmoji',
[SPECIAL_FILTER]: 'myReactionEmoji',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@ -383,9 +263,7 @@ export const filters = {
},
[TOKEN_TYPE_CONFIDENTIAL]: {
[API_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'confidential',
},
[NORMAL_FILTER]: 'confidential',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@ -395,33 +273,23 @@ export const filters = {
},
[TOKEN_TYPE_ITERATION]: {
[API_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'iteration_title',
[SPECIAL_FILTER]: 'iteration_id',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[iteration_title]',
},
[NORMAL_FILTER]: 'iterationId',
[SPECIAL_FILTER]: 'iterationWildcardId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'iteration_title',
[NORMAL_FILTER]: 'iteration_id',
[SPECIAL_FILTER]: 'iteration_id',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[iteration_title]',
[NORMAL_FILTER]: 'not[iteration_id]',
},
},
},
[TOKEN_TYPE_EPIC]: {
[API_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'epic_id',
[SPECIAL_FILTER]: 'epic_id',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[epic_id]',
},
[NORMAL_FILTER]: 'epicId',
[SPECIAL_FILTER]: 'epicId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@ -435,13 +303,8 @@ export const filters = {
},
[TOKEN_TYPE_WEIGHT]: {
[API_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'weight',
[SPECIAL_FILTER]: 'weight',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[weight]',
},
[NORMAL_FILTER]: 'weight',
[SPECIAL_FILTER]: 'weight',
},
[URL_PARAM]: {
[OPERATOR_IS]: {

View File

@ -73,6 +73,13 @@ export function mountIssuesListApp() {
return false;
}
Vue.use(VueApollo);
const defaultClient = createDefaultClient({}, { assumeImmutableResults: true });
const apolloProvider = new VueApollo({
defaultClient,
});
const {
autocompleteAwardEmojisPath,
autocompleteUsersPath,
@ -83,7 +90,6 @@ export function mountIssuesListApp() {
email,
emailsHelpPagePath,
emptyStateSvgPath,
endpoint,
exportCsvPath,
groupEpicsPath,
hasBlockedIssuesFeature,
@ -115,14 +121,13 @@ export function mountIssuesListApp() {
el,
// Currently does not use Vue Apollo, but need to provide {} for now until the
// issue is fixed upstream in https://github.com/vuejs/vue-apollo/pull/1153
apolloProvider: {},
apolloProvider,
provide: {
autocompleteAwardEmojisPath,
autocompleteUsersPath,
calendarPath,
canBulkUpdate: parseBoolean(canBulkUpdate),
emptyStateSvgPath,
endpoint,
groupEpicsPath,
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),

View File

@ -0,0 +1,45 @@
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "./issue_info.fragment.graphql"
query getProjectIssues(
$projectPath: ID!
$search: String
$sort: IssueSort
$state: IssuableState
$assigneeId: String
$authorUsername: String
$assigneeUsernames: [String!]
$milestoneTitle: [String]
$labelName: [String]
$not: NegatedIssueFilterInput
$beforeCursor: String
$afterCursor: String
$firstPageSize: Int
$lastPageSize: Int
) {
project(fullPath: $projectPath) {
issues(
search: $search
sort: $sort
state: $state
assigneeId: $assigneeId
authorUsername: $authorUsername
assigneeUsernames: $assigneeUsernames
milestoneTitle: $milestoneTitle
labelName: $labelName
not: $not
before: $beforeCursor
after: $afterCursor
first: $firstPageSize
last: $lastPageSize
) {
count
pageInfo {
...PageInfo
}
nodes {
...IssueInfo
}
}
}
}

View File

@ -0,0 +1,51 @@
fragment IssueInfo on Issue {
id
iid
closedAt
confidential
createdAt
downvotes
dueDate
humanTimeEstimate
moved
title
updatedAt
upvotes
userDiscussionsCount
webUrl
assignees {
nodes {
id
avatarUrl
name
username
webUrl
}
}
author {
id
avatarUrl
name
username
webUrl
}
labels {
nodes {
id
color
title
description
}
}
milestone {
id
dueDate
startDate
webPath
title
}
taskCompletionStatus {
completedCount
count
}
}

View File

@ -1,4 +1,5 @@
import {
API_PARAM,
BLOCKING_ISSUES_DESC,
CREATED_ASC,
CREATED_DESC,
@ -6,29 +7,36 @@ import {
DUE_DATE_DESC,
DUE_DATE_VALUES,
filters,
LABEL_PRIORITY_ASC,
LABEL_PRIORITY_DESC,
MILESTONE_DUE_ASC,
MILESTONE_DUE_DESC,
NORMAL_FILTER,
POPULARITY_ASC,
POPULARITY_DESC,
PRIORITY_ASC,
PRIORITY_DESC,
RELATIVE_POSITION_DESC,
RELATIVE_POSITION_ASC,
SPECIAL_FILTER,
SPECIAL_FILTER_VALUES,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_ITERATION,
UPDATED_ASC,
UPDATED_DESC,
URL_PARAM,
urlSortParams,
WEIGHT_ASC,
WEIGHT_DESC,
} from '~/issues_list/constants';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import {
FILTERED_SEARCH_TERM,
OPERATOR_IS_NOT,
} from '~/vue_shared/components/filtered_search_bar/constants';
export const getSortKey = (sort) =>
Object.keys(urlSortParams).find((key) => urlSortParams[key].sort === sort);
Object.keys(urlSortParams).find((key) => urlSortParams[key] === sort);
export const getDueDateValue = (value) => (DUE_DATE_VALUES.includes(value) ? value : undefined);
@ -38,7 +46,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
id: 1,
title: __('Priority'),
sortDirection: {
ascending: PRIORITY_DESC,
ascending: PRIORITY_ASC,
descending: PRIORITY_DESC,
},
},
@ -86,7 +94,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
id: 7,
title: __('Label priority'),
sortDirection: {
ascending: LABEL_PRIORITY_DESC,
ascending: LABEL_PRIORITY_ASC,
descending: LABEL_PRIORITY_DESC,
},
},
@ -94,8 +102,8 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
id: 8,
title: __('Manual'),
sortDirection: {
ascending: RELATIVE_POSITION_DESC,
descending: RELATIVE_POSITION_DESC,
ascending: RELATIVE_POSITION_ASC,
descending: RELATIVE_POSITION_ASC,
},
},
];
@ -178,12 +186,36 @@ const getFilterType = (data, tokenType = '') =>
? SPECIAL_FILTER
: NORMAL_FILTER;
export const convertToParams = (filterTokens, paramType) =>
const isIterationSpecialValue = (tokenType, value) =>
tokenType === TOKEN_TYPE_ITERATION && SPECIAL_FILTER_VALUES.includes(value);
export const convertToApiParams = (filterTokens) => {
const params = {};
const not = {};
filterTokens
.filter((token) => token.type !== FILTERED_SEARCH_TERM)
.forEach((token) => {
const filterType = getFilterType(token.value.data, token.type);
const field = filters[token.type][API_PARAM][filterType];
const obj = token.value.operator === OPERATOR_IS_NOT ? not : params;
const data = isIterationSpecialValue(token.type, token.value.data)
? token.value.data.toUpperCase()
: token.value.data;
Object.assign(obj, {
[field]: obj[field] ? [obj[field], data].flat() : data,
});
});
return Object.keys(not).length ? Object.assign(params, { not }) : params;
};
export const convertToUrlParams = (filterTokens) =>
filterTokens
.filter((token) => token.type !== FILTERED_SEARCH_TERM)
.reduce((acc, token) => {
const filterType = getFilterType(token.value.data, token.type);
const param = filters[token.type][paramType][token.value.operator]?.[filterType];
const param = filters[token.type][URL_PARAM][token.value.operator]?.[filterType];
return Object.assign(acc, {
[param]: acc[param] ? [acc[param], token.value.data].flat() : token.value.data,
});

View File

@ -214,7 +214,7 @@ export default {
<div></div>
</template>
</gl-modal>
<span class="gl-text-white">{{ title }}</span>
{{ title }}
<request-warning :html-id="htmlId" :warnings="warnings" />
</div>
</template>

View File

@ -141,7 +141,7 @@ export default {
<div id="peek-view-host" class="view">
<span
v-if="hasHost"
class="current-host gl-text-white"
class="current-host"
:class="{ canary: currentRequest.details.host.canary }"
>
<span v-html="birdEmoji"></span>

View File

@ -229,7 +229,7 @@ export default {
@expand-widget="expandWidget"
/>
</template>
<template #default>
<template #default="{ edit }">
<user-select
ref="userSelect"
v-model="selected"
@ -240,6 +240,7 @@ export default {
:allow-multiple-assignees="allowMultipleAssignees"
:current-user="currentUser"
:issuable-type="issuableType"
:is-editing="edit"
class="gl-w-full dropdown-menu-user"
@toggle="collapseWidget"
@error="showError"

View File

@ -39,7 +39,7 @@ export default {
return this.value.data;
},
activeIteration() {
return this.iterations.find((iteration) => iteration.title === this.currentValue);
return this.iterations.find((iteration) => iteration.id === Number(this.currentValue));
},
},
watch: {
@ -99,8 +99,8 @@ export default {
<template v-else>
<gl-filtered-search-suggestion
v-for="iteration in iterations"
:key="iteration.title"
:value="iteration.title"
:key="iteration.id"
:value="String(iteration.id)"
>
{{ iteration.title }}
</gl-filtered-search-suggestion>

View File

@ -60,6 +60,11 @@ export default {
required: false,
default: 'issue',
},
isEditing: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
@ -75,7 +80,7 @@ export default {
return participantsQueries[this.issuableType].query;
},
skip() {
return Boolean(participantsQueries[this.issuableType].skipQuery);
return Boolean(participantsQueries[this.issuableType].skipQuery) || !this.isEditing;
},
variables() {
return {
@ -102,6 +107,9 @@ export default {
first: 20,
};
},
skip() {
return !this.isEditing;
},
update(data) {
// TODO Remove null filter (BE fix required)
// https://gitlab.com/gitlab-org/gitlab/-/issues/329750

View File

@ -11,47 +11,40 @@
height: $performance-bar-height;
background: $black;
line-height: $performance-bar-height;
color: $gray-300;
color: $gray-100;
select {
color: $white;
width: 200px;
}
input {
color: $gray-300;
width: $input-short-width - 60px;
}
select,
input {
color: inherit;
background-color: inherit;
}
option {
color: initial;
}
&.disabled {
display: none;
}
&.production {
background-color: $perf-bar-production;
select,
input {
background: $perf-bar-production;
}
}
&.staging {
background-color: $perf-bar-staging;
select,
input {
background: $perf-bar-staging;
}
}
&.development {
background-color: $perf-bar-development;
select,
input {
background: $perf-bar-development;
}
}
// UI Elements
@ -61,7 +54,6 @@
padding: 4px 6px;
font-family: Consolas, 'Liberation Mono', Courier, monospace;
line-height: 1;
color: $gray-100;
border-radius: 3px;
box-shadow: 0 1px 0 $perf-bar-bucket-box-shadow-from,
inset 0 1px 2px $perf-bar-bucket-box-shadow-to;

View File

@ -3,6 +3,10 @@
class Projects::AlertManagementController < Projects::ApplicationController
before_action :authorize_read_alert_management_alert!
before_action(only: [:index]) do
push_frontend_feature_flag(:managed_alerts_deprecation, @project)
end
feature_category :incident_management
def index

View File

@ -6,7 +6,7 @@ module Types
graphql_name 'PackageMetadata'
description 'Represents metadata associated with a Package'
possible_types ::Types::Packages::Composer::MetadatumType, ::Types::Packages::Conan::MetadatumType, ::Types::Packages::Maven::MetadatumType, ::Types::Packages::Nuget::MetadatumType
possible_types ::Types::Packages::Composer::MetadatumType, ::Types::Packages::Conan::MetadatumType, ::Types::Packages::Maven::MetadatumType, ::Types::Packages::Nuget::MetadatumType, ::Types::Packages::Pypi::MetadatumType
def self.resolve_type(object, context)
case object
@ -18,6 +18,8 @@ module Types
::Types::Packages::Maven::MetadatumType
when ::Packages::Nuget::Metadatum
::Types::Packages::Nuget::MetadatumType
when ::Packages::Pypi::Metadatum
::Types::Packages::Pypi::MetadatumType
else
# NOTE: This method must be kept in sync with `PackageWithoutVersionsType#metadata`,
# which must never produce data that this discriminator cannot handle.

View File

@ -49,6 +49,8 @@ module Types
object.maven_metadatum
when 'nuget'
object.nuget_metadatum
when 'pypi'
object.pypi_metadatum
else
nil
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module Types
module Packages
module Pypi
class MetadatumType < BaseObject
graphql_name 'PypiMetadata'
description 'Pypi metadata'
authorize :read_package
field :id, ::Types::GlobalIDType[::Packages::Pypi::Metadatum], null: false, description: 'ID of the metadatum.'
field :required_python, GraphQL::STRING_TYPE, null: true, description: 'Required Python version of the Pypi package.'
end
end
end
end

View File

@ -190,7 +190,6 @@ module IssuesHelper
email: current_user&.notification_email,
emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'),
empty_state_svg_path: image_path('illustrations/issues.svg'),
endpoint: expose_path(api_v4_projects_issues_path(id: project.id)),
export_csv_path: export_csv_project_issues_path(project),
has_project_issues: project_issues(project).exists?.to_s,
import_csv_issues_path: import_csv_namespace_project_issues_path,

View File

@ -18,6 +18,8 @@ class LfsObject < ApplicationRecord
mount_file_store_uploader LfsObjectUploader
BATCH_SIZE = 3000
def self.not_linked_to_project(project)
where('NOT EXISTS (?)',
project.lfs_objects_projects.select(1).where('lfs_objects_projects.lfs_object_id = lfs_objects.id'))
@ -37,13 +39,14 @@ class LfsObject < ApplicationRecord
file_store == LfsObjectUploader::Store::LOCAL
end
# rubocop: disable Cop/DestroyAll
def self.destroy_unreferenced
joins("LEFT JOIN lfs_objects_projects ON lfs_objects_projects.lfs_object_id = #{table_name}.id")
.where(lfs_objects_projects: { id: nil })
.destroy_all
def self.unreferenced_in_batches
each_batch(of: BATCH_SIZE, order: :desc) do |lfs_objects|
relation = lfs_objects.where('NOT EXISTS (?)',
LfsObjectsProject.select(1).where('lfs_objects_projects.lfs_object_id = lfs_objects.id'))
yield relation if relation.any?
end
end
# rubocop: enable Cop/DestroyAll
def self.calculate_oid(path)
self.hexdigest(path)

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
module Packages
module Pypi
class MetadatumPolicy < BasePolicy
delegate { @subject.package }
end
end
end

View File

@ -6,6 +6,7 @@ module Commits
super
@commit = params[:commit]
@message = params[:message]
end
private
@ -14,7 +15,9 @@ module Commits
raise NotImplementedError unless repository.respond_to?(action)
# rubocop:disable GitlabSecurity/PublicSend
message = @commit.public_send(:"#{action}_message", current_user)
message =
@message || @commit.public_send(:"#{action}_message", current_user)
repository.public_send(
action,
current_user,

View File

@ -88,7 +88,7 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Manage Web IDE features')
= _('Manage Web IDE features.')
.settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: "#js-web-ide-settings"), html: { class: 'fieldset-form', id: 'web-ide-settings' } do |f|
= form_errors(@application_setting)
@ -100,7 +100,8 @@
= f.label :web_ide_clientside_preview_enabled, class: 'form-check-label' do
= s_('IDE|Live Preview')
%span.form-text.text-muted
= s_('IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview.')
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('user/project/web_ide/index', anchor: 'enable-live-preview') }
= s_('Preview JavaScript projects in the Web IDE with CodeSandbox Live Preview. %{link_start}Learn more.%{link_end} ').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
= render_if_exists 'admin/application_settings/maintenance_mode_settings_form'

View File

@ -467,7 +467,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
:idempotent: true
:tags: []
- :name: cronjob:repository_archive_cache
:worker_name: RepositoryArchiveCacheWorker
@ -1078,15 +1078,6 @@
:weight: 2
:idempotent:
:tags: []
- :name: incident_management:incident_management_process_alert
:worker_name: IncidentManagement::ProcessAlertWorker
:feature_category: :incident_management
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
:idempotent:
:tags: []
- :name: incident_management:incident_management_process_alert_worker_v2
:worker_name: IncidentManagement::ProcessAlertWorkerV2
:feature_category: :incident_management

View File

@ -1,56 +0,0 @@
# frozen_string_literal: true
module IncidentManagement
class ProcessAlertWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
sidekiq_options retry: 3
queue_namespace :incident_management
feature_category :incident_management
# `project_id` and `alert_payload` are deprecated and can be removed
# starting from 14.0 release
# https://gitlab.com/gitlab-org/gitlab/-/issues/224500
#
# This worker is not scheduled anymore since
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60285
# and will be removed completely via
# https://gitlab.com/gitlab-org/gitlab/-/issues/224500
# in 14.0.
def perform(_project_id = nil, _alert_payload = nil, alert_id = nil)
return unless alert_id
alert = find_alert(alert_id)
return unless alert
result = create_issue_for(alert)
return if result.success?
log_warning(alert, result)
end
private
def find_alert(alert_id)
AlertManagement::Alert.find_by_id(alert_id)
end
def create_issue_for(alert)
AlertManagement::CreateAlertIssueService
.new(alert, User.alert_bot)
.execute
end
def log_warning(alert, result)
issue_id = result.payload[:issue]&.id
Gitlab::AppLogger.warn(
message: 'Cannot process an Incident',
issue_id: issue_id,
alert_id: alert.id,
errors: result.message
)
end
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class RemoveUnreferencedLfsObjectsWorker # rubocop:disable Scalability/IdempotentWorker
class RemoveUnreferencedLfsObjectsWorker
include ApplicationWorker
sidekiq_options retry: 3
@ -10,8 +10,16 @@ class RemoveUnreferencedLfsObjectsWorker # rubocop:disable Scalability/Idempoten
# rubocop:enable Scalability/CronWorkerContext
feature_category :git_lfs
deduplicate :until_executed
idempotent!
def perform
LfsObject.destroy_unreferenced
number_of_removed_files = 0
LfsObject.unreferenced_in_batches do |lfs_objects_without_projects|
number_of_removed_files += lfs_objects_without_projects.destroy_all.count # rubocop: disable Cop/DestroyAll
end
number_of_removed_files
end
end

View File

@ -0,0 +1,8 @@
---
name: managed_alerts_deprecation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62528
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331863
milestone: '14.0'
type: development
group: group::monitor
default_enabled: false

View File

@ -208,6 +208,7 @@ formatters
Fugit
fuzzer
Gantt
Gemfile
Gemnasium
Gemojione
Getter

View File

@ -305,6 +305,7 @@ Parameters:
| `sha` | string | yes | The commit hash |
| `branch` | string | yes | The name of the branch |
| `dry_run` | boolean | no | Does not commit any changes. Default is false. [Introduced in GitLab 13.3](https://gitlab.com/gitlab-org/gitlab/-/issues/231032) |
| `message` | string | no | A custom commit message to use for the new commit. [Introduced in GitLab 14.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62481)
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "branch=master" "https://gitlab.example.com/api/v4/projects/5/repository/commits/master/cherry_pick"

View File

@ -11933,6 +11933,17 @@ Represents rules that commit pushes must follow.
| ---- | ---- | ----------- |
| <a id="pushrulesrejectunsignedcommits"></a>`rejectUnsignedCommits` | [`Boolean!`](#boolean) | Indicates whether commits not signed through GPG will be rejected. |
### `PypiMetadata`
Pypi metadata.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="pypimetadataid"></a>`id` | [`PackagesPypiMetadatumID!`](#packagespypimetadatumid) | ID of the metadatum. |
| <a id="pypimetadatarequiredpython"></a>`requiredPython` | [`String`](#string) | Required Python version of the Pypi package. |
### `RecentFailures`
Recent failure history of a test case.
@ -15298,6 +15309,12 @@ A `PackagesPackageID` is a global ID. It is encoded as a string.
An example `PackagesPackageID` is: `"gid://gitlab/Packages::Package/1"`.
### `PackagesPypiMetadatumID`
A `PackagesPypiMetadatumID` is a global ID. It is encoded as a string.
An example `PackagesPypiMetadatumID` is: `"gid://gitlab/Packages::Pypi::Metadatum/1"`.
### `PathLockID`
A `PathLockID` is a global ID. It is encoded as a string.
@ -15429,6 +15446,7 @@ One of:
- [`ConanMetadata`](#conanmetadata)
- [`MavenMetadata`](#mavenmetadata)
- [`NugetMetadata`](#nugetmetadata)
- [`PypiMetadata`](#pypimetadata)
#### `VulnerabilityDetail`

View File

@ -63,11 +63,11 @@ Example of response
"pipeline": {
"id": 6,
"project_id": 1,
"ref": "master",
"ref": "main",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
},
"ref": "master",
"ref": "main",
"runner": null,
"stage": "test",
"status": "failed",
@ -117,11 +117,11 @@ Example of response
"pipeline": {
"id": 6,
"project_id": 1,
"ref": "master",
"ref": "main",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
},
"ref": "master",
"ref": "main",
"artifacts": [],
"runner": null,
"stage": "test",
@ -198,11 +198,11 @@ Example of response
"pipeline": {
"id": 6,
"project_id": 1,
"ref": "master",
"ref": "main",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
},
"ref": "master",
"ref": "main",
"artifacts": [],
"runner": null,
"stage": "test",
@ -263,11 +263,11 @@ Example of response
"pipeline": {
"id": 6,
"project_id": 1,
"ref": "master",
"ref": "main",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
},
"ref": "master",
"ref": "main",
"runner": null,
"stage": "test",
"status": "failed",
@ -348,14 +348,14 @@ Example of response
"pipeline": {
"id": 6,
"project_id": 1,
"ref": "master",
"ref": "main",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending",
"created_at": "2015-12-24T15:50:16.123Z",
"updated_at": "2015-12-24T18:00:44.432Z",
"web_url": "https://example.com/foo/bar/pipelines/6"
},
"ref": "master",
"ref": "main",
"stage": "test",
"status": "pending",
"tag": false,
@ -380,7 +380,7 @@ Example of response
"downstream_pipeline": {
"id": 5,
"sha": "f62a4b2fb89754372a346f24659212eb8da13601",
"ref": "master",
"ref": "main",
"status": "pending",
"created_at": "2015-12-24T17:54:27.722Z",
"updated_at": "2015-12-24T17:58:27.896Z",
@ -433,11 +433,11 @@ Example of response
"pipeline": {
"id": 6,
"project_id": 1,
"ref": "master",
"ref": "main",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
},
"ref": "master",
"ref": "main",
"artifacts": [],
"runner": null,
"stage": "test",
@ -518,7 +518,7 @@ Example response:
"id": 1,
"project_id": 1,
"sha": "b83d6e391c22777fca1ed3012fce84f633d7fed0",
"ref": "master",
"ref": "main",
"status": "pending",
"created_at": "2021-03-26T14:51:51.107Z",
"updated_at": "2021-03-26T14:51:51.107Z",
@ -590,11 +590,11 @@ Example of response
"pipeline": {
"id": 6,
"project_id": 1,
"ref": "master",
"ref": "main",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
},
"ref": "master",
"ref": "main",
"artifacts": [],
"runner": null,
"stage": "test",
@ -684,7 +684,7 @@ Example of response
"queued_duration": 0.010,
"id": 42,
"name": "rubocop",
"ref": "master",
"ref": "main",
"artifacts": [],
"runner": null,
"stage": "test",
@ -734,7 +734,7 @@ Example of response
"queued_duration": 0.010,
"id": 42,
"name": "rubocop",
"ref": "master",
"ref": "main",
"artifacts": [],
"runner": null,
"stage": "test",
@ -784,7 +784,7 @@ Example of response
"download_url": null,
"id": 42,
"name": "rubocop",
"ref": "master",
"ref": "main",
"artifacts": [],
"runner": null,
"stage": "test",
@ -839,7 +839,7 @@ Example of response
"queued_duration": 0.010,
"id": 42,
"name": "rubocop",
"ref": "master",
"ref": "main",
"artifacts": [],
"runner": null,
"stage": "test",

View File

@ -30,7 +30,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
{
"id": 13,
"description": "Test schedule pipeline",
"ref": "master",
"ref": "main",
"cron": "* * * * *",
"cron_timezone": "Asia/Tokyo",
"next_run_at": "2017-05-19T13:41:00.000Z",
@ -70,7 +70,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
{
"id": 13,
"description": "Test schedule pipeline",
"ref": "master",
"ref": "main",
"cron": "* * * * *",
"cron_timezone": "Asia/Tokyo",
"next_run_at": "2017-05-19T13:41:00.000Z",
@ -80,7 +80,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
"last_pipeline": {
"id": 332,
"sha": "0e788619d0b5ec17388dffb973ecd505946156db",
"ref": "master",
"ref": "main",
"status": "pending"
},
"owner": {
@ -119,14 +119,14 @@ POST /projects/:id/pipeline_schedules
| `active` | boolean | no | The activation of pipeline schedule. If false is set, the pipeline schedule is initially deactivated (default: `true`). |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form description="Build packages" --form ref="master" --form cron="0 1 * * 5" --form cron_timezone="UTC" --form active="true" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules"
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form description="Build packages" --form ref="main" --form cron="0 1 * * 5" --form cron_timezone="UTC" --form active="true" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules"
```
```json
{
"id": 14,
"description": "Build packages",
"ref": "master",
"ref": "main",
"cron": "0 1 * * 5",
"cron_timezone": "UTC",
"next_run_at": "2017-05-26T01:00:00.000Z",
@ -171,7 +171,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --form cron="0
{
"id": 13,
"description": "Test schedule pipeline",
"ref": "master",
"ref": "main",
"cron": "0 2 * * *",
"cron_timezone": "Asia/Tokyo",
"next_run_at": "2017-05-19T17:00:00.000Z",
@ -181,7 +181,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --form cron="0
"last_pipeline": {
"id": 332,
"sha": "0e788619d0b5ec17388dffb973ecd505946156db",
"ref": "master",
"ref": "main",
"status": "pending"
},
"owner": {
@ -216,7 +216,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitla
{
"id": 13,
"description": "Test schedule pipeline",
"ref": "master",
"ref": "main",
"cron": "0 2 * * *",
"cron_timezone": "Asia/Tokyo",
"next_run_at": "2017-05-19T17:00:00.000Z",
@ -226,7 +226,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitla
"last_pipeline": {
"id": 332,
"sha": "0e788619d0b5ec17388dffb973ecd505946156db",
"ref": "master",
"ref": "main",
"status": "pending"
},
"owner": {
@ -261,7 +261,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
{
"id": 13,
"description": "Test schedule pipeline",
"ref": "master",
"ref": "main",
"cron": "0 2 * * *",
"cron_timezone": "Asia/Tokyo",
"next_run_at": "2017-05-19T17:00:00.000Z",
@ -271,7 +271,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
"last_pipeline": {
"id": 332,
"sha": "0e788619d0b5ec17388dffb973ecd505946156db",
"ref": "master",
"ref": "main",
"status": "pending"
},
"owner": {

View File

@ -99,7 +99,7 @@ Example of response
"id": 46,
"project_id": 1,
"status": "success",
"ref": "master",
"ref": "main",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"tag": false,
@ -226,7 +226,7 @@ POST /projects/:id/pipeline
| `variables` | array | no | An array containing the variables available in the pipeline, matching the structure `[{ 'key': 'UPLOAD_TO_S3', 'variable_type': 'file', 'value': 'true' }]` |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/pipeline?ref=master"
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/pipeline?ref=main"
```
Example of response
@ -236,7 +236,7 @@ Example of response
"id": 61,
"project_id": 1,
"sha": "384c444e840a515b23f21915ee5766b87068a70d",
"ref": "master",
"ref": "main",
"status": "pending",
"before_sha": "0000000000000000000000000000000000000000",
"tag": false,
@ -285,7 +285,7 @@ Response:
"id": 46,
"project_id": 1,
"status": "pending",
"ref": "master",
"ref": "main",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"tag": false,
@ -334,7 +334,7 @@ Response:
"id": 46,
"project_id": 1,
"status": "canceled",
"ref": "master",
"ref": "main",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"tag": false,

View File

@ -30,7 +30,7 @@ and allow us to have much better resiliency and much easier way to scale applica
The actual split was tested with the usage of [Rails Engines](https://guides.rubyonrails.org/engines.html)
implementing separate gems in a single repository. The [Rails Engines](https://guides.rubyonrails.org/engines.html)
allowed us to well describe the indivdual components with its dependencies and run an application
allowed us to well describe the individual components with its dependencies and run an application
consisting of many Rails Engines.
The blueprint aims to retain all key aspects of GitLab success: single and monolithic codebase (with a [single data-store](https://about.gitlab.com/handbook/product/single-application/#single-data-store)),
@ -52,7 +52,7 @@ codebase without clear boundaries results in a number of problems and inefficien
- The high memory usage results in slowing the whole application as it increases GC cycles duration
creating significantly longer latency for processing requests or worse cache usage of CPUs
- Increased application boot-up times as we need to load and parse significantly more files
- Longer boot-up times slows down the development, as running application or tests takes singificantly longer
- Longer boot-up times slows down the development, as running application or tests takes significantly longer
reducing velocity and amount of iterations
## Composable codebase dimensions
@ -123,8 +123,8 @@ application layers. This list is not exhaustive, but shows a general list of the
- Services/Models/DB: all code required to maintain our database structure, data validation, business logic and policies models that needs to be shared with other components
The best way to likely describe how the actual GitLab Rails split would look like. It is a satellite model.
Where we have a single core, that is shared across all satelitte components. The design of that implies
that satelitte components have a limited way to communicate with each other. In a single monolithic application
Where we have a single core, that is shared across all satellite components. The design of that implies
that satellite components have a limited way to communicate with each other. In a single monolithic application
in most of cases application would communicate with a code. In a satellite model the communication needs
to be performed externally to the component. This can be via Database, Redis or using a well defined exposed API.
@ -293,7 +293,7 @@ This allowed us to run Web Nodes with all dependencies, but measure the impact o
All work can be found in these merge requests:
- [Provide mechanism to load GraphQL with all dependencies only when needed](https://gitlab.com/gitlab-org/gitlab/-/issues/288044)
- [Draft: PoC - Move Graphql to the WebEngine](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50180)
- [Draft: PoC - Move GraphQL to the WebEngine](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50180)
- [Draft: PoC - Move Controllers and Grape API:API to the WebEngine](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53720)
- [Draft: PoC - Move only Grape API:API to the WebEngine](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53982)
- [Measure performance impact for proposed web_engine](https://gitlab.com/gitlab-org/gitlab/-/issues/300548)
@ -310,20 +310,20 @@ What was done?
#### Implementation details for proposed solution
1. Introduce new Rails Engine for each application layer.
1. Introduce new Rails Engine for each application layer.
We created `engines` folder, which could contain different engines for each application layer we introduce in the future.
In the above PoCs we introuced the new Web Application Layer, located in `engines/web_engine` folder.
In the above PoCs we introduced the new Web Application Layer, located in `engines/web_engine` folder.
1. Move all code and specs into `engines/web_engine/`
- We moved all Graphql code and specs into `engines/web_engine/` without changing files itself
- We moved all GraphQL code and specs into `engines/web_engine/` without changing files itself
- We moved all Grape API and Controllers code into `engines/web_engine/` without changing files itself
1. Move gems to the `engines/web_engine/`
- We moved all Graphql gems to the actual web_engine Gemfile
- We moved all GraphQL gems to the actual web_engine Gemfile
- We moved Grape API gem to the actual web_engine Gemfile
```ruby
@ -331,7 +331,7 @@ What was done?
spec.add_dependency 'apollo_upload_server'
spec.add_dependency 'graphql'
spec.add_dependency 'graphiql-rails'
spec.add_dependency 'graphql-docs'
spec.add_dependency 'grape'
end
@ -339,31 +339,31 @@ What was done?
1. Move routes to the `engines/web_engine/config/routes.rb` file
- We moved Graphql routes to the web_engine routes.
- We moved GraphQL routes to the web_engine routes.
- We moved API routes to the web_engine routes.
- We moved most of the controller routes to the web_engine routes.
```ruby
Rails.application.routes.draw do
post '/api/graphql', to: 'graphql#execute'
mount GraphiQL::Rails::Engine, at: '/-/graphql-explorer', graphql_path:
mount GraphiQL::Rails::Engine, at: '/-/graphql-explorer', graphql_path:
Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, '/api/graphql')
draw :api
#...
end
```
1. Move initializers to the `engines/web_engine/config/initializers` folder
- We moved graphql.rb initializer to the `web_engine` initializers folder
- We moved grape_patch.rb and graphe_validators to the `web_engine` initializers folder
- We moved `graphql.rb` initializer to the `web_engine` initializers folder
- We moved `grape_patch.rb` and `graphe_validators` to the `web_engine` initializers folder
1. Connect GitLab application with the WebEngine
In GitLab Gemfile.rb, add web_engine to the engines group
```ruby
# Gemfile
group :engines, :test do
@ -376,7 +376,7 @@ What was done?
1. Configure GitLab when to load the engine.
In GitLab `config/engines.rb`, we can configure when do we want to load our engines by relying on our `Gitlab::Runtime`
```ruby
# config/engines.rb
# Load only in case we are running web_server or rails console
@ -392,11 +392,11 @@ What was done?
the application, performing tasks such as adding the app directory of the engine to
the load path for models, mailers, controllers, and views.
A file at `lib/web_engine/engine.rb`, is identical in function to a standard Rails
application's `config/application.rb` file. This way engines can access a config
application's `config/application.rb` file. This way engines can access a configuration
object which contains configuration shared by all railties and the application.
Additionally, each engine can access autoload_paths, eager_load_paths, and autoload_once_paths
Additionally, each engine can access `autoload_paths`, `eager_load_paths`, and `autoload_once_paths`
settings which are scoped to that engine.
```ruby
module WebEngine
class Engine < ::Rails::Engine
@ -404,7 +404,7 @@ What was done?
#{config.root}/app/graphql/resolvers/concerns
#{config.root}/app/graphql/mutations/concerns
#{config.root}/app/graphql/types/concerns])
if Gitlab.ee?
ee_paths = config.eager_load_paths.each_with_object([]) do |path, memo|
ee_path = config.root
@ -422,11 +422,11 @@ What was done?
We adapted CI to test `engines/web_engine/` as a self-sufficient component of stack.
- We moved spec as-is files to the `engines/web_engine/spec` folder
- We moved ee/spec as-is files to the `engines/web_engine/ee/spec` folder
- We moved `spec` as-is files to the `engines/web_engine/spec` folder
- We moved `ee/spec` as-is files to the `engines/web_engine/ee/spec` folder
- We control specs from main application using environment variable `TEST_WEB_ENGINE`
- We added new CI job that will run `engines/web_engine/spec` tests separately using `TEST_WEB_ENGINE` env variable.
- We added new CI job that will run `engines/web_engine/ee/spec` tests separately using `TEST_WEB_ENGINE` env variable.
- We added new CI job that will run `engines/web_engine/spec` tests separately using `TEST_WEB_ENGINE` environment variable.
- We added new CI job that will run `engines/web_engine/ee/spec` tests separately using `TEST_WEB_ENGINE` environment variable.
- We are running all whitebox frontend tests with `TEST_WEB_ENGINE=true`
#### Results
@ -482,12 +482,12 @@ Pros:
- Significantly lower memory usage
- Significantly shorter application load time for Sidekiq
- Significantly improved responsivness of Sidekiq service due to much shorter GC cycles
- Significantly improved responsiveness of Sidekiq service due to much shorter GC cycles
- Significantly easier testing of a portion of application, ex. changing `web_engines/` does require
re-running test only for this application layer
- We retained a monolithic architecture of the codebase, but sharing database and application models
- A significant saving from the infrastracture side
- Ability to comfortably run on constrainted environments by reducing application footprint
- A significant saving from the infrastructure side
- Ability to comfortably run on constrained environments by reducing application footprint
Cons:
@ -497,7 +497,7 @@ Cons:
#### Example: GraphQL
[Draft: PoC - Move Graphql to the WebEngine](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50180)
[Draft: PoC - Move GraphQL to the WebEngine](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50180)
- The [99% of changes](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50180/diffs?commit_id=49c9881c6696eb620dccac71532a3173f5702ea8) as visible in the above MRs is moving files as-is.
- The [actual work](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50180/diffs?commit_id=1d9a9edfa29ea6638e7d8a6712ddf09f5be77a44) on fixing cross-dependencies, specs, and configuring web_engine
@ -506,8 +506,8 @@ Cons:
Today, loading GraphQL requires a bunch of [dependencies](https://gitlab.com/gitlab-org/gitlab/-/issues/288044):
> We also discovered that we load/require 14480 files, [memory-team-2gb-week#9](https://gitlab.com/gitlab-org/memory-team/memory-team-2gb-week/-/issues/9#note_452530513)
> when we start GitLab. 1274 files belong to Graphql. This means that if we don't load 1274 application files
> and all related Graphql gems when we don't need them (Sidekiq), we could save a lot of memory.
> when we start GitLab. 1274 files belong to GraphQL. This means that if we don't load 1274 application files
> and all related GraphQL gems when we don't need them (Sidekiq), we could save a lot of memory.
GraphQL only needs to run in a specific context. If we could limit when it is being loaded we could effectively improve application efficiency, by reducing application load time and required memory. This, for example, is applicable for every size installation.
@ -524,9 +524,9 @@ An alternative way is to use a notification system that would always make an `Ac
Grape::API is another example that only needs to run only in a web server context.
Potential chalenges with Grape API:
Potential challenges with Grape API:
- Currently there are some API::API dependencies in the models (e.g. `API::Helpers::Version` dependency in [project model](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/project.rb#L2019) or API::API dependency in GeoNode model for [geo_retrieve_url](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/models/geo_node.rb#L183))
- Currently there are some API::API dependencies in the models (e.g. `API::Helpers::Version` dependency in [project model](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/project.rb#L2019) or API::API dependency in GeoNode model for [`geo_retrieve_url`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/models/geo_node.rb#L183))
- `api_v4` paths are used in helpers, presenters, and views (e.g. `api_v4_projects_path` in [PackagesHelper](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/helpers/packages_helper.rb#L17))
#### Example: Controllers
@ -538,7 +538,7 @@ Potential chalenges with Grape API:
Controllers, Serializers, some presenters and some of the Grape:Entities are also good examples that only need to be run in web server context.
Potential chalenges with moving Controllers:
Potential challenges with moving Controllers:
- We needed to extend `Gitlab::Patch::DrawRoute` in order to support `engines/web_engine/config/routes` and `engines/web_engine/ee/config/routes` in case when `web_engine` is loaded. Here is potential [solution](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53720#note_506957398).
- `Gitlab::Routing.url_helpers` paths are used in models and services, that could be used by Sidekiq (e.g. `Gitlab::Routing.url_helpers.project_pipelines_path` is used by [ExpirePipelineCacheService](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/ci/expire_pipeline_cache_service.rb#L20) in [ExpirePipelineCacheWorker](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/workers/expire_pipeline_cache_worker.rb#L18)))
@ -552,7 +552,7 @@ Packwerk is currently accepting bug fixes only, and it is not being actively dev
**Application Layers** and this proposal currently defines only `web_engine`. Following the same pattern we could easily introduce
additional engines dedicated for supporting that would allow us to maintain much better separation, lower memory usage
and much better maintanability of GitLab Rails into the future.
and much better maintainability of GitLab Rails into the future.
This would be a framework for introducing all new interfaces for features that do not need to be part of the core codebase,
like support for additional Package services. Allowing us to better scale application in the future, but retaining a single codebase
@ -564,7 +564,7 @@ As of today, it seems reasonable to define three **application layers**:
- `gitlab-web`: a Controllers/API/GraphQL/ActionCable functionality needed to run in a web server context (depends on `gitlab-core`)
- `gitlab-sidekiq`: a background jobs functionality needed to run Sidekiq Workers (depends on `gitlab-core`)
This model is best described today as a shared core with satellite. The shared core defines data access layer, where as satellites define a way to present and process this data. Satelites can only talk with Core. They cannot directly load or talk to another satelitte unless they use a well defined interface in form of API, GraphQL or Redis (as for scheduling Sidekiq jobs).
This model is best described today as a shared core with satellite. The shared core defines data access layer, where as satellites define a way to present and process this data. Satellites can only talk with Core. They cannot directly load or talk to another satellite unless they use a well defined interface in form of API, GraphQL or Redis (as for scheduling Sidekiq jobs).
It is reasonable to assume that we limit how many `engines` we allow. Initial proposal is to allow up to 5 engines
to be created to ensure that we do not have explosion of engines.
@ -573,7 +573,7 @@ to be created to ensure that we do not have explosion of engines.
- [Split application into functional parts to ensure that only needed code is loaded with all dependencies](https://gitlab.com/gitlab-org/gitlab/-/issues/290935)
- [Provide mechanism to load GraphQL with all dependencies only when needed](https://gitlab.com/gitlab-org/gitlab/-/issues/288044)
- [Draft: PoC - Move Graphql to the WebEngine](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50180)
- [Draft: PoC - Move GraphQL to the WebEngine](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50180)
- [Draft: PoC - Move Controllers and Grape API:API to the WebEngine](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53720)
- [Draft: PoC - Move only Grape API:API to the WebEngine](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53982)
- [Measure performance impact for proposed web_engine](https://gitlab.com/gitlab-org/gitlab/-/issues/300548)

View File

@ -78,7 +78,7 @@ git init
git remote add origin git@gitlab.example.com:<USERNAME>/laravel-sample.git
git add .
git commit -m 'Initial Commit'
git push -u origin master
git push -u origin main
```
## Configure the production server
@ -260,7 +260,7 @@ Let's create these three tasks one by one.
#### Clone the repository
The first task will create the `releases` directory (if it doesn't exist), and then clone the `master` branch of the repository (by default) into the new release directory, given by the `$new_release_dir` variable.
The first task will create the `releases` directory (if it doesn't exist), and then clone the `main` branch of the repository (by default) into the new release directory, given by the `$new_release_dir` variable.
The `releases` directory will hold all our deployments:
```php
@ -378,14 +378,14 @@ These are persistent data and will be shared to every new release.
Now, we would need to deploy our app by running `envoy run deploy`, but it won't be necessary since GitLab can handle that for us with CI's [environments](../../environments/index.md), which will be described [later](#setting-up-gitlab-cicd) in this tutorial.
Now it's time to commit [Envoy.blade.php](https://gitlab.com/mehranrasulian/laravel-sample/blob/master/Envoy.blade.php) and push it to the `master` branch.
To keep things simple, we commit directly to `master`, without using [feature-branches](../../../topics/gitlab_flow.md#github-flow-as-a-simpler-alternative) since collaboration is beyond the scope of this tutorial.
Now it's time to commit [Envoy.blade.php](https://gitlab.com/mehranrasulian/laravel-sample/blob/master/Envoy.blade.php) and push it to the `main` branch.
To keep things simple, we commit directly to `main`, without using [feature-branches](../../../topics/gitlab_flow.md#github-flow-as-a-simpler-alternative) since collaboration is beyond the scope of this tutorial.
In a real world project, teams may use [Issue Tracker](../../../user/project/issues/index.md) and [Merge Requests](../../../user/project/merge_requests/index.md) to move their code across branches:
```shell
git add Envoy.blade.php
git commit -m 'Add Envoy'
git push origin master
git push origin main
```
## Continuous Integration with GitLab
@ -474,7 +474,7 @@ Let's commit the `Dockerfile` file.
```shell
git add Dockerfile
git commit -m 'Add Dockerfile'
git push origin master
git push origin main
```
### Setting up GitLab CI/CD
@ -523,7 +523,7 @@ deploy_production:
url: http://192.168.1.1
when: manual
only:
- master
- main
```
That's a lot to take in, isn't it? Let's run through it step by step.
@ -595,7 +595,7 @@ If the SSH keys have added successfully, we can run Envoy.
As mentioned before, GitLab supports [Continuous Delivery](https://about.gitlab.com/blog/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-delivery) methods as well.
The [environment](../../yaml/README.md#environment) keyword tells GitLab that this job deploys to the `production` environment.
The `url` keyword is used to generate a link to our application on the GitLab Environments page.
The `only` keyword tells GitLab CI/CD that the job should be executed only when the pipeline is building the `master` branch.
The `only` keyword tells GitLab CI/CD that the job should be executed only when the pipeline is building the `main` branch.
Lastly, `when: manual` is used to turn the job from running automatically to a manual action.
```yaml
@ -616,7 +616,7 @@ deploy_production:
url: http://192.168.1.1
when: manual
only:
- master
- main
```
You may also want to add another job for [staging environment](https://about.gitlab.com/blog/2021/02/05/ci-deployment-and-environments/), to final test your application before deploying to production.
@ -624,7 +624,7 @@ You may also want to add another job for [staging environment](https://about.git
### Turn on GitLab CI/CD
We have prepared everything we need to test and deploy our app with GitLab CI/CD.
To do that, commit and push `.gitlab-ci.yml` to the `master` branch. It will trigger a pipeline, which you can watch live under your project's **Pipelines**.
To do that, commit and push `.gitlab-ci.yml` to the `main` branch. It will trigger a pipeline, which you can watch live under your project's **Pipelines**.
![pipelines page](img/pipelines_page.png)

View File

@ -82,7 +82,7 @@ job1:
- echo This rule uses parentheses.
only:
variables:
- ($CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "develop") && $MY_VARIABLE
- ($CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop") && $MY_VARIABLE
```
### `only:changes` / `except:changes` examples

View File

@ -70,7 +70,7 @@ build:
stage: build
script: ./build
only:
- master
- main
test:
stage: test
@ -82,7 +82,7 @@ deploy:
stage: deploy
script: ./deploy
only:
- master
- main
```
#### Excluding certain jobs
@ -103,7 +103,7 @@ To achieve this, you can configure your `.gitlab-ci.yml` file as follows:
``` yaml
.only-default: &only-default
only:
- master
- main
- merge_requests
- tags

View File

@ -209,7 +209,7 @@ jobs:
deploy:
branches:
only:
- master
- main
- /rc-.*/
```
@ -221,7 +221,7 @@ deploy_prod:
script:
- echo "Deploy to production server"
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
- if: '$CI_COMMIT_BRANCH == "main"'
```
### Caching

View File

@ -318,7 +318,7 @@ Markdown code embeds the test coverage report badge of the `coverage` job
into your `README.md`:
```markdown
![coverage](https://gitlab.com/gitlab-org/gitlab/badges/master/coverage.svg?job=coverage)
![coverage](https://gitlab.com/gitlab-org/gitlab/badges/main/coverage.svg?job=coverage)
```
### Badge styles
@ -331,7 +331,7 @@ Pipeline badges can be rendered in different styles by adding the `style=style_n
https://gitlab.example.com/<namespace>/<project>/badges/<branch>/coverage.svg?style=flat
```
![Badge flat style](https://gitlab.com/gitlab-org/gitlab/badges/master/coverage.svg?job=coverage&style=flat)
![Badge flat style](https://gitlab.com/gitlab-org/gitlab/badges/main/coverage.svg?job=coverage&style=flat)
- Flat square ([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/30120) in GitLab 11.8):
@ -339,7 +339,7 @@ Pipeline badges can be rendered in different styles by adding the `style=style_n
https://gitlab.example.com/<namespace>/<project>/badges/<branch>/coverage.svg?style=flat-square
```
![Badge flat square style](https://gitlab.com/gitlab-org/gitlab/badges/master/coverage.svg?job=coverage&style=flat-square)
![Badge flat square style](https://gitlab.com/gitlab-org/gitlab/badges/main/coverage.svg?job=coverage&style=flat-square)
### Custom badge text
@ -348,10 +348,10 @@ Pipeline badges can be rendered in different styles by adding the `style=style_n
The text for a badge can be customized to differentiate between multiple coverage jobs that run in the same pipeline. Customize the badge text and width by adding the `key_text=custom_text` and `key_width=custom_key_width` parameters to the URL:
```plaintext
https://gitlab.com/gitlab-org/gitlab/badges/master/coverage.svg?job=karma&key_text=Frontend+Coverage&key_width=130
https://gitlab.com/gitlab-org/gitlab/badges/main/coverage.svg?job=karma&key_text=Frontend+Coverage&key_width=130
```
![Badge with custom text and width](https://gitlab.com/gitlab-org/gitlab/badges/master/coverage.svg?job=karma&key_text=Frontend+Coverage&key_width=130)
![Badge with custom text and width](https://gitlab.com/gitlab-org/gitlab/badges/main/coverage.svg?job=karma&key_text=Frontend+Coverage&key_width=130)
<!-- ## Troubleshooting

View File

@ -56,7 +56,7 @@ and it creates a dependent pipeline relation visible on the
trigger_pipeline:
stage: deploy
script:
- curl --request POST --form "token=$CI_JOB_TOKEN" --form ref=master "https://gitlab.example.com/api/v4/projects/9/trigger/pipeline"
- curl --request POST --form "token=$CI_JOB_TOKEN" --form ref=main "https://gitlab.example.com/api/v4/projects/9/trigger/pipeline"
only:
- tags
```
@ -81,7 +81,7 @@ build_submodule:
stage: test
script:
- apt update && apt install -y unzip
- curl --location --output artifacts.zip "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/download?job=test&job_token=$CI_JOB_TOKEN"
- curl --location --output artifacts.zip "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/main/download?job=test&job_token=$CI_JOB_TOKEN"
- unzip artifacts.zip
only:
- tags
@ -178,7 +178,7 @@ To trigger a job from a webhook of another project you need to add the following
webhook URL for Push and Tag events (change the project ID, ref and token):
```plaintext
https://gitlab.example.com/api/v4/projects/9/ref/master/trigger/pipeline?token=TOKEN
https://gitlab.example.com/api/v4/projects/9/ref/main/trigger/pipeline?token=TOKEN
```
You should pass `ref` as part of the URL, to take precedence over `ref` from
@ -250,7 +250,7 @@ and the script of the `upload_package` job is run:
```shell
curl --request POST \
--form token=TOKEN \
--form ref=master \
--form ref=main \
--form "variables[UPLOAD_TO_S3]=true" \
"https://gitlab.example.com/api/v4/projects/9/trigger/pipeline"
```

View File

@ -434,7 +434,7 @@ Example job log output:
export CI_JOB_ID="50"
export CI_COMMIT_SHA="1ecfd275763eff1d6b4844ea3168962458c9f27a"
export CI_COMMIT_SHORT_SHA="1ecfd275"
export CI_COMMIT_REF_NAME="master"
export CI_COMMIT_REF_NAME="main"
export CI_REPOSITORY_URL="https://gitlab-ci-token:[masked]@example.com/gitlab-org/gitlab-foss.git"
export CI_COMMIT_TAG="1.0.0"
export CI_JOB_NAME="spec:other"
@ -815,7 +815,7 @@ as well as from in a variable:
```yaml
variables:
MYSTRING: 'master'
MYSTRING: 'main'
MYREGEX: '/^mast.*/'
testdirect:
@ -943,8 +943,8 @@ if [[ -d "/builds/gitlab-examples/ci-debug-trace/.git" ]]; then
++ CI_PROJECT_VISIBILITY=public
++ export CI_PROJECT_REPOSITORY_LANGUAGES=
++ CI_PROJECT_REPOSITORY_LANGUAGES=
++ export CI_DEFAULT_BRANCH=master
++ CI_DEFAULT_BRANCH=master
++ export CI_DEFAULT_BRANCH=main
++ CI_DEFAULT_BRANCH=main
++ export CI_REGISTRY=registry.gitlab.com
++ CI_REGISTRY=registry.gitlab.com
++ export CI_API_V4_URL=https://gitlab.com/api/v4
@ -961,10 +961,10 @@ if [[ -d "/builds/gitlab-examples/ci-debug-trace/.git" ]]; then
++ CI_COMMIT_SHORT_SHA=dd648b2e
++ export CI_COMMIT_BEFORE_SHA=0000000000000000000000000000000000000000
++ CI_COMMIT_BEFORE_SHA=0000000000000000000000000000000000000000
++ export CI_COMMIT_REF_NAME=master
++ CI_COMMIT_REF_NAME=master
++ export CI_COMMIT_REF_SLUG=master
++ CI_COMMIT_REF_SLUG=master
++ export CI_COMMIT_REF_NAME=main
++ CI_COMMIT_REF_NAME=main
++ export CI_COMMIT_REF_SLUG=main
++ CI_COMMIT_REF_SLUG=main
...
```

View File

@ -1029,7 +1029,7 @@ levels. For example:
URL: "http://my-url.internal"
IMPORTANT_VAR: "the details"
only:
- master
- main
- stable
tags:
- production
@ -1062,7 +1062,7 @@ rspec:
IMPORTANT_VAR: "the details"
GITLAB: "is-awesome"
only:
- master
- main
- stable
tags:
- docker
@ -2390,7 +2390,7 @@ For example:
```yaml
deploy to production:
stage: deploy
script: git push production HEAD:master
script: git push production HEAD:main
environment: production
```
@ -2412,7 +2412,7 @@ Set a name for an [environment](../environments/index.md). For example:
```yaml
deploy to production:
stage: deploy
script: git push production HEAD:master
script: git push production HEAD:main
environment:
name: production
```
@ -2447,7 +2447,7 @@ Set a URL for an [environment](../environments/index.md). For example:
```yaml
deploy to production:
stage: deploy
script: git push production HEAD:master
script: git push production HEAD:main
environment:
name: production
url: https://prod.example.com

View File

@ -26,7 +26,7 @@ Array with `include` method implied:
```yaml
include:
- 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
- 'https://gitlab.com/awesome-project/raw/main/.before-script-template.yml'
- '/templates/.after-script-template.yml'
```
@ -34,21 +34,21 @@ Single string with `include` method specified explicitly:
```yaml
include:
remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
remote: 'https://gitlab.com/awesome-project/raw/main/.before-script-template.yml'
```
Array with `include:remote` being the single item:
```yaml
include:
- remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
- remote: 'https://gitlab.com/awesome-project/raw/main/.before-script-template.yml'
```
Array with multiple `include` methods specified explicitly:
```yaml
include:
- remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
- remote: 'https://gitlab.com/awesome-project/raw/main/.before-script-template.yml'
- local: '/templates/.after-script-template.yml'
- template: Auto-DevOps.gitlab-ci.yml
```
@ -57,11 +57,11 @@ Array mixed syntax:
```yaml
include:
- 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
- 'https://gitlab.com/awesome-project/raw/main/.before-script-template.yml'
- '/templates/.after-script-template.yml'
- template: Auto-DevOps.gitlab-ci.yml
- project: 'my-group/my-project'
ref: master
ref: main
file: '/templates/.gitlab-ci-template.yml'
```
@ -70,7 +70,7 @@ include:
In the following example, the content of `.before-script-template.yml` is
automatically fetched and evaluated along with the content of `.gitlab-ci.yml`.
Content of `https://gitlab.com/awesome-project/raw/master/.before-script-template.yml`:
Content of `https://gitlab.com/awesome-project/raw/main/.before-script-template.yml`:
```yaml
default:
@ -83,7 +83,7 @@ default:
Content of `.gitlab-ci.yml`:
```yaml
include: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
include: 'https://gitlab.com/awesome-project/raw/main/.before-script-template.yml'
rspec:
script:
@ -112,7 +112,7 @@ production:
name: production
url: https://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
only:
- master
- main
```
Content of `.gitlab-ci.yml`:

View File

@ -16778,15 +16778,15 @@ Tiers: `free`
### `usage_activity_by_stage.manage.value_stream_management_customized_group_stages`
Missing description
Number of custom value stream analytics stages.
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_all/20210216180803_value_stream_management_customized_group_stages.yml)
Group: `group::manage`
Group: `group::optimize`
Status: `data_available`
Tiers:
Tiers: `premium`, `ultimate`
### `usage_activity_by_stage.monitor.clusters`

View File

@ -15,7 +15,7 @@ documentation for all current settings and limits on the GitLab.com instance.
## General
Access the default page for admin area settings by navigating to **Admin Area > Settings > General**:
Access the default page for Admin Area settings by navigating to **Admin Area > Settings > General**:
| Option | Description |
| ------ | ----------- |
@ -27,7 +27,7 @@ Access the default page for admin area settings by navigating to **Admin Area >
| [Terms of Service and Privacy Policy](terms.md) | Include a Terms of Service agreement and Privacy Policy that all users must accept. |
| [External Authentication](external_authorization.md#configuration) | External Classification Policy Authorization |
| [Web terminal](../../../administration/integration/terminal.md#limiting-websocket-connection-time) | Set max session time for web terminal. |
| [Web IDE](../../project/web_ide/index.md#enabling-live-preview) | Manage Web IDE Features. |
| [Web IDE](../../project/web_ide/index.md#enable-live-preview) | Manage Web IDE features. |
| [FLoC](floc.md) | Enable or disable [Federated Learning of Cohorts (FLoC)](https://en.wikipedia.org/wiki/Federated_Learning_of_Cohorts) tracking. |
## Integrations

View File

@ -100,6 +100,11 @@ To disable it:
Feature.disable(:pick_into_project)
```
## Related links
- The [Commits API](../../../api/commits.md) enables you to add custom messages
to changes you cherry-pick through the API.
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues

View File

@ -259,19 +259,27 @@ Additionally, for public projects an **Open in CodeSandbox** button is available
to transfer the contents of the project into a public CodeSandbox project to
quickly share your project with others.
### Enabling Live Preview
### Enable Live Preview
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/268288) in GitLab 12.9, third-party assets and libraries required for Live Preview are hosted at `https://sandbox-prod.gitlab-static.net` when it is enabled. However, some libraries are still served from other third-party services which may or may not be desirable in your environment.
With Live Preview enabled, you can preview projects with a `package.json` file and
a `main` entry point inside the Web IDE.
The Live Preview feature needs to be enabled in the GitLab instance's
Admin Area. Live Preview is enabled for all projects on
GitLab.com
Live Preview is enabled for all projects on GitLab.com. If you are an administrator
of a self-managed GitLab instance, and you want to enable Live Preview:
![Administrator Live Preview setting](img/admin_live_preview_v13_0.png)
1. In the top navigation bar, go to **Admin Area**.
1. In the left sidebar, select **Settings > General**.
1. Scroll to **Web IDE** and select **Expand**:
![Administrator Live Preview setting](img/admin_live_preview_v13_0.png)
1. Select **Enable Live Preview** and select **Save changes**.
After you have done that, you can preview projects with a `package.json` file and
a `main` entry point inside the Web IDE. An example `package.json` is shown
below.
[In GitLab 12.9 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/268288),
third-party assets and libraries required for Live Preview are hosted at
`https://sandbox-prod.gitlab-static.net` when it is enabled. However, some libraries
are still served from other third-party services, which may or may not be desirable
in your environment.
An example `package.json`:
```json
{

View File

@ -203,6 +203,7 @@ module API
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag to be cherry picked'
requires :branch, type: String, desc: 'The name of the branch', allow_blank: false
optional :dry_run, type: Boolean, default: false, desc: "Does not commit any changes"
optional :message, type: String, desc: 'A custom commit message to use for the picked commit'
end
post ':id/repository/commits/:sha/cherry_pick', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
authorize_push_to_branch!(params[:branch])
@ -216,7 +217,8 @@ module API
commit: commit,
start_branch: params[:branch],
branch_name: params[:branch],
dry_run: params[:dry_run]
dry_run: params[:dry_run],
message: params[:message]
}
result = ::Commits::CherryPickService

View File

@ -136,7 +136,7 @@ module API
.preload(project_group_links: { group: :route },
fork_network: :root_project,
fork_network_member: :forked_from_project,
forked_from_project: [:route, :forks, :tags, :group, :project_feature, namespace: [:route, :owner]])
forked_from_project: [:route, :tags, :group, :project_feature, namespace: [:route, :owner]])
end
# rubocop: enable CodeReuse/ActiveRecord

View File

@ -92,9 +92,9 @@ namespace :gitlab do
task orphan_lfs_files: :gitlab_environment do
warn_user_is_not_gitlab
removed_files = RemoveUnreferencedLfsObjectsWorker.new.perform
number_of_removed_files = RemoveUnreferencedLfsObjectsWorker.new.perform
logger.info "Removed unreferenced LFS files: #{removed_files.count}".color(:green)
logger.info "Removed unreferenced LFS files: #{number_of_removed_files}".color(:green)
end
namespace :sessions do

View File

@ -13157,7 +13157,9 @@ msgid "Every two weeks"
msgstr ""
msgid "Every week"
msgstr ""
msgid_plural "Every %d weeks"
msgstr[0] ""
msgstr[1] ""
msgid "Every week (%{weekday} at %{time})"
msgstr ""
@ -16533,9 +16535,6 @@ msgstr ""
msgid "IDE"
msgstr ""
msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
@ -18536,6 +18535,9 @@ msgstr ""
msgid "Iterations|No iteration cadences to show."
msgstr ""
msgid "Iterations|No iterations in cadence."
msgstr ""
msgid "Iterations|Number of future iterations you would like to have scheduled"
msgstr ""
@ -20034,7 +20036,7 @@ msgstr ""
msgid "Manage"
msgstr ""
msgid "Manage Web IDE features"
msgid "Manage Web IDE features."
msgstr ""
msgid "Manage access"
@ -24936,6 +24938,9 @@ msgstr ""
msgid "Preview"
msgstr ""
msgid "Preview JavaScript projects in the Web IDE with CodeSandbox Live Preview. %{link_start}Learn more.%{link_end} "
msgstr ""
msgid "Preview Markdown"
msgstr ""

View File

@ -55,4 +55,28 @@ RSpec.describe 'Alert Management index', :js do
it_behaves_like 'alert page with title, filtered search, and table'
end
end
describe 'managed_alerts_deprecation feature flag' do
subject { page }
before do
stub_feature_flags(managed_alerts_deprecation: feature_flag_value)
sign_in(developer)
visit project_alert_management_index_path(project)
wait_for_requests
end
context 'feature flag on' do
let(:feature_flag_value) { true }
it { is_expected.to have_pushed_frontend_feature_flags(managedAlertsDeprecation: true) }
end
context 'feature flag off' do
let(:feature_flag_value) { false }
it { is_expected.to have_pushed_frontend_feature_flags(managedAlertsDeprecation: false) }
end
end
end

View File

@ -83,6 +83,7 @@
{ "$ref": "./package_conan_metadata.json" },
{ "$ref": "./package_maven_metadata.json" },
{ "$ref": "./package_nuget_metadata.json" },
{ "$ref": "./package_pypi_metadata.json" },
{ "type": "null" }
]
},

View File

@ -0,0 +1,13 @@
{
"type": "object",
"additionalProperties": false,
"required": ["id"],
"properties": {
"id": {
"type": "string"
},
"requiredPython": {
"type": "string"
}
}
}

View File

@ -13,12 +13,10 @@ describe('IssuesListApp component', () => {
dueDate: '2020-12-17',
startDate: '2020-12-10',
title: 'My milestone',
webUrl: '/milestone/webUrl',
webPath: '/milestone/webPath',
},
dueDate: '2020-12-12',
timeStats: {
humanTimeEstimate: '1w',
},
humanTimeEstimate: '1w',
};
const findMilestone = () => wrapper.find('[data-testid="issuable-milestone"]');
@ -56,7 +54,7 @@ describe('IssuesListApp component', () => {
expect(milestone.text()).toBe(issue.milestone.title);
expect(milestone.find(GlIcon).props('name')).toBe('clock');
expect(milestone.find(GlLink).attributes('href')).toBe(issue.milestone.webUrl);
expect(milestone.find(GlLink).attributes('href')).toBe(issue.milestone.webPath);
});
describe.each`
@ -102,7 +100,7 @@ describe('IssuesListApp component', () => {
const timeEstimate = wrapper.find('[data-testid="time-estimate"]');
expect(timeEstimate.text()).toBe(issue.timeStats.humanTimeEstimate);
expect(timeEstimate.text()).toBe(issue.humanTimeEstimate);
expect(timeEstimate.attributes('title')).toBe('Estimate');
expect(timeEstimate.find(GlIcon).props('name')).toBe('timer');
});

View File

@ -1,9 +1,18 @@
import { GlButton, GlEmptyState, GlLink } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import { apiParams, filteredTokens, locationSearch, urlParams } from 'jest/issues_list/mock_data';
import {
filteredTokens,
getIssuesQueryResponse,
locationSearch,
urlParams,
} from 'jest/issues_list/mock_data';
import createFlash from '~/flash';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
@ -11,13 +20,9 @@ import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
import IssuesListApp from '~/issues_list/components/issues_list_app.vue';
import {
apiSortParams,
CREATED_DESC,
DUE_DATE_OVERDUE,
PAGE_SIZE,
PAGE_SIZE_MANUAL,
PARAM_DUE_DATE,
RELATIVE_POSITION_DESC,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL,
@ -40,12 +45,14 @@ describe('IssuesListApp component', () => {
let axiosMock;
let wrapper;
const localVue = createLocalVue();
localVue.use(VueApollo);
const defaultProvide = {
autocompleteUsersPath: 'autocomplete/users/path',
calendarPath: 'calendar/path',
canBulkUpdate: false,
emptyStateSvgPath: 'empty-state.svg',
endpoint: 'api/endpoint',
exportCsvPath: 'export/csv/path',
hasBlockedIssuesFeature: true,
hasIssueWeightsFeature: true,
@ -61,22 +68,6 @@ describe('IssuesListApp component', () => {
signInPath: 'sign/in/path',
};
const state = 'opened';
const xPage = 1;
const xTotal = 25;
const tabCounts = {
opened: xTotal,
closed: undefined,
all: undefined,
};
const fetchIssuesResponse = {
data: [],
headers: {
'x-page': xPage,
'x-total': xTotal,
},
};
const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons);
const findIssuableByEmail = () => wrapper.findComponent(IssuableByEmail);
const findGlButton = () => wrapper.findComponent(GlButton);
@ -86,19 +77,26 @@ describe('IssuesListApp component', () => {
const findGlLink = () => wrapper.findComponent(GlLink);
const findIssuableList = () => wrapper.findComponent(IssuableList);
const mountComponent = ({ provide = {}, mountFn = shallowMount } = {}) =>
mountFn(IssuesListApp, {
const mountComponent = ({
provide = {},
response = getIssuesQueryResponse,
mountFn = shallowMount,
} = {}) => {
const requestHandlers = [[getIssuesQuery, jest.fn().mockResolvedValue(response)]];
const apolloProvider = createMockApollo(requestHandlers);
return mountFn(IssuesListApp, {
localVue,
apolloProvider,
provide: {
...defaultProvide,
...provide,
},
});
};
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
axiosMock
.onGet(defaultProvide.endpoint)
.reply(200, fetchIssuesResponse.data, fetchIssuesResponse.headers);
});
afterEach(() => {
@ -108,28 +106,37 @@ describe('IssuesListApp component', () => {
});
describe('IssuableList', () => {
beforeEach(async () => {
beforeEach(() => {
wrapper = mountComponent();
await waitForPromises();
jest.runOnlyPendingTimers();
});
it('renders', () => {
expect(findIssuableList().props()).toMatchObject({
namespace: defaultProvide.projectPath,
recentSearchesStorageKey: 'issues',
searchInputPlaceholder: 'Search or filter results…',
searchInputPlaceholder: IssuesListApp.i18n.searchPlaceholder,
sortOptions: getSortOptions(true, true),
initialSortBy: CREATED_DESC,
issuables: getIssuesQueryResponse.data.project.issues.nodes,
tabs: IssuableListTabs,
currentTab: IssuableStates.Opened,
tabCounts,
showPaginationControls: false,
issuables: [],
totalItems: xTotal,
currentPage: xPage,
previousPage: xPage - 1,
nextPage: xPage + 1,
urlParams: { page: xPage, state },
tabCounts: {
opened: 1,
closed: undefined,
all: undefined,
},
issuablesLoading: false,
isManualOrdering: false,
showBulkEditSidebar: false,
showPaginationControls: true,
currentPage: 1,
previousPage: Number(getIssuesQueryResponse.data.project.issues.pageInfo.hasPreviousPage),
nextPage: Number(getIssuesQueryResponse.data.project.issues.pageInfo.hasNextPage),
urlParams: {
sort: urlSortParams[CREATED_DESC],
state: IssuableStates.Opened,
},
});
});
});
@ -157,9 +164,9 @@ describe('IssuesListApp component', () => {
describe('csv import/export component', () => {
describe('when user is signed in', () => {
it('renders', async () => {
const search = '?page=1&search=refactor&state=opened&sort=created_date';
const search = '?search=refactor&sort=created_date&state=opened';
beforeEach(() => {
global.jsdom.reconfigure({ url: `${TEST_HOST}${search}` });
wrapper = mountComponent({
@ -167,11 +174,13 @@ describe('IssuesListApp component', () => {
mountFn: mount,
});
await waitForPromises();
jest.runOnlyPendingTimers();
});
it('renders', () => {
expect(findCsvImportExportButtons().props()).toMatchObject({
exportCsvPath: `${defaultProvide.exportCsvPath}${search}`,
issuableCount: xTotal,
issuableCount: 1,
});
});
});
@ -189,7 +198,7 @@ describe('IssuesListApp component', () => {
it('renders when user has permissions', () => {
wrapper = mountComponent({ provide: { canBulkUpdate: true }, mountFn: mount });
expect(findGlButtonAt(2).text()).toBe('Edit issues');
expect(findGlButtonAt(2).text()).toBe(IssuesListApp.i18n.editIssues);
});
it('does not render when user does not have permissions', () => {
@ -215,7 +224,7 @@ describe('IssuesListApp component', () => {
it('renders when user has permissions', () => {
wrapper = mountComponent({ provide: { showNewIssueLink: true }, mountFn: mount });
expect(findGlButtonAt(2).text()).toBe('New issue');
expect(findGlButtonAt(2).text()).toBe(IssuesListApp.i18n.newIssueLabel);
expect(findGlButtonAt(2).attributes('href')).toBe(defaultProvide.newIssuePath);
});
@ -238,18 +247,6 @@ describe('IssuesListApp component', () => {
});
});
describe('page', () => {
it('is set from the url params', () => {
const page = 5;
global.jsdom.reconfigure({ url: setUrlParams({ page }, TEST_HOST) });
wrapper = mountComponent();
expect(findIssuableList().props('currentPage')).toBe(page);
});
});
describe('search', () => {
it('is set from the url params', () => {
global.jsdom.reconfigure({ url: `${TEST_HOST}${locationSearch}` });
@ -262,13 +259,15 @@ describe('IssuesListApp component', () => {
describe('sort', () => {
it.each(Object.keys(urlSortParams))('is set as %s from the url params', (sortKey) => {
global.jsdom.reconfigure({ url: setUrlParams(urlSortParams[sortKey], TEST_HOST) });
global.jsdom.reconfigure({
url: setUrlParams({ sort: urlSortParams[sortKey] }, TEST_HOST),
});
wrapper = mountComponent();
expect(findIssuableList().props()).toMatchObject({
initialSortBy: sortKey,
urlParams: urlSortParams[sortKey],
urlParams: { sort: urlSortParams[sortKey] },
});
});
});
@ -326,12 +325,10 @@ describe('IssuesListApp component', () => {
describe('empty states', () => {
describe('when there are issues', () => {
describe('when search returns no results', () => {
beforeEach(async () => {
beforeEach(() => {
global.jsdom.reconfigure({ url: `${TEST_HOST}?search=no+results` });
wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
await waitForPromises();
});
it('shows empty state', () => {
@ -344,10 +341,8 @@ describe('IssuesListApp component', () => {
});
describe('when "Open" tab has no issues', () => {
beforeEach(async () => {
beforeEach(() => {
wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
await waitForPromises();
});
it('shows empty state', () => {
@ -360,14 +355,12 @@ describe('IssuesListApp component', () => {
});
describe('when "Closed" tab has no issues', () => {
beforeEach(async () => {
beforeEach(() => {
global.jsdom.reconfigure({
url: setUrlParams({ state: IssuableStates.Closed }, TEST_HOST),
});
wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
await waitForPromises();
});
it('shows empty state', () => {
@ -536,74 +529,67 @@ describe('IssuesListApp component', () => {
describe('events', () => {
describe('when "click-tab" event is emitted by IssuableList', () => {
beforeEach(() => {
axiosMock.onGet(defaultProvide.endpoint).reply(200, fetchIssuesResponse.data, {
'x-page': 2,
'x-total': xTotal,
});
wrapper = mountComponent();
findIssuableList().vm.$emit('click-tab', IssuableStates.Closed);
});
it('makes API call to filter the list by the new state and resets the page to 1', () => {
expect(axiosMock.history.get[1].params).toMatchObject({
page: 1,
state: IssuableStates.Closed,
});
it('updates to the new tab', () => {
expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed);
});
});
describe('when "page-change" event is emitted by IssuableList', () => {
const data = [{ id: 10, title: 'title', state }];
const page = 2;
const totalItems = 21;
beforeEach(async () => {
axiosMock.onGet(defaultProvide.endpoint).reply(200, data, {
'x-page': page,
'x-total': totalItems,
});
beforeEach(() => {
wrapper = mountComponent();
findIssuableList().vm.$emit('page-change', page);
await waitForPromises();
findIssuableList().vm.$emit('page-change', 2);
});
it('fetches issues with expected params', () => {
expect(axiosMock.history.get[1].params).toMatchObject({
page,
per_page: PAGE_SIZE,
state,
with_labels_details: true,
});
});
it('updates IssuableList with response data', () => {
expect(findIssuableList().props()).toMatchObject({
issuables: data,
totalItems,
currentPage: page,
previousPage: page - 1,
nextPage: page + 1,
urlParams: { page, state },
});
it('updates to the new page', () => {
expect(findIssuableList().props('currentPage')).toBe(2);
});
});
describe('when "reorder" event is emitted by IssuableList', () => {
const issueOne = { id: 1, iid: 101, title: 'Issue one' };
const issueTwo = { id: 2, iid: 102, title: 'Issue two' };
const issueThree = { id: 3, iid: 103, title: 'Issue three' };
const issueFour = { id: 4, iid: 104, title: 'Issue four' };
const issues = [issueOne, issueTwo, issueThree, issueFour];
const issueOne = {
...getIssuesQueryResponse.data.project.issues.nodes[0],
id: 1,
iid: 101,
title: 'Issue one',
};
const issueTwo = {
...getIssuesQueryResponse.data.project.issues.nodes[0],
id: 2,
iid: 102,
title: 'Issue two',
};
const issueThree = {
...getIssuesQueryResponse.data.project.issues.nodes[0],
id: 3,
iid: 103,
title: 'Issue three',
};
const issueFour = {
...getIssuesQueryResponse.data.project.issues.nodes[0],
id: 4,
iid: 104,
title: 'Issue four',
};
const response = {
data: {
project: {
issues: {
...getIssuesQueryResponse.data.project.issues,
nodes: [issueOne, issueTwo, issueThree, issueFour],
},
},
},
};
beforeEach(async () => {
axiosMock.onGet(defaultProvide.endpoint).reply(200, issues, fetchIssuesResponse.headers);
wrapper = mountComponent();
await waitForPromises();
beforeEach(() => {
wrapper = mountComponent({ response });
jest.runOnlyPendingTimers();
});
describe('when successful', () => {
@ -644,21 +630,18 @@ describe('IssuesListApp component', () => {
});
describe('when "sort" event is emitted by IssuableList', () => {
it.each(Object.keys(apiSortParams))(
'fetches issues with correct params with payload `%s`',
it.each(Object.keys(urlSortParams))(
'updates to the new sort when payload is `%s`',
async (sortKey) => {
wrapper = mountComponent();
findIssuableList().vm.$emit('sort', sortKey);
await waitForPromises();
jest.runOnlyPendingTimers();
await nextTick();
expect(axiosMock.history.get[1].params).toEqual({
page: xPage,
per_page: sortKey === RELATIVE_POSITION_DESC ? PAGE_SIZE_MANUAL : PAGE_SIZE,
state,
with_labels_details: true,
...apiSortParams[sortKey],
expect(findIssuableList().props('urlParams')).toMatchObject({
sort: urlSortParams[sortKey],
});
},
);
@ -668,13 +651,11 @@ describe('IssuesListApp component', () => {
beforeEach(() => {
wrapper = mountComponent();
jest.spyOn(eventHub, '$emit');
findIssuableList().vm.$emit('update-legacy-bulk-edit');
});
it('emits an "issuables:updateBulkEdit" event to the legacy bulk edit class', async () => {
findIssuableList().vm.$emit('update-legacy-bulk-edit');
await waitForPromises();
it('emits an "issuables:updateBulkEdit" event to the legacy bulk edit class', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('issuables:updateBulkEdit');
});
});
@ -686,10 +667,6 @@ describe('IssuesListApp component', () => {
findIssuableList().vm.$emit('filter', filteredTokens);
});
it('makes an API call to search for issues with the search term', () => {
expect(axiosMock.history.get[1].params).toMatchObject(apiParams);
});
it('updates IssuableList with url params', () => {
expect(findIssuableList().props('urlParams')).toMatchObject(urlParams);
});

View File

@ -3,6 +3,76 @@ import {
OPERATOR_IS_NOT,
} from '~/vue_shared/components/filtered_search_bar/constants';
export const getIssuesQueryResponse = {
data: {
project: {
issues: {
count: 1,
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: 'startcursor',
endCursor: 'endcursor',
},
nodes: [
{
id: 'gid://gitlab/Issue/123456',
iid: '789',
blockedByCount: 1,
closedAt: null,
confidential: false,
createdAt: '2021-05-22T04:08:01Z',
downvotes: 2,
dueDate: '2021-05-29',
healthStatus: null,
humanTimeEstimate: null,
moved: false,
title: 'Issue title',
updatedAt: '2021-05-22T04:08:01Z',
upvotes: 3,
userDiscussionsCount: 4,
webUrl: 'project/-/issues/789',
weight: 5,
assignees: {
nodes: [
{
id: 'gid://gitlab/User/234',
avatarUrl: 'avatar/url',
name: 'Marge Simpson',
username: 'msimpson',
webUrl: 'url/msimpson',
},
],
},
author: {
id: 'gid://gitlab/User/456',
avatarUrl: 'avatar/url',
name: 'Homer Simpson',
username: 'hsimpson',
webUrl: 'url/hsimpson',
},
labels: {
nodes: [
{
id: 'gid://gitlab/ProjectLabel/456',
color: '#333',
title: 'Label title',
description: 'Label description',
},
],
},
milestone: null,
taskCompletionStatus: {
completedCount: 1,
count: 2,
},
},
],
},
},
},
};
export const locationSearch = [
'?search=find+issues',
'author_username=homer',
@ -19,8 +89,8 @@ export const locationSearch = [
'not[label_name][]=drama',
'my_reaction_emoji=thumbsup',
'confidential=no',
'iteration_title=season:+%234',
'not[iteration_title]=season:+%2320',
'iteration_id=4',
'not[iteration_id]=20',
'epic_id=gitlab-org%3A%3A%2612',
'not[epic_id]=gitlab-org%3A%3A%2634',
'weight=1',
@ -51,8 +121,8 @@ export const filteredTokens = [
{ type: 'labels', value: { data: 'drama', operator: OPERATOR_IS_NOT } },
{ type: 'my_reaction_emoji', value: { data: 'thumbsup', operator: OPERATOR_IS } },
{ type: 'confidential', value: { data: 'no', operator: OPERATOR_IS } },
{ type: 'iteration', value: { data: 'season: #4', operator: OPERATOR_IS } },
{ type: 'iteration', value: { data: 'season: #20', operator: OPERATOR_IS_NOT } },
{ type: 'iteration', value: { data: '4', operator: OPERATOR_IS } },
{ type: 'iteration', value: { data: '20', operator: OPERATOR_IS_NOT } },
{ type: 'epic_id', value: { data: 'gitlab-org::&12', operator: OPERATOR_IS } },
{ type: 'epic_id', value: { data: 'gitlab-org::&34', operator: OPERATOR_IS_NOT } },
{ type: 'weight', value: { data: '1', operator: OPERATOR_IS } },
@ -71,30 +141,32 @@ export const filteredTokensWithSpecialValues = [
];
export const apiParams = {
author_username: 'homer',
'not[author_username]': 'marge',
assignee_username: ['bart', 'lisa'],
'not[assignee_username]': ['patty', 'selma'],
milestone: 'season 4',
'not[milestone]': 'season 20',
labels: ['cartoon', 'tv'],
'not[labels]': ['live action', 'drama'],
my_reaction_emoji: 'thumbsup',
authorUsername: 'homer',
assigneeUsernames: ['bart', 'lisa'],
milestoneTitle: 'season 4',
labelName: ['cartoon', 'tv'],
myReactionEmoji: 'thumbsup',
confidential: 'no',
iteration_title: 'season: #4',
'not[iteration_title]': 'season: #20',
epic_id: '12',
'not[epic_id]': 'gitlab-org::&34',
iterationId: '4',
epicId: 'gitlab-org::&12',
weight: '1',
'not[weight]': '3',
not: {
authorUsername: 'marge',
assigneeUsernames: ['patty', 'selma'],
milestoneTitle: 'season 20',
labelName: ['live action', 'drama'],
iterationId: '20',
epicId: 'gitlab-org::&34',
weight: '3',
},
};
export const apiParamsWithSpecialValues = {
assignee_id: '123',
assignee_username: 'bart',
my_reaction_emoji: 'None',
iteration_id: 'Current',
epic_id: 'None',
assigneeId: '123',
assigneeUsernames: 'bart',
myReactionEmoji: 'None',
iterationWildcardId: 'CURRENT',
epicId: 'None',
weight: 'None',
};
@ -109,8 +181,8 @@ export const urlParams = {
'not[label_name][]': ['live action', 'drama'],
my_reaction_emoji: 'thumbsup',
confidential: 'no',
iteration_title: 'season: #4',
'not[iteration_title]': 'season: #20',
iteration_id: '4',
'not[iteration_id]': '20',
epic_id: 'gitlab-org%3A%3A%2612',
'not[epic_id]': 'gitlab-org::&34',
weight: '1',

View File

@ -8,19 +8,20 @@ import {
urlParams,
urlParamsWithSpecialValues,
} from 'jest/issues_list/mock_data';
import { API_PARAM, DUE_DATE_VALUES, URL_PARAM, urlSortParams } from '~/issues_list/constants';
import { DUE_DATE_VALUES, urlSortParams } from '~/issues_list/constants';
import {
convertToParams,
convertToUrlParams,
convertToSearchQuery,
getDueDateValue,
getFilterTokens,
getSortKey,
getSortOptions,
convertToApiParams,
} from '~/issues_list/utils';
describe('getSortKey', () => {
it.each(Object.keys(urlSortParams))('returns %s given the correct inputs', (sortKey) => {
const { sort } = urlSortParams[sortKey];
const sort = urlSortParams[sortKey];
expect(getSortKey(sort)).toBe(sortKey);
});
});
@ -80,31 +81,26 @@ describe('getFilterTokens', () => {
});
});
describe('convertToParams', () => {
describe('convertToApiParams', () => {
it('returns api params given filtered tokens', () => {
expect(convertToParams(filteredTokens, API_PARAM)).toEqual({
...apiParams,
epic_id: 'gitlab-org::&12',
});
expect(convertToApiParams(filteredTokens)).toEqual(apiParams);
});
it('returns api params given filtered tokens with special values', () => {
expect(convertToParams(filteredTokensWithSpecialValues, API_PARAM)).toEqual(
apiParamsWithSpecialValues,
);
expect(convertToApiParams(filteredTokensWithSpecialValues)).toEqual(apiParamsWithSpecialValues);
});
});
describe('convertToUrlParams', () => {
it('returns url params given filtered tokens', () => {
expect(convertToParams(filteredTokens, URL_PARAM)).toEqual({
expect(convertToUrlParams(filteredTokens)).toEqual({
...urlParams,
epic_id: 'gitlab-org::&12',
});
});
it('returns url params given filtered tokens with special values', () => {
expect(convertToParams(filteredTokensWithSpecialValues, URL_PARAM)).toEqual(
urlParamsWithSpecialValues,
);
expect(convertToUrlParams(filteredTokensWithSpecialValues)).toEqual(urlParamsWithSpecialValues);
});
});

View File

@ -49,10 +49,13 @@ describe('User select dropdown', () => {
const findUnassignLink = () => wrapper.find('[data-testid="unassign"]');
const findEmptySearchResults = () => wrapper.find('[data-testid="empty-results"]');
const searchQueryHandlerSuccess = jest.fn().mockResolvedValue(projectMembersResponse);
const participantsQueryHandlerSuccess = jest.fn().mockResolvedValue(participantsQueryResponse);
const createComponent = ({
props = {},
searchQueryHandler = jest.fn().mockResolvedValue(projectMembersResponse),
participantsQueryHandler = jest.fn().mockResolvedValue(participantsQueryResponse),
searchQueryHandler = searchQueryHandlerSuccess,
participantsQueryHandler = participantsQueryHandlerSuccess,
} = {}) => {
fakeApollo = createMockApollo([
[searchUsersQuery, searchQueryHandler],
@ -91,6 +94,14 @@ describe('User select dropdown', () => {
expect(findParticipantsLoading().exists()).toBe(true);
});
it('skips the queries if `isEditing` prop is false', () => {
createComponent({ props: { isEditing: false } });
expect(findParticipantsLoading().exists()).toBe(false);
expect(searchQueryHandlerSuccess).not.toHaveBeenCalled();
expect(participantsQueryHandlerSuccess).not.toHaveBeenCalled();
});
it('emits an `error` event if participants query was rejected', async () => {
createComponent({ participantsQueryHandler: mockError });
await waitForPromises();

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['PypiMetadata'] do
it 'includes pypi metadatum fields' do
expected_fields = %w[
id required_python
]
expect(described_class).to include_graphql_fields(*expected_fields)
end
end

View File

@ -302,7 +302,6 @@ RSpec.describe IssuesHelper do
email: current_user&.notification_email,
emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'),
empty_state_svg_path: '#',
endpoint: expose_path(api_v4_projects_issues_path(id: project.id)),
export_csv_path: export_csv_project_issues_path(project),
has_project_issues: project_issues(project).exists?.to_s,
import_csv_issues_path: '#',

View File

@ -191,4 +191,21 @@ RSpec.describe LfsObject do
expect { lfs_object.destroy! }.not_to raise_error
end
end
describe '.unreferenced_in_batches' do
let!(:unreferenced_lfs_object1) { create(:lfs_object, oid: '1') }
let!(:referenced_lfs_object) { create(:lfs_objects_project).lfs_object }
let!(:unreferenced_lfs_object2) { create(:lfs_object, oid: '2') }
it 'returns lfs objects in batches' do
stub_const('LfsObject::BATCH_SIZE', 1)
batches = []
described_class.unreferenced_in_batches { |batch| batches << batch }
expect(batches.size).to eq(2)
expect(batches.first).to eq([unreferenced_lfs_object2])
expect(batches.last).to eq([unreferenced_lfs_object1])
end
end
end

View File

@ -1503,6 +1503,13 @@ RSpec.describe API::Commits do
expect(json_response).to eq("dry_run" => "success")
expect(project.commit(branch)).to eq(head)
end
it 'supports the use of a custom commit message' do
post api(route, user), params: { branch: branch, message: 'foo' }
expect(response).to have_gitlab_http_status(:created)
expect(json_response["message"]).to eq('foo')
end
end
context 'when repository is disabled' do

View File

@ -3,62 +3,35 @@ require 'spec_helper'
RSpec.describe 'package details' do
include GraphqlHelpers
include_context 'package details setup'
let_it_be(:project) { create(:project) }
let_it_be(:composer_package) { create(:composer_package, project: project) }
let_it_be(:package) { create(:composer_package, project: project) }
let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } }
let_it_be(:composer_metadatum) do
# we are forced to manually create the metadatum, without using the factory to force the sha to be a string
# and avoid an error where gitaly can't find the repository
create(:composer_metadatum, package: composer_package, target_sha: 'foo_sha', composer_json: composer_json)
create(:composer_metadatum, package: package, target_sha: 'foo_sha', composer_json: composer_json)
end
let(:depth) { 3 }
let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
let(:metadata) { query_graphql_fragment('ComposerMetadata') }
let(:package_files) { all_graphql_fields_for('PackageFile') }
let(:user) { project.owner }
let(:package_global_id) { global_id_of(composer_package) }
let(:package_details) { graphql_data_at(:package) }
let(:metadata_response) { graphql_data_at(:package, :metadata) }
let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
let(:query) do
graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
#{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
metadata {
#{metadata}
}
packageFiles {
nodes {
#{package_files}
}
}
FIELDS
end
subject { post_graphql(query, current_user: user) }
before do
subject
end
it_behaves_like 'a working graphql query' do
it 'matches the JSON schema' do
expect(package_details).to match_schema('graphql/packages/package_details')
end
it_behaves_like 'a package detail'
it 'has the correct metadata' do
expect(metadata_response).to include(
'targetSha' => 'foo_sha',
'composerJson' => composer_json.transform_keys(&:to_s).transform_values(&:to_s)
)
end
describe 'Composer' do
it 'has the correct metadata' do
expect(metadata_response).to include(
'targetSha' => 'foo_sha',
'composerJson' => composer_json.transform_keys(&:to_s).transform_values(&:to_s)
)
end
it 'does not have files' do
expect(package_files_response).to be_empty
end
it 'does not have files' do
expect(package_files_response).to be_empty
end
end

View File

@ -3,26 +3,13 @@ require 'spec_helper'
RSpec.describe 'conan package details' do
include GraphqlHelpers
include_context 'package details setup'
let_it_be(:project) { create(:project) }
let_it_be(:conan_package) { create(:conan_package, project: project) }
let_it_be(:package) { create(:conan_package, project: project) }
let(:package_global_id) { global_id_of(conan_package) }
let(:metadata) { query_graphql_fragment('ConanMetadata') }
let(:first_file) { conan_package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } }
let(:depth) { 3 }
let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
let(:package_files) { all_graphql_fields_for('PackageFile') }
let(:package_files_metadata) {query_graphql_fragment('ConanFileMetadata')}
let(:user) { project.owner }
let(:package_details) { graphql_data_at(:package) }
let(:metadata_response) { graphql_data_at(:package, :metadata) }
let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)}
let(:first_file_response_metadata) { graphql_data_at(:package, :package_files, :nodes, 0, :file_metadata)}
let(:query) do
graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
#{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
@ -46,35 +33,16 @@ RSpec.describe 'conan package details' do
subject
end
it_behaves_like 'a working graphql query' do
it 'matches the JSON schema' do
expect(package_details).to match_schema('graphql/packages/package_details')
end
end
it_behaves_like 'a package detail'
it_behaves_like 'a package with files'
it 'has the correct metadata' do
expect(metadata_response).to include(
'id' => global_id_of(conan_package.conan_metadatum),
'recipe' => conan_package.conan_metadatum.recipe,
'packageChannel' => conan_package.conan_metadatum.package_channel,
'packageUsername' => conan_package.conan_metadatum.package_username,
'recipePath' => conan_package.conan_metadatum.recipe_path
)
end
it 'has the right amount of files' do
expect(package_files_response.length).to be(conan_package.package_files.length)
end
it 'has the basic package files data' do
expect(first_file_response).to include(
'id' => global_id_of(first_file),
'fileName' => first_file.file_name,
'size' => first_file.size.to_s,
'downloadPath' => first_file.download_path,
'fileSha1' => first_file.file_sha1,
'fileMd5' => first_file.file_md5,
'fileSha256' => first_file.file_sha256
'id' => global_id_of(package.conan_metadatum),
'recipe' => package.conan_metadatum.recipe,
'packageChannel' => package.conan_metadatum.package_channel,
'packageUsername' => package.conan_metadatum.package_username,
'recipePath' => package.conan_metadatum.recipe_path
)
end

View File

@ -3,89 +3,51 @@ require 'spec_helper'
RSpec.describe 'maven package details' do
include GraphqlHelpers
include_context 'package details setup'
let_it_be(:project) { create(:project) }
let_it_be(:maven_package) { create(:maven_package, project: project) }
let_it_be(:package) { create(:maven_package, project: project) }
let(:package_global_id) { global_id_of(maven_package) }
let(:metadata) { query_graphql_fragment('MavenMetadata') }
let(:first_file) { maven_package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } }
let(:depth) { 3 }
let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
let(:package_files) { all_graphql_fields_for('PackageFile') }
let(:user) { project.owner }
let(:package_details) { graphql_data_at(:package) }
let(:metadata_response) { graphql_data_at(:package, :metadata) }
let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)}
let(:query) do
graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
#{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
metadata {
#{metadata}
}
packageFiles {
nodes {
#{package_files}
}
}
FIELDS
end
subject { post_graphql(query, current_user: user) }
shared_examples 'a working maven package' do
before do
subject
end
it_behaves_like 'a working graphql query' do
it 'matches the JSON schema' do
expect(package_details).to match_schema('graphql/packages/package_details')
end
end
shared_examples 'correct maven metadata' do
it 'has the correct metadata' do
expect(metadata_response).to include(
'id' => global_id_of(maven_package.maven_metadatum),
'path' => maven_package.maven_metadatum.path,
'appGroup' => maven_package.maven_metadatum.app_group,
'appVersion' => maven_package.maven_metadatum.app_version,
'appName' => maven_package.maven_metadatum.app_name
)
end
it 'has the right amount of files' do
expect(package_files_response.length).to be(maven_package.package_files.length)
end
it 'has the basic package files data' do
expect(first_file_response).to include(
'id' => global_id_of(first_file),
'fileName' => first_file.file_name,
'size' => first_file.size.to_s,
'downloadPath' => first_file.download_path,
'fileSha1' => first_file.file_sha1,
'fileMd5' => first_file.file_md5,
'fileSha256' => first_file.file_sha256
'id' => global_id_of(package.maven_metadatum),
'path' => package.maven_metadatum.path,
'appGroup' => package.maven_metadatum.app_group,
'appVersion' => package.maven_metadatum.app_version,
'appName' => package.maven_metadatum.app_name
)
end
end
context 'a maven package with version' do
it_behaves_like "a working maven package"
subject { post_graphql(query, current_user: user) }
before do
subject
end
it_behaves_like 'a package detail'
it_behaves_like 'correct maven metadata'
it_behaves_like 'a package with files'
end
context 'a versionless maven package' do
let_it_be(:maven_metadatum) { create(:maven_metadatum, app_version: nil) }
let_it_be(:maven_package) { create(:maven_package, project: project, version: nil, maven_metadatum: maven_metadatum) }
let_it_be(:package) { create(:maven_package, project: project, version: nil, maven_metadatum: maven_metadatum) }
it_behaves_like "a working maven package"
subject { post_graphql(query, current_user: user) }
it "has an empty version" do
before do
subject
end
it_behaves_like 'a package detail'
it_behaves_like 'correct maven metadata'
it_behaves_like 'a package with files'
it 'has an empty version' do
subject
expect(metadata_response['appVersion']).to eq(nil)

View File

@ -3,37 +3,11 @@ require 'spec_helper'
RSpec.describe 'nuget package details' do
include GraphqlHelpers
include_context 'package details setup'
let_it_be(:project) { create(:project) }
let_it_be(:nuget_package) { create(:nuget_package, :with_metadatum, project: project) }
let_it_be(:package) { create(:nuget_package, :with_metadatum, project: project) }
let(:package_global_id) { global_id_of(nuget_package) }
let(:metadata) { query_graphql_fragment('NugetMetadata') }
let(:first_file) { nuget_package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } }
let(:depth) { 3 }
let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
let(:package_files) { all_graphql_fields_for('PackageFile') }
let(:user) { project.owner }
let(:package_details) { graphql_data_at(:package) }
let(:metadata_response) { graphql_data_at(:package, :metadata) }
let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)}
let(:query) do
graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
#{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
metadata {
#{metadata}
}
packageFiles {
nodes {
#{package_files}
}
}
FIELDS
end
subject { post_graphql(query, current_user: user) }
@ -41,34 +15,15 @@ RSpec.describe 'nuget package details' do
subject
end
it_behaves_like 'a working graphql query' do
it 'matches the JSON schema' do
expect(package_details).to match_schema('graphql/packages/package_details')
end
end
it_behaves_like 'a package detail'
it_behaves_like 'a package with files'
it 'has the correct metadata' do
expect(metadata_response).to include(
'id' => global_id_of(nuget_package.nuget_metadatum),
'licenseUrl' => nuget_package.nuget_metadatum.license_url,
'projectUrl' => nuget_package.nuget_metadatum.project_url,
'iconUrl' => nuget_package.nuget_metadatum.icon_url
)
end
it 'has the right amount of files' do
expect(package_files_response.length).to be(nuget_package.package_files.length)
end
it 'has the basic package files data' do
expect(first_file_response).to include(
'id' => global_id_of(first_file),
'fileName' => first_file.file_name,
'size' => first_file.size.to_s,
'downloadPath' => first_file.download_path,
'fileSha1' => first_file.file_sha1,
'fileMd5' => first_file.file_md5,
'fileSha256' => first_file.file_sha256
'id' => global_id_of(package.nuget_metadatum),
'licenseUrl' => package.nuget_metadatum.license_url,
'projectUrl' => package.nuget_metadatum.project_url,
'iconUrl' => package.nuget_metadatum.icon_url
)
end
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'pypi package details' do
include GraphqlHelpers
include_context 'package details setup'
let_it_be(:package) { create(:pypi_package, project: project) }
let(:metadata) { query_graphql_fragment('PypiMetadata') }
subject { post_graphql(query, current_user: user) }
before do
subject
end
it_behaves_like 'a package detail'
it_behaves_like 'a package with files'
it 'has the correct metadata' do
expect(metadata_response).to include(
'id' => global_id_of(package.pypi_metadatum),
'requiredPython' => package.pypi_metadatum.required_python
)
end
end

View File

@ -24,7 +24,7 @@ RSpec.describe Commits::CherryPickService do
repository.add_branch(user, branch_name, merge_base_sha)
end
def cherry_pick(sha, branch_name)
def cherry_pick(sha, branch_name, message: nil)
commit = project.commit(sha)
described_class.new(
@ -32,7 +32,8 @@ RSpec.describe Commits::CherryPickService do
user,
commit: commit,
start_branch: branch_name,
branch_name: branch_name
branch_name: branch_name,
message: message
).execute
end
@ -45,6 +46,14 @@ RSpec.describe Commits::CherryPickService do
head = repository.find_branch(branch_name).target
expect(head).not_to eq(merge_base_sha)
end
it 'supports a custom commit message' do
result = cherry_pick(merge_commit_sha, branch_name, message: 'foo')
branch = repository.find_branch(branch_name)
expect(result[:status]).to eq(:success)
expect(branch.dereferenced_target.message).to eq('foo')
end
end
it_behaves_like 'successful cherry-pick'

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
RSpec.shared_context 'package details setup' do
let_it_be(:project) { create(:project) }
let_it_be(:package) { create(:package, project: project) }
let(:package_global_id) { global_id_of(package) }
let(:depth) { 3 }
let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
let(:package_files) { all_graphql_fields_for('PackageFile') }
let(:user) { project.owner }
let(:package_details) { graphql_data_at(:package) }
let(:metadata_response) { graphql_data_at(:package, :metadata) }
let(:first_file) { package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } }
let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)}
let(:first_file_response_metadata) { graphql_data_at(:package, :package_files, :nodes, 0, :file_metadata)}
let(:query) do
graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
#{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
metadata {
#{metadata}
}
packageFiles {
nodes {
#{package_files}
}
}
FIELDS
end
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
RSpec.shared_examples 'a package detail' do
it_behaves_like 'a working graphql query' do
it 'matches the JSON schema' do
expect(package_details).to match_schema('graphql/packages/package_details')
end
end
end
RSpec.shared_examples 'a package with files' do
it 'has the right amount of files' do
expect(package_files_response.length).to be(package.package_files.length)
end
it 'has the basic package files data' do
expect(first_file_response).to include(
'id' => global_id_of(first_file),
'fileName' => first_file.file_name,
'size' => first_file.size.to_s,
'downloadPath' => first_file.download_path,
'fileSha1' => first_file.file_sha1,
'fileMd5' => first_file.file_md5,
'fileSha256' => first_file.file_sha256
)
end
end

View File

@ -306,7 +306,6 @@ RSpec.describe 'Every Sidekiq worker' do
'IncidentManagement::OncallRotations::PersistAllRotationsShiftsJob' => 3,
'IncidentManagement::OncallRotations::PersistShiftsJob' => 3,
'IncidentManagement::PagerDuty::ProcessIncidentWorker' => 3,
'IncidentManagement::ProcessAlertWorker' => 3,
'IncidentManagement::ProcessPrometheusAlertWorker' => 3,
'InvalidGpgSignatureUpdateWorker' => 3,
'IrkerWorker' => 3,

View File

@ -1,88 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::ProcessAlertWorker do
let_it_be(:project) { create(:project) }
let_it_be(:settings) { create(:project_incident_management_setting, project: project, create_issue: true) }
describe '#perform' do
let_it_be(:started_at) { Time.now.rfc3339 }
let_it_be(:payload) { { 'title' => 'title', 'start_time' => started_at } }
let_it_be(:alert) { create(:alert_management_alert, project: project, payload: payload, started_at: started_at) }
let(:created_issue) { Issue.last! }
subject { described_class.new.perform(nil, nil, alert.id) }
before do
allow(Gitlab::AppLogger).to receive(:warn).and_call_original
allow(AlertManagement::CreateAlertIssueService)
.to receive(:new).with(alert, User.alert_bot)
.and_call_original
end
shared_examples 'creates issue successfully' do
it 'creates an issue' do
expect(AlertManagement::CreateAlertIssueService)
.to receive(:new).with(alert, User.alert_bot)
expect { subject }.to change { Issue.count }.by(1)
end
it 'updates AlertManagement::Alert#issue_id' do
subject
expect(alert.reload.issue_id).to eq(created_issue.id)
end
it 'does not write a warning to log' do
subject
expect(Gitlab::AppLogger).not_to have_received(:warn)
end
end
context 'with valid alert' do
it_behaves_like 'creates issue successfully'
context 'when alert cannot be updated' do
let_it_be(:alert) { create(:alert_management_alert, :with_validation_errors, project: project, payload: payload) }
it 'updates AlertManagement::Alert#issue_id' do
expect { subject }.not_to change { alert.reload.issue_id }
end
it 'logs a warning' do
subject
expect(Gitlab::AppLogger).to have_received(:warn).with(
message: 'Cannot process an Incident',
issue_id: created_issue.id,
alert_id: alert.id,
errors: 'Hosts hosts array is over 255 chars'
)
end
end
context 'prometheus alert' do
let_it_be(:alert) { create(:alert_management_alert, :prometheus, project: project, started_at: started_at) }
it_behaves_like 'creates issue successfully'
end
end
context 'with invalid alert' do
let(:invalid_alert_id) { non_existing_record_id }
subject { described_class.new.perform(nil, nil, invalid_alert_id) }
it 'does not create issues' do
expect(AlertManagement::CreateAlertIssueService).not_to receive(:new)
expect { subject }.not_to change { Issue.count }
end
end
end
end

View File

@ -34,14 +34,14 @@ RSpec.describe RemoveUnreferencedLfsObjectsWorker do
end
it 'removes unreferenced lfs objects' do
worker.perform
expect(worker.perform).to eq(2)
expect(LfsObject.where(id: unreferenced_lfs_object1.id)).to be_empty
expect(LfsObject.where(id: unreferenced_lfs_object2.id)).to be_empty
end
it 'leaves referenced lfs objects' do
worker.perform
expect(worker.perform).to eq(2)
expect(referenced_lfs_object1.reload).to be_present
expect(referenced_lfs_object2.reload).to be_present
@ -50,10 +50,12 @@ RSpec.describe RemoveUnreferencedLfsObjectsWorker do
it 'removes unreferenced lfs objects after project removal' do
project1.destroy!
worker.perform
expect(worker.perform).to eq(3)
expect(referenced_lfs_object1.reload).to be_present
expect(LfsObject.where(id: referenced_lfs_object2.id)).to be_empty
end
end
it_behaves_like 'an idempotent worker'
end