Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
15ae4a8da8
commit
6e91fbf774
|
|
@ -0,0 +1,40 @@
|
|||
import { masks } from 'dateformat';
|
||||
import { formatDate } from '~/lib/utils/datetime_utility';
|
||||
|
||||
const { isoDate } = masks;
|
||||
|
||||
/**
|
||||
* Takes an array of items and returns one item per month with the average of the `count`s from that month
|
||||
* @param {Array} items
|
||||
* @param {Number} items[index].count value to be averaged
|
||||
* @param {String} items[index].recordedAt item dateTime time stamp to be collected into a month
|
||||
* @param {Object} options
|
||||
* @param {Object} options.shouldRound an option to specify whether the retuned averages should be rounded
|
||||
* @return {Array} items collected into [month, average],
|
||||
* where month is a dateTime string representing the first of the given month
|
||||
* and average is the average of the count
|
||||
*/
|
||||
export function getAverageByMonth(items = [], options = {}) {
|
||||
const { shouldRound = false } = options;
|
||||
const itemsMap = items.reduce((memo, item) => {
|
||||
const { count, recordedAt } = item;
|
||||
const date = new Date(recordedAt);
|
||||
const month = formatDate(new Date(date.getFullYear(), date.getMonth(), 1), isoDate);
|
||||
if (memo[month]) {
|
||||
const { sum, recordCount } = memo[month];
|
||||
return { ...memo, [month]: { sum: sum + count, recordCount: recordCount + 1 } };
|
||||
}
|
||||
|
||||
return { ...memo, [month]: { sum: count, recordCount: 1 } };
|
||||
}, {});
|
||||
|
||||
return Object.keys(itemsMap).map(month => {
|
||||
const { sum, recordCount } = itemsMap[month];
|
||||
const avg = sum / recordCount;
|
||||
if (shouldRound) {
|
||||
return [month, Math.round(avg)];
|
||||
}
|
||||
|
||||
return [month, avg];
|
||||
});
|
||||
}
|
||||
|
|
@ -122,67 +122,20 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<li :class="{ 'js-toggle-container': collapsible }" class="commit flex-row">
|
||||
<div class="d-flex align-items-center align-self-start">
|
||||
<input
|
||||
v-if="isSelectable"
|
||||
class="mr-2"
|
||||
type="checkbox"
|
||||
:checked="checked"
|
||||
@change="$emit('handleCheckboxChange', $event.target.checked)"
|
||||
/>
|
||||
<user-avatar-link
|
||||
:link-href="authorUrl"
|
||||
:img-src="authorAvatar"
|
||||
:img-alt="authorName"
|
||||
:img-size="40"
|
||||
class="avatar-cell d-none d-sm-block"
|
||||
/>
|
||||
</div>
|
||||
<div class="commit-detail flex-list">
|
||||
<div class="commit-content qa-commit-content">
|
||||
<a
|
||||
:href="commit.commit_url"
|
||||
class="commit-row-message item-title"
|
||||
v-html="commit.title_html"
|
||||
></a>
|
||||
|
||||
<span class="commit-row-message d-block d-sm-none">· {{ commit.short_id }}</span>
|
||||
|
||||
<gl-button
|
||||
v-if="commit.description_html && collapsible"
|
||||
class="js-toggle-button"
|
||||
size="small"
|
||||
icon="ellipsis_h"
|
||||
:aria-label="__('Toggle commit description')"
|
||||
/>
|
||||
|
||||
<div class="committer">
|
||||
<a
|
||||
:href="authorUrl"
|
||||
:class="authorClass"
|
||||
:data-user-id="authorId"
|
||||
v-text="authorName"
|
||||
></a>
|
||||
{{ s__('CommitWidget|authored') }}
|
||||
<time-ago-tooltip :time="commit.authored_date" />
|
||||
</div>
|
||||
|
||||
<pre
|
||||
v-if="commit.description_html"
|
||||
:class="{ 'js-toggle-content': collapsible, 'd-block': !collapsible }"
|
||||
class="commit-row-description gl-mb-3 text-dark"
|
||||
v-html="commitDescription"
|
||||
></pre>
|
||||
</div>
|
||||
<div class="commit-actions flex-row d-none d-sm-flex">
|
||||
<li :class="{ 'js-toggle-container': collapsible }" class="commit">
|
||||
<div
|
||||
class="d-block d-sm-flex flex-row-reverse justify-content-between align-items-start flex-lg-row-reverse"
|
||||
>
|
||||
<div
|
||||
class="commit-actions flex-row d-none d-sm-flex align-items-start flex-wrap justify-content-end"
|
||||
>
|
||||
<div v-if="commit.signature_html" v-html="commit.signature_html"></div>
|
||||
<commit-pipeline-status
|
||||
v-if="commit.pipeline_status_path"
|
||||
:endpoint="commit.pipeline_status_path"
|
||||
class="d-inline-flex"
|
||||
class="d-inline-flex mb-2"
|
||||
/>
|
||||
<gl-button-group class="gl-ml-4" data-testid="commit-sha-group">
|
||||
<gl-button-group class="gl-ml-4 gl-mb-4" data-testid="commit-sha-group">
|
||||
<gl-button label class="gl-font-monospace" v-text="commit.short_id" />
|
||||
<clipboard-button
|
||||
:text="commit.id"
|
||||
|
|
@ -226,6 +179,62 @@ export default {
|
|||
</gl-button-group>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex float-left align-items-center align-self-start">
|
||||
<input
|
||||
v-if="isSelectable"
|
||||
class="mr-2"
|
||||
type="checkbox"
|
||||
:checked="checked"
|
||||
@change="$emit('handleCheckboxChange', $event.target.checked)"
|
||||
/>
|
||||
<user-avatar-link
|
||||
:link-href="authorUrl"
|
||||
:img-src="authorAvatar"
|
||||
:img-alt="authorName"
|
||||
:img-size="40"
|
||||
class="avatar-cell d-none d-sm-block"
|
||||
/>
|
||||
</div>
|
||||
<div class="commit-detail flex-list">
|
||||
<div class="commit-content qa-commit-content">
|
||||
<a
|
||||
:href="commit.commit_url"
|
||||
class="commit-row-message item-title"
|
||||
v-html="commit.title_html"
|
||||
></a>
|
||||
|
||||
<span class="commit-row-message d-block d-sm-none">· {{ commit.short_id }}</span>
|
||||
|
||||
<gl-button
|
||||
v-if="commit.description_html && collapsible"
|
||||
class="js-toggle-button"
|
||||
size="small"
|
||||
icon="ellipsis_h"
|
||||
:aria-label="__('Toggle commit description')"
|
||||
/>
|
||||
|
||||
<div class="committer">
|
||||
<a
|
||||
:href="authorUrl"
|
||||
:class="authorClass"
|
||||
:data-user-id="authorId"
|
||||
v-text="authorName"
|
||||
></a>
|
||||
{{ s__('CommitWidget|authored') }}
|
||||
<time-ago-tooltip :time="commit.authored_date" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<pre
|
||||
v-if="commit.description_html"
|
||||
:class="{ 'js-toggle-content': collapsible, 'd-block': !collapsible }"
|
||||
class="commit-row-description gl-mb-3 text-dark"
|
||||
v-html="commitDescription"
|
||||
></pre>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import CommitMessageField from './message_field.vue';
|
|||
import Actions from './actions.vue';
|
||||
import SuccessMessage from './success_message.vue';
|
||||
import { leftSidebarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
|
||||
import consts from '../../stores/modules/commit/constants';
|
||||
import { createUnexpectedCommitError } from '../../lib/errors';
|
||||
|
||||
export default {
|
||||
|
|
@ -45,12 +44,11 @@ export default {
|
|||
return this.currentActivityView === leftSidebarViews.commit.name;
|
||||
},
|
||||
commitErrorPrimaryAction() {
|
||||
if (!this.lastCommitError?.canCreateBranch) {
|
||||
return undefined;
|
||||
}
|
||||
const { primaryAction } = this.lastCommitError || {};
|
||||
|
||||
return {
|
||||
text: __('Create new branch'),
|
||||
button: primaryAction ? { text: primaryAction.text } : undefined,
|
||||
callback: primaryAction?.callback?.bind(this, this.$store) || (() => {}),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
@ -78,9 +76,6 @@ export default {
|
|||
commit() {
|
||||
return this.commitChanges();
|
||||
},
|
||||
forceCreateNewBranch() {
|
||||
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commit());
|
||||
},
|
||||
handleCompactState() {
|
||||
if (this.lastCommitMsg) {
|
||||
this.isCompact = false;
|
||||
|
|
@ -188,9 +183,9 @@ export default {
|
|||
ref="commitErrorModal"
|
||||
modal-id="ide-commit-error-modal"
|
||||
:title="lastCommitError.title"
|
||||
:action-primary="commitErrorPrimaryAction"
|
||||
:action-primary="commitErrorPrimaryAction.button"
|
||||
:action-cancel="{ text: __('Cancel') }"
|
||||
@ok="forceCreateNewBranch"
|
||||
@ok="commitErrorPrimaryAction.callback"
|
||||
>
|
||||
<div v-safe-html="lastCommitError.messageHTML"></div>
|
||||
</gl-modal>
|
||||
|
|
|
|||
|
|
@ -1,25 +1,49 @@
|
|||
import { escape } from 'lodash';
|
||||
import { __ } from '~/locale';
|
||||
import consts from '../stores/modules/commit/constants';
|
||||
|
||||
const CODEOWNERS_REGEX = /Push.*protected branches.*CODEOWNERS/;
|
||||
const BRANCH_CHANGED_REGEX = /changed.*since.*start.*edit/;
|
||||
const BRANCH_ALREADY_EXISTS = /branch.*already.*exists/;
|
||||
|
||||
export const createUnexpectedCommitError = () => ({
|
||||
const createNewBranchAndCommit = store =>
|
||||
store
|
||||
.dispatch('commit/updateCommitAction', consts.COMMIT_TO_NEW_BRANCH)
|
||||
.then(() => store.dispatch('commit/commitChanges'));
|
||||
|
||||
export const createUnexpectedCommitError = message => ({
|
||||
title: __('Unexpected error'),
|
||||
messageHTML: __('Could not commit. An unexpected error occurred.'),
|
||||
canCreateBranch: false,
|
||||
messageHTML: escape(message) || __('Could not commit. An unexpected error occurred.'),
|
||||
});
|
||||
|
||||
export const createCodeownersCommitError = message => ({
|
||||
title: __('CODEOWNERS rule violation'),
|
||||
messageHTML: escape(message),
|
||||
canCreateBranch: true,
|
||||
primaryAction: {
|
||||
text: __('Create new branch'),
|
||||
callback: createNewBranchAndCommit,
|
||||
},
|
||||
});
|
||||
|
||||
export const createBranchChangedCommitError = message => ({
|
||||
title: __('Branch changed'),
|
||||
messageHTML: `${escape(message)}<br/><br/>${__('Would you like to create a new branch?')}`,
|
||||
canCreateBranch: true,
|
||||
primaryAction: {
|
||||
text: __('Create new branch'),
|
||||
callback: createNewBranchAndCommit,
|
||||
},
|
||||
});
|
||||
|
||||
export const branchAlreadyExistsCommitError = message => ({
|
||||
title: __('Branch already exists'),
|
||||
messageHTML: `${escape(message)}<br/><br/>${__(
|
||||
'Would you like to try auto-generating a branch name?',
|
||||
)}`,
|
||||
primaryAction: {
|
||||
text: __('Create new branch'),
|
||||
callback: store =>
|
||||
store.dispatch('commit/addSuffixToBranchName').then(() => createNewBranchAndCommit(store)),
|
||||
},
|
||||
});
|
||||
|
||||
export const parseCommitError = e => {
|
||||
|
|
@ -33,7 +57,9 @@ export const parseCommitError = e => {
|
|||
return createCodeownersCommitError(message);
|
||||
} else if (BRANCH_CHANGED_REGEX.test(message)) {
|
||||
return createBranchChangedCommitError(message);
|
||||
} else if (BRANCH_ALREADY_EXISTS.test(message)) {
|
||||
return branchAlreadyExistsCommitError(message);
|
||||
}
|
||||
|
||||
return createUnexpectedCommitError();
|
||||
return createUnexpectedCommitError(message);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
PERMISSION_CREATE_MR,
|
||||
PERMISSION_PUSH_CODE,
|
||||
} from '../constants';
|
||||
import { addNumericSuffix } from '~/ide/utils';
|
||||
import Api from '~/api';
|
||||
|
||||
export const activeFile = state => state.openFiles.find(file => file.active) || null;
|
||||
|
|
@ -167,10 +168,7 @@ export const getAvailableFileName = (state, getters) => path => {
|
|||
let newPath = path;
|
||||
|
||||
while (getters.entryExists(newPath)) {
|
||||
newPath = newPath.replace(
|
||||
/([ _-]?)(\d*)(\..+?$|$)/,
|
||||
(_, before, number, after) => `${before || '_'}${Number(number) + 1}${after}`,
|
||||
);
|
||||
newPath = addNumericSuffix(newPath);
|
||||
}
|
||||
|
||||
return newPath;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import consts from './constants';
|
|||
import { leftSidebarViews } from '../../../constants';
|
||||
import eventHub from '../../../eventhub';
|
||||
import { parseCommitError } from '../../../lib/errors';
|
||||
import { addNumericSuffix } from '~/ide/utils';
|
||||
|
||||
export const updateCommitMessage = ({ commit }, message) => {
|
||||
commit(types.UPDATE_COMMIT_MESSAGE, message);
|
||||
|
|
@ -17,11 +18,8 @@ export const discardDraft = ({ commit }) => {
|
|||
commit(types.UPDATE_COMMIT_MESSAGE, '');
|
||||
};
|
||||
|
||||
export const updateCommitAction = ({ commit, getters }, commitAction) => {
|
||||
commit(types.UPDATE_COMMIT_ACTION, {
|
||||
commitAction,
|
||||
});
|
||||
commit(types.TOGGLE_SHOULD_CREATE_MR, !getters.shouldHideNewMrOption);
|
||||
export const updateCommitAction = ({ commit }, commitAction) => {
|
||||
commit(types.UPDATE_COMMIT_ACTION, { commitAction });
|
||||
};
|
||||
|
||||
export const toggleShouldCreateMR = ({ commit }) => {
|
||||
|
|
@ -32,6 +30,12 @@ export const updateBranchName = ({ commit }, branchName) => {
|
|||
commit(types.UPDATE_NEW_BRANCH_NAME, branchName);
|
||||
};
|
||||
|
||||
export const addSuffixToBranchName = ({ commit, state }) => {
|
||||
const newBranchName = addNumericSuffix(state.newBranchName, true);
|
||||
|
||||
commit(types.UPDATE_NEW_BRANCH_NAME, newBranchName);
|
||||
};
|
||||
|
||||
export const setLastCommitMessage = ({ commit, rootGetters }, data) => {
|
||||
const { currentProject } = rootGetters;
|
||||
const commitStats = data.stats
|
||||
|
|
@ -107,7 +111,7 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState, rootGetter
|
|||
export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => {
|
||||
// Pull commit options out because they could change
|
||||
// During some of the pre and post commit processing
|
||||
const { shouldCreateMR, isCreatingNewBranch, branchName } = getters;
|
||||
const { shouldCreateMR, shouldHideNewMrOption, isCreatingNewBranch, branchName } = getters;
|
||||
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
|
||||
const stageFilesPromise = rootState.stagedFiles.length
|
||||
? Promise.resolve()
|
||||
|
|
@ -167,7 +171,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
|
|||
commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
|
||||
}, 5000);
|
||||
|
||||
if (shouldCreateMR) {
|
||||
if (shouldCreateMR && !shouldHideNewMrOption) {
|
||||
const { currentProject } = rootGetters;
|
||||
const targetBranch = isCreatingNewBranch
|
||||
? rootState.currentBranchId
|
||||
|
|
|
|||
|
|
@ -10,9 +10,7 @@ export default {
|
|||
Object.assign(state, { commitAction });
|
||||
},
|
||||
[types.UPDATE_NEW_BRANCH_NAME](state, newBranchName) {
|
||||
Object.assign(state, {
|
||||
newBranchName,
|
||||
});
|
||||
Object.assign(state, { newBranchName });
|
||||
},
|
||||
[types.UPDATE_LOADING](state, submitCommitLoading) {
|
||||
Object.assign(state, {
|
||||
|
|
|
|||
|
|
@ -139,6 +139,34 @@ export function getFileEOL(content = '') {
|
|||
return content.includes('\r\n') ? 'CRLF' : 'LF';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or increments the numeric suffix to a filename/branch name.
|
||||
* Retains underscore or dash before the numeric suffix if it already exists.
|
||||
*
|
||||
* Examples:
|
||||
* hello -> hello-1
|
||||
* hello-2425 -> hello-2425
|
||||
* hello.md -> hello-1.md
|
||||
* hello_2.md -> hello_3.md
|
||||
* hello_ -> hello_1
|
||||
* master-patch-22432 -> master-patch-22433
|
||||
* patch_332 -> patch_333
|
||||
*
|
||||
* @param {string} filename File name or branch name
|
||||
* @param {number} [randomize] Should randomize the numeric suffix instead of auto-incrementing?
|
||||
*/
|
||||
export function addNumericSuffix(filename, randomize = false) {
|
||||
return filename.replace(/([ _-]?)(\d*)(\..+?$|$)/, (_, before, number, after) => {
|
||||
const n = randomize
|
||||
? Math.random()
|
||||
.toString()
|
||||
.substring(2, 7)
|
||||
.slice(-5)
|
||||
: Number(number) + 1;
|
||||
return `${before || '-'}${n}${after}`;
|
||||
});
|
||||
}
|
||||
|
||||
export const measurePerformance = (
|
||||
mark,
|
||||
measureName,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
GlEmptyState,
|
||||
} from '@gitlab/ui';
|
||||
import Api from '~/api';
|
||||
import Tracking from '~/tracking';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
|
||||
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
|
||||
|
|
@ -41,6 +42,7 @@ import {
|
|||
TH_SEVERITY_TEST_ID,
|
||||
TH_PUBLISHED_TEST_ID,
|
||||
INCIDENT_DETAILS_PATH,
|
||||
trackIncidentCreateNewOptions,
|
||||
} from '../constants';
|
||||
|
||||
const tdClass =
|
||||
|
|
@ -58,6 +60,7 @@ const initialPaginationState = {
|
|||
};
|
||||
|
||||
export default {
|
||||
trackIncidentCreateNewOptions,
|
||||
i18n: I18N,
|
||||
statusTabs: INCIDENT_STATUS_TABS,
|
||||
fields: [
|
||||
|
|
@ -335,6 +338,11 @@ export default {
|
|||
navigateToIncidentDetails({ iid }) {
|
||||
return visitUrl(joinPaths(this.issuePath, INCIDENT_DETAILS_PATH, iid));
|
||||
},
|
||||
navigateToCreateNewIncident() {
|
||||
const { category, action } = this.$options.trackIncidentCreateNewOptions;
|
||||
Tracking.event(category, action);
|
||||
this.redirecting = true;
|
||||
},
|
||||
handlePageChange(page) {
|
||||
const { startCursor, endCursor } = this.incidents.pageInfo;
|
||||
|
||||
|
|
@ -458,7 +466,7 @@ export default {
|
|||
category="primary"
|
||||
variant="success"
|
||||
:href="newIncidentPath"
|
||||
@click="redirecting = true"
|
||||
@click="navigateToCreateNewIncident"
|
||||
>
|
||||
{{ $options.i18n.createIncidentBtnLabel }}
|
||||
</gl-button>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @gitlab/require-i18n-strings */
|
||||
import { s__, __ } from '~/locale';
|
||||
|
||||
export const I18N = {
|
||||
|
|
@ -34,6 +35,14 @@ export const INCIDENT_STATUS_TABS = [
|
|||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Tracks snowplow event when user clicks create new incident
|
||||
*/
|
||||
export const trackIncidentCreateNewOptions = {
|
||||
category: 'Incident Management',
|
||||
action: 'create_incident_button_clicks',
|
||||
};
|
||||
|
||||
export const DEFAULT_PAGE_SIZE = 20;
|
||||
export const TH_CREATED_AT_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' };
|
||||
export const TH_SEVERITY_TEST_ID = { 'data-testid': 'incident-management-severity-sort' };
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
|
||||
import '~/behaviors/markdown/render_gfm';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
SafeHtml,
|
||||
},
|
||||
props: {
|
||||
issuable: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.renderGFM();
|
||||
},
|
||||
methods: {
|
||||
renderGFM() {
|
||||
$(this.$refs.gfmContainer).renderGFM();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="description">
|
||||
<div ref="gfmContainer" v-safe-html="issuable.descriptionHtml" class="md"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import { GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui';
|
||||
|
||||
import Autosave from '~/autosave';
|
||||
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
||||
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlForm,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
MarkdownField,
|
||||
},
|
||||
props: {
|
||||
issuable: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
enableAutocomplete: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
descriptionPreviewPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
descriptionHelpPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const { title, description } = this.issuable;
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
eventHub.$on('update.issuable', this.resetAutosave);
|
||||
eventHub.$on('close.form', this.resetAutosave);
|
||||
},
|
||||
mounted() {
|
||||
this.initAutosave();
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('update.issuable', this.resetAutosave);
|
||||
eventHub.$off('close.form', this.resetAutosave);
|
||||
},
|
||||
methods: {
|
||||
initAutosave() {
|
||||
const { titleInput, descriptionInput } = this.$refs;
|
||||
|
||||
if (!titleInput || !descriptionInput) return;
|
||||
|
||||
this.autosaveTitle = new Autosave($(titleInput.$el), [
|
||||
document.location.pathname,
|
||||
document.location.search,
|
||||
'title',
|
||||
]);
|
||||
|
||||
this.autosaveDescription = new Autosave($(descriptionInput.$el), [
|
||||
document.location.pathname,
|
||||
document.location.search,
|
||||
'description',
|
||||
]);
|
||||
},
|
||||
resetAutosave() {
|
||||
this.autosaveTitle.reset();
|
||||
this.autosaveDescription.reset();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-form>
|
||||
<gl-form-group
|
||||
data-testid="title"
|
||||
:label="__('Title')"
|
||||
:label-sr-only="true"
|
||||
label-for="issuable-title"
|
||||
class="col-12"
|
||||
>
|
||||
<gl-form-input
|
||||
id="issuable-title"
|
||||
ref="titleInput"
|
||||
v-model.trim="title"
|
||||
:placeholder="__('Title')"
|
||||
:aria-label="__('Title')"
|
||||
:autofocus="true"
|
||||
class="qa-title-input"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group
|
||||
data-testid="description"
|
||||
:label="__('Description')"
|
||||
:label-sr-only="true"
|
||||
label-for="issuable-description"
|
||||
class="col-12 common-note-form"
|
||||
>
|
||||
<markdown-field
|
||||
:markdown-preview-path="descriptionPreviewPath"
|
||||
:markdown-docs-path="descriptionHelpPath"
|
||||
:enable-autocomplete="enableAutocomplete"
|
||||
:textarea-value="description"
|
||||
>
|
||||
<template #textarea>
|
||||
<textarea
|
||||
id="issuable-description"
|
||||
ref="descriptionInput"
|
||||
v-model="description"
|
||||
:data-supports-quick-actions="enableAutocomplete"
|
||||
:aria-label="__('Description')"
|
||||
:placeholder="__('Write a comment or drag your files here…')"
|
||||
class="note-textarea js-gfm-input js-autosize markdown-area
|
||||
qa-description-textarea"
|
||||
dir="auto"
|
||||
></textarea>
|
||||
</template>
|
||||
</markdown-field>
|
||||
</gl-form-group>
|
||||
<div data-testid="actions" class="col-12 gl-mt-3 gl-mb-3 clearfix">
|
||||
<slot
|
||||
name="edit-form-actions"
|
||||
:issuable-title="title"
|
||||
:issuable-description="description"
|
||||
></slot>
|
||||
</div>
|
||||
</gl-form>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
<script>
|
||||
import {
|
||||
GlIcon,
|
||||
GlButton,
|
||||
GlIntersectionObserver,
|
||||
GlTooltipDirective,
|
||||
GlSafeHtmlDirective as SafeHtml,
|
||||
} from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
GlButton,
|
||||
GlIntersectionObserver,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
SafeHtml,
|
||||
},
|
||||
props: {
|
||||
issuable: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
statusBadgeClass: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
statusIcon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
enableEdit: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
stickyTitleVisible: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleTitleAppear() {
|
||||
this.stickyTitleVisible = false;
|
||||
},
|
||||
handleTitleDisappear() {
|
||||
this.stickyTitleVisible = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="title-container">
|
||||
<h2 v-safe-html="issuable.titleHtml" class="title qa-title" dir="auto"></h2>
|
||||
<gl-button
|
||||
v-if="enableEdit"
|
||||
v-gl-tooltip.bottom
|
||||
:title="__('Edit title and description')"
|
||||
icon="pencil"
|
||||
class="btn-edit js-issuable-edit qa-edit-button"
|
||||
@click="$emit('edit-issuable', $event)"
|
||||
/>
|
||||
</div>
|
||||
<gl-intersection-observer @appear="handleTitleAppear" @disappear="handleTitleDisappear">
|
||||
<transition name="issuable-header-slide">
|
||||
<div
|
||||
v-if="stickyTitleVisible"
|
||||
class="issue-sticky-header gl-fixed gl-z-index-3 gl-bg-white gl-border-1 gl-border-b-solid gl-border-b-gray-100 gl-py-3"
|
||||
data-testid="header"
|
||||
>
|
||||
<div
|
||||
class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-mx-auto gl-px-5"
|
||||
>
|
||||
<p
|
||||
data-testid="status"
|
||||
class="issuable-status-box status-box gl-my-0"
|
||||
:class="statusBadgeClass"
|
||||
>
|
||||
<gl-icon :name="statusIcon" class="gl-display-block d-sm-none gl-h-6!" />
|
||||
<span class="gl-display-none d-sm-block"><slot name="status-badge"></slot></span>
|
||||
</p>
|
||||
<p
|
||||
class="gl-font-weight-bold gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis gl-my-0"
|
||||
:title="issuable.title"
|
||||
>
|
||||
{{ issuable.title }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</gl-intersection-observer>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
export default createEventHub();
|
||||
|
|
@ -14,12 +14,12 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapState(['composerHelpPath']),
|
||||
...mapGetters(['composerRegistryInclude', 'composerPackageInclude']),
|
||||
...mapGetters(['composerRegistryInclude', 'composerPackageInclude', 'groupExists']),
|
||||
},
|
||||
i18n: {
|
||||
registryInclude: s__('PackageRegistry|composer.json registry include'),
|
||||
registryInclude: s__('PackageRegistry|Add composer registry'),
|
||||
copyRegistryInclude: s__('PackageRegistry|Copy registry include'),
|
||||
packageInclude: s__('PackageRegistry|composer.json require package include'),
|
||||
packageInclude: s__('PackageRegistry|Install package version'),
|
||||
copyPackageInclude: s__('PackageRegistry|Copy require package include'),
|
||||
infoLine: s__(
|
||||
'PackageRegistry|For more information on Composer packages in GitLab, %{linkStart}see the documentation.%{linkEnd}',
|
||||
|
|
@ -32,31 +32,33 @@ export default {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<h3 class="gl-font-lg">{{ __('Installation') }}</h3>
|
||||
<div v-if="groupExists">
|
||||
<h3 class="gl-font-lg">{{ __('Installation') }}</h3>
|
||||
|
||||
<code-instruction
|
||||
:label="$options.i18n.registryInclude"
|
||||
:instruction="composerRegistryInclude"
|
||||
:copy-text="$options.i18n.copyRegistryInclude"
|
||||
:tracking-action="$options.trackingActions.COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND"
|
||||
:tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
|
||||
data-testid="registry-include"
|
||||
/>
|
||||
<code-instruction
|
||||
:label="$options.i18n.registryInclude"
|
||||
:instruction="composerRegistryInclude"
|
||||
:copy-text="$options.i18n.copyRegistryInclude"
|
||||
:tracking-action="$options.trackingActions.COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND"
|
||||
:tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
|
||||
data-testid="registry-include"
|
||||
/>
|
||||
|
||||
<code-instruction
|
||||
:label="$options.i18n.packageInclude"
|
||||
:instruction="composerPackageInclude"
|
||||
:copy-text="$options.i18n.copyPackageInclude"
|
||||
:tracking-action="$options.trackingActions.COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND"
|
||||
:tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
|
||||
data-testid="package-include"
|
||||
/>
|
||||
<span data-testid="help-text">
|
||||
<gl-sprintf :message="$options.i18n.infoLine">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="composerHelpPath" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
<code-instruction
|
||||
:label="$options.i18n.packageInclude"
|
||||
:instruction="composerPackageInclude"
|
||||
:copy-text="$options.i18n.copyPackageInclude"
|
||||
:tracking-action="$options.trackingActions.COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND"
|
||||
:tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
|
||||
data-testid="package-include"
|
||||
/>
|
||||
<span data-testid="help-text">
|
||||
<gl-sprintf :message="$options.i18n.infoLine">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="composerHelpPath" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -102,11 +102,12 @@ repository = ${pypiSetupPath}
|
|||
username = __token__
|
||||
password = <your personal access token>`;
|
||||
|
||||
export const composerRegistryInclude = ({ composerPath }) => {
|
||||
const base = { type: 'composer', url: composerPath };
|
||||
return JSON.stringify(base);
|
||||
};
|
||||
export const composerPackageInclude = ({ packageEntity }) => {
|
||||
const base = { [packageEntity.name]: packageEntity.version };
|
||||
return JSON.stringify(base);
|
||||
};
|
||||
export const composerRegistryInclude = ({ composerPath, composerConfigRepositoryName }) =>
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
`composer config repositories.${composerConfigRepositoryName} '{"type": "composer", "url": "${composerPath}"}'`;
|
||||
|
||||
export const composerPackageInclude = ({ packageEntity }) =>
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
`composer req ${[packageEntity.name]}:${packageEntity.version}`;
|
||||
|
||||
export const groupExists = ({ groupListUrl }) => groupListUrl.length > 0;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,12 @@
|
|||
<script>
|
||||
import { GlButton, GlFormSelect, GlToggle, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
name: 'ServiceDeskSetting',
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
components: {
|
||||
ClipboardButton,
|
||||
GlButton,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
/* eslint-disable vue/no-v-html */
|
||||
import { GlTooltipDirective, GlLink, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui';
|
||||
import defaultAvatarUrl from 'images/no_avatar.png';
|
||||
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
|
||||
import { sprintf, s__ } from '~/locale';
|
||||
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
|
||||
|
|
@ -9,7 +10,6 @@ import CiIcon from '../../vue_shared/components/ci_icon.vue';
|
|||
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
|
||||
import getRefMixin from '../mixins/get_ref';
|
||||
import projectPathQuery from '../queries/project_path.query.graphql';
|
||||
import pathLastCommitQuery from '../queries/path_last_commit.query.graphql';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
import PathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
|
||||
import { escapeFileUrl, joinPaths, webIDEUrl } from '../lib/utils/url_utility';
|
||||
import createRouter from './router';
|
||||
import App from './components/app.vue';
|
||||
|
|
@ -18,6 +19,10 @@ export default function setupVueRepositoryList() {
|
|||
const { dataset } = el;
|
||||
const { projectPath, projectShortPath, ref, escapedRef, fullName } = dataset;
|
||||
const router = createRouter(projectPath, escapedRef);
|
||||
const pathRegex = /-\/tree\/[^/]+\/(.+$)/;
|
||||
const matches = window.location.href.match(pathRegex);
|
||||
|
||||
const currentRoutePath = matches ? matches[1] : '';
|
||||
|
||||
apolloProvider.clients.defaultClient.cache.writeData({
|
||||
data: {
|
||||
|
|
@ -29,6 +34,43 @@ export default function setupVueRepositoryList() {
|
|||
},
|
||||
});
|
||||
|
||||
const initLastCommitApp = () =>
|
||||
new Vue({
|
||||
el: document.getElementById('js-last-commit'),
|
||||
router,
|
||||
apolloProvider,
|
||||
render(h) {
|
||||
return h(LastCommit, {
|
||||
props: {
|
||||
currentPath: this.$route.params.path,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (window.gl.startup_graphql_calls) {
|
||||
const query = window.gl.startup_graphql_calls.find(
|
||||
call => call.operationName === 'pathLastCommit',
|
||||
);
|
||||
query.fetchCall
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
apolloProvider.clients.defaultClient.writeQuery({
|
||||
query: PathLastCommitQuery,
|
||||
data: res.data,
|
||||
variables: {
|
||||
projectPath,
|
||||
ref,
|
||||
path: currentRoutePath,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => initLastCommitApp());
|
||||
} else {
|
||||
initLastCommitApp();
|
||||
}
|
||||
|
||||
router.afterEach(({ params: { path } }) => {
|
||||
setTitle(path, ref, fullName);
|
||||
});
|
||||
|
|
@ -77,20 +119,6 @@ export default function setupVueRepositoryList() {
|
|||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el: document.getElementById('js-last-commit'),
|
||||
router,
|
||||
apolloProvider,
|
||||
render(h) {
|
||||
return h(LastCommit, {
|
||||
props: {
|
||||
currentPath: this.$route.params.path,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const treeHistoryLinkEl = document.getElementById('js-tree-history-link');
|
||||
const { historyLink } = treeHistoryLinkEl.dataset;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_
|
|||
import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
|
||||
import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/approvals/approvals.vue';
|
||||
import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
|
||||
import { GlSafeHtmlDirective } from '@gitlab/ui';
|
||||
import { sprintf, s__, __ } from '~/locale';
|
||||
import Project from '~/pages/projects/project';
|
||||
import SmartInterval from '~/smart_interval';
|
||||
|
|
@ -52,6 +53,9 @@ export default {
|
|||
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
name: 'MRWidget',
|
||||
directives: {
|
||||
SafeHtml: GlSafeHtmlDirective,
|
||||
},
|
||||
components: {
|
||||
Loading,
|
||||
'mr-widget-header': WidgetHeader,
|
||||
|
|
@ -510,7 +514,7 @@ export default {
|
|||
</mr-widget-alert-message>
|
||||
|
||||
<mr-widget-alert-message v-if="mr.mergeError" type="danger">
|
||||
{{ mergeError }}
|
||||
<span v-safe-html="mergeError"></span>
|
||||
</mr-widget-alert-message>
|
||||
|
||||
<source-branch-removal-status v-if="shouldRenderSourceBranchRemovalStatus" />
|
||||
|
|
|
|||
|
|
@ -1,19 +1,15 @@
|
|||
<script>
|
||||
import { isString } from 'lodash';
|
||||
import {
|
||||
GlDeprecatedDropdown,
|
||||
GlDeprecatedDropdownDivider,
|
||||
GlDeprecatedDropdownItem,
|
||||
} from '@gitlab/ui';
|
||||
import { GlDropdown, GlDropdownDivider, GlDropdownItem } from '@gitlab/ui';
|
||||
|
||||
const isValidItem = item =>
|
||||
isString(item.eventName) && isString(item.title) && isString(item.description);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDeprecatedDropdown,
|
||||
GlDeprecatedDropdownDivider,
|
||||
GlDeprecatedDropdownItem,
|
||||
GlDropdown,
|
||||
GlDropdownDivider,
|
||||
GlDropdownItem,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
|
@ -32,7 +28,7 @@ export default {
|
|||
variant: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'secondary',
|
||||
default: 'default',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -61,8 +57,8 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<gl-deprecated-dropdown
|
||||
:menu-class="`dropdown-menu-selectable ${menuClass}`"
|
||||
<gl-dropdown
|
||||
:menu-class="menuClass"
|
||||
split
|
||||
:text="dropdownToggleText"
|
||||
:variant="variant"
|
||||
|
|
@ -70,20 +66,20 @@ export default {
|
|||
@click="triggerEvent"
|
||||
>
|
||||
<template v-for="(item, itemIndex) in actionItems">
|
||||
<gl-deprecated-dropdown-item
|
||||
<gl-dropdown-item
|
||||
:key="item.eventName"
|
||||
:active="selectedItem === item"
|
||||
active-class="is-active"
|
||||
:is-check-item="true"
|
||||
:is-checked="selectedItem === item"
|
||||
@click="changeSelectedItem(item)"
|
||||
>
|
||||
<strong>{{ item.title }}</strong>
|
||||
<div>{{ item.description }}</div>
|
||||
</gl-deprecated-dropdown-item>
|
||||
</gl-dropdown-item>
|
||||
|
||||
<gl-deprecated-dropdown-divider
|
||||
<gl-dropdown-divider
|
||||
v-if="itemIndex < actionItems.length - 1"
|
||||
:key="`${item.eventName}-divider`"
|
||||
/>
|
||||
</template>
|
||||
</gl-deprecated-dropdown>
|
||||
</gl-dropdown>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@
|
|||
@import './pages/detail_page';
|
||||
@import './pages/editor';
|
||||
@import './pages/environment_logs';
|
||||
@import './pages/error_list';
|
||||
@import './pages/error_tracking_list';
|
||||
@import './pages/events';
|
||||
@import './pages/experience_level';
|
||||
@import './pages/experimental_separate_sign_up';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
@import 'page_bundles/mixins_and_variables_and_functions';
|
||||
|
||||
.error-list {
|
||||
.dropdown {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.sort-control {
|
||||
.btn {
|
||||
padding-right: 2rem;
|
||||
|
|
@ -17,7 +23,7 @@
|
|||
min-height: 68px;
|
||||
|
||||
&:last-child {
|
||||
background-color: $gray-10;
|
||||
background-color: var(--gray-10, $gray-10);
|
||||
|
||||
&::before {
|
||||
content: none !important;
|
||||
|
|
@ -226,6 +226,14 @@ $colors: (
|
|||
.solarized-dark {
|
||||
@include color-scheme('solarized-dark'); }
|
||||
|
||||
.none {
|
||||
.line_content.header {
|
||||
button {
|
||||
color: $gray-900;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.diff-wrap-lines .line_content {
|
||||
white-space: normal;
|
||||
min-height: 19px;
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
.error-list {
|
||||
.dropdown {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
|
||||
project(fullPath: $projectPath) {
|
||||
__typename
|
||||
repository {
|
||||
__typename
|
||||
tree(path: $path, ref: $ref) {
|
||||
__typename
|
||||
lastCommit {
|
||||
__typename
|
||||
sha
|
||||
title
|
||||
titleHtml
|
||||
|
|
@ -13,15 +17,20 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
|
|||
authorName
|
||||
authorGravatar
|
||||
author {
|
||||
__typename
|
||||
name
|
||||
avatarUrl
|
||||
webPath
|
||||
}
|
||||
signatureHtml
|
||||
pipelines(ref: $ref, first: 1) {
|
||||
__typename
|
||||
edges {
|
||||
__typename
|
||||
node {
|
||||
__typename
|
||||
detailedStatus {
|
||||
__typename
|
||||
detailsPath
|
||||
icon
|
||||
tooltip
|
||||
|
|
@ -34,6 +34,10 @@ module PackagesHelper
|
|||
expose_url(api_v4_group___packages_composer_packages_path(id: group_id, format: '.json'))
|
||||
end
|
||||
|
||||
def composer_config_repository_name(group_id)
|
||||
"#{Gitlab.config.gitlab.host}/#{group_id}"
|
||||
end
|
||||
|
||||
def packages_list_data(type, resource)
|
||||
{
|
||||
resource_id: resource.id,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module StartupjsHelper
|
||||
def page_startup_graphql_calls
|
||||
@graphql_startup_calls
|
||||
end
|
||||
|
||||
def add_page_startup_graphql_call(query, variables = {})
|
||||
@graphql_startup_calls ||= []
|
||||
file_location = File.join(Rails.root, "app/graphql/queries/#{query}.query.graphql")
|
||||
|
||||
return unless File.exist?(file_location)
|
||||
|
||||
query_str = File.read(file_location)
|
||||
@graphql_startup_calls << { query: query_str, variables: variables }
|
||||
end
|
||||
end
|
||||
|
|
@ -74,8 +74,8 @@ class Commit
|
|||
sha[0..MIN_SHA_LENGTH]
|
||||
end
|
||||
|
||||
def diff_safe_lines
|
||||
Gitlab::Git::DiffCollection.default_limits[:max_lines]
|
||||
def diff_safe_lines(project: nil)
|
||||
Gitlab::Git::DiffCollection.default_limits(project: project)[:max_lines]
|
||||
end
|
||||
|
||||
def diff_hard_limit_files(project: nil)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ module Members
|
|||
def execute(source)
|
||||
return error(s_('AddMember|No users specified.')) if params[:user_ids].blank?
|
||||
|
||||
user_ids = params[:user_ids].split(',').uniq
|
||||
user_ids = params[:user_ids].split(',').uniq.flatten
|
||||
|
||||
return error(s_("AddMember|Too many users specified (limit is %{user_limit})") % { user_limit: user_limit }) if
|
||||
user_limit && user_ids.size > user_limit
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
- @metric.cards.each do |card|
|
||||
= render 'card', card: card
|
||||
|
||||
.devops-steps.d-none.d-lg-block.d-xl-block
|
||||
.devops-steps.d-none.d-lg-block
|
||||
- @metric.idea_to_production_steps.each_with_index do |step, index|
|
||||
.devops-step{ class: "devops-#{score_level(step.percentage_score)}-score" }
|
||||
= custom_icon("i2p_step_#{index + 1}")
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
- return unless page_startup_api_calls.present?
|
||||
- return unless page_startup_api_calls.present? || page_startup_graphql_calls.present?
|
||||
|
||||
= javascript_tag nonce: true do
|
||||
:plain
|
||||
var gl = window.gl || {};
|
||||
gl.startup_calls = #{page_startup_api_calls.to_json};
|
||||
gl.startup_graphql_calls = #{page_startup_graphql_calls.to_json};
|
||||
|
||||
if (gl.startup_calls && window.fetch) {
|
||||
Object.keys(gl.startup_calls).forEach(apiCall => {
|
||||
// fetch won’t send cookies in older browsers, unless you set the credentials init option.
|
||||
|
|
@ -14,3 +16,21 @@
|
|||
};
|
||||
});
|
||||
}
|
||||
if (gl.startup_graphql_calls && window.fetch) {
|
||||
const url = `#{api_graphql_url}`
|
||||
|
||||
const opts = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", 'X-CSRF-Token': "#{form_authenticity_token}" },
|
||||
};
|
||||
|
||||
gl.startup_graphql_calls = gl.startup_graphql_calls.map(call => ({
|
||||
operationName: call.query.match(/^query (.+)\(/)[1],
|
||||
fetchCall: fetch(url, {
|
||||
...opts,
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify(call)
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
- too_big = diff_file.diff_lines.count > Commit.diff_safe_lines
|
||||
- too_big = diff_file.diff_lines.count > Commit.diff_safe_lines(project: @project)
|
||||
- if too_big
|
||||
.suppressed-container
|
||||
%a.show-suppressed-diff.cursor-pointer.js-show-suppressed-diff= _("Changes suppressed. Click to show.")
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
- page_title _('Errors')
|
||||
- add_page_specific_style 'page_bundles/error_tracking_index'
|
||||
|
||||
#js-error_tracking{ data: error_tracking_data(@current_user, @project) }
|
||||
|
|
|
|||
|
|
@ -24,4 +24,5 @@
|
|||
composer_help_path: help_page_path('user/packages/composer_repository/index'),
|
||||
project_name: @project.name,
|
||||
project_list_url: project_packages_path(@project),
|
||||
group_list_url: @project.group ? group_packages_path(@project.group) : ''} }
|
||||
group_list_url: @project.group ? group_packages_path(@project.group) : '',
|
||||
composer_config_repository_name: composer_config_repository_name(@project.group&.id)} }
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
- current_route_path = request.fullpath.match(/-\/tree\/[^\/]+\/(.+$)/).to_a[1]
|
||||
- add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, currentRoutePath: current_route_path })
|
||||
- breadcrumb_title _("Repository")
|
||||
- @content_class = "limit-container-width" unless fluid_layout
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Improve WebIDE error messages on committing
|
||||
merge_request: 43408
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Improve the Commit box on the Merge Request Changs tab when browsing per commit
|
||||
merge_request: 43613
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix merge conflict button text if "None" code style selected
|
||||
merge_request: 44427
|
||||
author: David Barr @davebarr
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove duplicated BS display properties from Admin DevOps report' HAML
|
||||
merge_request: 44846
|
||||
author: Takuya Noguchi
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Replace `GlDeprecatedDropdown` with `GlDropdown` in app/assets/javascripts/vue_shared/components/split_button.vue
|
||||
merge_request: 41433
|
||||
author: nuwe1
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix unnecessarily escaped merge error text
|
||||
merge_request: 44844
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update Add Members API to accept user_id array
|
||||
merge_request: 44051
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -178,6 +178,7 @@ module Gitlab
|
|||
config.assets.precompile << "page_bundles/dev_ops_report.css"
|
||||
config.assets.precompile << "page_bundles/environments.css"
|
||||
config.assets.precompile << "page_bundles/error_tracking_details.css"
|
||||
config.assets.precompile << "page_bundles/error_tracking_index.css"
|
||||
config.assets.precompile << "page_bundles/ide.css"
|
||||
config.assets.precompile << "page_bundles/issues_list.css"
|
||||
config.assets.precompile << "page_bundles/jira_connect.css"
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ const alias = {
|
|||
vue$: 'vue/dist/vue.esm.js',
|
||||
spec: path.join(ROOT_PATH, 'spec/javascripts'),
|
||||
jest: path.join(ROOT_PATH, 'spec/frontend'),
|
||||
shared_queries: path.join(ROOT_PATH, 'app/graphql/queries'),
|
||||
|
||||
// the following resolves files which are different between CE and EE
|
||||
ee_else_ce: path.join(ROOT_PATH, 'app/assets/javascripts'),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
gitlab_danger = GitlabDanger.new(helper.gitlab_helper)
|
||||
|
||||
TEMPLATE_MESSAGE = <<~MSG
|
||||
This merge request requires a CI/CD Template review. To make sure these
|
||||
changes are reviewed, take the following steps:
|
||||
|
||||
1. Ensure the merge request has the ~"ci::templates" label.
|
||||
If the merge request modifies CI/CD Template files, Danger will do this for you.
|
||||
1. Prepare your MR for a CI/CD Template review according to the
|
||||
[template development guide](https://docs.gitlab.com/ee/development/cicd/templates.html).
|
||||
1. Assign and `@` mention the CI/CD Template reviewer suggested by Reviewer Roulette.
|
||||
MSG
|
||||
|
||||
TEMPLATE_FILES_MESSAGE = <<~MSG
|
||||
The following files require a review from the CI/CD Templates maintainers:
|
||||
MSG
|
||||
|
||||
return unless gitlab_danger.ci?
|
||||
|
||||
template_paths_to_review = helper.changes_by_category[:ci_template]
|
||||
|
||||
if gitlab.mr_labels.include?('ci::templates') || template_paths_to_review.any?
|
||||
message 'This merge request adds or changes files that require a ' \
|
||||
'review from the CI/CD Templates maintainers.'
|
||||
|
||||
markdown(TEMPLATE_MESSAGE)
|
||||
markdown(TEMPLATE_FILES_MESSAGE + helper.markdown_list(template_paths_to_review)) if template_paths_to_review.any?
|
||||
end
|
||||
|
|
@ -10,7 +10,8 @@ SPECIALIZATIONS = {
|
|||
frontend: 'frontend',
|
||||
docs: 'documentation',
|
||||
qa: 'QA',
|
||||
engineering_productivity: 'Engineering Productivity'
|
||||
engineering_productivity: 'Engineering Productivity',
|
||||
ci_template: 'ci::templates'
|
||||
}.freeze
|
||||
|
||||
labels_to_add = helper.changes_by_category.each_with_object([]) do |(category, _changes), memo|
|
||||
|
|
|
|||
|
|
@ -290,7 +290,7 @@ POST /projects/:id/members
|
|||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `user_id` | integer | yes | The user ID of the new member |
|
||||
| `user_id` | integer/string | yes | The user ID of the new member or multiple IDs separated by commas |
|
||||
| `access_level` | integer | yes | A valid access level |
|
||||
| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
|
||||
|
||||
|
|
|
|||
|
|
@ -163,6 +163,8 @@ have permission to run CI/CD pipelines against the protected branch, the pipelin
|
|||
|
||||
### Passing variables to a downstream pipeline
|
||||
|
||||
#### With the `variables` keyword
|
||||
|
||||
Sometimes you might want to pass variables to a downstream pipeline.
|
||||
You can do that using the `variables` keyword, just like you would when
|
||||
defining a regular job.
|
||||
|
|
@ -216,6 +218,46 @@ Upstream pipelines take precedence over downstream ones. If there are two
|
|||
variables with the same name defined in both upstream and downstream projects,
|
||||
the ones defined in the upstream project will take precedence.
|
||||
|
||||
#### With variable inheritance
|
||||
|
||||
You can pass variables to a downstream pipeline with [`dotenv` variable inheritance](variables/README.md#inherit-environment-variables) and [cross project artifact downloads](yaml/README.md#cross-project-artifact-downloads-with-needs).
|
||||
|
||||
In the upstream pipeline:
|
||||
|
||||
1. Save the variables in a `.env` file.
|
||||
1. Save the `.env` file as a `dotenv` report.
|
||||
1. Trigger the downstream pipeline.
|
||||
|
||||
```yaml
|
||||
build_vars:
|
||||
stage: build
|
||||
script:
|
||||
- echo "BUILD_VERSION=hello" >> build.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: build.env
|
||||
|
||||
deploy:
|
||||
stage: deploy
|
||||
trigger: my/downstream_project
|
||||
```
|
||||
|
||||
Set the `test` job in the downstream pipeline to inherit the variables from the `build_vars`
|
||||
job in the upstream project with `needs:`. The `test` job inherits the variables in the
|
||||
`dotenv` report and it can access `BUILD_VERSION` in the script:
|
||||
|
||||
```yaml
|
||||
test:
|
||||
stage: test
|
||||
script:
|
||||
- echo $BUILD_VERSION
|
||||
needs:
|
||||
- project: my/upstream_project
|
||||
job: build_vars
|
||||
ref: master
|
||||
artifacts: true
|
||||
```
|
||||
|
||||
### Mirroring status from triggered pipeline
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11238) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3.
|
||||
|
|
|
|||
|
|
@ -194,3 +194,7 @@ To disable it:
|
|||
```ruby
|
||||
Feature.disable(:ci_child_of_child_pipeline)
|
||||
```
|
||||
|
||||
## Pass variables to a child pipeline
|
||||
|
||||
You can [pass variables to a downstream pipeline](multi_project_pipelines.md#passing-variables-to-a-downstream-pipeline).
|
||||
|
|
|
|||
|
|
@ -323,19 +323,25 @@ to work on a different **branch**.
|
|||
|
||||
When you create a branch in a Git repository, you make a copy of its files at the time of branching. You're free
|
||||
to do whatever you want with the code in your branch without impacting the main branch or other branches. And when
|
||||
you're ready to bring your changes to the main codebase, you can merge your branch into the main one
|
||||
you're ready to bring your changes to the main codebase, you can merge your branch into the default branch
|
||||
used in your project (such as `master`).
|
||||
|
||||
A new branch is often called **feature branch** to differentiate from the
|
||||
**default branch**.
|
||||
|
||||
### Create a branch
|
||||
|
||||
To create a new branch, to work from without affecting the `master` branch, type
|
||||
the following (spaces won't be recognized in the branch name, so you will need to
|
||||
use a hyphen or underscore):
|
||||
To create a new feature branch and work from without affecting the `master`
|
||||
branch:
|
||||
|
||||
```shell
|
||||
git checkout -b <name-of-branch>
|
||||
```
|
||||
|
||||
Note that Git does **not** accept empty spaces and special characters in branch
|
||||
names, so use only lowercase letters, numbers, hyphens (`-`), and underscores
|
||||
(`_`). Do not use capital letters, as it may cause duplications.
|
||||
|
||||
### Switch to the master branch
|
||||
|
||||
You are always in a branch when working with Git. The main branch is the master
|
||||
|
|
@ -411,6 +417,9 @@ For example, to push your local commits to the _`master`_ branch of the _`origin
|
|||
git push origin master
|
||||
```
|
||||
|
||||
On certain occasions, Git won't allow you to push to your repository, and then
|
||||
you'll need to [force an update](../topics/git/git_rebase.md#force-push).
|
||||
|
||||
NOTE: **Note:**
|
||||
To create a merge request from a fork to an upstream repository, see the
|
||||
[forking workflow](../user/project/repository/forking_workflow.md).
|
||||
|
|
@ -459,6 +468,10 @@ git checkout <name-of-branch>
|
|||
git merge master
|
||||
```
|
||||
|
||||
## Advanced use of Git through the command line
|
||||
|
||||
For an introduction of more advanced Git techniques, see [Git rebase, force-push, and merge conflicts](../topics/git/git_rebase.md).
|
||||
|
||||
## Synchronize changes in a forked repository with the upstream
|
||||
|
||||
[Forking a repository](../user/project/repository/forking_workflow.md) lets you create
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: Create
|
||||
group: Source Code
|
||||
stage: Manage
|
||||
group: Access
|
||||
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers"
|
||||
type: howto, reference
|
||||
---
|
||||
|
|
|
|||
|
|
@ -150,10 +150,11 @@ _true up_ process.
|
|||
|
||||
### Renew or change a GitLab.com subscription
|
||||
|
||||
NOTE: **Note:**
|
||||
To renew for more users than are currently active in your GitLab.com plan,
|
||||
contact our sales team via `renewals@gitlab.com` for assistance as this can't be
|
||||
done in the Customers Portal.
|
||||
You can adjust the number of users before renewing your GitLab.com subscription.
|
||||
|
||||
- To renew for more users than are currently included in your GitLab.com plan, [add users to your subscription](#add-users-to-your-subscription).
|
||||
- To renew for fewer users than are currently included in your GitLab.com plan,
|
||||
either [disable](../../user/admin_area/activating_deactivating_users.md#deactivating-a-user) or [block](../../user/admin_area/blocking_unblocking_users.md#blocking-a-user) the user accounts you no longer need.
|
||||
|
||||
For details on upgrading your subscription tier, see
|
||||
[Upgrade your GitLab.com subscription tier](#upgrade-your-gitlabcom-subscription-tier).
|
||||
|
|
|
|||
|
|
@ -0,0 +1,272 @@
|
|||
---
|
||||
stage: Create
|
||||
group: Source Code
|
||||
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers"
|
||||
type: concepts, howto
|
||||
description: "Introduction to Git rebase, force-push, and resolving merge conflicts through the command line."
|
||||
---
|
||||
|
||||
# Introduction to Git rebase, force-push, and merge conflicts
|
||||
|
||||
This guide helps you to get started with rebasing, force-pushing, and fixing
|
||||
merge conflicts locally.
|
||||
|
||||
Before diving into this document, make sure you are familiar with using
|
||||
[Git through the command line](../../gitlab-basics/start-using-git.md).
|
||||
|
||||
## Git rebase
|
||||
|
||||
[Rebasing](https://git-scm.com/docs/git-rebase) is a very common operation in
|
||||
Git. There are the following rebase options:
|
||||
|
||||
- [Regular rebase](#regular-rebase).
|
||||
- [Interactive rebase](#interactive-rebase).
|
||||
|
||||
### Before rebasing
|
||||
|
||||
CAUTION: **Warning:**
|
||||
`git rebase` rewrites the commit history. It **can be harmful** to do it in
|
||||
shared branches. It can cause complex and hard to resolve merge conflicts. In
|
||||
these cases, instead of rebasing your branch against the default branch,
|
||||
consider pulling it instead (`git pull origin master`). It has a similar
|
||||
effect without compromising the work of your contributors.
|
||||
|
||||
It's safer to back up your branch before rebasing to make sure you don't lose
|
||||
any changes. For example, consider a [feature branch](../../gitlab-basics/start-using-git.md#branching)
|
||||
called `my-feature-branch`:
|
||||
|
||||
1. Open your feature branch in the terminal:
|
||||
|
||||
```shell
|
||||
git checkout my-feature-branch
|
||||
```
|
||||
|
||||
1. Checkout a new branch from it:
|
||||
|
||||
```shell
|
||||
git checkout -b my-feature-branch-backup
|
||||
```
|
||||
|
||||
1. Go back to your original branch:
|
||||
|
||||
```shell
|
||||
git checkout my-feature-branch
|
||||
```
|
||||
|
||||
Now you can safely rebase it. If anything goes wrong, you can recover your
|
||||
changes by resetting `my-feature-branch` against `my-feature-branch-backup`:
|
||||
|
||||
1. Make sure you're in the correct branch (`my-feature-branch`):
|
||||
|
||||
```shell
|
||||
git checkout my-feature-branch
|
||||
```
|
||||
|
||||
1. Reset it against `my-feature-branch-backup`:
|
||||
|
||||
```shell
|
||||
git reset --hard my-feature-branch-backup
|
||||
```
|
||||
|
||||
Note that if you added changes to `my-feature-branch` after creating the backup branch,
|
||||
you will lose them when resetting.
|
||||
|
||||
### Regular rebase
|
||||
|
||||
With a regular rebase you can update your feature branch with the default
|
||||
branch (or any other branch).
|
||||
This is an important step for Git-based development strategies. You can
|
||||
ensure that the changes you're adding to the codebase do not break any
|
||||
existing changes added to the target branch _after_ you created your feature
|
||||
branch.
|
||||
|
||||
For example, to update your branch `my-feature-branch` with `master`:
|
||||
|
||||
1. Fetch the latest changes from `master`:
|
||||
|
||||
```shell
|
||||
git fetch origin master
|
||||
```
|
||||
|
||||
1. Checkout your feature branch:
|
||||
|
||||
```shell
|
||||
git checkout my-feature-branch
|
||||
```
|
||||
|
||||
1. Rebase it against `master`:
|
||||
|
||||
```shell
|
||||
git rebase origin/master
|
||||
```
|
||||
|
||||
1. [Force-push](#force-push) to your branch.
|
||||
|
||||
When you rebase:
|
||||
|
||||
1. Git imports all the commits submitted to `master` _after_ the
|
||||
moment you created your feature branch until the present moment.
|
||||
1. Git puts the commits you have in your feature branch on top of all
|
||||
the commits imported from `master`:
|
||||
|
||||

|
||||
|
||||
You can replace `master` with any other branch you want to rebase against, for
|
||||
example, `release-10-3`. You can also replace `origin` with other remote
|
||||
repositories, for example, `upstream`. To check what remotes you have linked to your local
|
||||
repository, you can run `git remote -v`.
|
||||
|
||||
If there are [merge conflicts](#merge-conflicts), Git will prompt you to fix
|
||||
them before continuing the rebase.
|
||||
|
||||
To learn more, check Git's documentation on [rebasing](ttps://git-scm.com/book/en/v2/Git-Branching-Rebasing)
|
||||
and [rebasing strategies](https://git-scm.com/book/en/v2/Git-Branching-Rebasing).
|
||||
|
||||
### Interactive rebase
|
||||
|
||||
You can use interactive rebase to modify commits. For example, amend a commit
|
||||
message, squash (join multiple commits into one), edit, or delete
|
||||
commits. It is handy for changing past commit messages,
|
||||
as well as for organizing the commit history of your branch to keep it clean.
|
||||
|
||||
TIP: **Tip:**
|
||||
If you want to keep the default branch commit history clean, you don't need to
|
||||
manually squash all your commits before merging every merge request;
|
||||
with [Squash and Merge](../../user/project/merge_requests/squash_and_merge.md)
|
||||
GitLab does it automatically.
|
||||
|
||||
When you want to change anything in recent commits, use interactive
|
||||
rebase by passing the flag `--interactive` (or `-i`) to the rebase command.
|
||||
|
||||
For example, if you want to edit the last three commits in your branch
|
||||
(`HEAD~3`), run:
|
||||
|
||||
```shell
|
||||
git rebase -i HEAD~3
|
||||
```
|
||||
|
||||
Git opens the last three commits in your terminal text editor and describes
|
||||
all the interactive rebase options you can use. The default option is `pick`,
|
||||
which maintains the commit unchanged. Replace the keyword `pick` according to
|
||||
the operation you want to perform in each commit. To do so, you need to edit
|
||||
the commits in your terminal's text editor.
|
||||
|
||||
For example, if you're using [Vim](https://www.vim.org/) as the text editor in
|
||||
a macOS's `ZSH` shell, and you want to **squash** all the three commits
|
||||
(join them into one):
|
||||
|
||||
1. Press <kbd>i</kbd> on your keyboard to switch to Vim's editing mode.
|
||||
1. Navigate with your keyboard arrows to edit the **second** commit keyword
|
||||
from `pick` to `squash` (or `s`). Do the same to the **third** commit.
|
||||
The first commit should be left **unchanged** (`pick`) as we want to squash
|
||||
the second and third into the first.
|
||||
1. Press <kbd>Esc</kbd> to leave the editing mode.
|
||||
1. Type `:wq` to "write" (save) and "quit".
|
||||
1. Git outputs the commit message so you have a chance to edit it:
|
||||
- All lines starting with `#` will be ignored and not included in the commit
|
||||
message. Everything else will be included.
|
||||
- To leave it as it is, type `:wq`. To edit the commit message: switch to the
|
||||
editing mode, edit the commit message, and save it as you just did.
|
||||
1. If you haven't pushed your commits to the remote branch before rebasing,
|
||||
push your changes normally. If you had pushed these commits already,
|
||||
[force-push](#force-push) instead.
|
||||
|
||||
Note that the steps for editing through the command line can be slightly
|
||||
different depending on your operating system and the shell you're using.
|
||||
|
||||
See [Numerous undo possibilities in Git](numerous_undo_possibilities_in_git/index.md#with-history-modification)
|
||||
for a deeper look into interactive rebase.
|
||||
|
||||
## Force-push
|
||||
|
||||
When you perform more complex operations, for example, squash commits, reset or
|
||||
rebase your branch, you'll have to _force_ an update to the remote branch,
|
||||
since these operations imply rewriting the commit history of the branch.
|
||||
To force an update, pass the flag `--force` or `-f` to the `push` command. For
|
||||
example:
|
||||
|
||||
```shell
|
||||
git push --force origin my-feature-branch
|
||||
```
|
||||
|
||||
Forcing an update is **not** recommended when you're working on shared
|
||||
branches.
|
||||
|
||||
Alternatively, you can pass the flag [`--force-with-lease`](https://git-scm.com/docs/git-push#Documentation/git-push.txt---force-with-leaseltrefnamegt)
|
||||
instead. It is safer, as it does not overwrite any work on the remote
|
||||
branch if more commits were added to the remote branch by someone else:
|
||||
|
||||
```shell
|
||||
git push --force-with-lease origin my-feature-branch
|
||||
```
|
||||
|
||||
If the branch you want to force-push is [protected](../../user/project/protected_branches.md),
|
||||
you can't force-push to it unless you unprotect it first. Then you can
|
||||
force-push and re-protect it.
|
||||
|
||||
## Merge conflicts
|
||||
|
||||
As Git is based on comparing versions of a file
|
||||
line-by-line, whenever a line changed in your branch coincides with the same
|
||||
line changed in the target branch (after the moment you created your feature branch from it), Git
|
||||
identifies these changes as a merge conflict. To fix it, you need to choose
|
||||
which version of that line you want to keep.
|
||||
|
||||
Most conflicts can be [resolved through the GitLab UI](../../user/project/merge_requests/resolve_conflicts.md).
|
||||
|
||||
For more complex cases, there are various methods for resolving them. There are
|
||||
also [Git GUI apps](https://git-scm.com/downloads/guis) that can help by
|
||||
visualizing the differences.
|
||||
|
||||
To fix conflicts locally, you can use the following method:
|
||||
|
||||
1. Open the terminal and checkout your feature branch, for example, `my-feature-branch`:
|
||||
|
||||
```shell
|
||||
git checkout my-feature-branch
|
||||
```
|
||||
|
||||
1. [Rebase](#regular-rebase) your branch against the target branch so Git
|
||||
prompts you with the conflicts:
|
||||
|
||||
```shell
|
||||
git rebase origin/master
|
||||
```
|
||||
|
||||
1. Open the conflicting file in a code editor of your preference.
|
||||
1. Look for the conflict block:
|
||||
- It begins with the marker: `<<<<<<< HEAD`.
|
||||
- Below, there is the content with your changes.
|
||||
- The marker: `=======` indicates the end of your changes.
|
||||
- Below, there's the content of the latest changes in the target branch.
|
||||
- The marker `>>>>>>>` indicates the end of the conflict.
|
||||
1. Edit the file: choose which version (before or after `=======`) you want to
|
||||
keep, and then delete the portion of the content you don't want in the file.
|
||||
1. Delete the markers.
|
||||
1. Save the file.
|
||||
1. Repeat the process if there are other conflicting files.
|
||||
1. Stage your changes:
|
||||
|
||||
```shell
|
||||
git add .
|
||||
```
|
||||
|
||||
1. Commit your changes:
|
||||
|
||||
```shell
|
||||
git commit -m "Fix merge conflicts"
|
||||
```
|
||||
|
||||
1. Continue rebasing:
|
||||
|
||||
```shell
|
||||
git rebase --continue
|
||||
```
|
||||
|
||||
CAUTION: **Caution:**
|
||||
Up to this point, you can run `git rebase --abort` to stop the process.
|
||||
Git aborts the rebase and rolls back the branch to the state you had before
|
||||
running `git rebase`.
|
||||
Once you run `git rebase --continue` the rebase **cannot** be aborted.
|
||||
|
||||
1. [Force-push](#force-push) to your remote branch.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
|
|
@ -81,6 +81,7 @@ If you have problems with Git, the following may help:
|
|||
|
||||
The following are advanced topics for those who want to get the most out of Git:
|
||||
|
||||
- [Introduction to Git rebase, force-push, and merge conflicts](git_rebase.md)
|
||||
- [Server Hooks](../../administration/server_hooks.md)
|
||||
- [Git Attributes](../../user/project/git_attributes.md)
|
||||
- Git Submodules: [Using Git submodules with GitLab CI](../../ci/git_submodules.md#using-git-submodules-with-gitlab-ci)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ module.exports = path => {
|
|||
'^~(/.*)$': '<rootDir>/app/assets/javascripts$1',
|
||||
'^ee_component(/.*)$':
|
||||
'<rootDir>/app/assets/javascripts/vue_shared/components/empty_component.js',
|
||||
'^shared_queries(/.*)$': '<rootDir>/app/graphql/queries$1',
|
||||
'^ee_else_ce(/.*)$': '<rootDir>/app/assets/javascripts$1',
|
||||
'^helpers(/.*)$': '<rootDir>/spec/frontend/helpers$1',
|
||||
'^vendor(/.*)$': '<rootDir>/vendor/assets/javascripts$1',
|
||||
|
|
|
|||
|
|
@ -88,8 +88,8 @@ module API
|
|||
success Entities::Member
|
||||
end
|
||||
params do
|
||||
requires :user_id, type: Integer, desc: 'The user ID of the new member'
|
||||
requires :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, developer access level)'
|
||||
requires :user_id, types: [Integer, String], desc: 'The user ID of the new member or multiple IDs separated by commas.'
|
||||
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
|
||||
end
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
|
|
@ -97,20 +97,26 @@ module API
|
|||
source = find_source(source_type, params[:id])
|
||||
authorize_admin_source!(source_type, source)
|
||||
|
||||
member = source.members.find_by(user_id: params[:user_id])
|
||||
conflict!('Member already exists') if member
|
||||
if params[:user_id].to_s.include?(',')
|
||||
create_service_params = params.except(:user_id).merge({ user_ids: params[:user_id] })
|
||||
|
||||
user = User.find_by_id(params[:user_id])
|
||||
not_found!('User') unless user
|
||||
::Members::CreateService.new(current_user, create_service_params).execute(source)
|
||||
elsif params[:user_id].present?
|
||||
member = source.members.find_by(user_id: params[:user_id])
|
||||
conflict!('Member already exists') if member
|
||||
|
||||
member = create_member(current_user, user, source, params)
|
||||
user = User.find_by_id(params[:user_id])
|
||||
not_found!('User') unless user
|
||||
|
||||
if !member
|
||||
not_allowed! # This currently can only be reached in EE
|
||||
elsif member.valid? && member.persisted?
|
||||
present_members(member)
|
||||
else
|
||||
render_validation_error!(member)
|
||||
member = create_member(current_user, user, source, params)
|
||||
|
||||
if !member
|
||||
not_allowed! # This currently can only be reached in EE
|
||||
elsif member.valid? && member.persisted?
|
||||
present_members(member)
|
||||
else
|
||||
render_validation_error!(member)
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
|
|
|||
|
|
@ -123,7 +123,8 @@ module Gitlab
|
|||
none: "",
|
||||
qa: "~QA",
|
||||
test: "~test ~Quality for `spec/features/*`",
|
||||
engineering_productivity: '~"Engineering Productivity" for CI, Danger'
|
||||
engineering_productivity: '~"Engineering Productivity" for CI, Danger',
|
||||
ci_template: '~"ci::templates"'
|
||||
}.freeze
|
||||
# First-match win, so be sure to put more specific regex at the top...
|
||||
CATEGORIES = {
|
||||
|
|
@ -176,6 +177,8 @@ module Gitlab
|
|||
%r{(CODEOWNERS)} => :engineering_productivity,
|
||||
%r{(tests.yml)} => :engineering_productivity,
|
||||
|
||||
%r{\Alib/gitlab/ci/templates} => :ci_template,
|
||||
|
||||
%r{\A(ee/)?spec/features/} => :test,
|
||||
%r{\A(ee/)?spec/support/shared_examples/features/} => :test,
|
||||
%r{\A(ee/)?spec/support/shared_contexts/features/} => :test,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,11 @@ module Gitlab
|
|||
# Fetch an already picked backend maintainer, or pick one otherwise
|
||||
spin.maintainer = backend_spin&.maintainer || spin_for_category(project, :backend, timezone_experiment: including_timezone).maintainer
|
||||
end
|
||||
when :ci_template
|
||||
if spin.maintainer.nil?
|
||||
# Fetch an already picked backend maintainer, or pick one otherwise
|
||||
spin.maintainer = backend_spin&.maintainer || spin_for_category(project, :backend, timezone_experiment: including_timezone).maintainer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,9 @@ module Gitlab
|
|||
end
|
||||
|
||||
def merged_diff_options(diff_options)
|
||||
max_diff_options = ::Commit.max_diff_options(project: @merge_request_diff.project)
|
||||
project = @merge_request_diff.project
|
||||
max_diff_options = ::Commit.max_diff_options(project: project).merge(project: project)
|
||||
|
||||
diff_options.present? ? diff_options.merge(max_diff_options) : max_diff_options
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,13 +11,17 @@ module Gitlab
|
|||
|
||||
delegate :max_files, :max_lines, :max_bytes, :safe_max_files, :safe_max_lines, :safe_max_bytes, to: :limits
|
||||
|
||||
def self.default_limits
|
||||
{ max_files: 100, max_lines: 5000 }
|
||||
def self.default_limits(project: nil)
|
||||
if Feature.enabled?(:increased_diff_limits, project)
|
||||
{ max_files: 200, max_lines: 7500 }
|
||||
else
|
||||
{ max_files: 100, max_lines: 5000 }
|
||||
end
|
||||
end
|
||||
|
||||
def self.limits(options = {})
|
||||
limits = {}
|
||||
defaults = default_limits
|
||||
defaults = default_limits(project: options[:project])
|
||||
limits[:max_files] = options.fetch(:max_files, defaults[:max_files])
|
||||
limits[:max_lines] = options.fetch(:max_lines, defaults[:max_lines])
|
||||
limits[:max_bytes] = limits[:max_files] * 5.kilobytes # Average 5 KB per file
|
||||
|
|
|
|||
|
|
@ -4215,6 +4215,9 @@ msgstr ""
|
|||
msgid "Branch %{branch_name} was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Branch already exists"
|
||||
msgstr ""
|
||||
|
||||
msgid "Branch changed"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -8610,18 +8613,27 @@ msgstr ""
|
|||
msgid "Dependencies|Job failed to generate the dependency list"
|
||||
msgstr ""
|
||||
|
||||
msgid "Dependencies|Learn more about dependency paths"
|
||||
msgstr ""
|
||||
|
||||
msgid "Dependencies|License"
|
||||
msgstr ""
|
||||
|
||||
msgid "Dependencies|Location"
|
||||
msgstr ""
|
||||
|
||||
msgid "Dependencies|Location and dependency path"
|
||||
msgstr ""
|
||||
|
||||
msgid "Dependencies|Packager"
|
||||
msgstr ""
|
||||
|
||||
msgid "Dependencies|The %{codeStartTag}dependency_scanning%{codeEndTag} job has failed and cannot generate the list. Please ensure the job is running properly and run the pipeline again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Dependencies|The component dependency path is based on the lock file. There may be several paths. In these cases, the longest path is displayed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Dependencies|There may be multiple paths"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -9512,6 +9524,9 @@ msgstr ""
|
|||
msgid "Edit this release"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit title and description"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit wiki page"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -18389,6 +18404,9 @@ msgstr ""
|
|||
msgid "PackageRegistry|Add NuGet Source"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|Add composer registry"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|App group: %{group}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -18485,6 +18503,9 @@ msgstr ""
|
|||
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|Install package version"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -18575,12 +18596,6 @@ msgstr ""
|
|||
msgid "PackageRegistry|You may also need to setup authentication using an auth token. %{linkStart}See the documentation%{linkEnd} to find out more."
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|composer.json registry include"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|composer.json require package include"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|npm command"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -29467,6 +29482,9 @@ msgstr ""
|
|||
msgid "Would you like to create a new branch?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Would you like to try auto-generating a branch name?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Write"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@
|
|||
"vue": "^2.6.12",
|
||||
"vue-apollo": "^3.0.3",
|
||||
"vue-loader": "^15.9.3",
|
||||
"vue-router": "^3.4.5",
|
||||
"vue-router": "^3.4.6",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"vue-virtual-scroll-list": "^1.4.4",
|
||||
"vuedraggable": "^2.23.0",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import InstanceCounts from '~/analytics/instance_statistics/components/instance_counts.vue';
|
||||
import MetricCard from '~/analytics/shared/components/metric_card.vue';
|
||||
import countsMockData from '../mock_data';
|
||||
import { mockInstanceCounts } from '../mock_data';
|
||||
|
||||
describe('InstanceCounts', () => {
|
||||
let wrapper;
|
||||
|
|
@ -44,11 +44,11 @@ describe('InstanceCounts', () => {
|
|||
|
||||
describe('with data', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ data: { counts: countsMockData } });
|
||||
createComponent({ data: { counts: mockInstanceCounts } });
|
||||
});
|
||||
|
||||
it('passes the counts data to the metric card', () => {
|
||||
expect(findMetricCard().props('metrics')).toEqual(countsMockData);
|
||||
expect(findMetricCard().props('metrics')).toEqual(mockInstanceCounts);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,30 @@
|
|||
export default [
|
||||
export const mockInstanceCounts = [
|
||||
{ key: 'projects', value: 10, label: 'Projects' },
|
||||
{ key: 'groups', value: 20, label: 'Group' },
|
||||
];
|
||||
|
||||
export const mockCountsData1 = [
|
||||
{ recordedAt: '2020-07-23', count: 52 },
|
||||
{ recordedAt: '2020-07-22', count: 40 },
|
||||
{ recordedAt: '2020-07-21', count: 31 },
|
||||
{ recordedAt: '2020-06-14', count: 23 },
|
||||
{ recordedAt: '2020-06-12', count: 20 },
|
||||
];
|
||||
|
||||
export const countsMonthlyChartData1 = [
|
||||
['2020-07-01', 41], // average of 2020-07-x items
|
||||
['2020-06-01', 21.5], // average of 2020-06-x items
|
||||
];
|
||||
|
||||
export const mockCountsData2 = [
|
||||
{ recordedAt: '2020-07-28', count: 10 },
|
||||
{ recordedAt: '2020-07-27', count: 9 },
|
||||
{ recordedAt: '2020-06-26', count: 14 },
|
||||
{ recordedAt: '2020-06-25', count: 23 },
|
||||
{ recordedAt: '2020-06-24', count: 25 },
|
||||
];
|
||||
|
||||
export const countsMonthlyChartData2 = [
|
||||
['2020-07-01', 9.5], // average of 2020-07-x items
|
||||
['2020-06-01', 20.666666666666668], // average of 2020-06-x items
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import { getAverageByMonth } from '~/analytics/instance_statistics/utils';
|
||||
import {
|
||||
mockCountsData1,
|
||||
mockCountsData2,
|
||||
countsMonthlyChartData1,
|
||||
countsMonthlyChartData2,
|
||||
} from './mock_data';
|
||||
|
||||
describe('getAverageByMonth', () => {
|
||||
it('collects data into average by months', () => {
|
||||
expect(getAverageByMonth(mockCountsData1)).toStrictEqual(countsMonthlyChartData1);
|
||||
expect(getAverageByMonth(mockCountsData2)).toStrictEqual(countsMonthlyChartData2);
|
||||
});
|
||||
|
||||
it('it transforms a data point to the first of the month', () => {
|
||||
const item = mockCountsData1[0];
|
||||
const firstOfTheMonth = item.recordedAt.replace(/-[0-9]{2}$/, '-01');
|
||||
expect(getAverageByMonth([item])).toStrictEqual([[firstOfTheMonth, item.count]]);
|
||||
});
|
||||
|
||||
it('it uses sane defaults', () => {
|
||||
expect(getAverageByMonth()).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('it errors when passing null', () => {
|
||||
expect(() => {
|
||||
getAverageByMonth(null);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
describe('when shouldRound = true', () => {
|
||||
const options = { shouldRound: true };
|
||||
|
||||
it('rounds the averages', () => {
|
||||
const roundedData1 = countsMonthlyChartData1.map(([date, avg]) => [date, Math.round(avg)]);
|
||||
const roundedData2 = countsMonthlyChartData2.map(([date, avg]) => [date, Math.round(avg)]);
|
||||
expect(getAverageByMonth(mockCountsData1, options)).toStrictEqual(roundedData1);
|
||||
expect(getAverageByMonth(mockCountsData2, options)).toStrictEqual(roundedData2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -5,14 +5,17 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ
|
|||
class="gl-display-flex gl-justify-content-end"
|
||||
>
|
||||
<div
|
||||
class="dropdown b-dropdown gl-dropdown btn-group"
|
||||
class="dropdown b-dropdown gl-new-dropdown btn-group"
|
||||
menu-class="dropdown-menu-large"
|
||||
>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
class="btn btn-danger btn-md gl-button split-content-button"
|
||||
type="button"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<span
|
||||
class="gl-dropdown-toggle-text"
|
||||
class="gl-new-dropdown-button-text"
|
||||
>
|
||||
Remove integration and resources
|
||||
</span>
|
||||
|
|
@ -22,7 +25,7 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ
|
|||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
class="btn dropdown-toggle btn-danger dropdown-toggle-split"
|
||||
class="btn dropdown-toggle btn-danger btn-md gl-button gl-dropdown-toggle dropdown-toggle-split"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
|
|
@ -32,29 +35,58 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ
|
|||
</span>
|
||||
</button>
|
||||
<ul
|
||||
class="dropdown-menu dropdown-menu-selectable dropdown-menu-large"
|
||||
class="dropdown-menu dropdown-menu-large"
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<li
|
||||
class="gl-new-dropdown-item"
|
||||
role="presentation"
|
||||
>
|
||||
<button
|
||||
class="dropdown-item is-active"
|
||||
class="dropdown-item"
|
||||
role="menuitem"
|
||||
type="button"
|
||||
>
|
||||
<strong>
|
||||
Remove integration and resources
|
||||
</strong>
|
||||
<svg
|
||||
class="gl-icon s16 gl-new-dropdown-item-check-icon"
|
||||
data-testid="mobile-issue-close-icon"
|
||||
>
|
||||
<use
|
||||
href="#mobile-issue-close"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div>
|
||||
Deletes all GitLab resources attached to this cluster during removal
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<div
|
||||
class="gl-new-dropdown-item-text-wrapper"
|
||||
>
|
||||
<p
|
||||
class="gl-new-dropdown-item-text-primary"
|
||||
>
|
||||
<strong>
|
||||
Remove integration and resources
|
||||
</strong>
|
||||
|
||||
<div>
|
||||
Deletes all GitLab resources attached to this cluster during removal
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<!---->
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li
|
||||
class="gl-new-dropdown-divider"
|
||||
role="presentation"
|
||||
>
|
||||
<hr
|
||||
|
|
@ -64,6 +96,7 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ
|
|||
/>
|
||||
</li>
|
||||
<li
|
||||
class="gl-new-dropdown-item"
|
||||
role="presentation"
|
||||
>
|
||||
<button
|
||||
|
|
@ -71,13 +104,38 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ
|
|||
role="menuitem"
|
||||
type="button"
|
||||
>
|
||||
<strong>
|
||||
Remove integration
|
||||
</strong>
|
||||
<svg
|
||||
class="gl-icon s16 gl-new-dropdown-item-check-icon gl-visibility-hidden"
|
||||
data-testid="mobile-issue-close-icon"
|
||||
>
|
||||
<use
|
||||
href="#mobile-issue-close"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div>
|
||||
Removes cluster from project but keeps associated resources
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<div
|
||||
class="gl-new-dropdown-item-text-wrapper"
|
||||
>
|
||||
<p
|
||||
class="gl-new-dropdown-item-text-primary"
|
||||
>
|
||||
<strong>
|
||||
Remove integration
|
||||
</strong>
|
||||
|
||||
<div>
|
||||
Removes cluster from project but keeps associated resources
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<!---->
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
</button>
|
||||
</li>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,12 @@ import { createStore } from '~/ide/stores';
|
|||
import consts from '~/ide/stores/modules/commit/constants';
|
||||
import CommitForm from '~/ide/components/commit_sidebar/form.vue';
|
||||
import { leftSidebarViews } from '~/ide/constants';
|
||||
import { createCodeownersCommitError, createUnexpectedCommitError } from '~/ide/lib/errors';
|
||||
import {
|
||||
createCodeownersCommitError,
|
||||
createUnexpectedCommitError,
|
||||
createBranchChangedCommitError,
|
||||
branchAlreadyExistsCommitError,
|
||||
} from '~/ide/lib/errors';
|
||||
|
||||
describe('IDE commit form', () => {
|
||||
const Component = Vue.extend(CommitForm);
|
||||
|
|
@ -290,20 +295,30 @@ describe('IDE commit form', () => {
|
|||
jest.spyOn(vm.$store, 'dispatch').mockReturnValue(Promise.resolve());
|
||||
});
|
||||
|
||||
it('updates commit action and commits', async () => {
|
||||
store.state.commit.commitError = createCodeownersCommitError('test message');
|
||||
const commitActions = [
|
||||
['commit/updateCommitAction', consts.COMMIT_TO_NEW_BRANCH],
|
||||
['commit/commitChanges'],
|
||||
];
|
||||
|
||||
await vm.$nextTick();
|
||||
it.each`
|
||||
commitError | expectedActions
|
||||
${createCodeownersCommitError} | ${commitActions}
|
||||
${createBranchChangedCommitError} | ${commitActions}
|
||||
${branchAlreadyExistsCommitError} | ${[['commit/addSuffixToBranchName'], ...commitActions]}
|
||||
`(
|
||||
'updates commit action and commits for error: $commitError',
|
||||
async ({ commitError, expectedActions }) => {
|
||||
store.state.commit.commitError = commitError('test message');
|
||||
|
||||
getByText(document.body, 'Create new branch').click();
|
||||
await vm.$nextTick();
|
||||
|
||||
await waitForPromises();
|
||||
getByText(document.body, 'Create new branch').click();
|
||||
|
||||
expect(vm.$store.dispatch.mock.calls).toEqual([
|
||||
['commit/updateCommitAction', consts.COMMIT_TO_NEW_BRANCH],
|
||||
['commit/commitChanges', undefined],
|
||||
]);
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(vm.$store.dispatch.mock.calls).toEqual(expectedActions);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {
|
|||
createUnexpectedCommitError,
|
||||
createCodeownersCommitError,
|
||||
createBranchChangedCommitError,
|
||||
branchAlreadyExistsCommitError,
|
||||
parseCommitError,
|
||||
} from '~/ide/lib/errors';
|
||||
|
||||
|
|
@ -21,35 +22,22 @@ describe('~/ide/lib/errors', () => {
|
|||
},
|
||||
});
|
||||
|
||||
describe('createCodeownersCommitError', () => {
|
||||
it('uses given message', () => {
|
||||
expect(createCodeownersCommitError(TEST_MESSAGE)).toEqual({
|
||||
title: 'CODEOWNERS rule violation',
|
||||
messageHTML: TEST_MESSAGE,
|
||||
canCreateBranch: true,
|
||||
});
|
||||
});
|
||||
const NEW_BRANCH_SUFFIX = `<br/><br/>Would you like to create a new branch?`;
|
||||
const AUTOGENERATE_SUFFIX = `<br/><br/>Would you like to try auto-generating a branch name?`;
|
||||
|
||||
it('escapes special chars', () => {
|
||||
expect(createCodeownersCommitError(TEST_SPECIAL)).toEqual({
|
||||
title: 'CODEOWNERS rule violation',
|
||||
messageHTML: TEST_SPECIAL_ESCAPED,
|
||||
canCreateBranch: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createBranchChangedCommitError', () => {
|
||||
it.each`
|
||||
message | expectedMessage
|
||||
${TEST_MESSAGE} | ${`${TEST_MESSAGE}<br/><br/>Would you like to create a new branch?`}
|
||||
${TEST_SPECIAL} | ${`${TEST_SPECIAL_ESCAPED}<br/><br/>Would you like to create a new branch?`}
|
||||
`('uses given message="$message"', ({ message, expectedMessage }) => {
|
||||
expect(createBranchChangedCommitError(message)).toEqual({
|
||||
title: 'Branch changed',
|
||||
messageHTML: expectedMessage,
|
||||
canCreateBranch: true,
|
||||
});
|
||||
it.each`
|
||||
fn | title | message | messageHTML
|
||||
${createCodeownersCommitError} | ${'CODEOWNERS rule violation'} | ${TEST_MESSAGE} | ${TEST_MESSAGE}
|
||||
${createCodeownersCommitError} | ${'CODEOWNERS rule violation'} | ${TEST_SPECIAL} | ${TEST_SPECIAL_ESCAPED}
|
||||
${branchAlreadyExistsCommitError} | ${'Branch already exists'} | ${TEST_MESSAGE} | ${`${TEST_MESSAGE}${AUTOGENERATE_SUFFIX}`}
|
||||
${branchAlreadyExistsCommitError} | ${'Branch already exists'} | ${TEST_SPECIAL} | ${`${TEST_SPECIAL_ESCAPED}${AUTOGENERATE_SUFFIX}`}
|
||||
${createBranchChangedCommitError} | ${'Branch changed'} | ${TEST_MESSAGE} | ${`${TEST_MESSAGE}${NEW_BRANCH_SUFFIX}`}
|
||||
${createBranchChangedCommitError} | ${'Branch changed'} | ${TEST_SPECIAL} | ${`${TEST_SPECIAL_ESCAPED}${NEW_BRANCH_SUFFIX}`}
|
||||
`('$fn escapes and uses given message="$message"', ({ fn, title, message, messageHTML }) => {
|
||||
expect(fn(message)).toEqual({
|
||||
title,
|
||||
messageHTML,
|
||||
primaryAction: { text: 'Create new branch', callback: expect.any(Function) },
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -60,7 +48,7 @@ describe('~/ide/lib/errors', () => {
|
|||
${{}} | ${createUnexpectedCommitError()}
|
||||
${{ response: {} }} | ${createUnexpectedCommitError()}
|
||||
${{ response: { data: {} } }} | ${createUnexpectedCommitError()}
|
||||
${createResponseError('test')} | ${createUnexpectedCommitError()}
|
||||
${createResponseError(TEST_MESSAGE)} | ${createUnexpectedCommitError(TEST_MESSAGE)}
|
||||
${createResponseError(CODEOWNERS_MESSAGE)} | ${createCodeownersCommitError(CODEOWNERS_MESSAGE)}
|
||||
${createResponseError(CHANGED_MESSAGE)} | ${createBranchChangedCommitError(CHANGED_MESSAGE)}
|
||||
`('parses message into error object with "$message"', ({ message, expectation }) => {
|
||||
|
|
|
|||
|
|
@ -449,16 +449,16 @@ describe('IDE store getters', () => {
|
|||
describe('getAvailableFileName', () => {
|
||||
it.each`
|
||||
path | newPath
|
||||
${'foo'} | ${'foo_1'}
|
||||
${'foo'} | ${'foo-1'}
|
||||
${'foo__93.png'} | ${'foo__94.png'}
|
||||
${'foo/bar.png'} | ${'foo/bar_1.png'}
|
||||
${'foo/bar.png'} | ${'foo/bar-1.png'}
|
||||
${'foo/bar--34.png'} | ${'foo/bar--35.png'}
|
||||
${'foo/bar 2.png'} | ${'foo/bar 3.png'}
|
||||
${'foo/bar-621.png'} | ${'foo/bar-622.png'}
|
||||
${'jquery.min.js'} | ${'jquery_1.min.js'}
|
||||
${'jquery.min.js'} | ${'jquery-1.min.js'}
|
||||
${'my_spec_22.js.snap'} | ${'my_spec_23.js.snap'}
|
||||
${'subtitles5.mp4.srt'} | ${'subtitles_6.mp4.srt'}
|
||||
${'sample_file.mp3'} | ${'sample_file_1.mp3'}
|
||||
${'subtitles5.mp4.srt'} | ${'subtitles-6.mp4.srt'}
|
||||
${'sample-file.mp3'} | ${'sample-file-1.mp3'}
|
||||
${'Screenshot 2020-05-26 at 10.53.08 PM.png'} | ${'Screenshot 2020-05-26 at 11.53.08 PM.png'}
|
||||
`('suffixes the path with a number if the path already exists', ({ path, newPath }) => {
|
||||
localState.entries[path] = file();
|
||||
|
|
|
|||
|
|
@ -76,59 +76,38 @@ describe('IDE commit module actions', () => {
|
|||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('sets shouldCreateMR to true if "Create new MR" option is visible', done => {
|
||||
Object.assign(store.state, {
|
||||
shouldHideNewMrOption: false,
|
||||
});
|
||||
|
||||
testAction(
|
||||
actions.updateCommitAction,
|
||||
{},
|
||||
store.state,
|
||||
[
|
||||
{
|
||||
type: mutationTypes.UPDATE_COMMIT_ACTION,
|
||||
payload: { commitAction: expect.anything() },
|
||||
},
|
||||
{ type: mutationTypes.TOGGLE_SHOULD_CREATE_MR, payload: true },
|
||||
],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
||||
it('sets shouldCreateMR to false if "Create new MR" option is hidden', done => {
|
||||
Object.assign(store.state, {
|
||||
shouldHideNewMrOption: true,
|
||||
});
|
||||
|
||||
testAction(
|
||||
actions.updateCommitAction,
|
||||
{},
|
||||
store.state,
|
||||
[
|
||||
{
|
||||
type: mutationTypes.UPDATE_COMMIT_ACTION,
|
||||
payload: { commitAction: expect.anything() },
|
||||
},
|
||||
{ type: mutationTypes.TOGGLE_SHOULD_CREATE_MR, payload: false },
|
||||
],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateBranchName', () => {
|
||||
it('updates store with new branch name', done => {
|
||||
store
|
||||
.dispatch('commit/updateBranchName', 'branch-name')
|
||||
.then(() => {
|
||||
expect(store.state.commit.newBranchName).toBe('branch-name');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
let originalGon;
|
||||
|
||||
beforeEach(() => {
|
||||
originalGon = window.gon;
|
||||
window.gon = { current_username: 'johndoe' };
|
||||
|
||||
store.state.currentBranchId = 'master';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.gon = originalGon;
|
||||
});
|
||||
|
||||
it('updates store with new branch name', async () => {
|
||||
await store.dispatch('commit/updateBranchName', 'branch-name');
|
||||
|
||||
expect(store.state.commit.newBranchName).toBe('branch-name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('addSuffixToBranchName', () => {
|
||||
it('adds suffix to branchName', async () => {
|
||||
jest.spyOn(Math, 'random').mockReturnValue(0.391352525);
|
||||
|
||||
store.state.commit.newBranchName = 'branch-name';
|
||||
|
||||
await store.dispatch('commit/addSuffixToBranchName');
|
||||
|
||||
expect(store.state.commit.newBranchName).toBe('branch-name-39135');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -318,13 +297,16 @@ describe('IDE commit module actions', () => {
|
|||
currentBranchId: 'master',
|
||||
projects: {
|
||||
abcproject: {
|
||||
default_branch: 'master',
|
||||
web_url: 'webUrl',
|
||||
branches: {
|
||||
master: {
|
||||
name: 'master',
|
||||
workingReference: '1',
|
||||
commit: {
|
||||
id: TEST_COMMIT_SHA,
|
||||
},
|
||||
can_push: true,
|
||||
},
|
||||
},
|
||||
userPermissions: {
|
||||
|
|
@ -499,6 +481,16 @@ describe('IDE commit module actions', () => {
|
|||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('does not redirect to merge request page if shouldCreateMR is checked, but branch is the default branch', async () => {
|
||||
jest.spyOn(eventHub, '$on').mockImplementation();
|
||||
|
||||
store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
|
||||
store.state.commit.shouldCreateMR = true;
|
||||
|
||||
await store.dispatch('commit/commitChanges');
|
||||
expect(visitUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('resets changed files before redirecting', () => {
|
||||
jest.spyOn(eventHub, '$on').mockImplementation();
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
getPathParents,
|
||||
getPathParent,
|
||||
readFileAsDataURL,
|
||||
addNumericSuffix,
|
||||
} from '~/ide/utils';
|
||||
|
||||
describe('WebIDE utils', () => {
|
||||
|
|
@ -291,4 +292,43 @@ describe('WebIDE utils', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* hello-2425 -> hello-2425
|
||||
* hello.md -> hello-1.md
|
||||
* hello_2.md -> hello_3.md
|
||||
* hello_ -> hello_1
|
||||
* master-patch-22432 -> master-patch-22433
|
||||
* patch_332 -> patch_333
|
||||
*/
|
||||
|
||||
describe('addNumericSuffix', () => {
|
||||
it.each`
|
||||
input | output
|
||||
${'hello'} | ${'hello-1'}
|
||||
${'hello2'} | ${'hello-3'}
|
||||
${'hello.md'} | ${'hello-1.md'}
|
||||
${'hello_2.md'} | ${'hello_3.md'}
|
||||
${'hello_'} | ${'hello_1'}
|
||||
${'master-patch-22432'} | ${'master-patch-22433'}
|
||||
${'patch_332'} | ${'patch_333'}
|
||||
`('adds a numeric suffix to a given filename/branch name: $input', ({ input, output }) => {
|
||||
expect(addNumericSuffix(input)).toBe(output);
|
||||
});
|
||||
|
||||
it.each`
|
||||
input | output
|
||||
${'hello'} | ${'hello-39135'}
|
||||
${'hello2'} | ${'hello-39135'}
|
||||
${'hello.md'} | ${'hello-39135.md'}
|
||||
${'hello_2.md'} | ${'hello_39135.md'}
|
||||
${'hello_'} | ${'hello_39135'}
|
||||
${'master-patch-22432'} | ${'master-patch-39135'}
|
||||
${'patch_332'} | ${'patch_39135'}
|
||||
`('adds a random suffix if randomize=true is passed for name: $input', ({ input, output }) => {
|
||||
jest.spyOn(Math, 'random').mockReturnValue(0.391352525);
|
||||
|
||||
expect(addNumericSuffix(input, true)).toBe(output);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
GlBadge,
|
||||
GlEmptyState,
|
||||
} from '@gitlab/ui';
|
||||
import Tracking from '~/tracking';
|
||||
import { visitUrl, joinPaths, mergeUrlParams } from '~/lib/utils/url_utility';
|
||||
import IncidentsList from '~/incidents/components/incidents_list.vue';
|
||||
import SeverityToken from '~/sidebar/components/severity/severity.vue';
|
||||
|
|
@ -22,6 +23,7 @@ import {
|
|||
TH_CREATED_AT_TEST_ID,
|
||||
TH_SEVERITY_TEST_ID,
|
||||
TH_PUBLISHED_TEST_ID,
|
||||
trackIncidentCreateNewOptions,
|
||||
} from '~/incidents/constants';
|
||||
import mockIncidents from '../mocks/incidents.json';
|
||||
import mockFilters from '../mocks/incidents_filter.json';
|
||||
|
|
@ -33,6 +35,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
|
|||
setUrlParams: jest.fn(),
|
||||
updateHistory: jest.fn(),
|
||||
}));
|
||||
jest.mock('~/tracking');
|
||||
|
||||
describe('Incidents List', () => {
|
||||
let wrapper;
|
||||
|
|
@ -52,7 +55,7 @@ describe('Incidents List', () => {
|
|||
const findLoader = () => wrapper.find(GlLoadingIcon);
|
||||
const findTimeAgo = () => wrapper.findAll(TimeAgoTooltip);
|
||||
const findSearch = () => wrapper.find(FilteredSearchBar);
|
||||
const findAssingees = () => wrapper.findAll('[data-testid="incident-assignees"]');
|
||||
const findAssignees = () => wrapper.findAll('[data-testid="incident-assignees"]');
|
||||
const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]');
|
||||
const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']");
|
||||
const findPagination = () => wrapper.find(GlPagination);
|
||||
|
|
@ -164,14 +167,14 @@ describe('Incidents List', () => {
|
|||
describe('Assignees', () => {
|
||||
it('shows Unassigned when there are no assignees', () => {
|
||||
expect(
|
||||
findAssingees()
|
||||
findAssignees()
|
||||
.at(0)
|
||||
.text(),
|
||||
).toBe(I18N.unassigned);
|
||||
});
|
||||
|
||||
it('renders an avatar component when there is an assignee', () => {
|
||||
const avatar = findAssingees()
|
||||
const avatar = findAssignees()
|
||||
.at(1)
|
||||
.find(GlAvatar);
|
||||
const { src, label } = avatar.attributes();
|
||||
|
|
@ -211,7 +214,7 @@ describe('Incidents List', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('shows the button linking to new incidents page with prefilled incident template when clicked', () => {
|
||||
it('shows the button linking to new incidents page with pre-filled incident template when clicked', () => {
|
||||
expect(findCreateIncidentBtn().exists()).toBe(true);
|
||||
findCreateIncidentBtn().trigger('click');
|
||||
expect(mergeUrlParams).toHaveBeenCalledWith(
|
||||
|
|
@ -233,6 +236,13 @@ describe('Incidents List', () => {
|
|||
});
|
||||
expect(findCreateIncidentBtn().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should track alert list page views', async () => {
|
||||
findCreateIncidentBtn().vm.$emit('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
const { category, action } = trackIncidentCreateNewOptions;
|
||||
expect(Tracking.event).toHaveBeenCalledWith(category, action);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pagination', () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import $ from 'jquery';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
|
||||
import IssuableDescription from '~/issuable_show/components/issuable_description.vue';
|
||||
|
||||
import { mockIssuable } from '../mock_data';
|
||||
|
||||
const createComponent = (issuable = mockIssuable) =>
|
||||
shallowMount(IssuableDescription, {
|
||||
propsData: { issuable },
|
||||
});
|
||||
|
||||
describe('IssuableDescription', () => {
|
||||
let renderGFMSpy;
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('mounted', () => {
|
||||
it('calls `renderGFM`', () => {
|
||||
expect(renderGFMSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
describe('renderGFM', () => {
|
||||
it('calls `renderGFM` on container element', () => {
|
||||
wrapper.vm.renderGFM();
|
||||
|
||||
expect(renderGFMSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlFormInput } from '@gitlab/ui';
|
||||
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
||||
|
||||
import IssuableEditForm from '~/issuable_show/components/issuable_edit_form.vue';
|
||||
import IssuableEventHub from '~/issuable_show/event_hub';
|
||||
|
||||
import { mockIssuableShowProps, mockIssuable } from '../mock_data';
|
||||
|
||||
const issuableEditFormProps = {
|
||||
issuable: mockIssuable,
|
||||
...mockIssuableShowProps,
|
||||
};
|
||||
|
||||
const createComponent = ({ propsData = issuableEditFormProps } = {}) =>
|
||||
shallowMount(IssuableEditForm, {
|
||||
propsData,
|
||||
stubs: {
|
||||
MarkdownField,
|
||||
},
|
||||
slots: {
|
||||
'edit-form-actions': `
|
||||
<button class="js-save">Save changes</button>
|
||||
<button class="js-cancel">Cancel</button>
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
describe('IssuableEditForm', () => {
|
||||
let wrapper;
|
||||
const assertEvent = eventSpy => {
|
||||
expect(eventSpy).toHaveBeenNthCalledWith(1, 'update.issuable', expect.any(Function));
|
||||
expect(eventSpy).toHaveBeenNthCalledWith(2, 'close.form', expect.any(Function));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('created', () => {
|
||||
it('binds `update.issuable` and `close.form` event listeners', () => {
|
||||
const eventOnSpy = jest.spyOn(IssuableEventHub, '$on');
|
||||
const wrapperTemp = createComponent();
|
||||
|
||||
assertEvent(eventOnSpy);
|
||||
|
||||
wrapperTemp.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('beforeDestroy', () => {
|
||||
it('unbinds `update.issuable` and `close.form` event listeners', () => {
|
||||
const wrapperTemp = createComponent();
|
||||
const eventOffSpy = jest.spyOn(IssuableEventHub, '$off');
|
||||
|
||||
wrapperTemp.destroy();
|
||||
|
||||
assertEvent(eventOffSpy);
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
describe('initAutosave', () => {
|
||||
it('initializes `autosaveTitle` and `autosaveDescription` props', () => {
|
||||
expect(wrapper.vm.autosaveTitle).toBeDefined();
|
||||
expect(wrapper.vm.autosaveDescription).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetAutosave', () => {
|
||||
it('calls `reset` on `autosaveTitle` and `autosaveDescription` props', () => {
|
||||
jest.spyOn(wrapper.vm.autosaveTitle, 'reset').mockImplementation(jest.fn);
|
||||
jest.spyOn(wrapper.vm.autosaveDescription, 'reset').mockImplementation(jest.fn);
|
||||
|
||||
wrapper.vm.resetAutosave();
|
||||
|
||||
expect(wrapper.vm.autosaveTitle.reset).toHaveBeenCalled();
|
||||
expect(wrapper.vm.autosaveDescription.reset).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('renders title input field', () => {
|
||||
const titleInputEl = wrapper.find('[data-testid="title"]');
|
||||
|
||||
expect(titleInputEl.exists()).toBe(true);
|
||||
expect(titleInputEl.find(GlFormInput).attributes()).toMatchObject({
|
||||
'aria-label': 'Title',
|
||||
placeholder: 'Title',
|
||||
});
|
||||
});
|
||||
|
||||
it('renders description textarea field', () => {
|
||||
const descriptionEl = wrapper.find('[data-testid="description"]');
|
||||
|
||||
expect(descriptionEl.exists()).toBe(true);
|
||||
expect(descriptionEl.find(MarkdownField).props()).toMatchObject({
|
||||
markdownPreviewPath: issuableEditFormProps.descriptionPreviewPath,
|
||||
markdownDocsPath: issuableEditFormProps.descriptionHelpPath,
|
||||
enableAutocomplete: issuableEditFormProps.enableAutocomplete,
|
||||
textareaValue: mockIssuable.description,
|
||||
});
|
||||
expect(descriptionEl.find('textarea').attributes()).toMatchObject({
|
||||
'data-supports-quick-actions': 'true',
|
||||
'aria-label': 'Description',
|
||||
placeholder: 'Write a comment or drag your files here…',
|
||||
});
|
||||
});
|
||||
|
||||
it('renders form actions', () => {
|
||||
const actionsEl = wrapper.find('[data-testid="actions"]');
|
||||
|
||||
expect(actionsEl.find('button.js-save').exists()).toBe(true);
|
||||
expect(actionsEl.find('button.js-cancel').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlIcon, GlButton, GlIntersectionObserver } from '@gitlab/ui';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
|
||||
import IssuableTitle from '~/issuable_show/components/issuable_title.vue';
|
||||
|
||||
import { mockIssuableShowProps, mockIssuable } from '../mock_data';
|
||||
|
||||
const issuableTitleProps = {
|
||||
issuable: mockIssuable,
|
||||
...mockIssuableShowProps,
|
||||
};
|
||||
|
||||
const createComponent = (propsData = issuableTitleProps) =>
|
||||
shallowMount(IssuableTitle, {
|
||||
propsData,
|
||||
stubs: {
|
||||
transition: true,
|
||||
},
|
||||
slots: {
|
||||
'status-badge': 'Open',
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: createMockDirective(),
|
||||
},
|
||||
});
|
||||
|
||||
describe('IssuableTitle', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
describe('handleTitleAppear', () => {
|
||||
it('sets value of `stickyTitleVisible` prop to false', () => {
|
||||
wrapper.find(GlIntersectionObserver).vm.$emit('appear');
|
||||
|
||||
expect(wrapper.vm.stickyTitleVisible).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleTitleDisappear', () => {
|
||||
it('sets value of `stickyTitleVisible` prop to true', () => {
|
||||
wrapper.find(GlIntersectionObserver).vm.$emit('disappear');
|
||||
|
||||
expect(wrapper.vm.stickyTitleVisible).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('renders issuable title', async () => {
|
||||
const wrapperWithTitle = createComponent({
|
||||
...mockIssuableShowProps,
|
||||
issuable: {
|
||||
...mockIssuable,
|
||||
titleHtml: '<b>Sample</b> title',
|
||||
},
|
||||
});
|
||||
|
||||
await wrapperWithTitle.vm.$nextTick();
|
||||
const titleEl = wrapperWithTitle.find('h2');
|
||||
|
||||
expect(titleEl.exists()).toBe(true);
|
||||
expect(titleEl.html()).toBe('<h2 dir="auto" class="title qa-title"><b>Sample</b> title</h2>');
|
||||
|
||||
wrapperWithTitle.destroy();
|
||||
});
|
||||
|
||||
it('renders edit button', () => {
|
||||
const editButtonEl = wrapper.find(GlButton);
|
||||
const tooltip = getBinding(editButtonEl.element, 'gl-tooltip');
|
||||
|
||||
expect(editButtonEl.exists()).toBe(true);
|
||||
expect(editButtonEl.props('icon')).toBe('pencil');
|
||||
expect(editButtonEl.attributes('title')).toBe('Edit title and description');
|
||||
expect(tooltip).toBeDefined();
|
||||
});
|
||||
|
||||
it('renders sticky header when `stickyTitleVisible` prop is true', async () => {
|
||||
wrapper.setData({
|
||||
stickyTitleVisible: true,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
const stickyHeaderEl = wrapper.find('[data-testid="header"]');
|
||||
|
||||
expect(stickyHeaderEl.exists()).toBe(true);
|
||||
expect(stickyHeaderEl.find(GlIcon).props('name')).toBe(issuableTitleProps.statusIcon);
|
||||
expect(stickyHeaderEl.text()).toContain('Open');
|
||||
expect(stickyHeaderEl.text()).toContain(issuableTitleProps.issuable.title);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -105,8 +105,7 @@ describe('Monitoring router', () => {
|
|||
path | currentDashboard
|
||||
${'/panel/new'} | ${undefined}
|
||||
${'/dashboard.yml/panel/new'} | ${'dashboard.yml'}
|
||||
${'/config/prometheus/common_metrics.yml/panel/new'} | ${'config/prometheus/common_metrics.yml'}
|
||||
${'/config%2Fprometheus%2Fcommon_metrics.yml/panel/new'} | ${'config/prometheus/common_metrics.yml'}
|
||||
${'/config%2Fprometheus%2Fcommon_metrics.yml/panel/new'} | ${'config%2Fprometheus%2Fcommon_metrics.yml'}
|
||||
`('"$path" renders page with dashboard "$currentDashboard"', ({ path, currentDashboard }) => {
|
||||
const wrapper = createWrapper(BASE_PATH, path);
|
||||
|
||||
|
|
|
|||
|
|
@ -15,15 +15,18 @@ describe('ComposerInstallation', () => {
|
|||
|
||||
const composerRegistryIncludeStr = 'foo/registry';
|
||||
const composerPackageIncludeStr = 'foo/package';
|
||||
const groupExists = true;
|
||||
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
packageEntity,
|
||||
composerHelpPath,
|
||||
groupExists,
|
||||
},
|
||||
getters: {
|
||||
composerRegistryInclude: () => composerRegistryIncludeStr,
|
||||
composerPackageInclude: () => composerPackageIncludeStr,
|
||||
groupExists: () => groupExists,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -62,7 +65,7 @@ describe('ComposerInstallation', () => {
|
|||
});
|
||||
|
||||
it('has the correct title', () => {
|
||||
expect(findRegistryInclude().props('label')).toBe('composer.json registry include');
|
||||
expect(findRegistryInclude().props('label')).toBe('Add composer registry');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -78,7 +81,7 @@ describe('ComposerInstallation', () => {
|
|||
});
|
||||
|
||||
it('has the correct title', () => {
|
||||
expect(findPackageInclude().props('label')).toBe('composer.json require package include');
|
||||
expect(findPackageInclude().props('label')).toBe('Install package version');
|
||||
});
|
||||
|
||||
it('has the correct help text', () => {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
pypiSetupCommand,
|
||||
composerRegistryInclude,
|
||||
composerPackageInclude,
|
||||
groupExists,
|
||||
} from '~/packages/details/store/getters';
|
||||
import {
|
||||
conanPackage,
|
||||
|
|
@ -68,10 +69,11 @@ describe('Getters PackageDetails Store', () => {
|
|||
const nugetSetupCommandStr = `nuget source Add -Name "GitLab" -Source "${registryUrl}" -UserName <your_username> -Password <your_token>`;
|
||||
|
||||
const pypiPipCommandStr = `pip install ${pypiPackage.name} --extra-index-url ${registryUrl}`;
|
||||
const composerRegistryIncludeStr = '{"type":"composer","url":"foo"}';
|
||||
const composerPackageIncludeStr = JSON.stringify({
|
||||
[packageWithoutBuildInfo.name]: packageWithoutBuildInfo.version,
|
||||
});
|
||||
const composerRegistryIncludeStr =
|
||||
'composer config repositories.gitlab.com/123 \'{"type": "composer", "url": "foo"}\'';
|
||||
const composerPackageIncludeStr = `composer req ${[packageWithoutBuildInfo.name]}:${
|
||||
packageWithoutBuildInfo.version
|
||||
}`;
|
||||
|
||||
describe('packagePipeline', () => {
|
||||
it('should return the pipeline info when pipeline exists', () => {
|
||||
|
|
@ -221,7 +223,7 @@ describe('Getters PackageDetails Store', () => {
|
|||
|
||||
describe('composer string getters', () => {
|
||||
it('gets the correct composerRegistryInclude command', () => {
|
||||
setupState({ composerPath: 'foo' });
|
||||
setupState({ composerPath: 'foo', composerConfigRepositoryName: 'gitlab.com/123' });
|
||||
|
||||
expect(composerRegistryInclude(state)).toBe(composerRegistryIncludeStr);
|
||||
});
|
||||
|
|
@ -232,4 +234,18 @@ describe('Getters PackageDetails Store', () => {
|
|||
expect(composerPackageInclude(state)).toBe(composerPackageIncludeStr);
|
||||
});
|
||||
});
|
||||
|
||||
describe('check if group', () => {
|
||||
it('is set', () => {
|
||||
setupState({ groupListUrl: '/groups/composer/-/packages' });
|
||||
|
||||
expect(groupExists(state)).toBe(true);
|
||||
});
|
||||
|
||||
it('is not set', () => {
|
||||
setupState({ groupListUrl: '' });
|
||||
|
||||
expect(groupExists(state)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,15 +1,23 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SplitButton renders actionItems 1`] = `
|
||||
<gl-deprecated-dropdown-stub
|
||||
menu-class="dropdown-menu-selectable "
|
||||
<gl-dropdown-stub
|
||||
category="tertiary"
|
||||
headertext=""
|
||||
menu-class=""
|
||||
size="medium"
|
||||
split="true"
|
||||
text="professor"
|
||||
variant="secondary"
|
||||
variant="default"
|
||||
>
|
||||
<gl-deprecated-dropdown-item-stub
|
||||
active="true"
|
||||
active-class="is-active"
|
||||
<gl-dropdown-item-stub
|
||||
avatarurl=""
|
||||
iconcolor=""
|
||||
iconname=""
|
||||
iconrightname=""
|
||||
ischecked="true"
|
||||
ischeckitem="true"
|
||||
secondarytext=""
|
||||
>
|
||||
<strong>
|
||||
professor
|
||||
|
|
@ -18,11 +26,16 @@ exports[`SplitButton renders actionItems 1`] = `
|
|||
<div>
|
||||
very symphonic
|
||||
</div>
|
||||
</gl-deprecated-dropdown-item-stub>
|
||||
</gl-dropdown-item-stub>
|
||||
|
||||
<gl-deprecated-dropdown-divider-stub />
|
||||
<gl-deprecated-dropdown-item-stub
|
||||
active-class="is-active"
|
||||
<gl-dropdown-divider-stub />
|
||||
<gl-dropdown-item-stub
|
||||
avatarurl=""
|
||||
iconcolor=""
|
||||
iconname=""
|
||||
iconrightname=""
|
||||
ischeckitem="true"
|
||||
secondarytext=""
|
||||
>
|
||||
<strong>
|
||||
captain
|
||||
|
|
@ -31,8 +44,8 @@ exports[`SplitButton renders actionItems 1`] = `
|
|||
<div>
|
||||
warp drive
|
||||
</div>
|
||||
</gl-deprecated-dropdown-item-stub>
|
||||
</gl-dropdown-item-stub>
|
||||
|
||||
<!---->
|
||||
</gl-deprecated-dropdown-stub>
|
||||
</gl-dropdown-stub>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
|
||||
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
|
||||
import SplitButton from '~/vue_shared/components/split_button.vue';
|
||||
|
|
@ -25,10 +25,10 @@ describe('SplitButton', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const findDropdown = () => wrapper.find(GlDeprecatedDropdown);
|
||||
const findDropdown = () => wrapper.find(GlDropdown);
|
||||
const findDropdownItem = (index = 0) =>
|
||||
findDropdown()
|
||||
.findAll(GlDeprecatedDropdownItem)
|
||||
.findAll(GlDropdownItem)
|
||||
.at(index);
|
||||
const selectItem = index => {
|
||||
findDropdownItem(index).vm.$emit('click');
|
||||
|
|
|
|||
|
|
@ -51,4 +51,15 @@ RSpec.describe PackagesHelper do
|
|||
expect(url).to eq("#{base_url}group/1/-/packages/composer/packages.json")
|
||||
end
|
||||
end
|
||||
|
||||
describe 'composer_config_repository_name' do
|
||||
let(:host) { Gitlab.config.gitlab.host }
|
||||
let(:group_id) { 1 }
|
||||
|
||||
it 'return global unique composer registry id' do
|
||||
id = helper.composer_config_repository_name(group_id)
|
||||
|
||||
expect(id).to eq("#{host}/#{group_id}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe StartupjsHelper do
|
||||
describe '#page_startup_graphql_calls' do
|
||||
let(:query_location) { 'repository/path_last_commit' }
|
||||
let(:query_content) do
|
||||
File.read(File.join(Rails.root, 'app/graphql/queries', "#{query_location}.query.graphql"))
|
||||
end
|
||||
|
||||
it 'returns an array containing GraphQL Page Startup Calls' do
|
||||
helper.add_page_startup_graphql_call(query_location, { ref: 'foo' })
|
||||
|
||||
startup_graphql_calls = helper.page_startup_graphql_calls
|
||||
|
||||
expect(startup_graphql_calls).to include({ query: query_content, variables: { ref: 'foo' } })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -284,7 +284,8 @@ RSpec.describe Gitlab::Danger::Helper do
|
|||
'.codeclimate.yml' | [:engineering_productivity]
|
||||
'.gitlab/CODEOWNERS' | [:engineering_productivity]
|
||||
|
||||
'lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml' | [:backend]
|
||||
'lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml' | [:ci_template]
|
||||
'lib/gitlab/ci/templates/dotNET-Core.yml' | [:ci_template]
|
||||
|
||||
'ee/FOO_VERSION' | [:unknown]
|
||||
|
||||
|
|
@ -376,6 +377,7 @@ RSpec.describe Gitlab::Danger::Helper do
|
|||
:none | ''
|
||||
:qa | '~QA'
|
||||
:engineering_productivity | '~"Engineering Productivity" for CI, Danger'
|
||||
:ci_template | '~"ci::templates"'
|
||||
end
|
||||
|
||||
with_them do
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ require 'webmock/rspec'
|
|||
require 'timecop'
|
||||
|
||||
require 'gitlab/danger/roulette'
|
||||
require 'active_support/testing/time_helpers'
|
||||
|
||||
RSpec.describe Gitlab::Danger::Roulette do
|
||||
include ActiveSupport::Testing::TimeHelpers
|
||||
|
||||
around do |example|
|
||||
travel_to(Time.utc(2020, 06, 22, 10)) { example.run }
|
||||
end
|
||||
|
|
@ -67,13 +70,25 @@ RSpec.describe Gitlab::Danger::Roulette do
|
|||
)
|
||||
end
|
||||
|
||||
let(:ci_template_reviewer) do
|
||||
Gitlab::Danger::Teammate.new(
|
||||
'username' => 'ci-template-maintainer',
|
||||
'name' => 'CI Template engineer',
|
||||
'role' => '~"ci::templates"',
|
||||
'projects' => { 'gitlab' => 'reviewer ci_template' },
|
||||
'available' => true,
|
||||
'tz_offset_hours' => 2.0
|
||||
)
|
||||
end
|
||||
|
||||
let(:teammates) do
|
||||
[
|
||||
backend_maintainer.to_h,
|
||||
frontend_maintainer.to_h,
|
||||
frontend_reviewer.to_h,
|
||||
software_engineer_in_test.to_h,
|
||||
engineering_productivity_reviewer.to_h
|
||||
engineering_productivity_reviewer.to_h,
|
||||
ci_template_reviewer.to_h
|
||||
]
|
||||
end
|
||||
|
||||
|
|
@ -166,6 +181,14 @@ RSpec.describe Gitlab::Danger::Roulette do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when change contains CI/CD Template category' do
|
||||
let(:categories) { [:ci_template] }
|
||||
|
||||
it 'assigns CI/CD Template reviewer and fallback to backend maintainer' do
|
||||
expect(spins).to eq([described_class::Spin.new(:ci_template, ci_template_reviewer, backend_maintainer, false, false)])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when change contains test category' do
|
||||
let(:categories) { [:test] }
|
||||
|
||||
|
|
@ -332,7 +355,8 @@ RSpec.describe Gitlab::Danger::Roulette do
|
|||
frontend_reviewer,
|
||||
frontend_maintainer,
|
||||
software_engineer_in_test,
|
||||
engineering_productivity_reviewer
|
||||
engineering_productivity_reviewer,
|
||||
ci_template_reviewer
|
||||
])
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ require 'timecop'
|
|||
require 'rspec-parameterized'
|
||||
|
||||
require 'gitlab/danger/teammate'
|
||||
require 'active_support/testing/time_helpers'
|
||||
|
||||
RSpec.describe Gitlab::Danger::Teammate do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
|
@ -148,6 +149,8 @@ RSpec.describe Gitlab::Danger::Teammate do
|
|||
end
|
||||
|
||||
describe '#local_hour' do
|
||||
include ActiveSupport::Testing::TimeHelpers
|
||||
|
||||
around do |example|
|
||||
travel_to(Time.utc(2020, 6, 23, 10)) { example.run }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -251,6 +251,36 @@ RSpec.describe API::Members do
|
|||
expect(json_response['id']).to eq(stranger.id)
|
||||
expect(json_response['access_level']).to eq(Member::DEVELOPER)
|
||||
end
|
||||
|
||||
describe 'executes the Members::CreateService for multiple user_ids' do
|
||||
it 'returns success when it successfully create all members' do
|
||||
expect do
|
||||
user_ids = [stranger.id, access_requester.id].join(',')
|
||||
|
||||
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
|
||||
params: { user_id: user_ids, access_level: Member::DEVELOPER }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
end.to change { source.members.count }.by(2)
|
||||
expect(json_response['status']).to eq('success')
|
||||
end
|
||||
|
||||
it 'returns the error message if there was an error adding members to group' do
|
||||
error_message = 'Unable to find User ID'
|
||||
user_ids = [stranger.id, access_requester.id].join(',')
|
||||
|
||||
allow_next_instance_of(::Members::CreateService) do |service|
|
||||
expect(service).to receive(:execute).with(source).and_return({ status: :error, message: error_message })
|
||||
end
|
||||
|
||||
expect do
|
||||
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
|
||||
params: { user_id: user_ids, access_level: Member::DEVELOPER }
|
||||
end.not_to change { source.members.count }
|
||||
expect(json_response['status']).to eq('error')
|
||||
expect(json_response['message']).to eq(error_message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'access levels' do
|
||||
|
|
|
|||
|
|
@ -12363,10 +12363,10 @@ vue-loader@^15.9.3:
|
|||
vue-hot-reload-api "^2.3.0"
|
||||
vue-style-loader "^4.1.0"
|
||||
|
||||
vue-router@^3.4.5:
|
||||
version "3.4.5"
|
||||
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.4.5.tgz#d396ec037b35931bdd1e9b7edd86f9788dc15175"
|
||||
integrity sha512-ioRY5QyDpXM9TDjOX6hX79gtaMXSVDDzSlbIlyAmbHNteIL81WIVB2e+jbzV23vzxtoV0krdS2XHm+GxFg+Nxg==
|
||||
vue-router@^3.4.6:
|
||||
version "3.4.6"
|
||||
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.4.6.tgz#f7bda2c9a43d39837621c9a02ba7789f5daa24b2"
|
||||
integrity sha512-kaXnB3pfFxhAJl/Mp+XG1HJMyFqrL/xPqV7oXlpXn4AwMmm6VNgf0nllW8ksflmZANfI4kdo0bVn/FYSsAolPQ==
|
||||
|
||||
vue-runtime-helpers@^1.1.2:
|
||||
version "1.1.2"
|
||||
|
|
|
|||
Loading…
Reference in New Issue