Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-13 12:08:41 +00:00
parent 15ae4a8da8
commit 6e91fbf774
89 changed files with 1822 additions and 360 deletions

View File

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

View File

@ -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">&middot; {{ 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">&middot; {{ 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
import createEventHub from '~/helpers/event_hub_factory';
export default createEventHub();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
.error-list {
.dropdown {
min-width: auto;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
---
title: Improve WebIDE error messages on committing
merge_request: 43408
author:
type: changed

View File

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

View File

@ -0,0 +1,5 @@
---
title: Fix merge conflict button text if "None" code style selected
merge_request: 44427
author: David Barr @davebarr
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Remove duplicated BS display properties from Admin DevOps report' HAML
merge_request: 44846
author: Takuya Noguchi
type: other

View File

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

View File

@ -0,0 +1,5 @@
---
title: Fix unnecessarily escaped merge error text
merge_request: 44844
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Update Add Members API to accept user_id array
merge_request: 44051
author:
type: added

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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`:
![Git rebase illustration](img/git_rebase_v13_5.png)
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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();

View File

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

View File

@ -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', () => {

View File

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

View File

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

View File

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

View File

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

View File

@ -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', () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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