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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/analytics/instance_statistics/utils.js40
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue121
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue15
-rw-r--r--app/assets/javascripts/ide/lib/errors.js38
-rw-r--r--app/assets/javascripts/ide/stores/getters.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js18
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/mutations.js4
-rw-r--r--app/assets/javascripts/ide/utils.js28
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue10
-rw-r--r--app/assets/javascripts/incidents/constants.js9
-rw-r--r--app/assets/javascripts/issuable_show/components/issuable_description.vue31
-rw-r--r--app/assets/javascripts/issuable_show/components/issuable_edit_form.vue135
-rw-r--r--app/assets/javascripts/issuable_show/components/issuable_title.vue96
-rw-r--r--app/assets/javascripts/issuable_show/event_hub.js3
-rw-r--r--app/assets/javascripts/packages/details/components/composer_installation.vue56
-rw-r--r--app/assets/javascripts/packages/details/store/getters.js17
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue4
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue2
-rw-r--r--app/assets/javascripts/repository/index.js56
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/split_button.vue30
-rw-r--r--app/assets/stylesheets/_page_specific_files.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/error_tracking_index.scss (renamed from app/assets/stylesheets/pages/error_list.scss)8
-rw-r--r--app/assets/stylesheets/page_bundles/merge_conflicts.scss8
-rw-r--r--app/assets/stylesheets/pages/error_tracking_list.scss5
-rw-r--r--app/graphql/queries/repository/path_last_commit.query.graphql (renamed from app/assets/javascripts/repository/queries/path_last_commit.query.graphql)9
-rw-r--r--app/helpers/packages_helper.rb4
-rw-r--r--app/helpers/startupjs_helper.rb17
-rw-r--r--app/models/commit.rb4
-rw-r--r--app/services/members/create_service.rb2
-rw-r--r--app/views/admin/dev_ops_report/show.html.haml2
-rw-r--r--app/views/layouts/_startup_js.html.haml22
-rw-r--r--app/views/projects/diffs/_text_file.html.haml2
-rw-r--r--app/views/projects/error_tracking/index.html.haml1
-rw-r--r--app/views/projects/packages/packages/show.html.haml3
-rw-r--r--app/views/projects/tree/show.html.haml2
-rw-r--r--changelogs/unreleased/249819-ide-improve-errors.yml5
-rw-r--r--changelogs/unreleased/254706-fix-commit-item-layout-on-mrs.yml5
-rw-r--r--changelogs/unreleased/262073-fix-merge-conflict-button-text-none.yml5
-rw-r--r--changelogs/unreleased/264790-bs4-optimization-admin-devops-report.yml5
-rw-r--r--changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-vu.yml5
-rw-r--r--changelogs/unreleased/sh-improve-merge-error-display.yml5
-rw-r--r--changelogs/unreleased/update-members-api-for-multiple-user_ids.yml5
-rw-r--r--config/application.rb1
-rw-r--r--config/webpack.config.js1
-rw-r--r--danger/ci_templates/Dangerfile30
-rw-r--r--danger/specialization_labels/Dangerfile3
-rw-r--r--doc/api/members.md2
-rw-r--r--doc/ci/multi_project_pipelines.md42
-rw-r--r--doc/ci/parent_child_pipelines.md4
-rw-r--r--doc/gitlab-basics/start-using-git.md21
-rw-r--r--doc/ssh/README.md4
-rw-r--r--doc/subscriptions/gitlab_com/index.md9
-rw-r--r--doc/topics/git/git_rebase.md272
-rw-r--r--doc/topics/git/img/git_rebase_v13_5.pngbin0 -> 49048 bytes
-rw-r--r--doc/topics/git/index.md1
-rw-r--r--jest.config.base.js1
-rw-r--r--lib/api/members.rb30
-rw-r--r--lib/gitlab/danger/helper.rb5
-rw-r--r--lib/gitlab/danger/roulette.rb5
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff_base.rb4
-rw-r--r--lib/gitlab/git/diff_collection.rb10
-rw-r--r--locale/gitlab.pot30
-rw-r--r--package.json2
-rw-r--r--spec/frontend/analytics/instance_statistics/components/instance_counts_spec.js6
-rw-r--r--spec/frontend/analytics/instance_statistics/mock_data.js28
-rw-r--r--spec/frontend/analytics/instance_statistics/utils_spec.js41
-rw-r--r--spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap90
-rw-r--r--spec/frontend/ide/components/commit_sidebar/form_spec.js37
-rw-r--r--spec/frontend/ide/lib/errors_spec.js46
-rw-r--r--spec/frontend/ide/stores/getters_spec.js10
-rw-r--r--spec/frontend/ide/stores/modules/commit/actions_spec.js82
-rw-r--r--spec/frontend/ide/utils_spec.js40
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js18
-rw-r--r--spec/frontend/issuable_show/components/issuable_description_spec.js41
-rw-r--r--spec/frontend/issuable_show/components/issuable_edit_form_spec.js122
-rw-r--r--spec/frontend/issuable_show/components/issuable_title_spec.js100
-rw-r--r--spec/frontend/monitoring/router_spec.js3
-rw-r--r--spec/frontend/packages/details/components/composer_installation_spec.js7
-rw-r--r--spec/frontend/packages/details/store/getters_spec.js26
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap37
-rw-r--r--spec/frontend/vue_shared/components/split_button_spec.js6
-rw-r--r--spec/helpers/packages_helper_spec.rb11
-rw-r--r--spec/helpers/startupjs_helper_spec.rb20
-rw-r--r--spec/lib/gitlab/danger/helper_spec.rb4
-rw-r--r--spec/lib/gitlab/danger/roulette_spec.rb28
-rw-r--r--spec/lib/gitlab/danger/teammate_spec.rb3
-rw-r--r--spec/requests/api/members_spec.rb30
-rw-r--r--yarn.lock8
89 files changed, 1817 insertions, 355 deletions
diff --git a/app/assets/javascripts/analytics/instance_statistics/utils.js b/app/assets/javascripts/analytics/instance_statistics/utils.js
new file mode 100644
index 00000000000..30c6205b7ff
--- /dev/null
+++ b/app/assets/javascripts/analytics/instance_statistics/utils.js
@@ -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];
+ });
+}
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 09abdbe25d7..1b747fb7f20 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -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>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 73c56514fce..f36fe87ccfa 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -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>
diff --git a/app/assets/javascripts/ide/lib/errors.js b/app/assets/javascripts/ide/lib/errors.js
index 6ae18bc8180..e62d9d1e77f 100644
--- a/app/assets/javascripts/ide/lib/errors.js
+++ b/app/assets/javascripts/ide/lib/errors.js
@@ -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);
};
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index b8304a9b68d..500ce9f32d5 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -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;
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 90a6c644d17..e0d2028d2e1 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -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
diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutations.js b/app/assets/javascripts/ide/stores/modules/commit/mutations.js
index 2cf6e8e6f36..c4bfad6405e 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/mutations.js
@@ -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, {
diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js
index f7ecf6340d9..404c5c571ba 100644
--- a/app/assets/javascripts/ide/utils.js
+++ b/app/assets/javascripts/ide/utils.js
@@ -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,
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index 061a5becbed..50f2136325d 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -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>
diff --git a/app/assets/javascripts/incidents/constants.js b/app/assets/javascripts/incidents/constants.js
index 797439495e3..bdabf1c3e42 100644
--- a/app/assets/javascripts/incidents/constants.js
+++ b/app/assets/javascripts/incidents/constants.js
@@ -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' };
diff --git a/app/assets/javascripts/issuable_show/components/issuable_description.vue b/app/assets/javascripts/issuable_show/components/issuable_description.vue
new file mode 100644
index 00000000000..091a4be5bd8
--- /dev/null
+++ b/app/assets/javascripts/issuable_show/components/issuable_description.vue
@@ -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>
diff --git a/app/assets/javascripts/issuable_show/components/issuable_edit_form.vue b/app/assets/javascripts/issuable_show/components/issuable_edit_form.vue
new file mode 100644
index 00000000000..7b9a83a740f
--- /dev/null
+++ b/app/assets/javascripts/issuable_show/components/issuable_edit_form.vue
@@ -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>
diff --git a/app/assets/javascripts/issuable_show/components/issuable_title.vue b/app/assets/javascripts/issuable_show/components/issuable_title.vue
new file mode 100644
index 00000000000..d3b42fd2ffb
--- /dev/null
+++ b/app/assets/javascripts/issuable_show/components/issuable_title.vue
@@ -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>
diff --git a/app/assets/javascripts/issuable_show/event_hub.js b/app/assets/javascripts/issuable_show/event_hub.js
new file mode 100644
index 00000000000..e31806ad199
--- /dev/null
+++ b/app/assets/javascripts/issuable_show/event_hub.js
@@ -0,0 +1,3 @@
+import createEventHub from '~/helpers/event_hub_factory';
+
+export default createEventHub();
diff --git a/app/assets/javascripts/packages/details/components/composer_installation.vue b/app/assets/javascripts/packages/details/components/composer_installation.vue
index 60ad468c293..0518fac98fc 100644
--- a/app/assets/javascripts/packages/details/components/composer_installation.vue
+++ b/app/assets/javascripts/packages/details/components/composer_installation.vue
@@ -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>
diff --git a/app/assets/javascripts/packages/details/store/getters.js b/app/assets/javascripts/packages/details/store/getters.js
index bb0ae3e9ab7..14e76ac84bd 100644
--- a/app/assets/javascripts/packages/details/store/getters.js
+++ b/app/assets/javascripts/packages/details/store/getters.js
@@ -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;
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
index 089cac9ee4c..e18cfefc3ca 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
@@ -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,
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 3e87833f7f5..0e2bccfabdd 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -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: {
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index 65da8f70b40..0e4d724e949 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -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;
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index acf6f65b1a0..46749fc5e87 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -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" />
diff --git a/app/assets/javascripts/vue_shared/components/split_button.vue b/app/assets/javascripts/vue_shared/components/split_button.vue
index e9b99c6ea78..11049028ff6 100644
--- a/app/assets/javascripts/vue_shared/components/split_button.vue
+++ b/app/assets/javascripts/vue_shared/components/split_button.vue
@@ -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>
diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss
index d3ab4be925b..d49134eb648 100644
--- a/app/assets/stylesheets/_page_specific_files.scss
+++ b/app/assets/stylesheets/_page_specific_files.scss
@@ -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';
diff --git a/app/assets/stylesheets/pages/error_list.scss b/app/assets/stylesheets/page_bundles/error_tracking_index.scss
index 3ec3e4f6b43..65bddfb7890 100644
--- a/app/assets/stylesheets/pages/error_list.scss
+++ b/app/assets/stylesheets/page_bundles/error_tracking_index.scss
@@ -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;
diff --git a/app/assets/stylesheets/page_bundles/merge_conflicts.scss b/app/assets/stylesheets/page_bundles/merge_conflicts.scss
index 25d913c79de..b0655408edf 100644
--- a/app/assets/stylesheets/page_bundles/merge_conflicts.scss
+++ b/app/assets/stylesheets/page_bundles/merge_conflicts.scss
@@ -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;
diff --git a/app/assets/stylesheets/pages/error_tracking_list.scss b/app/assets/stylesheets/pages/error_tracking_list.scss
deleted file mode 100644
index cc391ca6c97..00000000000
--- a/app/assets/stylesheets/pages/error_tracking_list.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.error-list {
- .dropdown {
- min-width: auto;
- }
-}
diff --git a/app/assets/javascripts/repository/queries/path_last_commit.query.graphql b/app/graphql/queries/repository/path_last_commit.query.graphql
index 51f3f790a5d..d845f7c6224 100644
--- a/app/assets/javascripts/repository/queries/path_last_commit.query.graphql
+++ b/app/graphql/queries/repository/path_last_commit.query.graphql
@@ -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
diff --git a/app/helpers/packages_helper.rb b/app/helpers/packages_helper.rb
index a3a899dbe1a..8f365fd0786 100644
--- a/app/helpers/packages_helper.rb
+++ b/app/helpers/packages_helper.rb
@@ -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,
diff --git a/app/helpers/startupjs_helper.rb b/app/helpers/startupjs_helper.rb
new file mode 100644
index 00000000000..b595590c7c9
--- /dev/null
+++ b/app/helpers/startupjs_helper.rb
@@ -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
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 5fe1b451ccd..83400c9e533 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -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)
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
index 610288c5e76..088e6f031c8 100644
--- a/app/services/members/create_service.rb
+++ b/app/services/members/create_service.rb
@@ -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
diff --git a/app/views/admin/dev_ops_report/show.html.haml b/app/views/admin/dev_ops_report/show.html.haml
index f8dadaaa175..88105be70fb 100644
--- a/app/views/admin/dev_ops_report/show.html.haml
+++ b/app/views/admin/dev_ops_report/show.html.haml
@@ -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}")
diff --git a/app/views/layouts/_startup_js.html.haml b/app/views/layouts/_startup_js.html.haml
index 33c759b7a7c..f312e00c394 100644
--- a/app/views/layouts/_startup_js.html.haml
+++ b/app/views/layouts/_startup_js.html.haml
@@ -1,9 +1,11 @@
-- return unless page_startup_api_calls.present?
+- return unless page_startup_api_calls.present? || page_startup_graphql_calls.present?
= javascript_tag nonce: true do
:plain
var gl = window.gl || {};
gl.startup_calls = #{page_startup_api_calls.to_json};
+ gl.startup_graphql_calls = #{page_startup_graphql_calls.to_json};
+
if (gl.startup_calls && window.fetch) {
Object.keys(gl.startup_calls).forEach(apiCall => {
// fetch wonā€™t send cookies in older browsers, unless you set the credentials init option.
@@ -14,3 +16,21 @@
};
});
}
+ if (gl.startup_graphql_calls && window.fetch) {
+ const url = `#{api_graphql_url}`
+
+ const opts = {
+ method: "POST",
+ headers: { "Content-Type": "application/json", 'X-CSRF-Token': "#{form_authenticity_token}" },
+ };
+
+ gl.startup_graphql_calls = gl.startup_graphql_calls.map(call => ({
+ operationName: call.query.match(/^query (.+)\(/)[1],
+ fetchCall: fetch(url, {
+ ...opts,
+ credentials: 'same-origin',
+ body: JSON.stringify(call)
+ })
+ }))
+ }
+
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index a945ff5aedf..5a7830e306a 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -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.")
diff --git a/app/views/projects/error_tracking/index.html.haml b/app/views/projects/error_tracking/index.html.haml
index 96f61584a99..ffe0785d327 100644
--- a/app/views/projects/error_tracking/index.html.haml
+++ b/app/views/projects/error_tracking/index.html.haml
@@ -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) }
diff --git a/app/views/projects/packages/packages/show.html.haml b/app/views/projects/packages/packages/show.html.haml
index 97a3c6e7092..aeca3f5b3e3 100644
--- a/app/views/projects/packages/packages/show.html.haml
+++ b/app/views/projects/packages/packages/show.html.haml
@@ -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)} }
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 3dd12a7b641..8c874ed1505 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -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
diff --git a/changelogs/unreleased/249819-ide-improve-errors.yml b/changelogs/unreleased/249819-ide-improve-errors.yml
new file mode 100644
index 00000000000..925f7e5f647
--- /dev/null
+++ b/changelogs/unreleased/249819-ide-improve-errors.yml
@@ -0,0 +1,5 @@
+---
+title: Improve WebIDE error messages on committing
+merge_request: 43408
+author:
+type: changed
diff --git a/changelogs/unreleased/254706-fix-commit-item-layout-on-mrs.yml b/changelogs/unreleased/254706-fix-commit-item-layout-on-mrs.yml
new file mode 100644
index 00000000000..bfa48ad3d93
--- /dev/null
+++ b/changelogs/unreleased/254706-fix-commit-item-layout-on-mrs.yml
@@ -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
diff --git a/changelogs/unreleased/262073-fix-merge-conflict-button-text-none.yml b/changelogs/unreleased/262073-fix-merge-conflict-button-text-none.yml
new file mode 100644
index 00000000000..2c819d7485d
--- /dev/null
+++ b/changelogs/unreleased/262073-fix-merge-conflict-button-text-none.yml
@@ -0,0 +1,5 @@
+---
+title: Fix merge conflict button text if "None" code style selected
+merge_request: 44427
+author: David Barr @davebarr
+type: fixed
diff --git a/changelogs/unreleased/264790-bs4-optimization-admin-devops-report.yml b/changelogs/unreleased/264790-bs4-optimization-admin-devops-report.yml
new file mode 100644
index 00000000000..cd3f178a3ef
--- /dev/null
+++ b/changelogs/unreleased/264790-bs4-optimization-admin-devops-report.yml
@@ -0,0 +1,5 @@
+---
+title: Remove duplicated BS display properties from Admin DevOps report' HAML
+merge_request: 44846
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-vu.yml b/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-vu.yml
new file mode 100644
index 00000000000..e2ba6e3c553
--- /dev/null
+++ b/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-vu.yml
@@ -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
diff --git a/changelogs/unreleased/sh-improve-merge-error-display.yml b/changelogs/unreleased/sh-improve-merge-error-display.yml
new file mode 100644
index 00000000000..dd008563b04
--- /dev/null
+++ b/changelogs/unreleased/sh-improve-merge-error-display.yml
@@ -0,0 +1,5 @@
+---
+title: Fix unnecessarily escaped merge error text
+merge_request: 44844
+author:
+type: fixed
diff --git a/changelogs/unreleased/update-members-api-for-multiple-user_ids.yml b/changelogs/unreleased/update-members-api-for-multiple-user_ids.yml
new file mode 100644
index 00000000000..49c3c97c5c1
--- /dev/null
+++ b/changelogs/unreleased/update-members-api-for-multiple-user_ids.yml
@@ -0,0 +1,5 @@
+---
+title: Update Add Members API to accept user_id array
+merge_request: 44051
+author:
+type: added
diff --git a/config/application.rb b/config/application.rb
index 9c8a7d946b1..bb961877f4c 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -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"
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 147f8c25802..821ddc84e1a 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -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'),
diff --git a/danger/ci_templates/Dangerfile b/danger/ci_templates/Dangerfile
new file mode 100644
index 00000000000..34b4bbff7a5
--- /dev/null
+++ b/danger/ci_templates/Dangerfile
@@ -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
diff --git a/danger/specialization_labels/Dangerfile b/danger/specialization_labels/Dangerfile
index 919f7313b49..ac93eb4c3e1 100644
--- a/danger/specialization_labels/Dangerfile
+++ b/danger/specialization_labels/Dangerfile
@@ -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|
diff --git a/doc/api/members.md b/doc/api/members.md
index 6886ed410e0..ccd8a8aaf61 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -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 |
diff --git a/doc/ci/multi_project_pipelines.md b/doc/ci/multi_project_pipelines.md
index a379a6e8211..edca93a92d0 100644
--- a/doc/ci/multi_project_pipelines.md
+++ b/doc/ci/multi_project_pipelines.md
@@ -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.
diff --git a/doc/ci/parent_child_pipelines.md b/doc/ci/parent_child_pipelines.md
index 7fdf9f785ce..fbae20defea 100644
--- a/doc/ci/parent_child_pipelines.md
+++ b/doc/ci/parent_child_pipelines.md
@@ -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).
diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md
index 3812fd3b92a..b6192700d29 100644
--- a/doc/gitlab-basics/start-using-git.md
+++ b/doc/gitlab-basics/start-using-git.md
@@ -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
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index 6a4dc5426f8..9d851edb688 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -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
---
diff --git a/doc/subscriptions/gitlab_com/index.md b/doc/subscriptions/gitlab_com/index.md
index cc80d57dc2f..128e9d07282 100644
--- a/doc/subscriptions/gitlab_com/index.md
+++ b/doc/subscriptions/gitlab_com/index.md
@@ -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).
diff --git a/doc/topics/git/git_rebase.md b/doc/topics/git/git_rebase.md
new file mode 100644
index 00000000000..6f50dea26dd
--- /dev/null
+++ b/doc/topics/git/git_rebase.md
@@ -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.
diff --git a/doc/topics/git/img/git_rebase_v13_5.png b/doc/topics/git/img/git_rebase_v13_5.png
new file mode 100644
index 00000000000..ff29fa97798
--- /dev/null
+++ b/doc/topics/git/img/git_rebase_v13_5.png
Binary files differ
diff --git a/doc/topics/git/index.md b/doc/topics/git/index.md
index 92181fb7bb0..cb2d7b74522 100644
--- a/doc/topics/git/index.md
+++ b/doc/topics/git/index.md
@@ -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)
diff --git a/jest.config.base.js b/jest.config.base.js
index 80fe18b628d..95b3e810200 100644
--- a/jest.config.base.js
+++ b/jest.config.base.js
@@ -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',
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 4edf94c6350..309d3023c63 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -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
diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb
index d01455c9ec4..783a5f1715c 100644
--- a/lib/gitlab/danger/helper.rb
+++ b/lib/gitlab/danger/helper.rb
@@ -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,
diff --git a/lib/gitlab/danger/roulette.rb b/lib/gitlab/danger/roulette.rb
index e67e4a45bfe..23f877b4e0f 100644
--- a/lib/gitlab/danger/roulette.rb
+++ b/lib/gitlab/danger/roulette.rb
@@ -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
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb
index 341572f9c94..16257bb5ff5 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb
@@ -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
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index 2fa88973bae..2771057f51b 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 4db6f3b8b55..f12c22becbd 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/package.json b/package.json
index a5fb7b59a5e..ea8cb94792d 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/spec/frontend/analytics/instance_statistics/components/instance_counts_spec.js b/spec/frontend/analytics/instance_statistics/components/instance_counts_spec.js
index 2274f4c3fde..12b5e14b9c4 100644
--- a/spec/frontend/analytics/instance_statistics/components/instance_counts_spec.js
+++ b/spec/frontend/analytics/instance_statistics/components/instance_counts_spec.js
@@ -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);
});
});
});
diff --git a/spec/frontend/analytics/instance_statistics/mock_data.js b/spec/frontend/analytics/instance_statistics/mock_data.js
index 9fabf3a4c65..c3f5069da28 100644
--- a/spec/frontend/analytics/instance_statistics/mock_data.js
+++ b/spec/frontend/analytics/instance_statistics/mock_data.js
@@ -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
+];
diff --git a/spec/frontend/analytics/instance_statistics/utils_spec.js b/spec/frontend/analytics/instance_statistics/utils_spec.js
new file mode 100644
index 00000000000..f6ea81eb678
--- /dev/null
+++ b/spec/frontend/analytics/instance_statistics/utils_spec.js
@@ -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);
+ });
+ });
+});
diff --git a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
index 93b757e008a..15eeadcc8b8 100644
--- a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
+++ b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
@@ -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>
diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js
index 56667d6b03d..abd7e3bb8fc 100644
--- a/spec/frontend/ide/components/commit_sidebar/form_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js
@@ -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);
+ },
+ );
});
});
diff --git a/spec/frontend/ide/lib/errors_spec.js b/spec/frontend/ide/lib/errors_spec.js
index 8c3fb378302..733d5a5da3c 100644
--- a/spec/frontend/ide/lib/errors_spec.js
+++ b/spec/frontend/ide/lib/errors_spec.js
@@ -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 }) => {
diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js
index e24f08fa802..5ae87f5f9cd 100644
--- a/spec/frontend/ide/stores/getters_spec.js
+++ b/spec/frontend/ide/stores/getters_spec.js
@@ -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();
diff --git a/spec/frontend/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js
index babc50e54f1..cfe2bddf76c 100644
--- a/spec/frontend/ide/stores/modules/commit/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/actions_spec.js
@@ -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,
- });
+ describe('updateBranchName', () => {
+ let originalGon;
- testAction(
- actions.updateCommitAction,
- {},
- store.state,
- [
- {
- type: mutationTypes.UPDATE_COMMIT_ACTION,
- payload: { commitAction: expect.anything() },
- },
- { type: mutationTypes.TOGGLE_SHOULD_CREATE_MR, payload: true },
- ],
- [],
- done,
- );
+ beforeEach(() => {
+ originalGon = window.gon;
+ window.gon = { current_username: 'johndoe' };
+
+ store.state.currentBranchId = 'master';
});
- it('sets shouldCreateMR to false if "Create new MR" option is hidden', done => {
- Object.assign(store.state, {
- shouldHideNewMrOption: true,
- });
+ afterEach(() => {
+ window.gon = originalGon;
+ });
- testAction(
- actions.updateCommitAction,
- {},
- store.state,
- [
- {
- type: mutationTypes.UPDATE_COMMIT_ACTION,
- payload: { commitAction: expect.anything() },
- },
- { type: mutationTypes.TOGGLE_SHOULD_CREATE_MR, payload: false },
- ],
- [],
- done,
- );
+ it('updates store with new branch name', async () => {
+ await store.dispatch('commit/updateBranchName', 'branch-name');
+
+ expect(store.state.commit.newBranchName).toBe('branch-name');
});
});
- 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);
+ 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();
diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js
index 97dc8217ecc..6cd2128d356 100644
--- a/spec/frontend/ide/utils_spec.js
+++ b/spec/frontend/ide/utils_spec.js
@@ -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);
+ });
+ });
});
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
index c4dfaabe36d..935e4ec8b8e 100644
--- a/spec/frontend/incidents/components/incidents_list_spec.js
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -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', () => {
diff --git a/spec/frontend/issuable_show/components/issuable_description_spec.js b/spec/frontend/issuable_show/components/issuable_description_spec.js
new file mode 100644
index 00000000000..1dd8348b098
--- /dev/null
+++ b/spec/frontend/issuable_show/components/issuable_description_spec.js
@@ -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();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issuable_show/components/issuable_edit_form_spec.js b/spec/frontend/issuable_show/components/issuable_edit_form_spec.js
new file mode 100644
index 00000000000..352e66cdffe
--- /dev/null
+++ b/spec/frontend/issuable_show/components/issuable_edit_form_spec.js
@@ -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);
+ });
+ });
+});
diff --git a/spec/frontend/issuable_show/components/issuable_title_spec.js b/spec/frontend/issuable_show/components/issuable_title_spec.js
new file mode 100644
index 00000000000..e8621c763b3
--- /dev/null
+++ b/spec/frontend/issuable_show/components/issuable_title_spec.js
@@ -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);
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/router_spec.js b/spec/frontend/monitoring/router_spec.js
index 8b97c8ed125..2bf2065b178 100644
--- a/spec/frontend/monitoring/router_spec.js
+++ b/spec/frontend/monitoring/router_spec.js
@@ -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);
diff --git a/spec/frontend/packages/details/components/composer_installation_spec.js b/spec/frontend/packages/details/components/composer_installation_spec.js
index c13981fbb87..f5dab1b3b7c 100644
--- a/spec/frontend/packages/details/components/composer_installation_spec.js
+++ b/spec/frontend/packages/details/components/composer_installation_spec.js
@@ -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', () => {
diff --git a/spec/frontend/packages/details/store/getters_spec.js b/spec/frontend/packages/details/store/getters_spec.js
index 378d259ad3f..b8c2138e7f5 100644
--- a/spec/frontend/packages/details/store/getters_spec.js
+++ b/spec/frontend/packages/details/store/getters_spec.js
@@ -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);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
index fcb9c4b8b02..8eb0e8f9550 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
@@ -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>
`;
diff --git a/spec/frontend/vue_shared/components/split_button_spec.js b/spec/frontend/vue_shared/components/split_button_spec.js
index f3bd4c14717..e09bc073042 100644
--- a/spec/frontend/vue_shared/components/split_button_spec.js
+++ b/spec/frontend/vue_shared/components/split_button_spec.js
@@ -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');
diff --git a/spec/helpers/packages_helper_spec.rb b/spec/helpers/packages_helper_spec.rb
index 3988b80dd13..dacd386d01c 100644
--- a/spec/helpers/packages_helper_spec.rb
+++ b/spec/helpers/packages_helper_spec.rb
@@ -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
diff --git a/spec/helpers/startupjs_helper_spec.rb b/spec/helpers/startupjs_helper_spec.rb
new file mode 100644
index 00000000000..6d61c38d4a5
--- /dev/null
+++ b/spec/helpers/startupjs_helper_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb
index 708e9a13aed..509649f08c6 100644
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ b/spec/lib/gitlab/danger/helper_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/danger/roulette_spec.rb b/spec/lib/gitlab/danger/roulette_spec.rb
index 9acaa57ee10..1a900dfba22 100644
--- a/spec/lib/gitlab/danger/roulette_spec.rb
+++ b/spec/lib/gitlab/danger/roulette_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/danger/teammate_spec.rb b/spec/lib/gitlab/danger/teammate_spec.rb
index 5a47d74a7f3..eebe14ed5e1 100644
--- a/spec/lib/gitlab/danger/teammate_spec.rb
+++ b/spec/lib/gitlab/danger/teammate_spec.rb
@@ -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
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 55b2447fc68..6a2419fbbf3 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -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
diff --git a/yarn.lock b/yarn.lock
index 0fcd75fc023..be26e0074f6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"