summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/diffs/components/app.vue22
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue8
-rw-r--r--app/assets/javascripts/diffs/store/actions.js49
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js80
-rw-r--r--app/assets/javascripts/diffs/store/utils.js287
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue99
-rw-r--r--app/assets/javascripts/error_tracking/details.js19
-rw-r--r--app/assets/javascripts/error_tracking/queries/details.query.graphql18
-rw-r--r--app/assets/javascripts/helpers/diffs_helper.js19
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js37
-rw-r--r--app/helpers/projects/error_tracking_helper.rb4
-rw-r--r--app/models/ci/trigger.rb13
-rw-r--r--app/policies/ci/trigger_policy.rb3
-rw-r--r--app/views/errors/_footer.html.haml2
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml2
-rw-r--r--app/views/projects/triggers/_content.html.haml9
-rw-r--r--app/views/projects/triggers/_index.html.haml1
-rw-r--r--app/views/projects/triggers/_trigger.html.haml7
-rw-r--r--app/views/projects/triggers/edit.html.haml4
-rw-r--r--changelogs/unreleased/14857-activate-promethus-integration-for-projects.yml5
-rw-r--r--changelogs/unreleased/29772-completely-removing-use_legacy_pipeline_triggers.yml5
-rw-r--r--changelogs/unreleased/34867-epics_to_project_import_export.yml5
-rw-r--r--changelogs/unreleased/39979-grapql-for-error-details.yml5
-rw-r--r--changelogs/unreleased/enable-parent-child-pipelines.yml5
-rw-r--r--changelogs/unreleased/feature-split-diff-attempt-3.yml5
-rw-r--r--changelogs/unreleased/refactor-session-disable-with-post.yml5
-rw-r--r--config/initializers/8_devise.rb2
-rw-r--r--config/routes/admin.rb2
-rw-r--r--db/post_migrate/20191204114127_delete_legacy_triggers.rb17
-rw-r--r--db/post_migrate/20191220102807_patch_prometheus_services_for_shared_cluster_applications.rb88
-rw-r--r--db/schema.rb2
-rw-r--r--doc/ci/img/parent_pipeline_graph_expanded_v12_6.pngbin0 -> 298902 bytes
-rw-r--r--doc/ci/parent_child_pipelines.md86
-rw-r--r--doc/ci/pipelines.md7
-rw-r--r--doc/ci/yaml/README.md31
-rw-r--r--lib/gitlab/background_migration/activate_prometheus_services_for_shared_cluster_applications.rb52
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb6
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/abilities.rb26
-rw-r--r--lib/gitlab/import_export/group_project_object_builder.rb14
-rw-r--r--lib/gitlab/import_export/import_export.yml8
-rw-r--r--lib/gitlab/import_export/relation_factory.rb23
-rw-r--r--lib/gitlab/import_export/relation_tree_restorer.rb2
-rw-r--r--package.json1
-rw-r--r--qa/qa/page/project/web_ide/edit.rb6
-rw-r--r--spec/controllers/admin/sessions_controller_spec.rb4
-rw-r--r--spec/features/merge_request/maintainer_edits_fork_spec.rb2
-rw-r--r--spec/features/merge_request/user_comments_on_diff_spec.rb3
-rw-r--r--spec/features/merge_request/user_creates_image_diff_notes_spec.rb3
-rw-r--r--spec/features/merge_request/user_expands_diff_spec.rb3
-rw-r--r--spec/features/merge_request/user_posts_diff_notes_spec.rb3
-rw-r--r--spec/features/merge_request/user_posts_notes_spec.rb4
-rw-r--r--spec/features/merge_request/user_resolves_conflicts_spec.rb3
-rw-r--r--spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb3
-rw-r--r--spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb3
-rw-r--r--spec/features/merge_request/user_sees_diff_spec.rb3
-rw-r--r--spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb3
-rw-r--r--spec/features/merge_request/user_sees_versions_spec.rb3
-rw-r--r--spec/features/merge_request/user_suggests_changes_on_diff_spec.rb3
-rw-r--r--spec/features/merge_request/user_toggles_whitespace_changes_spec.rb3
-rw-r--r--spec/features/merge_request/user_views_diffs_spec.rb3
-rw-r--r--spec/features/projects/blobs/edit_spec.rb4
-rw-r--r--spec/features/projects/view_on_env_spec.rb3
-rw-r--r--spec/features/triggers_spec.rb92
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group/project.json61
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js54
-rw-r--r--spec/frontend/helpers/diffs_helper_spec.js113
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js2
-rw-r--r--spec/frontend/monitoring/mock_data.js141
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js4
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js8
-rw-r--r--spec/helpers/projects/error_tracking_helper_spec.rb9
-rw-r--r--spec/javascripts/diffs/components/app_spec.js145
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js4
-rw-r--r--spec/javascripts/diffs/store/mutations_spec.js58
-rw-r--r--spec/javascripts/diffs/store/utils_spec.js33
-rw-r--r--spec/lib/gitlab/background_migration/activate_prometheus_services_for_shared_cluster_applications_spec.rb75
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb41
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml27
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb8
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml30
-rw-r--r--spec/migrations/20191204114127_delete_legacy_triggers_spec.rb23
-rw-r--r--spec/migrations/patch_prometheus_services_for_shared_cluster_applications_spec.rb134
-rw-r--r--spec/models/ci/trigger_spec.rb59
-rw-r--r--spec/policies/ci/trigger_policy_spec.rb54
-rw-r--r--spec/requests/api/triggers_spec.rb16
-rw-r--r--spec/routing/admin_routing_spec.rb14
-rw-r--r--spec/routing/routing_spec.rb6
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb15
-rw-r--r--spec/support/migrations_helpers/prometheus_service_helpers.rb35
-rw-r--r--spec/support/shared_examples/merge_requests_rendering_a_single_diff_version.rb21
-rw-r--r--yarn.lock5
93 files changed, 1311 insertions, 1127 deletions
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index c07850b1a4f..c6d32ffef34 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -6,6 +6,7 @@ import { __ } from '~/locale';
import createFlash from '~/flash';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { isSingleViewStyle } from '~/helpers/diffs_helper';
import eventHub from '../../notes/event_hub';
import CompareVersions from './compare_versions.vue';
import DiffFile from './diff_file.vue';
@@ -145,6 +146,9 @@ export default {
},
watch: {
diffViewType() {
+ if (this.needsReload() || this.needsFirstLoad()) {
+ this.refetchDiffData();
+ }
this.adjustView();
},
shouldShow() {
@@ -224,6 +228,16 @@ export default {
{ timeout: 1000 },
);
},
+ needsReload() {
+ return (
+ this.glFeatures.singleMrDiffView &&
+ this.diffFiles.length &&
+ isSingleViewStyle(this.diffFiles[0])
+ );
+ },
+ needsFirstLoad() {
+ return this.glFeatures.singleMrDiffView && !this.diffFiles.length;
+ },
fetchData(toggleTree = true) {
if (this.glFeatures.diffsBatchLoad) {
this.fetchDiffFilesMeta()
@@ -237,6 +251,13 @@ export default {
});
this.fetchDiffFilesBatch()
+ .then(() => {
+ // Guarantee the discussions are assigned after the batch finishes.
+ // Just watching the length of the discussions or the diff files
+ // isn't enough, because with split diff loading, neither will
+ // change when loading the other half of the diff files.
+ this.setDiscussions();
+ })
.then(() => this.startDiffRendering())
.catch(() => {
createFlash(__('Something went wrong on our end. Please try again!'));
@@ -250,6 +271,7 @@ export default {
requestIdleCallback(
() => {
+ this.setDiscussions();
this.startRenderDiffsQueue();
},
{ timeout: 1000 },
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 0dbff4ffcec..f5051748f10 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -4,6 +4,7 @@ import _ from 'underscore';
import { GlLoadingIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import createFlash from '~/flash';
+import { hasDiff } from '~/helpers/diffs_helper';
import eventHub from '../../notes/event_hub';
import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue';
@@ -55,12 +56,7 @@ export default {
return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed);
},
hasDiff() {
- return (
- (this.file.highlighted_diff_lines &&
- this.file.parallel_diff_lines &&
- this.file.parallel_diff_lines.length > 0) ||
- !this.file.blob.readable_text
- );
+ return hasDiff(this.file);
},
isFileTooLarge() {
return this.file.viewer.error === diffViewerErrors.too_large;
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 6f2467ddf71..6714f4e62b8 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -65,6 +65,10 @@ export const fetchDiffFiles = ({ state, commit }) => {
w: state.showWhitespace ? '0' : '1',
};
+ if (state.useSingleDiffStyle) {
+ urlParams.view = state.diffViewType;
+ }
+
commit(types.SET_LOADING, true);
worker.addEventListener('message', ({ data }) => {
@@ -90,13 +94,22 @@ export const fetchDiffFiles = ({ state, commit }) => {
};
export const fetchDiffFilesBatch = ({ commit, state }) => {
+ const urlParams = {
+ per_page: DIFFS_PER_PAGE,
+ w: state.showWhitespace ? '0' : '1',
+ };
+
+ if (state.useSingleDiffStyle) {
+ urlParams.view = state.diffViewType;
+ }
+
commit(types.SET_BATCH_LOADING, true);
commit(types.SET_RETRIEVING_BATCHES, true);
const getBatch = page =>
axios
.get(state.endpointBatch, {
- params: { page, per_page: DIFFS_PER_PAGE, w: state.showWhitespace ? '0' : '1' },
+ params: { ...urlParams, page },
})
.then(({ data: { pagination, diff_files } }) => {
commit(types.SET_DIFF_DATA_BATCH, { diff_files });
@@ -150,7 +163,10 @@ export const assignDiscussionsToDiff = (
{ commit, state, rootState },
discussions = rootState.notes.discussions,
) => {
- const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
+ const diffPositionByLineCode = getDiffPositionByLineCode(
+ state.diffFiles,
+ state.useSingleDiffStyle,
+ );
const hash = getLocationHash();
discussions
@@ -339,24 +355,23 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
export const toggleFileDiscussionWrappers = ({ commit }, diff) => {
const discussionWrappersExpanded = allDiscussionWrappersExpanded(diff);
- let linesWithDiscussions;
- if (diff.highlighted_diff_lines) {
- linesWithDiscussions = diff.highlighted_diff_lines.filter(line => line.discussions.length);
- }
- if (diff.parallel_diff_lines) {
- linesWithDiscussions = diff.parallel_diff_lines.filter(
- line =>
- (line.left && line.left.discussions.length) ||
- (line.right && line.right.discussions.length),
- );
- }
-
- if (linesWithDiscussions.length) {
- linesWithDiscussions.forEach(line => {
+ const lineCodesWithDiscussions = new Set();
+ const { parallel_diff_lines: parallelLines, highlighted_diff_lines: inlineLines } = diff;
+ const allLines = inlineLines.concat(
+ parallelLines.map(line => line.left),
+ parallelLines.map(line => line.right),
+ );
+ const lineHasDiscussion = line => Boolean(line?.discussions.length);
+ const registerDiscussionLine = line => lineCodesWithDiscussions.add(line.line_code);
+
+ allLines.filter(lineHasDiscussion).forEach(registerDiscussionLine);
+
+ if (lineCodesWithDiscussions.size) {
+ Array.from(lineCodesWithDiscussions).forEach(lineCode => {
commit(types.TOGGLE_LINE_DISCUSSIONS, {
fileHash: diff.file_hash,
- lineCode: line.line_code,
expanded: !discussionWrappersExpanded,
+ lineCode,
});
});
}
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index a4986e26966..8cfdded1f9b 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -45,26 +45,28 @@ export default {
},
[types.SET_DIFF_DATA](state, data) {
+ let files = state.diffFiles;
+
if (
- !(
- gon &&
- gon.features &&
- gon.features.diffsBatchLoad &&
- window.location.search.indexOf('diff_id') === -1
- )
+ !(gon?.features?.diffsBatchLoad && window.location.search.indexOf('diff_id') === -1) &&
+ data.diff_files
) {
- prepareDiffData(data);
+ files = prepareDiffData(data, files);
}
Object.assign(state, {
...convertObjectPropsToCamelCase(data),
+ diffFiles: files,
});
},
[types.SET_DIFF_DATA_BATCH](state, data) {
- prepareDiffData(data);
+ const files = prepareDiffData(data, state.diffFiles);
- state.diffFiles.push(...data.diff_files);
+ Object.assign(state, {
+ ...convertObjectPropsToCamelCase(data),
+ diffFiles: files,
+ });
},
[types.RENDER_FILE](state, file) {
@@ -88,11 +90,11 @@ export default {
if (!diffFile) return;
- if (diffFile.highlighted_diff_lines) {
+ if (diffFile.highlighted_diff_lines.length) {
diffFile.highlighted_diff_lines.find(l => l.line_code === lineCode).hasForm = hasForm;
}
- if (diffFile.parallel_diff_lines) {
+ if (diffFile.parallel_diff_lines.length) {
const line = diffFile.parallel_diff_lines.find(l => {
const { left, right } = l;
@@ -153,13 +155,13 @@ export default {
},
[types.EXPAND_ALL_FILES](state) {
- state.diffFiles = state.diffFiles.map(file => ({
- ...file,
- viewer: {
- ...file.viewer,
- collapsed: false,
- },
- }));
+ state.diffFiles.forEach(file => {
+ Object.assign(file, {
+ viewer: Object.assign(file.viewer, {
+ collapsed: false,
+ }),
+ });
+ });
},
[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode, hash }) {
@@ -197,29 +199,29 @@ export default {
};
};
- state.diffFiles = state.diffFiles.map(diffFile => {
- if (diffFile.file_hash === fileHash) {
- const file = { ...diffFile };
-
- if (file.highlighted_diff_lines) {
- file.highlighted_diff_lines = file.highlighted_diff_lines.map(line =>
- setDiscussionsExpanded(lineCheck(line) ? mapDiscussions(line) : line),
- );
+ state.diffFiles.forEach(file => {
+ if (file.file_hash === fileHash) {
+ if (file.highlighted_diff_lines.length) {
+ file.highlighted_diff_lines.forEach(line => {
+ Object.assign(
+ line,
+ setDiscussionsExpanded(lineCheck(line) ? mapDiscussions(line) : line),
+ );
+ });
}
- if (file.parallel_diff_lines) {
- file.parallel_diff_lines = file.parallel_diff_lines.map(line => {
+ if (file.parallel_diff_lines.length) {
+ file.parallel_diff_lines.forEach(line => {
const left = line.left && lineCheck(line.left);
const right = line.right && lineCheck(line.right);
if (left || right) {
- return {
- ...line,
+ Object.assign(line, {
left: line.left ? setDiscussionsExpanded(mapDiscussions(line.left)) : null,
right: line.right
? setDiscussionsExpanded(mapDiscussions(line.right, () => !left))
: null,
- };
+ });
}
return line;
@@ -227,15 +229,15 @@ export default {
}
if (!file.parallel_diff_lines || !file.highlighted_diff_lines) {
- file.discussions = (file.discussions || [])
+ const newDiscussions = (file.discussions || [])
.filter(d => d.id !== discussion.id)
.concat(discussion);
- }
- return file;
+ Object.assign(file, {
+ discussions: newDiscussions,
+ });
+ }
}
-
- return diffFile;
});
},
@@ -259,9 +261,9 @@ export default {
[types.TOGGLE_LINE_DISCUSSIONS](state, { fileHash, lineCode, expanded }) {
const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash);
- updateLineInFile(selectedFile, lineCode, line =>
- Object.assign(line, { discussionsExpanded: expanded }),
- );
+ updateLineInFile(selectedFile, lineCode, line => {
+ Object.assign(line, { discussionsExpanded: expanded });
+ });
},
[types.TOGGLE_FOLDER_OPEN](state, path) {
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 281a0de1fc2..b379f1fabef 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -185,6 +185,7 @@ export function addContextLines(options) {
* Trims the first char of the `richText` property when it's either a space or a diff symbol.
* @param {Object} line
* @returns {Object}
+ * @deprecated
*/
export function trimFirstCharOfLineContent(line = {}) {
// eslint-disable-next-line no-param-reassign
@@ -212,79 +213,171 @@ function getLineCode({ left, right }, index) {
return index;
}
-// This prepares and optimizes the incoming diff data from the server
-// by setting up incremental rendering and removing unneeded data
-export function prepareDiffData(diffData) {
- const filesLength = diffData.diff_files.length;
- let showingLines = 0;
- for (let i = 0; i < filesLength; i += 1) {
- const file = diffData.diff_files[i];
-
- if (file.parallel_diff_lines) {
- const linesLength = file.parallel_diff_lines.length;
- for (let u = 0; u < linesLength; u += 1) {
- const line = file.parallel_diff_lines[u];
-
- line.line_code = getLineCode(line, u);
- if (line.left) {
- line.left = trimFirstCharOfLineContent(line.left);
- line.left.discussions = [];
- line.left.hasForm = false;
- }
- if (line.right) {
- line.right = trimFirstCharOfLineContent(line.right);
- line.right.discussions = [];
- line.right.hasForm = false;
- }
+function diffFileUniqueId(file) {
+ return `${file.content_sha}-${file.file_hash}`;
+}
+
+function combineDiffFilesWithPriorFiles(files, prior = []) {
+ files.forEach(file => {
+ const id = diffFileUniqueId(file);
+ const oldMatch = prior.find(oldFile => diffFileUniqueId(oldFile) === id);
+
+ if (oldMatch) {
+ const missingInline = !file.highlighted_diff_lines;
+ const missingParallel = !file.parallel_diff_lines;
+
+ if (missingInline) {
+ Object.assign(file, {
+ highlighted_diff_lines: oldMatch.highlighted_diff_lines,
+ });
}
- }
- if (file.highlighted_diff_lines) {
- const linesLength = file.highlighted_diff_lines.length;
- for (let u = 0; u < linesLength; u += 1) {
- const line = file.highlighted_diff_lines[u];
- Object.assign(line, {
- ...trimFirstCharOfLineContent(line),
- discussions: [],
- hasForm: false,
+ if (missingParallel) {
+ Object.assign(file, {
+ parallel_diff_lines: oldMatch.parallel_diff_lines,
});
}
- showingLines += file.parallel_diff_lines.length;
+ }
+ });
+
+ return files;
+}
+
+function ensureBasicDiffFileLines(file) {
+ const missingInline = !file.highlighted_diff_lines;
+ const missingParallel = !file.parallel_diff_lines;
+
+ Object.assign(file, {
+ highlighted_diff_lines: missingInline ? [] : file.highlighted_diff_lines,
+ parallel_diff_lines: missingParallel ? [] : file.parallel_diff_lines,
+ });
+
+ return file;
+}
+
+function cleanRichText(text) {
+ return text ? text.replace(/^[+ -]/, '') : undefined;
+}
+
+function prepareLine(line) {
+ return Object.assign(line, {
+ rich_text: cleanRichText(line.rich_text),
+ discussionsExpanded: true,
+ discussions: [],
+ hasForm: false,
+ text: undefined,
+ });
+}
+
+function prepareDiffFileLines(file) {
+ const inlineLines = file.highlighted_diff_lines;
+ const parallelLines = file.parallel_diff_lines;
+ let parallelLinesCount = 0;
+
+ inlineLines.forEach(prepareLine);
+
+ parallelLines.forEach((line, index) => {
+ Object.assign(line, { line_code: getLineCode(line, index) });
+
+ if (line.left) {
+ parallelLinesCount += 1;
+ prepareLine(line.left);
}
- const name = (file.viewer && file.viewer.name) || diffViewerModes.text;
+ if (line.right) {
+ parallelLinesCount += 1;
+ prepareLine(line.right);
+ }
Object.assign(file, {
- renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY,
- collapsed: name === diffViewerModes.text && showingLines > MAX_LINES_TO_BE_RENDERED,
- isShowingFullFile: false,
- isLoadingFullFile: false,
- discussions: [],
- renderingLines: false,
+ inlineLinesCount: inlineLines.length,
+ parallelLinesCount,
});
- }
+ });
+
+ return file;
}
-export function getDiffPositionByLineCode(diffFiles) {
- return diffFiles.reduce((acc, diffFile) => {
- // We can only use highlightedDiffLines to create the map of diff lines because
- // highlightedDiffLines will also include every parallel diff line in it.
- if (diffFile.highlighted_diff_lines) {
+function getVisibleDiffLines(file) {
+ return Math.max(file.inlineLinesCount, file.parallelLinesCount);
+}
+
+function finalizeDiffFile(file) {
+ const name = (file.viewer && file.viewer.name) || diffViewerModes.text;
+ const lines = getVisibleDiffLines(file);
+
+ Object.assign(file, {
+ renderIt: lines < LINES_TO_BE_RENDERED_DIRECTLY,
+ collapsed: name === diffViewerModes.text && lines > MAX_LINES_TO_BE_RENDERED,
+ isShowingFullFile: false,
+ isLoadingFullFile: false,
+ discussions: [],
+ renderingLines: false,
+ });
+
+ return file;
+}
+
+export function prepareDiffData(diffData, priorFiles) {
+ return combineDiffFilesWithPriorFiles(diffData.diff_files, priorFiles)
+ .map(ensureBasicDiffFileLines)
+ .map(prepareDiffFileLines)
+ .map(finalizeDiffFile);
+}
+
+export function getDiffPositionByLineCode(diffFiles, useSingleDiffStyle) {
+ let lines = [];
+ const hasInlineDiffs = diffFiles.some(file => file.highlighted_diff_lines.length > 0);
+
+ if (!useSingleDiffStyle || hasInlineDiffs) {
+ // In either of these cases, we can use `highlighted_diff_lines` because
+ // that will include all of the parallel diff lines, too
+
+ lines = diffFiles.reduce((acc, diffFile) => {
diffFile.highlighted_diff_lines.forEach(line => {
- if (line.line_code) {
- acc[line.line_code] = {
- base_sha: diffFile.diff_refs.base_sha,
- head_sha: diffFile.diff_refs.head_sha,
- start_sha: diffFile.diff_refs.start_sha,
- new_path: diffFile.new_path,
- old_path: diffFile.old_path,
- old_line: line.old_line,
- new_line: line.new_line,
- line_code: line.line_code,
- position_type: 'text',
- };
+ acc.push({ file: diffFile, line });
+ });
+
+ return acc;
+ }, []);
+ } else {
+ // If we're in single diff view mode and the inline lines haven't been
+ // loaded yet, we need to parse the parallel lines
+
+ lines = diffFiles.reduce((acc, diffFile) => {
+ diffFile.parallel_diff_lines.forEach(pair => {
+ // It's possible for a parallel line to have an opposite line that doesn't exist
+ // For example: *deleted* lines will have `null` right lines, while
+ // *added* lines will have `null` left lines.
+ // So we have to check each line before we push it onto the array so we're not
+ // pushing null line diffs
+
+ if (pair.left) {
+ acc.push({ file: diffFile, line: pair.left });
+ }
+
+ if (pair.right) {
+ acc.push({ file: diffFile, line: pair.right });
}
});
+
+ return acc;
+ }, []);
+ }
+
+ return lines.reduce((acc, { file, line }) => {
+ if (line.line_code) {
+ acc[line.line_code] = {
+ base_sha: file.diff_refs.base_sha,
+ head_sha: file.diff_refs.head_sha,
+ start_sha: file.diff_refs.start_sha,
+ new_path: file.new_path,
+ old_path: file.old_path,
+ old_line: line.old_line,
+ new_line: line.new_line,
+ line_code: line.line_code,
+ position_type: 'text',
+ };
}
return acc;
@@ -462,47 +555,47 @@ export const convertExpandLines = ({
export const idleCallback = cb => requestIdleCallback(cb);
-export const updateLineInFile = (selectedFile, lineCode, updateFn) => {
- if (selectedFile.parallel_diff_lines) {
- const targetLine = selectedFile.parallel_diff_lines.find(
- line =>
- (line.left && line.left.line_code === lineCode) ||
- (line.right && line.right.line_code === lineCode),
- );
- if (targetLine) {
- const side = targetLine.left && targetLine.left.line_code === lineCode ? 'left' : 'right';
+function getLinesFromFileByLineCode(file, lineCode) {
+ const parallelLines = file.parallel_diff_lines;
+ const inlineLines = file.highlighted_diff_lines;
+ const matchesCode = line => line.line_code === lineCode;
- updateFn(targetLine[side]);
- }
- }
- if (selectedFile.highlighted_diff_lines) {
- const targetInlineLine = selectedFile.highlighted_diff_lines.find(
- line => line.line_code === lineCode,
- );
+ return [
+ ...parallelLines.reduce((acc, line) => {
+ if (line.left) {
+ acc.push(line.left);
+ }
- if (targetInlineLine) {
- updateFn(targetInlineLine);
- }
- }
+ if (line.right) {
+ acc.push(line.right);
+ }
+
+ return acc;
+ }, []),
+ ...inlineLines,
+ ].filter(matchesCode);
+}
+
+export const updateLineInFile = (selectedFile, lineCode, updateFn) => {
+ getLinesFromFileByLineCode(selectedFile, lineCode).forEach(updateFn);
};
export const allDiscussionWrappersExpanded = diff => {
- const discussionsExpandedArray = [];
- if (diff.parallel_diff_lines) {
- diff.parallel_diff_lines.forEach(line => {
- if (line.left && line.left.discussions.length) {
- discussionsExpandedArray.push(line.left.discussionsExpanded);
- }
- if (line.right && line.right.discussions.length) {
- discussionsExpandedArray.push(line.right.discussionsExpanded);
- }
- });
- } else if (diff.highlighted_diff_lines) {
- diff.highlighted_diff_lines.forEach(line => {
- if (line.discussions.length) {
- discussionsExpandedArray.push(line.discussionsExpanded);
- }
- });
- }
- return discussionsExpandedArray.every(el => el);
+ let discussionsExpanded = true;
+ const changeExpandedResult = line => {
+ if (line && line.discussions.length) {
+ discussionsExpanded = discussionsExpanded && line.discussionsExpanded;
+ }
+ };
+
+ diff.parallel_diff_lines.forEach(line => {
+ changeExpandedResult(line.left);
+ changeExpandedResult(line.right);
+ });
+
+ diff.highlighted_diff_lines.forEach(line => {
+ changeExpandedResult(line);
+ });
+
+ return discussionsExpanded;
};
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index d4562d4a9a5..23a1ec4e367 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -1,6 +1,7 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import dateFormat from 'dateformat';
+import createFlash from '~/flash';
import { GlFormInput, GlLink, GlLoadingIcon, GlBadge } from '@gitlab/ui';
import { __, sprintf, n__ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
@@ -11,6 +12,8 @@ import TrackEventDirective from '~/vue_shared/directives/track_event';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { trackClickErrorLinkToSentryOptions } from '../utils';
+import query from '../queries/details.query.graphql';
+
export default {
components: {
LoadingButton,
@@ -27,6 +30,14 @@ export default {
},
mixins: [timeagoMixin],
props: {
+ issueId: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
issueDetailsPath: {
type: String,
required: true,
@@ -44,8 +55,28 @@ export default {
required: true,
},
},
+ apollo: {
+ GQLerror: {
+ query,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ errorId: `gid://gitlab/Gitlab::ErrorTracking::DetailedError/${this.issueId}`,
+ };
+ },
+ pollInterval: 2000,
+ update: data => data.project.sentryDetailedError,
+ error: () => createFlash(__('Failed to load error details from Sentry.')),
+ result(res) {
+ if (res.data.project?.sentryDetailedError) {
+ this.$apollo.queries.GQLerror.stopPolling();
+ }
+ },
+ },
+ },
data() {
return {
+ GQLerror: null,
issueCreationInProgress: false,
};
},
@@ -56,26 +87,28 @@ export default {
return sprintf(
__('Reported %{timeAgo} by %{reportedBy}'),
{
- reportedBy: `<strong>${this.error.culprit}</strong>`,
+ reportedBy: `<strong>${this.GQLerror.culprit}</strong>`,
timeAgo: this.timeFormatted(this.stacktraceData.date_received),
},
false,
);
},
firstReleaseLink() {
- return `${this.error.external_base_url}/releases/${this.error.first_release_short_version}`;
+ return `${this.error.external_base_url}/releases/${this.GQLerror.firstReleaseShortVersion}`;
},
lastReleaseLink() {
- return `${this.error.external_base_url}releases/${this.error.last_release_short_version}`;
+ return `${this.error.external_base_url}releases/${this.GQLerror.lastReleaseShortVersion}`;
},
showDetails() {
- return Boolean(!this.loading && this.error && this.error.id);
+ return Boolean(
+ !this.loading && !this.$apollo.queries.GQLerror.loading && this.error && this.GQLerror,
+ );
},
showStacktrace() {
return Boolean(!this.loadingStacktrace && this.stacktrace && this.stacktrace.length);
},
issueTitle() {
- return this.error.title;
+ return this.GQLerror.title;
},
issueDescription() {
return sprintf(
@@ -84,13 +117,13 @@ export default {
),
{
description: '# Error Details:\n',
- errorUrl: `${this.error.external_url}\n`,
- firstSeen: `\n${this.error.first_seen}\n`,
- lastSeen: `${this.error.last_seen}\n`,
- countLabel: n__('- Event', '- Events', this.error.count),
- count: `${this.error.count}\n`,
- userCountLabel: n__('- User', '- Users', this.error.user_count),
- userCount: `${this.error.user_count}\n`,
+ errorUrl: `${this.GQLerror.externalUrl}\n`,
+ firstSeen: `\n${this.GQLerror.firstSeen}\n`,
+ lastSeen: `${this.GQLerror.lastSeen}\n`,
+ countLabel: n__('- Event', '- Events', this.GQLerror.count),
+ count: `${this.GQLerror.count}\n`,
+ userCountLabel: n__('- User', '- Users', this.GQLerror.userCount),
+ userCount: `${this.GQLerror.userCount}\n`,
},
false,
);
@@ -119,7 +152,7 @@ export default {
<template>
<div>
- <div v-if="loading" class="py-3">
+ <div v-if="$apollo.queries.GQLerror.loading || loading" class="py-3">
<gl-loading-icon :size="3" />
</div>
<div v-else-if="showDetails" class="error-details">
@@ -129,7 +162,7 @@ export default {
<gl-form-input class="hidden" name="issue[title]" :value="issueTitle" />
<input name="issue[description]" :value="issueDescription" type="hidden" />
<gl-form-input
- :value="error.id"
+ :value="GQLerror.id"
class="hidden"
name="issue[sentry_issue_attributes][sentry_issue_identifier]"
/>
@@ -145,16 +178,16 @@ export default {
</form>
</div>
<div>
- <tooltip-on-truncate :title="error.title" truncate-target="child" placement="top">
- <h2 class="text-truncate">{{ error.title }}</h2>
+ <tooltip-on-truncate :title="GQLerror.title" truncate-target="child" placement="top">
+ <h2 class="text-truncate">{{ GQLerror.title }}</h2>
</tooltip-on-truncate>
<template v-if="error.tags">
- <gl-badge v-if="error.tags.level" variant="danger" class="rounded-pill mr-2">{{
- errorLevel
- }}</gl-badge>
- <gl-badge v-if="error.tags.logger" variant="light" class="rounded-pill">{{
- error.tags.logger
- }}</gl-badge>
+ <gl-badge v-if="error.tags.level" variant="danger" class="rounded-pill mr-2"
+ >{{ errorLevel }}
+ </gl-badge>
+ <gl-badge v-if="error.tags.logger" variant="light" class="rounded-pill"
+ >{{ error.tags.logger }}
+ </gl-badge>
</template>
<h3>{{ __('Error details') }}</h3>
@@ -168,35 +201,35 @@ export default {
<li>
<span class="bold">{{ __('Sentry event') }}:</span>
<gl-link
- v-track-event="trackClickErrorLinkToSentryOptions(error.external_url)"
- :href="error.external_url"
+ v-track-event="trackClickErrorLinkToSentryOptions(GQLerror.externalUrl)"
+ :href="GQLerror.externalUrl"
target="_blank"
>
- <span class="text-truncate">{{ error.external_url }}</span>
+ <span class="text-truncate">{{ GQLerror.externalUrl }}</span>
<icon name="external-link" class="ml-1 flex-shrink-0" />
</gl-link>
</li>
- <li v-if="error.first_release_short_version">
+ <li v-if="GQLerror.firstReleaseShortVersion">
<span class="bold">{{ __('First seen') }}:</span>
- {{ formatDate(error.first_seen) }}
+ {{ formatDate(GQLerror.firstSeen) }}
<gl-link :href="firstReleaseLink" target="_blank">
- <span>{{ __('Release') }}: {{ error.first_release_short_version }}</span>
+ <span>{{ __('Release') }}: {{ GQLerror.firstReleaseShortVersion }}</span>
</gl-link>
</li>
- <li v-if="error.last_release_short_version">
+ <li v-if="GQLerror.lastReleaseShortVersion">
<span class="bold">{{ __('Last seen') }}:</span>
- {{ formatDate(error.last_seen) }}
+ {{ formatDate(GQLerror.lastSeen) }}
<gl-link :href="lastReleaseLink" target="_blank">
- <span>{{ __('Release') }}: {{ error.last_release_short_version }}</span>
+ <span>{{ __('Release') }}: {{ GQLerror.lastReleaseShortVersion }}</span>
</gl-link>
</li>
<li>
<span class="bold">{{ __('Events') }}:</span>
- <span>{{ error.count }}</span>
+ <span>{{ GQLerror.count }}</span>
</li>
<li>
<span class="bold">{{ __('Users') }}:</span>
- <span>{{ error.user_count }}</span>
+ <span>{{ GQLerror.userCount }}</span>
</li>
</ul>
diff --git a/app/assets/javascripts/error_tracking/details.js b/app/assets/javascripts/error_tracking/details.js
index 872cb8868a2..b9761cdf2c1 100644
--- a/app/assets/javascripts/error_tracking/details.js
+++ b/app/assets/javascripts/error_tracking/details.js
@@ -1,22 +1,39 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import store from './store';
import ErrorDetails from './components/error_details.vue';
import csrf from '~/lib/utils/csrf';
+Vue.use(VueApollo);
+
export default () => {
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
// eslint-disable-next-line no-new
new Vue({
el: '#js-error_details',
+ apolloProvider,
components: {
ErrorDetails,
},
store,
render(createElement) {
const domEl = document.querySelector(this.$options.el);
- const { issueDetailsPath, issueStackTracePath, projectIssuesPath } = domEl.dataset;
+ const {
+ issueId,
+ projectPath,
+ issueDetailsPath,
+ issueStackTracePath,
+ projectIssuesPath,
+ } = domEl.dataset;
return createElement('error-details', {
props: {
+ issueId,
+ projectPath,
issueDetailsPath,
issueStackTracePath,
projectIssuesPath,
diff --git a/app/assets/javascripts/error_tracking/queries/details.query.graphql b/app/assets/javascripts/error_tracking/queries/details.query.graphql
new file mode 100644
index 00000000000..f65bdb9b968
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/queries/details.query.graphql
@@ -0,0 +1,18 @@
+query errorDetails($fullPath: ID!, $errorId: ID!) {
+ project(fullPath: $fullPath) {
+ sentryDetailedError(id: $errorId) {
+ id
+ sentryId
+ title
+ userCount
+ count
+ firstSeen
+ lastSeen
+ message
+ culprit
+ externalUrl
+ firstReleaseShortVersion
+ lastReleaseShortVersion
+ }
+ }
+}
diff --git a/app/assets/javascripts/helpers/diffs_helper.js b/app/assets/javascripts/helpers/diffs_helper.js
new file mode 100644
index 00000000000..9695d01ad3d
--- /dev/null
+++ b/app/assets/javascripts/helpers/diffs_helper.js
@@ -0,0 +1,19 @@
+export function hasInlineLines(diffFile) {
+ return diffFile?.highlighted_diff_lines?.length > 0; /* eslint-disable-line camelcase */
+}
+
+export function hasParallelLines(diffFile) {
+ return diffFile?.parallel_diff_lines?.length > 0; /* eslint-disable-line camelcase */
+}
+
+export function isSingleViewStyle(diffFile) {
+ return !hasParallelLines(diffFile) || !hasInlineLines(diffFile);
+}
+
+export function hasDiff(diffFile) {
+ return (
+ hasInlineLines(diffFile) ||
+ hasParallelLines(diffFile) ||
+ !diffFile?.blob?.readable_text /* eslint-disable-line camelcase */
+ );
+}
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 1cb82ce0083..fce89b450e4 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -39,7 +39,7 @@ export const requestMetricsDashboard = ({ commit }) => {
};
export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response, params }) => {
commit(types.SET_ALL_DASHBOARDS, response.all_dashboards);
- commit(types.RECEIVE_METRICS_DATA_SUCCESS, response.dashboard.panel_groups);
+ commit(types.RECEIVE_METRICS_DATA_SUCCESS, response.dashboard);
return dispatch('fetchPrometheusMetrics', params);
};
export const receiveMetricsDashboardFailure = ({ commit }, error) => {
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index 16a34a6c026..0b848de9562 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -84,23 +84,26 @@ export default {
state.emptyState = 'loading';
state.showEmptyState = true;
},
- [types.RECEIVE_METRICS_DATA_SUCCESS](state, groupData) {
- state.dashboard.panel_groups = groupData.map((group, i) => {
- const key = `${slugify(group.group || 'default')}-${i}`;
- let { panels = [] } = group;
-
- // each panel has metric information that needs to be normalized
- panels = panels.map(panel => ({
- ...panel,
- metrics: normalizePanelMetrics(panel.metrics, panel.y_label),
- }));
-
- return {
- ...group,
- panels,
- key,
- };
- });
+ [types.RECEIVE_METRICS_DATA_SUCCESS](state, dashboard) {
+ state.dashboard = {
+ ...dashboard,
+ panel_groups: dashboard.panel_groups.map((group, i) => {
+ const key = `${slugify(group.group || 'default')}-${i}`;
+ let { panels = [] } = group;
+
+ // each panel has metric information that needs to be normalized
+ panels = panels.map(panel => ({
+ ...panel,
+ metrics: normalizePanelMetrics(panel.metrics, panel.y_label),
+ }));
+
+ return {
+ ...group,
+ panels,
+ key,
+ };
+ }),
+ };
if (!state.dashboard.panel_groups.length) {
state.emptyState = 'noData';
diff --git a/app/helpers/projects/error_tracking_helper.rb b/app/helpers/projects/error_tracking_helper.rb
index 2ec7fa91c86..f2d16c30fb4 100644
--- a/app/helpers/projects/error_tracking_helper.rb
+++ b/app/helpers/projects/error_tracking_helper.rb
@@ -18,9 +18,11 @@ module Projects::ErrorTrackingHelper
opts = [project, issue_id, { format: :json }]
{
- 'project-issues-path' => project_issues_path(project),
+ 'issue-id' => issue_id,
+ 'project-path' => project.full_path,
'issue-details-path' => details_project_error_tracking_index_path(*opts),
'issue-update-path' => update_project_error_tracking_index_path(*opts),
+ 'project-issues-path' => project_issues_path(project),
'issue-stack-trace-path' => stack_trace_project_error_tracking_index_path(*opts)
}
end
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 68548bd2fdc..85cb3f5b46a 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -11,7 +11,7 @@ module Ci
has_many :trigger_requests
validates :token, presence: true, uniqueness: true
- validates :owner, presence: true, unless: :supports_legacy_tokens?
+ validates :owner, presence: true
before_validation :set_default_values
@@ -31,17 +31,8 @@ module Ci
token[0...4] if token.present?
end
- def legacy?
- self.owner_id.blank?
- end
-
- def supports_legacy_tokens?
- Feature.enabled?(:use_legacy_pipeline_triggers, project)
- end
-
def can_access_project?
- supports_legacy_tokens? && legacy? ||
- Ability.allowed?(self.owner, :create_build, project)
+ Ability.allowed?(self.owner, :create_build, project)
end
end
end
diff --git a/app/policies/ci/trigger_policy.rb b/app/policies/ci/trigger_policy.rb
index 578301d7f7e..e26f96a4b2b 100644
--- a/app/policies/ci/trigger_policy.rb
+++ b/app/policies/ci/trigger_policy.rb
@@ -5,13 +5,12 @@ module Ci
delegate { @subject.project }
with_options scope: :subject, score: 0
- condition(:legacy) { @subject.supports_legacy_tokens? && @subject.legacy? }
with_score 0
condition(:is_owner) { @user && @subject.owner_id == @user.id }
rule { ~can?(:admin_build) }.prevent :admin_trigger
- rule { legacy | is_owner }.enable :admin_trigger
+ rule { is_owner }.enable :admin_trigger
rule { can?(:admin_build) }.enable :manage_trigger
end
diff --git a/app/views/errors/_footer.html.haml b/app/views/errors/_footer.html.haml
index e67a3a142f6..bb9edc54b4b 100644
--- a/app/views/errors/_footer.html.haml
+++ b/app/views/errors/_footer.html.haml
@@ -4,7 +4,7 @@
= link_to s_('Nav|Home'), root_path
%li
- if current_user
- = link_to s_('Nav|Sign out and sign in with a different account'), destroy_user_session_path
+ = link_to s_('Nav|Sign out and sign in with a different account'), destroy_user_session_path, method: :post
- else
= link_to s_('Nav|Sign In / Register'), new_session_path(:user, redirect_to_referer: 'yes')
%li
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index 88803f982e8..84906c305a7 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -47,4 +47,4 @@
- if current_user_menu?(:sign_out)
%li.divider
%li
- = link_to _("Sign out"), destroy_user_session_path, class: "sign-out-link", data: { qa_selector: 'sign_out_link' }
+ = link_to _("Sign out"), destroy_user_session_path, method: :post, class: "sign-out-link", data: { qa_selector: 'sign_out_link' }
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 9ae06ae77f1..379ba976040 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -55,7 +55,7 @@
- if Feature.enabled?(:user_mode_in_session)
- if header_link?(:admin_mode)
= nav_link(controller: 'admin/sessions') do
- = link_to destroy_admin_session_path, class: 'd-lg-none lock-open-icon' do
+ = link_to destroy_admin_session_path, method: :post, class: 'd-lg-none lock-open-icon' do
= _('Leave Admin Mode')
- elsif current_user.admin?
= nav_link(controller: 'admin/sessions') do
diff --git a/app/views/projects/triggers/_content.html.haml b/app/views/projects/triggers/_content.html.haml
deleted file mode 100644
index e686068657c..00000000000
--- a/app/views/projects/triggers/_content.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-- if Feature.enabled?(:use_legacy_pipeline_triggers, @project)
- %p.append-bottom-default
- Triggers with the
- %span.badge.badge-primary legacy
- label do not have an associated user and only have access to the current project.
- %br
- = succeed '.' do
- Learn more in the
- = link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank'
diff --git a/app/views/projects/triggers/_index.html.haml b/app/views/projects/triggers/_index.html.haml
index a559ce41e57..55a9234f01a 100644
--- a/app/views/projects/triggers/_index.html.haml
+++ b/app/views/projects/triggers/_index.html.haml
@@ -1,6 +1,5 @@
.row.prepend-top-default.append-bottom-default.triggers-container
.col-lg-12
- = render "projects/triggers/content"
.card
.card-header
Manage your project's triggers
diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml
index 60de3630bb5..d80248f7e80 100644
--- a/app/views/projects/triggers/_trigger.html.haml
+++ b/app/views/projects/triggers/_trigger.html.haml
@@ -7,12 +7,7 @@
%span= trigger.short_token
.label-container
- - if trigger.legacy?
- - if trigger.supports_legacy_tokens?
- %span.badge.badge-primary.has-tooltip{ title: "Trigger makes use of deprecated functionality" } legacy
- - else
- %span.badge.badge-danger.has-tooltip{ title: "Trigger is invalid due to being a legacy trigger. We recommend replacing it with a new trigger" } invalid
- - elsif !trigger.can_access_project?
+ - unless trigger.can_access_project?
%span.badge.badge-danger.has-tooltip{ title: "Trigger user has insufficient permissions to project" } invalid
%td
diff --git a/app/views/projects/triggers/edit.html.haml b/app/views/projects/triggers/edit.html.haml
index c35df322b9d..0f74d733c06 100644
--- a/app/views/projects/triggers/edit.html.haml
+++ b/app/views/projects/triggers/edit.html.haml
@@ -1,9 +1,7 @@
- page_title "Trigger"
.row.prepend-top-default.append-bottom-default
- .col-lg-3
- = render "content"
- .col-lg-9
+ .col-lg-12
%h4.prepend-top-0
Update trigger
= render "form", btn_text: "Save trigger"
diff --git a/changelogs/unreleased/14857-activate-promethus-integration-for-projects.yml b/changelogs/unreleased/14857-activate-promethus-integration-for-projects.yml
deleted file mode 100644
index a83008ee848..00000000000
--- a/changelogs/unreleased/14857-activate-promethus-integration-for-projects.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Migrate the database to activate projects prometheus service integration for projects with prometheus installed on shared k8s cluster.
-merge_request: 19956
-author:
-type: fixed
diff --git a/changelogs/unreleased/29772-completely-removing-use_legacy_pipeline_triggers.yml b/changelogs/unreleased/29772-completely-removing-use_legacy_pipeline_triggers.yml
new file mode 100644
index 00000000000..d9ac063f310
--- /dev/null
+++ b/changelogs/unreleased/29772-completely-removing-use_legacy_pipeline_triggers.yml
@@ -0,0 +1,5 @@
+---
+title: Remove feature flag 'use_legacy_pipeline_triggers' and remove legacy tokens
+merge_request: 21732
+author:
+type: removed
diff --git a/changelogs/unreleased/34867-epics_to_project_import_export.yml b/changelogs/unreleased/34867-epics_to_project_import_export.yml
new file mode 100644
index 00000000000..a7c2b2e3655
--- /dev/null
+++ b/changelogs/unreleased/34867-epics_to_project_import_export.yml
@@ -0,0 +1,5 @@
+---
+title: Add epics to project import/export
+merge_request: 19883
+author:
+type: added
diff --git a/changelogs/unreleased/39979-grapql-for-error-details.yml b/changelogs/unreleased/39979-grapql-for-error-details.yml
new file mode 100644
index 00000000000..6ae3f203f56
--- /dev/null
+++ b/changelogs/unreleased/39979-grapql-for-error-details.yml
@@ -0,0 +1,5 @@
+---
+title: Use GraphQL to load error tracking detail page content
+merge_request: 22422
+author:
+type: performance
diff --git a/changelogs/unreleased/enable-parent-child-pipelines.yml b/changelogs/unreleased/enable-parent-child-pipelines.yml
new file mode 100644
index 00000000000..aa38b06839a
--- /dev/null
+++ b/changelogs/unreleased/enable-parent-child-pipelines.yml
@@ -0,0 +1,5 @@
+---
+title: Allow a pipeline (parent) to create a child pipeline as downstream pipeline within the same project
+merge_request: 21830
+author:
+type: added
diff --git a/changelogs/unreleased/feature-split-diff-attempt-3.yml b/changelogs/unreleased/feature-split-diff-attempt-3.yml
new file mode 100644
index 00000000000..8adda8be3af
--- /dev/null
+++ b/changelogs/unreleased/feature-split-diff-attempt-3.yml
@@ -0,0 +1,5 @@
+---
+title: Load MR diff types lazily to reduce initial diff payload size
+merge_request: 19930
+author:
+type: added
diff --git a/changelogs/unreleased/refactor-session-disable-with-post.yml b/changelogs/unreleased/refactor-session-disable-with-post.yml
new file mode 100644
index 00000000000..63908947095
--- /dev/null
+++ b/changelogs/unreleased/refactor-session-disable-with-post.yml
@@ -0,0 +1,5 @@
+---
+title: User signout and admin mode disable use now POST instead of GET
+merge_request: 22113
+author: Diego Louzán
+type: other
diff --git a/config/initializers/8_devise.rb b/config/initializers/8_devise.rb
index 8d4c5fa382c..e1c37caaafd 100644
--- a/config/initializers/8_devise.rb
+++ b/config/initializers/8_devise.rb
@@ -203,7 +203,7 @@ Devise.setup do |config|
config.navigational_formats = [:"*/*", "*/*", :html, :zip]
# The default HTTP method used to sign out a resource. Default is :delete.
- config.sign_out_via = :get
+ config.sign_out_via = :post
# ==> OmniAuth
# To configure a new OmniAuth provider copy and edit omniauth.rb.sample
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 45cca1d3191..f363823f80c 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -24,7 +24,7 @@ namespace :admin do
end
resource :session, only: [:new, :create] do
- get 'destroy', action: :destroy, as: :destroy
+ post 'destroy', action: :destroy, as: :destroy
end
resource :impersonation, only: :destroy
diff --git a/db/post_migrate/20191204114127_delete_legacy_triggers.rb b/db/post_migrate/20191204114127_delete_legacy_triggers.rb
new file mode 100644
index 00000000000..82d901ae689
--- /dev/null
+++ b/db/post_migrate/20191204114127_delete_legacy_triggers.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class DeleteLegacyTriggers < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def up
+ execute <<~SQL
+ DELETE FROM ci_triggers WHERE owner_id IS NULL
+ SQL
+
+ change_column_null :ci_triggers, :owner_id, false
+ end
+
+ def down
+ change_column_null :ci_triggers, :owner_id, true
+ end
+end
diff --git a/db/post_migrate/20191220102807_patch_prometheus_services_for_shared_cluster_applications.rb b/db/post_migrate/20191220102807_patch_prometheus_services_for_shared_cluster_applications.rb
deleted file mode 100644
index 68361f7b176..00000000000
--- a/db/post_migrate/20191220102807_patch_prometheus_services_for_shared_cluster_applications.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-# frozen_string_literal: true
-
-class PatchPrometheusServicesForSharedClusterApplications < ActiveRecord::Migration[5.2]
- include Gitlab::Database::MigrationHelpers
-
- DOWNTIME = false
- MIGRATION = 'ActivatePrometheusServicesForSharedClusterApplications'.freeze
- BATCH_SIZE = 500
- DELAY = 2.minutes
-
- disable_ddl_transaction!
-
- module Migratable
- module Applications
- class Prometheus < ActiveRecord::Base
- self.table_name = 'clusters_applications_prometheus'
-
- enum status: {
- errored: -1,
- installed: 3,
- updated: 5
- }
- end
- end
-
- class Project < ActiveRecord::Base
- self.table_name = 'projects'
- include ::EachBatch
-
- scope :with_application_on_group_clusters, -> {
- joins("INNER JOIN namespaces ON namespaces.id = projects.namespace_id")
- .joins("INNER JOIN cluster_groups ON cluster_groups.group_id = namespaces.id")
- .joins("INNER JOIN clusters ON clusters.id = cluster_groups.cluster_id AND clusters.cluster_type = #{Cluster.cluster_types['group_type']}")
- .joins("INNER JOIN clusters_applications_prometheus ON clusters_applications_prometheus.cluster_id = clusters.id
- AND clusters_applications_prometheus.status IN (#{Applications::Prometheus.statuses[:installed]}, #{Applications::Prometheus.statuses[:updated]})")
- }
-
- scope :without_active_prometheus_services, -> {
- joins("LEFT JOIN services ON services.project_id = projects.id AND services.type = 'PrometheusService'")
- .where("services.id IS NULL OR (services.active = FALSE AND services.properties = '{}')")
- }
- end
-
- class Cluster < ActiveRecord::Base
- self.table_name = 'clusters'
-
- enum cluster_type: {
- instance_type: 1,
- group_type: 2
- }
-
- def self.has_prometheus_application?
- joins("INNER JOIN clusters_applications_prometheus ON clusters_applications_prometheus.cluster_id = clusters.id
- AND clusters_applications_prometheus.status IN (#{Applications::Prometheus.statuses[:installed]}, #{Applications::Prometheus.statuses[:updated]})").exists?
- end
- end
- end
-
- def up
- projects_without_active_prometheus_service.group('projects.id').each_batch(of: BATCH_SIZE) do |batch, index|
- bg_migrations_batch = batch.select('projects.id').map { |project| [MIGRATION, project.id] }
- delay = index * DELAY
- BackgroundMigrationWorker.bulk_perform_in(delay.seconds, bg_migrations_batch)
- end
- end
-
- def down
- # no-op
- end
-
- private
-
- def projects_without_active_prometheus_service
- scope = Migratable::Project.without_active_prometheus_services
-
- return scope if migrate_instance_cluster?
-
- scope.with_application_on_group_clusters
- end
-
- def migrate_instance_cluster?
- if instance_variable_defined?('@migrate_instance_cluster')
- @migrate_instance_cluster
- else
- @migrate_instance_cluster = Migratable::Cluster.instance_type.has_prometheus_application?
- end
- end
-end
diff --git a/db/schema.rb b/db/schema.rb
index 689428ea4cb..56b0cbde61a 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -994,7 +994,7 @@ ActiveRecord::Schema.define(version: 2020_01_08_233040) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "project_id"
- t.integer "owner_id"
+ t.integer "owner_id", null: false
t.string "description"
t.string "ref"
t.index ["owner_id"], name: "index_ci_triggers_on_owner_id"
diff --git a/doc/ci/img/parent_pipeline_graph_expanded_v12_6.png b/doc/ci/img/parent_pipeline_graph_expanded_v12_6.png
new file mode 100644
index 00000000000..5c493109a54
--- /dev/null
+++ b/doc/ci/img/parent_pipeline_graph_expanded_v12_6.png
Binary files differ
diff --git a/doc/ci/parent_child_pipelines.md b/doc/ci/parent_child_pipelines.md
new file mode 100644
index 00000000000..269cbd75a9a
--- /dev/null
+++ b/doc/ci/parent_child_pipelines.md
@@ -0,0 +1,86 @@
+---
+type: reference
+---
+
+# Parent-child pipelines
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/16094) in GitLab Starter 12.7.
+
+As pipelines grow more complex, a few related problems start to emerge:
+
+- The staged structure, where all steps in a stage must be completed before the first
+ job in next stage begins, causes arbitrary waits, slowing things down.
+- Configuration for the single global pipeline becomes very long and complicated,
+ making it hard to manage.
+- Imports with [`include`](yaml/README.md#include) increase the complexity of the configuration, and create the potential
+ for namespace collisions where jobs are unintentionally duplicated.
+- Pipeline UX can become unwieldy with so many jobs and stages to work with.
+
+Additionally, sometimes the behavior of a pipeline needs to be more dynamic. The ability
+to choose to start sub-pipelines (or not) is a powerful ability, especially if the
+YAML is dynamically generated.
+
+![Parent pipeline graph expanded](img/parent_pipeline_graph_expanded_v12_6.png)
+
+Similarly to [multi-project pipelines](multi_project_pipelines.md), a pipeline can trigger a
+set of concurrently running child pipelines, but within the same project:
+
+- Child pipelines still execute each of their jobs according to a stage sequence, but
+ would be free to continue forward through their stages without waiting for unrelated
+ jobs in the parent pipeline to finish.
+- The configuration is split up into smaller child pipeline configurations, which are
+ easier to understand. This reduces the cognitive load to understand the overall configuration.
+- Imports are done at the child pipeline level, reducing the likelihood of collisions.
+- Each pipeline has only the steps relevant steps, making it easier to understand what's going on.
+
+Child pipelines work well with other GitLab CI features:
+
+- Use [`only: changes`](yaml/README.md#onlychangesexceptchanges) to trigger pipelines only when
+ certain files change. This is useful for monorepos, for example.
+- Since the parent pipeline in `.gitlab-ci.yml` and the child pipeline run as normal
+ pipelines, they can have their own behaviors and sequencing in relation to triggers.
+
+All of this will work with [`include:`](yaml/README.md#include) feature so you can compose
+the child pipeline configuration.
+
+## Examples
+
+The simplest case is [triggering a child pipeline](yaml/README.md#trigger-premium) using a
+local YAML file to define the pipeline configuration. In this case, the parent pipeline will
+trigger the child pipeline, and continue without waiting:
+
+```yaml
+microservice_a:
+ trigger:
+ include: path/to/microservice_a.yml
+```
+
+You can include multiple files when composing a child pipeline:
+
+```yaml
+microservice_a:
+ trigger:
+ include:
+ - local: path/to/microservice_a.yml
+ - template: SAST.gitlab-ci.yml
+```
+
+NOTE: **Note:**
+The max number of entries that are accepted for `trigger:include:` is three.
+
+Similar to [multi-project pipelines](multi_project_pipelines.md#mirroring-status-from-triggered-pipeline),
+we can set the parent pipeline to depend on the status of the child pipeline upon completion:
+
+```yaml
+microservice_a:
+ trigger:
+ include:
+ - local: path/to/microservice_a.yml
+ - template: SAST.gitlab-ci.yml
+ strategy: depend
+```
+
+## Limitations
+
+A parent pipeline can trigger many child pipelines, but a child pipeline cannot trigger
+further child pipelines. See the [related issue](https://gitlab.com/gitlab-org/gitlab/issues/29651) for discussion on possible future improvements.
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index 4d942ea3d54..71c4c9ca0ec 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -246,6 +246,13 @@ Pipelines for different projects can be combined and visualized together.
For more information, see [Multi-project pipelines](multi_project_pipelines.md).
+## Parent-child pipelines
+
+Complex pipelines can be broken down into one parent pipeline that can trigger
+multiple child sub-pipelines, which all run in the same project and with the same SHA.
+
+For more information, see [Parent-Child pipelines](parent_child_pipelines.md).
+
## Working with pipelines
In general, pipelines are executed automatically and require no intervention once created.
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index e388fdde58a..aafbe4c9189 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -2600,14 +2600,17 @@ job split into three separate jobs.
from `trigger` definition is started by GitLab, a downstream pipeline gets
created.
-Learn more about [multi-project pipelines](../multi_project_pipelines.md#creating-multi-project-pipelines-from-gitlab-ciyml).
+This keyword allows the creation of two different types of downstream pipelines:
+
+- [Multi-project pipelines](../multi_project_pipelines.md#creating-multi-project-pipelines-from-gitlab-ciyml)
+- [Child pipelines](../parent_child_pipelines.md)
NOTE: **Note:**
Using a `trigger` with `when:manual` together results in the error `jobs:#{job-name}
when should be on_success, on_failure or always`, because `when:manual` prevents
triggers being used.
-#### Simple `trigger` syntax
+#### Simple `trigger` syntax for multi-project pipelines
The simplest way to configure a downstream trigger is to use `trigger` keyword
with a full path to a downstream project:
@@ -2622,7 +2625,7 @@ staging:
trigger: my/deployment
```
-#### Complex `trigger` syntax
+#### Complex `trigger` syntax for multi-project pipelines
It is possible to configure a branch name that GitLab will use to create
a downstream pipeline with:
@@ -2657,6 +2660,28 @@ upstream_bridge:
pipeline: other/project
```
+#### `trigger` syntax for child pipeline
+
+To create a [child pipeline](../parent_child_pipelines.md), specify the path to the
+YAML file containing the CI config of the child pipeline:
+
+```yaml
+trigger_job:
+ trigger:
+ include: path/to/child-pipeline.yml
+```
+
+Similar to [multi-project pipelines](../multi_project_pipelines.md#mirroring-status-from-triggered-pipeline),
+it is possible to mirror the status from a triggered pipeline:
+
+```yaml
+trigger_job:
+ trigger:
+ include:
+ - local: path/to/child-pipeline.yml
+ strategy: depend
+```
+
### `interruptible`
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/23464) in GitLab 12.3.
diff --git a/lib/gitlab/background_migration/activate_prometheus_services_for_shared_cluster_applications.rb b/lib/gitlab/background_migration/activate_prometheus_services_for_shared_cluster_applications.rb
deleted file mode 100644
index 19f5821d449..00000000000
--- a/lib/gitlab/background_migration/activate_prometheus_services_for_shared_cluster_applications.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # Create missing PrometheusServices records or sets active attribute to true
- # for all projects which belongs to cluster with Prometheus Application installed.
- class ActivatePrometheusServicesForSharedClusterApplications
- module Migratable
- # Migration model namespace isolated from application code.
- class PrometheusService < ActiveRecord::Base
- self.inheritance_column = :_type_disabled
- self.table_name = 'services'
-
- default_scope { where("services.type = 'PrometheusService'") }
-
- def self.for_project(project_id)
- new(
- project_id: project_id,
- active: true,
- properties: '{}',
- type: 'PrometheusService',
- template: false,
- push_events: true,
- issues_events: true,
- merge_requests_events: true,
- tag_push_events: true,
- note_events: true,
- category: 'monitoring',
- default: false,
- wiki_page_events: true,
- pipeline_events: true,
- confidential_issues_events: true,
- commit_events: true,
- job_events: true,
- confidential_note_events: true,
- deployment_events: false
- )
- end
-
- def managed?
- properties == '{}'
- end
- end
- end
-
- def perform(project_id)
- service = Migratable::PrometheusService.find_by(project_id: project_id) || Migratable::PrometheusService.for_project(project_id)
- service.update!(active: true) if service.managed?
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index 0f355906948..6a16e6df23d 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -22,12 +22,6 @@ module Gitlab
end
end
- def uses_unsupported_legacy_trigger?
- trigger_request.present? &&
- trigger_request.trigger.legacy? &&
- !trigger_request.trigger.supports_legacy_tokens?
- end
-
def branch_exists?
strong_memoize(:is_branch) do
project.repository.branch_exists?(ref)
diff --git a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
index f9ed9d91177..a30b6c6ef0e 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
@@ -14,16 +14,12 @@ module Gitlab
return error('Pipelines are disabled!')
end
- if @command.uses_unsupported_legacy_trigger?
- return error('Trigger token is invalid because is not owned by any user')
+ unless allowed_to_create_pipeline?
+ return error('Insufficient permissions to create a new pipeline')
end
- unless allowed_to_trigger_pipeline?
- if can?(current_user, :create_pipeline, project)
- return error("Insufficient permissions for protected ref '#{command.ref}'")
- else
- return error('Insufficient permissions to create a new pipeline')
- end
+ unless allowed_to_write_ref?
+ return error("Insufficient permissions for protected ref '#{command.ref}'")
end
end
@@ -31,17 +27,13 @@ module Gitlab
@pipeline.errors.any?
end
- def allowed_to_trigger_pipeline?
- if current_user
- allowed_to_create?
- else # legacy triggers don't have a corresponding user
- !@command.protected_ref?
- end
- end
+ private
- def allowed_to_create?
- return unless can?(current_user, :create_pipeline, project)
+ def allowed_to_create_pipeline?
+ can?(current_user, :create_pipeline, project)
+ end
+ def allowed_to_write_ref?
access = Gitlab::UserAccess.new(current_user, project: project)
if @command.branch_exists?
diff --git a/lib/gitlab/import_export/group_project_object_builder.rb b/lib/gitlab/import_export/group_project_object_builder.rb
index b94839363df..2e7ab3d4b69 100644
--- a/lib/gitlab/import_export/group_project_object_builder.rb
+++ b/lib/gitlab/import_export/group_project_object_builder.rb
@@ -26,6 +26,8 @@ module Gitlab
end
def find
+ return if epic? && group.nil?
+
find_object || klass.create(project_attributes)
end
@@ -54,10 +56,10 @@ module Gitlab
# or, if group is present:
# `"{table_name}"."project_id" = {project.id} OR "{table_name}"."group_id" = {group.id}`
def where_clause_base
- clause = table[:project_id].eq(project.id) if project
- clause = clause.or(table[:group_id].eq(group.id)) if group
-
- clause
+ [].tap do |clauses|
+ clauses << table[:project_id].eq(project.id) if project
+ clauses << table[:group_id].eq(group.id) if group
+ end.reduce(:or)
end
# Returns Arel clause `"{table_name}"."title" = '{attributes['title']}'`
@@ -108,6 +110,10 @@ module Gitlab
klass == MergeRequest
end
+ def epic?
+ klass == Epic
+ end
+
# If an existing group milestone used the IID
# claim the IID back and set the group milestone to use one available
# This is necessary to fix situations like the following:
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 3208e6648a7..0f0397ec13b 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -322,6 +322,13 @@ excluded_attributes:
- :board_id
- :label_id
- :milestone_id
+ epic:
+ - :start_date_sourcing_milestone_id
+ - :due_date_sourcing_milestone_id
+ - :parent_id
+ - :state_id
+ - :start_date_sourcing_epic_id
+ - :due_date_sourcing_epic_id
methods:
notes:
- :type
@@ -374,6 +381,7 @@ ee:
- design_versions:
- actions:
- :design # Duplicate export of issues.designs in order to link the record to both Issue and Action
+ - :epic
- protected_branches:
- :unprotect_access_levels
- protected_environments:
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 9dd30803196..9a5e01462fb 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -40,7 +40,21 @@ module Gitlab
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
- EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels project_feature merge_request ProjectCiCdSetting container_expiration_policy].freeze
+ EXISTING_OBJECT_CHECK = %i[
+ milestone
+ milestones
+ label
+ labels
+ project_label
+ project_labels
+ group_label
+ group_labels
+ project_feature
+ merge_request
+ epic
+ ProjectCiCdSetting
+ container_expiration_policy
+ ].freeze
TOKEN_RESET_MODELS = %i[Project Namespace Ci::Trigger Ci::Build Ci::Runner ProjectHook].freeze
@@ -86,9 +100,6 @@ module Gitlab
def create
return if unknown_service?
- # Do not import legacy triggers
- return if !Feature.enabled?(:use_legacy_pipeline_triggers, @project) && legacy_trigger?
-
setup_models
object = generate_imported_object
@@ -345,10 +356,6 @@ module Gitlab
!Object.const_defined?(parsed_relation_hash['type'])
end
- def legacy_trigger?
- @relation_name == :'Ci::Trigger' && @relation_hash['owner_id'].nil?
- end
-
def find_or_create_object!
if UNIQUE_RELATIONS.include?(@relation_name)
unique_relation_object = relation_class.find_or_create_by(project_id: @project.id)
diff --git a/lib/gitlab/import_export/relation_tree_restorer.rb b/lib/gitlab/import_export/relation_tree_restorer.rb
index 886087bc73a..4f58406360e 100644
--- a/lib/gitlab/import_export/relation_tree_restorer.rb
+++ b/lib/gitlab/import_export/relation_tree_restorer.rb
@@ -4,7 +4,7 @@ module Gitlab
module ImportExport
class RelationTreeRestorer
# Relations which cannot be saved at project level (and have a group assigned)
- GROUP_MODELS = [GroupLabel, Milestone].freeze
+ GROUP_MODELS = [GroupLabel, Milestone, Epic].freeze
attr_reader :user
attr_reader :shared
diff --git a/package.json b/package.json
index 95bf5214b78..d658b8ff2ec 100644
--- a/package.json
+++ b/package.json
@@ -82,6 +82,7 @@
"fuzzaldrin-plus": "^0.5.0",
"glob": "^7.1.2",
"graphql": "^14.0.2",
+ "immer": "^5.2.1",
"imports-loader": "^0.8.0",
"jed": "^1.1.1",
"jest-transform-graphql": "^2.1.0",
diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb
index 4d26cadcdfe..b22828e554f 100644
--- a/qa/qa/page/project/web_ide/edit.rb
+++ b/qa/qa/page/project/web_ide/edit.rb
@@ -108,9 +108,9 @@ module QA
# Click :commit_button and keep retrying just in case part of the
# animation is still in process even when the buttons have the
# expected visibility.
- commit_success_msg_shown = retry_until do
- click_element :commit_to_current_branch_radio
- click_element :commit_button
+ commit_success_msg_shown = retry_until(sleep_interval: 5) do
+ click_element(:commit_to_current_branch_radio) if has_element?(:commit_to_current_branch_radio)
+ click_element(:commit_button) if has_element?(:commit_button)
wait(reload: false) do
has_text?('Your changes have been committed')
diff --git a/spec/controllers/admin/sessions_controller_spec.rb b/spec/controllers/admin/sessions_controller_spec.rb
index bd0bb0bd81f..be996aee1d2 100644
--- a/spec/controllers/admin/sessions_controller_spec.rb
+++ b/spec/controllers/admin/sessions_controller_spec.rb
@@ -122,7 +122,7 @@ describe Admin::SessionsController, :do_not_mock_admin_mode do
describe '#destroy' do
context 'for regular users' do
it 'shows error page' do
- get :destroy
+ post :destroy
expect(response).to have_gitlab_http_status(404)
expect(controller.current_user_mode.admin_mode?).to be(false)
@@ -139,7 +139,7 @@ describe Admin::SessionsController, :do_not_mock_admin_mode do
post :create, params: { password: user.password }
expect(controller.current_user_mode.admin_mode?).to be(true)
- get :destroy
+ post :destroy
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(root_path)
diff --git a/spec/features/merge_request/maintainer_edits_fork_spec.rb b/spec/features/merge_request/maintainer_edits_fork_spec.rb
index 4e161d530d3..4f2c5fc73d8 100644
--- a/spec/features/merge_request/maintainer_edits_fork_spec.rb
+++ b/spec/features/merge_request/maintainer_edits_fork_spec.rb
@@ -32,8 +32,6 @@ describe 'a maintainer edits files on a source-branch of an MR from a fork', :js
wait_for_requests
end
- it_behaves_like 'rendering a single diff version'
-
it 'mentions commits will go to the source branch' do
expect(page).to have_content('Your changes can be committed to fix because a merge request is open.')
end
diff --git a/spec/features/merge_request/user_comments_on_diff_spec.rb b/spec/features/merge_request/user_comments_on_diff_spec.rb
index 6a23b6cdf60..19b8a7f74b7 100644
--- a/spec/features/merge_request/user_comments_on_diff_spec.rb
+++ b/spec/features/merge_request/user_comments_on_diff_spec.rb
@@ -13,15 +13,12 @@ describe 'User comments on a diff', :js do
let(:user) { create(:user) }
before do
- stub_feature_flags(single_mr_diff_view: false)
project.add_maintainer(user)
sign_in(user)
visit(diffs_project_merge_request_path(project, merge_request))
end
- it_behaves_like 'rendering a single diff version'
-
context 'when viewing comments' do
context 'when toggling inline comments' do
context 'in a single file' do
diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
index e6634a8ff39..e0724a04ea3 100644
--- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
@@ -9,7 +9,6 @@ describe 'Merge request > User creates image diff notes', :js do
let(:user) { project.creator }
before do
- stub_feature_flags(single_mr_diff_view: false)
sign_in(user)
# Stub helper to return any blob file as image from public app folder.
@@ -18,8 +17,6 @@ describe 'Merge request > User creates image diff notes', :js do
allow_any_instance_of(DiffHelper).to receive(:diff_file_old_blob_raw_url).and_return('/favicon.png')
end
- it_behaves_like 'rendering a single diff version'
-
context 'create commit diff notes' do
commit_id = '2f63565e7aac07bcdadb654e253078b727143ec4'
diff --git a/spec/features/merge_request/user_expands_diff_spec.rb b/spec/features/merge_request/user_expands_diff_spec.rb
index 9b040271468..9bce5264817 100644
--- a/spec/features/merge_request/user_expands_diff_spec.rb
+++ b/spec/features/merge_request/user_expands_diff_spec.rb
@@ -7,7 +7,6 @@ describe 'User expands diff', :js do
let(:merge_request) { create(:merge_request, source_branch: 'expand-collapse-files', source_project: project, target_project: project) }
before do
- stub_feature_flags(single_mr_diff_view: false)
stub_feature_flags(diffs_batch_load: false)
allow(Gitlab::Git::Diff).to receive(:size_limit).and_return(100.kilobytes)
@@ -18,8 +17,6 @@ describe 'User expands diff', :js do
wait_for_requests
end
- it_behaves_like 'rendering a single diff version'
-
it 'allows user to expand diff' do
page.within find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9"]') do
click_link 'Click to expand it.'
diff --git a/spec/features/merge_request/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb
index 6328c0a5133..8b16760606c 100644
--- a/spec/features/merge_request/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb
@@ -14,15 +14,12 @@ describe 'Merge request > User posts diff notes', :js do
let(:test_note_comment) { 'this is a test note!' }
before do
- stub_feature_flags(single_mr_diff_view: false)
set_cookie('sidebar_collapsed', 'true')
project.add_developer(user)
sign_in(user)
end
- it_behaves_like 'rendering a single diff version'
-
context 'when hovering over a parallel view diff file' do
before do
visit diffs_project_merge_request_path(project, merge_request, view: 'parallel')
diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb
index c0655581b18..f24e7090605 100644
--- a/spec/features/merge_request/user_posts_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_notes_spec.rb
@@ -165,9 +165,9 @@ describe 'Merge request > User posts notes', :js do
find('.js-note-edit').click
page.within('.current-note-edit-form') do
- expect(find('#note_note').value).to eq('This is the new content')
+ expect(find('#note_note').value).to include('This is the new content')
first('.js-md').click
- expect(find('#note_note').value).to eq('This is the new content****')
+ expect(find('#note_note').value).to include('This is the new content****')
end
end
diff --git a/spec/features/merge_request/user_resolves_conflicts_spec.rb b/spec/features/merge_request/user_resolves_conflicts_spec.rb
index f0949fefa3b..ce85e81868d 100644
--- a/spec/features/merge_request/user_resolves_conflicts_spec.rb
+++ b/spec/features/merge_request/user_resolves_conflicts_spec.rb
@@ -9,7 +9,6 @@ describe 'Merge request > User resolves conflicts', :js do
before do
# In order to have the diffs collapsed, we need to disable the increase feature
stub_feature_flags(gitlab_git_diff_size_limit_increase: false)
- stub_feature_flags(single_mr_diff_view: false)
end
def create_merge_request(source_branch)
@@ -18,8 +17,6 @@ describe 'Merge request > User resolves conflicts', :js do
end
end
- it_behaves_like 'rendering a single diff version'
-
shared_examples 'conflicts are resolved in Interactive mode' do
it 'conflicts are resolved in Interactive mode' do
within find('.files-wrapper .diff-file', text: 'files/ruby/popen.rb') do
diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
index 9cbea8a8466..eb86b1e33af 100644
--- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
+++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
@@ -20,12 +20,9 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
before do
- stub_feature_flags(single_mr_diff_view: false)
stub_feature_flags(diffs_batch_load: false)
end
- it_behaves_like 'rendering a single diff version'
-
context 'no threads' do
before do
project.add_maintainer(user)
diff --git a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
index 70afe056c64..3e77b9e75d6 100644
--- a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
@@ -21,7 +21,6 @@ describe 'Merge request > User sees avatars on diff notes', :js do
let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) }
before do
- stub_feature_flags(single_mr_diff_view: false)
stub_feature_flags(diffs_batch_load: false)
project.add_maintainer(user)
sign_in user
@@ -29,8 +28,6 @@ describe 'Merge request > User sees avatars on diff notes', :js do
set_cookie('sidebar_collapsed', 'true')
end
- it_behaves_like 'rendering a single diff version'
-
context 'discussion tab' do
before do
visit project_merge_request_path(project, merge_request)
diff --git a/spec/features/merge_request/user_sees_diff_spec.rb b/spec/features/merge_request/user_sees_diff_spec.rb
index de142344c26..2d91d09a486 100644
--- a/spec/features/merge_request/user_sees_diff_spec.rb
+++ b/spec/features/merge_request/user_sees_diff_spec.rb
@@ -10,12 +10,9 @@ describe 'Merge request > User sees diff', :js do
let(:merge_request) { create(:merge_request, source_project: project) }
before do
- stub_feature_flags(single_mr_diff_view: false)
stub_feature_flags(diffs_batch_load: false)
end
- it_behaves_like 'rendering a single diff version'
-
context 'when linking to note' do
describe 'with unresolved note' do
let(:note) { create :diff_note_on_merge_request, project: project, noteable: merge_request }
diff --git a/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb b/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
index e28d2ca5536..59e5f5c847d 100644
--- a/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
+++ b/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
@@ -11,14 +11,11 @@ describe 'Merge request > User sees MR with deleted source branch', :js do
let(:user) { project.creator }
before do
- stub_feature_flags(single_mr_diff_view: false)
merge_request.update!(source_branch: 'this-branch-does-not-exist')
sign_in(user)
visit project_merge_request_path(project, merge_request)
end
- it_behaves_like 'rendering a single diff version'
-
it 'shows a message about missing source branch' do
expect(page).to have_content('Source branch does not exist.')
end
diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb
index b3aef601c7b..cab86f3fd94 100644
--- a/spec/features/merge_request/user_sees_versions_spec.rb
+++ b/spec/features/merge_request/user_sees_versions_spec.rb
@@ -16,7 +16,6 @@ describe 'Merge request > User sees versions', :js do
let!(:params) { {} }
before do
- stub_feature_flags(single_mr_diff_view: false)
stub_feature_flags(diffs_batch_load: false)
project.add_maintainer(user)
@@ -24,8 +23,6 @@ describe 'Merge request > User sees versions', :js do
visit diffs_project_merge_request_path(project, merge_request, params)
end
- it_behaves_like 'rendering a single diff version'
-
shared_examples 'allows commenting' do |file_id:, line_code:, comment:|
it do
diff_file_selector = ".diff-file[id='#{file_id}']"
diff --git a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
index 085fe38b47a..95cb0a2dee3 100644
--- a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
+++ b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
@@ -25,15 +25,12 @@ describe 'User comments on a diff', :js do
let(:user) { create(:user) }
before do
- stub_feature_flags(single_mr_diff_view: false)
project.add_maintainer(user)
sign_in(user)
visit(diffs_project_merge_request_path(project, merge_request))
end
- it_behaves_like 'rendering a single diff version'
-
context 'single suggestion note' do
it 'hides suggestion popover' do
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
diff --git a/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb b/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb
index 5e59bc87e68..4db067a4e41 100644
--- a/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb
+++ b/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb
@@ -8,7 +8,6 @@ describe 'Merge request > User toggles whitespace changes', :js do
let(:user) { project.creator }
before do
- stub_feature_flags(single_mr_diff_view: false)
project.add_maintainer(user)
sign_in(user)
visit diffs_project_merge_request_path(project, merge_request)
@@ -16,8 +15,6 @@ describe 'Merge request > User toggles whitespace changes', :js do
find('.js-show-diff-settings').click
end
- it_behaves_like 'rendering a single diff version'
-
it 'has a button to toggle whitespace changes' do
expect(page).to have_content 'Show whitespace changes'
end
diff --git a/spec/features/merge_request/user_views_diffs_spec.rb b/spec/features/merge_request/user_views_diffs_spec.rb
index 313f438e23b..e0e4058dd47 100644
--- a/spec/features/merge_request/user_views_diffs_spec.rb
+++ b/spec/features/merge_request/user_views_diffs_spec.rb
@@ -9,7 +9,6 @@ describe 'User views diffs', :js do
let(:project) { create(:project, :public, :repository) }
before do
- stub_feature_flags(single_mr_diff_view: false)
stub_feature_flags(diffs_batch_load: false)
visit(diffs_project_merge_request_path(project, merge_request))
@@ -18,8 +17,6 @@ describe 'User views diffs', :js do
find('.js-toggle-tree-list').click
end
- it_behaves_like 'rendering a single diff version'
-
shared_examples 'unfold diffs' do
it 'unfolds diffs upwards' do
first('.js-unfold').click
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index 0a5bc64b429..a1d6a8896c7 100644
--- a/spec/features/projects/blobs/edit_spec.rb
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -12,11 +12,9 @@ describe 'Editing file blob', :js do
let(:readme_file_path) { 'README.md' }
before do
- stub_feature_flags(web_ide_default: false, single_mr_diff_view: false)
+ stub_feature_flags(web_ide_default: false)
end
- it_behaves_like 'rendering a single diff version'
-
context 'as a developer' do
let(:user) { create(:user) }
let(:role) { :developer }
diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb
index c2d4cefad12..8b25565c08a 100644
--- a/spec/features/projects/view_on_env_spec.rb
+++ b/spec/features/projects/view_on_env_spec.rb
@@ -9,14 +9,11 @@ describe 'View on environment', :js do
let(:user) { project.creator }
before do
- stub_feature_flags(single_mr_diff_view: false)
stub_feature_flags(diffs_batch_load: false)
project.add_maintainer(user)
end
- it_behaves_like 'rendering a single diff version'
-
context 'when the branch has a route map' do
let(:route_map) do
<<-MAP.strip_heredoc
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 19cd21e4161..af406961bbc 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -65,22 +65,6 @@ describe 'Triggers', :js do
expect(page.find('.triggers-list')).to have_content new_trigger_title
expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
end
-
- it 'edit "legacy" trigger and save' do
- # Create new trigger without owner association, i.e. Legacy trigger
- create(:ci_trigger, owner: user, project: @project).update_attribute(:owner, nil)
- visit project_settings_ci_cd_path(@project)
-
- # See if the trigger can be edited and description is blank
- find('a[title="Edit"]').send_keys(:return)
- expect(page.find('#trigger_description').value).to have_content ''
-
- # See if trigger can be updated with description and saved successfully
- fill_in 'trigger_description', with: new_trigger_title
- click_button 'Save trigger'
- expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.'
- expect(page.find('.triggers-list')).to have_content new_trigger_title
- end
end
describe 'trigger "Revoke" workflow' do
@@ -106,43 +90,18 @@ describe 'Triggers', :js do
end
describe 'show triggers workflow' do
- before do
- stub_feature_flags(use_legacy_pipeline_triggers: false)
- end
-
it 'contains trigger description placeholder' do
expect(page.find('#trigger_description')['placeholder']).to eq 'Trigger description'
end
- it 'show "invalid" badge for legacy trigger' do
- create(:ci_trigger, owner: user, project: @project).update_attribute(:owner, nil)
- visit project_settings_ci_cd_path(@project)
-
- expect(page.find('.triggers-list')).to have_content 'invalid'
- end
-
it 'show "invalid" badge for trigger with owner having insufficient permissions' do
create(:ci_trigger, owner: guest_user, project: @project, description: trigger_title)
visit project_settings_ci_cd_path(@project)
- # See if trigger without owner (i.e. legacy) shows "legacy" badge and is non-editable
expect(page.find('.triggers-list')).to have_content 'invalid'
expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]')
end
- it 'do not show "Edit" or full token for legacy trigger' do
- create(:ci_trigger, owner: user, project: @project, description: trigger_title)
- .update_attribute(:owner, nil)
- visit project_settings_ci_cd_path(@project)
-
- # See if trigger not owned shows only first few token chars and doesn't have copy-to-clipboard button
- expect(page.find('.triggers-list')).to have_content(@project.triggers.first.token[0..3])
- expect(page.find('.triggers-list')).not_to have_selector('button.btn-clipboard')
-
- # See if trigger is non-editable
- expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]')
- end
-
it 'do not show "Edit" or full token for not owned trigger' do
# Create trigger with user different from current_user
create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
@@ -169,56 +128,5 @@ describe 'Triggers', :js do
expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
expect(page.find('.triggers-list')).to have_selector('a[title="Edit"]')
end
-
- context 'when :use_legacy_pipeline_triggers feature flag is enabled' do
- before do
- stub_feature_flags(use_legacy_pipeline_triggers: true)
- end
-
- it 'show "legacy" badge for legacy trigger' do
- create(:ci_trigger, owner: nil, project: @project)
- visit project_settings_ci_cd_path(@project)
-
- # See if trigger without owner (i.e. legacy) shows "legacy" badge and is editable
- expect(page.find('.triggers-list')).to have_content 'legacy'
- expect(page.find('.triggers-list')).to have_selector('a[title="Edit"]')
- end
-
- it 'show "invalid" badge for trigger with owner having insufficient permissions' do
- create(:ci_trigger, owner: guest_user, project: @project, description: trigger_title)
- visit project_settings_ci_cd_path(@project)
-
- # See if trigger without owner (i.e. legacy) shows "legacy" badge and is non-editable
- expect(page.find('.triggers-list')).to have_content 'invalid'
- expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]')
- end
-
- it 'do not show "Edit" or full token for not owned trigger' do
- # Create trigger with user different from current_user
- create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
- visit project_settings_ci_cd_path(@project)
-
- # See if trigger not owned by current_user shows only first few token chars and doesn't have copy-to-clipboard button
- expect(page.find('.triggers-list')).to have_content(@project.triggers.first.token[0..3])
- expect(page.find('.triggers-list')).not_to have_selector('button.btn-clipboard')
-
- # See if trigger owner name doesn't match with current_user and trigger is non-editable
- expect(page.find('.triggers-list .trigger-owner')).not_to have_content user.name
- expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]')
- end
-
- it 'show "Edit" and full token for owned trigger' do
- create(:ci_trigger, owner: user, project: @project, description: trigger_title)
- visit project_settings_ci_cd_path(@project)
-
- # See if trigger shows full token and has copy-to-clipboard button
- expect(page.find('.triggers-list')).to have_content @project.triggers.first.token
- expect(page.find('.triggers-list')).to have_selector('button.btn-clipboard')
-
- # See if trigger owner name matches with current_user and is editable
- expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
- expect(page.find('.triggers-list')).to have_selector('a[title="Edit"]')
- end
- end
end
end
diff --git a/spec/fixtures/lib/gitlab/import_export/group/project.json b/spec/fixtures/lib/gitlab/import_export/group/project.json
index 47faf271cca..ce4fa1981ff 100644
--- a/spec/fixtures/lib/gitlab/import_export/group/project.json
+++ b/spec/fixtures/lib/gitlab/import_export/group/project.json
@@ -175,6 +175,67 @@
}
}
]
+ },
+ {
+ "id": 3,
+ "title": "Issue with Epic",
+ "author_id": 1,
+ "project_id": 8,
+ "created_at": "2019-12-08T19:41:11.233Z",
+ "updated_at": "2019-12-08T19:41:53.194Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Donec at nulla vitae sem molestie rutrum ut at sem.",
+ "state": "opened",
+ "iid": 3,
+ "updated_by_id": null,
+ "confidential": false,
+ "due_date": null,
+ "moved_to_id": null,
+ "issue_assignees": [],
+ "notes": [],
+ "milestone": {
+ "id": 2,
+ "title": "A group milestone",
+ "description": "Group-level milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "group_id": 100
+ },
+ "epic": {
+ "id": 1,
+ "group_id": 5,
+ "author_id": 1,
+ "assignee_id": null,
+ "iid": 1,
+ "updated_by_id": null,
+ "last_edited_by_id": null,
+ "lock_version": 0,
+ "start_date": null,
+ "end_date": null,
+ "last_edited_at": null,
+ "created_at": "2019-12-08T19:37:07.098Z",
+ "updated_at": "2019-12-08T19:43:11.568Z",
+ "title": "An epic",
+ "description": null,
+ "start_date_sourcing_milestone_id": null,
+ "due_date_sourcing_milestone_id": null,
+ "start_date_fixed": null,
+ "due_date_fixed": null,
+ "start_date_is_fixed": null,
+ "due_date_is_fixed": null,
+ "closed_by_id": null,
+ "closed_at": null,
+ "parent_id": null,
+ "relative_position": null,
+ "state_id": "opened",
+ "start_date_sourcing_epic_id": null,
+ "due_date_sourcing_epic_id": null,
+ "milestone_id": null
+ }
}
],
"snippets": [
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index eefaff4aba7..b5ce9383eb9 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -13,6 +13,7 @@ describe('ErrorDetails', () => {
let wrapper;
let actions;
let getters;
+ let mocks;
const findInput = name => {
const inputs = wrapper.findAll(GlFormInput).filter(c => c.attributes('name') === name);
@@ -24,13 +25,27 @@ describe('ErrorDetails', () => {
stubs: { LoadingButton },
localVue,
store,
+ mocks,
propsData: {
+ issueId: '123',
+ projectPath: '/root/gitlab-test',
issueDetailsPath: '/123/details',
issueStackTracePath: '/stacktrace',
projectIssuesPath: '/test-project/issues/',
csrfToken: 'fakeToken',
},
});
+ wrapper.setData({
+ GQLerror: {
+ id: 129381,
+ title: 'Issue title',
+ externalUrl: 'http://sentry.gitlab.net/gitlab',
+ firstSeen: '2017-05-26T13:32:48Z',
+ lastSeen: '2018-05-26T13:32:48Z',
+ count: 12,
+ userCount: 2,
+ },
+ });
}
beforeEach(() => {
@@ -61,6 +76,19 @@ describe('ErrorDetails', () => {
},
},
});
+
+ const query = jest.fn();
+ mocks = {
+ $apollo: {
+ query,
+ queries: {
+ GQLerror: {
+ loading: true,
+ stopPolling: jest.fn(),
+ },
+ },
+ },
+ };
});
afterEach(() => {
@@ -85,10 +113,11 @@ describe('ErrorDetails', () => {
beforeEach(() => {
store.state.details.loading = false;
store.state.details.error.id = 1;
+ mocks.$apollo.queries.GQLerror.loading = false;
+ mountComponent();
});
it('should show Sentry error details without stacktrace', () => {
- mountComponent();
expect(wrapper.find(GlLink).exists()).toBe(true);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.find(Stacktrace).exists()).toBe(false);
@@ -99,13 +128,17 @@ describe('ErrorDetails', () => {
it('should show language and error level badges', () => {
store.state.details.error.tags = { level: 'error', logger: 'ruby' };
mountComponent();
- expect(wrapper.findAll(GlBadge).length).toBe(2);
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.findAll(GlBadge).length).toBe(2);
+ });
});
it('should NOT show the badge if the tag is not present', () => {
store.state.details.error.tags = { level: 'error' };
mountComponent();
- expect(wrapper.findAll(GlBadge).length).toBe(1);
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.findAll(GlBadge).length).toBe(1);
+ });
});
});
@@ -113,8 +146,10 @@ describe('ErrorDetails', () => {
it('should show stacktrace', () => {
store.state.details.loadingStacktrace = false;
mountComponent();
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find(Stacktrace).exists()).toBe(true);
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find(Stacktrace).exists()).toBe(true);
+ });
});
it('should NOT show stacktrace if no entries', () => {
@@ -128,15 +163,6 @@ describe('ErrorDetails', () => {
describe('When a user clicks the create issue button', () => {
beforeEach(() => {
- store.state.details.error = {
- id: 129381,
- title: 'Issue title',
- external_url: 'http://sentry.gitlab.net/gitlab',
- first_seen: '2017-05-26T13:32:48Z',
- last_seen: '2018-05-26T13:32:48Z',
- count: 12,
- user_count: 2,
- };
mountComponent();
});
diff --git a/spec/frontend/helpers/diffs_helper_spec.js b/spec/frontend/helpers/diffs_helper_spec.js
new file mode 100644
index 00000000000..b223d48bf5c
--- /dev/null
+++ b/spec/frontend/helpers/diffs_helper_spec.js
@@ -0,0 +1,113 @@
+import * as diffsHelper from '~/helpers/diffs_helper';
+
+describe('diffs helper', () => {
+ function getDiffFile(withOverrides = {}) {
+ return {
+ parallel_diff_lines: ['line'],
+ highlighted_diff_lines: ['line'],
+ blob: {
+ readable_text: 'text',
+ },
+ ...withOverrides,
+ };
+ }
+
+ describe('hasInlineLines', () => {
+ it('is false when the file does not exist', () => {
+ expect(diffsHelper.hasInlineLines()).toBeFalsy();
+ });
+
+ it('is false when the file does not have the highlighted_diff_lines property', () => {
+ const missingInline = getDiffFile({ highlighted_diff_lines: undefined });
+
+ expect(diffsHelper.hasInlineLines(missingInline)).toBeFalsy();
+ });
+
+ it('is false when the file has zero highlighted_diff_lines', () => {
+ const emptyInline = getDiffFile({ highlighted_diff_lines: [] });
+
+ expect(diffsHelper.hasInlineLines(emptyInline)).toBeFalsy();
+ });
+
+ it('is true when the file has at least 1 highlighted_diff_lines', () => {
+ expect(diffsHelper.hasInlineLines(getDiffFile())).toBeTruthy();
+ });
+ });
+
+ describe('hasParallelLines', () => {
+ it('is false when the file does not exist', () => {
+ expect(diffsHelper.hasParallelLines()).toBeFalsy();
+ });
+
+ it('is false when the file does not have the parallel_diff_lines property', () => {
+ const missingInline = getDiffFile({ parallel_diff_lines: undefined });
+
+ expect(diffsHelper.hasParallelLines(missingInline)).toBeFalsy();
+ });
+
+ it('is false when the file has zero parallel_diff_lines', () => {
+ const emptyInline = getDiffFile({ parallel_diff_lines: [] });
+
+ expect(diffsHelper.hasParallelLines(emptyInline)).toBeFalsy();
+ });
+
+ it('is true when the file has at least 1 parallel_diff_lines', () => {
+ expect(diffsHelper.hasParallelLines(getDiffFile())).toBeTruthy();
+ });
+ });
+
+ describe('isSingleViewStyle', () => {
+ it('is true when the file has at least 1 inline line but no parallel lines for any reason', () => {
+ const noParallelLines = getDiffFile({ parallel_diff_lines: undefined });
+ const emptyParallelLines = getDiffFile({ parallel_diff_lines: [] });
+
+ expect(diffsHelper.isSingleViewStyle(noParallelLines)).toBeTruthy();
+ expect(diffsHelper.isSingleViewStyle(emptyParallelLines)).toBeTruthy();
+ });
+
+ it('is true when the file has at least 1 parallel line but no inline lines for any reason', () => {
+ const noInlineLines = getDiffFile({ highlighted_diff_lines: undefined });
+ const emptyInlineLines = getDiffFile({ highlighted_diff_lines: [] });
+
+ expect(diffsHelper.isSingleViewStyle(noInlineLines)).toBeTruthy();
+ expect(diffsHelper.isSingleViewStyle(emptyInlineLines)).toBeTruthy();
+ });
+
+ it('is true when the file does not have any inline lines or parallel lines for any reason', () => {
+ const noLines = getDiffFile({
+ highlighted_diff_lines: undefined,
+ parallel_diff_lines: undefined,
+ });
+ const emptyLines = getDiffFile({
+ highlighted_diff_lines: [],
+ parallel_diff_lines: [],
+ });
+
+ expect(diffsHelper.isSingleViewStyle(noLines)).toBeTruthy();
+ expect(diffsHelper.isSingleViewStyle(emptyLines)).toBeTruthy();
+ expect(diffsHelper.isSingleViewStyle()).toBeTruthy();
+ });
+
+ it('is false when the file has both inline and parallel lines', () => {
+ expect(diffsHelper.isSingleViewStyle(getDiffFile())).toBeFalsy();
+ });
+ });
+
+ describe.each`
+ context | inline | parallel | blob | expected
+ ${'only has inline lines'} | ${['line']} | ${undefined} | ${undefined} | ${true}
+ ${'only has parallel lines'} | ${undefined} | ${['line']} | ${undefined} | ${true}
+ ${"doesn't have inline, parallel, or blob"} | ${undefined} | ${undefined} | ${undefined} | ${true}
+ ${'has blob readable text'} | ${undefined} | ${undefined} | ${{ readable_text: 'text' }} | ${false}
+ `('when hasDiff', ({ context, inline, parallel, blob, expected }) => {
+ it(`${context}`, () => {
+ const diffFile = getDiffFile({
+ highlighted_diff_lines: inline,
+ parallel_diff_lines: parallel,
+ blob,
+ });
+
+ expect(diffsHelper.hasDiff(diffFile)).toEqual(expected);
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 4caa66cf600..efae8b941ee 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -345,7 +345,7 @@ describe('Dashboard', () => {
it('metrics can be swapped', done => {
const firstDraggable = findDraggables().at(0);
- const mockMetrics = [...metricsGroupsAPIResponse[1].panels];
+ const mockMetrics = [...metricsGroupsAPIResponse.panel_groups[1].panels];
const firstTitle = mockMetrics[0].title;
const secondTitle = mockMetrics[1].title;
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index 6ded22b4a3f..77c92d0eca6 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -331,77 +331,80 @@ export const mockedQueryResultPayloadCoresTotal = {
],
};
-export const metricsGroupsAPIResponse = [
- {
- group: 'Response metrics (NGINX Ingress VTS)',
- priority: 10,
- panels: [
- {
- metrics: [
- {
- id: 'response_metrics_nginx_ingress_throughput_status_code',
- label: 'Status Code',
- metric_id: 1,
- prometheus_endpoint_path:
- '/root/autodevops-deploy/environments/32/prometheus/api/v1/query_range?query=sum%28rate%28nginx_upstream_responses_total%7Bupstream%3D~%22%25%7Bkube_namespace%7D-%25%7Bci_environment_slug%7D-.%2A%22%7D%5B2m%5D%29%29+by+%28status_code%29',
- query_range:
- 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code)',
- unit: 'req / sec',
- },
- ],
- title: 'Throughput',
- type: 'area-chart',
- weight: 1,
- y_label: 'Requests / Sec',
- },
- ],
- },
- {
- group: 'System metrics (Kubernetes)',
- priority: 5,
- panels: [
- {
- title: 'Memory Usage (Pod average)',
- type: 'area-chart',
- y_label: 'Memory Used per Pod',
- weight: 2,
- metrics: [
- {
- id: 'system_metrics_kubernetes_container_memory_average',
- query_range:
- 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024',
- label: 'Pod average',
- unit: 'MB',
- metric_id: 17,
- prometheus_endpoint_path:
- '/root/autodevops-deploy/environments/32/prometheus/api/v1/query_range?query=avg%28sum%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+by+%28job%29%29+without+%28job%29+%2F+count%28avg%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+without+%28job%29%29+%2F1024%2F1024',
- appearance: {
- line: {
- width: 2,
+export const metricsGroupsAPIResponse = {
+ dashboard: 'Environment metrics',
+ panel_groups: [
+ {
+ group: 'Response metrics (NGINX Ingress VTS)',
+ priority: 10,
+ panels: [
+ {
+ metrics: [
+ {
+ id: 'response_metrics_nginx_ingress_throughput_status_code',
+ label: 'Status Code',
+ metric_id: 1,
+ prometheus_endpoint_path:
+ '/root/autodevops-deploy/environments/32/prometheus/api/v1/query_range?query=sum%28rate%28nginx_upstream_responses_total%7Bupstream%3D~%22%25%7Bkube_namespace%7D-%25%7Bci_environment_slug%7D-.%2A%22%7D%5B2m%5D%29%29+by+%28status_code%29',
+ query_range:
+ 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code)',
+ unit: 'req / sec',
+ },
+ ],
+ title: 'Throughput',
+ type: 'area-chart',
+ weight: 1,
+ y_label: 'Requests / Sec',
+ },
+ ],
+ },
+ {
+ group: 'System metrics (Kubernetes)',
+ priority: 5,
+ panels: [
+ {
+ title: 'Memory Usage (Pod average)',
+ type: 'area-chart',
+ y_label: 'Memory Used per Pod',
+ weight: 2,
+ metrics: [
+ {
+ id: 'system_metrics_kubernetes_container_memory_average',
+ query_range:
+ 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024',
+ label: 'Pod average',
+ unit: 'MB',
+ metric_id: 17,
+ prometheus_endpoint_path:
+ '/root/autodevops-deploy/environments/32/prometheus/api/v1/query_range?query=avg%28sum%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+by+%28job%29%29+without+%28job%29+%2F+count%28avg%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+without+%28job%29%29+%2F1024%2F1024',
+ appearance: {
+ line: {
+ width: 2,
+ },
},
},
- },
- ],
- },
- {
- title: 'Core Usage (Total)',
- type: 'area-chart',
- y_label: 'Total Cores',
- weight: 3,
- metrics: [
- {
- id: 'system_metrics_kubernetes_container_cores_total',
- query_range:
- 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job)',
- label: 'Total',
- unit: 'cores',
- metric_id: 13,
- },
- ],
- },
- ],
- },
-];
+ ],
+ },
+ {
+ title: 'Core Usage (Total)',
+ type: 'area-chart',
+ y_label: 'Total Cores',
+ weight: 3,
+ metrics: [
+ {
+ id: 'system_metrics_kubernetes_container_cores_total',
+ query_range:
+ 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job)',
+ label: 'Total',
+ unit: 'cores',
+ metric_id: 13,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+};
export const environmentData = [
{
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index f38bd4384e2..c1ad59ac95b 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -298,7 +298,7 @@ describe('Monitoring store actions', () => {
);
expect(commit).toHaveBeenCalledWith(
types.RECEIVE_METRICS_DATA_SUCCESS,
- metricsDashboardResponse.dashboard.panel_groups,
+ metricsDashboardResponse.dashboard,
);
expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetrics', params);
});
@@ -441,7 +441,7 @@ describe('Monitoring store actions', () => {
beforeEach(() => {
state = storeState();
[metric] = metricsDashboardResponse.dashboard.panel_groups[0].panels[0].metrics;
- [data] = metricsGroupsAPIResponse[0].panels[0].metrics;
+ [data] = metricsGroupsAPIResponse.panel_groups[0].panels[0].metrics;
});
it('commits result', done => {
diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
index 60107a03674..e89c22268d4 100644
--- a/spec/frontend/monitoring/store/mutations_spec.js
+++ b/spec/frontend/monitoring/store/mutations_spec.js
@@ -100,12 +100,12 @@ describe('Monitoring mutations', () => {
values: [[0, 1], [1, 1], [1, 3]],
},
];
- const dashboardGroups = metricsDashboardResponse.dashboard.panel_groups;
+ const { dashboard } = metricsDashboardResponse;
const getMetric = () => stateCopy.dashboard.panel_groups[0].panels[0].metrics[0];
describe('REQUEST_METRIC_RESULT', () => {
beforeEach(() => {
- mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups);
+ mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboard);
});
it('stores a loading state on a metric', () => {
expect(stateCopy.showEmptyState).toBe(true);
@@ -128,7 +128,7 @@ describe('Monitoring mutations', () => {
describe('RECEIVE_METRIC_RESULT_SUCCESS', () => {
beforeEach(() => {
- mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups);
+ mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboard);
});
it('clears empty state', () => {
expect(stateCopy.showEmptyState).toBe(true);
@@ -161,7 +161,7 @@ describe('Monitoring mutations', () => {
describe('RECEIVE_METRIC_RESULT_FAILURE', () => {
beforeEach(() => {
- mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups);
+ mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboard);
});
it('maintains the loading state when a metric fails', () => {
expect(stateCopy.showEmptyState).toBe(true);
diff --git a/spec/helpers/projects/error_tracking_helper_spec.rb b/spec/helpers/projects/error_tracking_helper_spec.rb
index 753144eef89..7c67448034b 100644
--- a/spec/helpers/projects/error_tracking_helper_spec.rb
+++ b/spec/helpers/projects/error_tracking_helper_spec.rb
@@ -80,11 +80,20 @@ describe Projects::ErrorTrackingHelper do
let(:issue_id) { 1234 }
let(:route_params) { [project.owner, project, issue_id, { format: :json }] }
let(:details_path) { details_namespace_project_error_tracking_index_path(*route_params) }
+ let(:project_path) { project.full_path }
let(:stack_trace_path) { stack_trace_namespace_project_error_tracking_index_path(*route_params) }
let(:issues_path) { project_issues_path(project) }
let(:result) { helper.error_details_data(project, issue_id) }
+ it 'returns the correct issue id' do
+ expect(result['issue-id']).to eq issue_id
+ end
+
+ it 'returns the correct project path' do
+ expect(result['project-path']).to eq project_path
+ end
+
it 'returns the correct details path' do
expect(result['issue-details-path']).to eq details_path
end
diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js
index 266bbc1eadd..4b4a710df2d 100644
--- a/spec/javascripts/diffs/components/app_spec.js
+++ b/spec/javascripts/diffs/components/app_spec.js
@@ -10,6 +10,7 @@ import CompareVersions from '~/diffs/components/compare_versions.vue';
import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
import CommitWidget from '~/diffs/components/commit_widget.vue';
import TreeList from '~/diffs/components/tree_list.vue';
+import { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '~/diffs/constants';
import createDiffsStore from '../create_diffs_store';
import diffsMockData from '../mock_data/merge_request_diffs';
@@ -41,7 +42,6 @@ describe('diffs/components/app', () => {
changesEmptyStateIllustration: '',
dismissEndpoint: '',
showSuggestPopover: true,
- useSingleDiffStyle: false,
...props,
},
store,
@@ -53,6 +53,12 @@ describe('diffs/components/app', () => {
});
}
+ function getOppositeViewType(currentViewType) {
+ return currentViewType === INLINE_DIFF_VIEW_TYPE
+ ? PARALLEL_DIFF_VIEW_TYPE
+ : INLINE_DIFF_VIEW_TYPE;
+ }
+
beforeEach(() => {
// setup globals (needed for component to mount :/)
window.mrTabs = jasmine.createSpyObj('mrTabs', ['resetViewContainer']);
@@ -82,9 +88,146 @@ describe('diffs/components/app', () => {
spyOn(wrapper.vm, 'startRenderDiffsQueue');
spyOn(wrapper.vm, 'unwatchDiscussions');
store.state.diffs.retrievingBatches = true;
+ store.state.diffs.diffFiles = [];
wrapper.vm.$nextTick(done);
});
+ describe('when the diff view type changes and it should load a single diff view style', () => {
+ const noLinesDiff = {
+ highlighted_diff_lines: [],
+ parallel_diff_lines: [],
+ };
+ const parallelLinesDiff = {
+ highlighted_diff_lines: [],
+ parallel_diff_lines: ['line'],
+ };
+ const inlineLinesDiff = {
+ highlighted_diff_lines: ['line'],
+ parallel_diff_lines: [],
+ };
+ const fullDiff = {
+ highlighted_diff_lines: ['line'],
+ parallel_diff_lines: ['line'],
+ };
+
+ function expectFetchToOccur({
+ vueInstance,
+ done = () => {},
+ batch = false,
+ existingFiles = 1,
+ } = {}) {
+ vueInstance.$nextTick(() => {
+ expect(vueInstance.diffFiles.length).toEqual(existingFiles);
+
+ if (!batch) {
+ expect(vueInstance.fetchDiffFiles).toHaveBeenCalled();
+ expect(vueInstance.fetchDiffFilesBatch).not.toHaveBeenCalled();
+ } else {
+ expect(vueInstance.fetchDiffFiles).not.toHaveBeenCalled();
+ expect(vueInstance.fetchDiffFilesBatch).toHaveBeenCalled();
+ }
+
+ done();
+ });
+ }
+
+ beforeEach(() => {
+ wrapper.vm.glFeatures.singleMrDiffView = true;
+ });
+
+ it('fetches diffs if it has none', done => {
+ wrapper.vm.isLatestVersion = () => false;
+
+ store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
+
+ expectFetchToOccur({ vueInstance: wrapper.vm, batch: false, existingFiles: 0, done });
+ });
+
+ it('fetches diffs if it has both view styles, but no lines in either', done => {
+ wrapper.vm.isLatestVersion = () => false;
+
+ store.state.diffs.diffFiles.push(noLinesDiff);
+ store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
+
+ expectFetchToOccur({ vueInstance: wrapper.vm, done });
+ });
+
+ it('fetches diffs if it only has inline view style', done => {
+ wrapper.vm.isLatestVersion = () => false;
+
+ store.state.diffs.diffFiles.push(inlineLinesDiff);
+ store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
+
+ expectFetchToOccur({ vueInstance: wrapper.vm, done });
+ });
+
+ it('fetches diffs if it only has parallel view style', done => {
+ wrapper.vm.isLatestVersion = () => false;
+
+ store.state.diffs.diffFiles.push(parallelLinesDiff);
+ store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
+
+ expectFetchToOccur({ vueInstance: wrapper.vm, done });
+ });
+
+ it('fetches batch diffs if it has none', done => {
+ wrapper.vm.glFeatures.diffsBatchLoad = true;
+
+ store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
+
+ expectFetchToOccur({ vueInstance: wrapper.vm, batch: true, existingFiles: 0, done });
+ });
+
+ it('fetches batch diffs if it has both view styles, but no lines in either', done => {
+ wrapper.vm.glFeatures.diffsBatchLoad = true;
+
+ store.state.diffs.diffFiles.push(noLinesDiff);
+ store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
+
+ expectFetchToOccur({ vueInstance: wrapper.vm, batch: true, done });
+ });
+
+ it('fetches batch diffs if it only has inline view style', done => {
+ wrapper.vm.glFeatures.diffsBatchLoad = true;
+
+ store.state.diffs.diffFiles.push(inlineLinesDiff);
+ store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
+
+ expectFetchToOccur({ vueInstance: wrapper.vm, batch: true, done });
+ });
+
+ it('fetches batch diffs if it only has parallel view style', done => {
+ wrapper.vm.glFeatures.diffsBatchLoad = true;
+
+ store.state.diffs.diffFiles.push(parallelLinesDiff);
+ store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
+
+ expectFetchToOccur({ vueInstance: wrapper.vm, batch: true, done });
+ });
+
+ it('does not fetch diffs if it has already fetched both styles of diff', () => {
+ wrapper.vm.glFeatures.diffsBatchLoad = false;
+
+ store.state.diffs.diffFiles.push(fullDiff);
+ store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
+
+ expect(wrapper.vm.diffFiles.length).toEqual(1);
+ expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled();
+ expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled();
+ });
+
+ it('does not fetch batch diffs if it has already fetched both styles of diff', () => {
+ wrapper.vm.glFeatures.diffsBatchLoad = true;
+
+ store.state.diffs.diffFiles.push(fullDiff);
+ store.state.diffs.diffViewType = getOppositeViewType(wrapper.vm.diffViewType);
+
+ expect(wrapper.vm.diffFiles.length).toEqual(1);
+ expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled();
+ expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled();
+ });
+ });
+
it('calls fetchDiffFiles if diffsBatchLoad is not enabled', done => {
wrapper.vm.glFeatures.diffsBatchLoad = false;
wrapper.vm.fetchData(false);
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index 98a5348c3bc..436d7338361 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -120,7 +120,7 @@ describe('DiffsStoreActions', () => {
describe('fetchDiffFiles', () => {
it('should fetch diff files', done => {
- const endpoint = '/fetch/diff/files?w=1';
+ const endpoint = '/fetch/diff/files?view=inline&w=1';
const mock = new MockAdapter(axios);
const res = { diff_files: 1, merge_request_diffs: [] };
mock.onGet(endpoint).reply(200, res);
@@ -128,7 +128,7 @@ describe('DiffsStoreActions', () => {
testAction(
fetchDiffFiles,
{},
- { endpoint },
+ { endpoint, diffFiles: [], showWhitespace: false, diffViewType: 'inline' },
[
{ type: types.SET_LOADING, payload: true },
{ type: types.SET_LOADING, payload: false },
diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js
index 93dbf03e1ed..24405dcc796 100644
--- a/spec/javascripts/diffs/store/mutations_spec.js
+++ b/spec/javascripts/diffs/store/mutations_spec.js
@@ -52,7 +52,14 @@ describe('DiffsStoreMutations', () => {
describe('SET_DIFF_DATA', () => {
it('should set diff data type properly', () => {
- const state = {};
+ const state = {
+ diffFiles: [
+ {
+ content_sha: diffFileMockData.content_sha,
+ file_hash: diffFileMockData.file_hash,
+ },
+ ],
+ };
const diffMock = {
diff_files: [diffFileMockData],
};
@@ -62,9 +69,41 @@ describe('DiffsStoreMutations', () => {
const firstLine = state.diffFiles[0].parallel_diff_lines[0];
expect(firstLine.right.text).toBeUndefined();
+ expect(state.diffFiles.length).toEqual(1);
expect(state.diffFiles[0].renderIt).toEqual(true);
expect(state.diffFiles[0].collapsed).toEqual(false);
});
+
+ describe('given diffsBatchLoad feature flag is enabled', () => {
+ beforeEach(() => {
+ gon.features = { diffsBatchLoad: true };
+ });
+
+ afterEach(() => {
+ delete gon.features;
+ });
+
+ it('should not modify the existing state', () => {
+ const state = {
+ diffFiles: [
+ {
+ content_sha: diffFileMockData.content_sha,
+ file_hash: diffFileMockData.file_hash,
+ highlighted_diff_lines: [],
+ },
+ ],
+ };
+ const diffMock = {
+ diff_files: [diffFileMockData],
+ };
+
+ mutations[types.SET_DIFF_DATA](state, diffMock);
+
+ // If the batch load is enabled, there shouldn't be any processing
+ // done on the existing state object, so we shouldn't have this.
+ expect(state.diffFiles[0].parallel_diff_lines).toBeUndefined();
+ });
+ });
});
describe('SET_DIFFSET_DIFF_DATA_BATCH_DATA', () => {
@@ -168,11 +207,17 @@ describe('DiffsStoreMutations', () => {
it('should update the state with the given data for the given file hash', () => {
const fileHash = 123;
const state = {
- diffFiles: [{}, { file_hash: fileHash, existing_field: 0 }],
+ diffFiles: [{}, { content_sha: 'abc', file_hash: fileHash, existing_field: 0 }],
};
const data = {
diff_files: [
- { file_hash: fileHash, extra_field: 1, existing_field: 1, viewer: { name: 'text' } },
+ {
+ content_sha: 'abc',
+ file_hash: fileHash,
+ extra_field: 1,
+ existing_field: 1,
+ viewer: { name: 'text' },
+ },
],
};
@@ -208,7 +253,7 @@ describe('DiffsStoreMutations', () => {
discussions: [],
},
right: {
- line_code: 'ABC_1',
+ line_code: 'ABC_2',
discussions: [],
},
},
@@ -274,7 +319,7 @@ describe('DiffsStoreMutations', () => {
discussions: [],
},
right: {
- line_code: 'ABC_1',
+ line_code: 'ABC_2',
discussions: [],
},
},
@@ -352,7 +397,7 @@ describe('DiffsStoreMutations', () => {
discussions: [],
},
right: {
- line_code: 'ABC_1',
+ line_code: 'ABC_2',
discussions: [],
},
},
@@ -448,6 +493,7 @@ describe('DiffsStoreMutations', () => {
discussions: [],
},
],
+ parallel_diff_lines: [],
},
],
};
diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js
index 65eb4c9d2a3..638b4510221 100644
--- a/spec/javascripts/diffs/store/utils_spec.js
+++ b/spec/javascripts/diffs/store/utils_spec.js
@@ -314,11 +314,29 @@ describe('DiffsStoreUtils', () => {
});
describe('prepareDiffData', () => {
+ let mock;
let preparedDiff;
+ let splitInlineDiff;
+ let splitParallelDiff;
+ let completedDiff;
beforeEach(() => {
- preparedDiff = { diff_files: [getDiffFileMock()] };
+ mock = getDiffFileMock();
+ preparedDiff = { diff_files: [mock] };
+ splitInlineDiff = {
+ diff_files: [Object.assign({}, mock, { parallel_diff_lines: undefined })],
+ };
+ splitParallelDiff = {
+ diff_files: [Object.assign({}, mock, { highlighted_diff_lines: undefined })],
+ };
+ completedDiff = {
+ diff_files: [Object.assign({}, mock, { highlighted_diff_lines: undefined })],
+ };
+
utils.prepareDiffData(preparedDiff);
+ utils.prepareDiffData(splitInlineDiff);
+ utils.prepareDiffData(splitParallelDiff);
+ utils.prepareDiffData(completedDiff, [mock]);
});
it('sets the renderIt and collapsed attribute on files', () => {
@@ -359,6 +377,19 @@ describe('DiffsStoreUtils', () => {
expect(firstLine.line_code).toEqual(firstLine.right.line_code);
});
+
+ it('guarantees an empty array for both diff styles', () => {
+ expect(splitInlineDiff.diff_files[0].parallel_diff_lines.length).toEqual(0);
+ expect(splitInlineDiff.diff_files[0].highlighted_diff_lines.length).toBeGreaterThan(0);
+ expect(splitParallelDiff.diff_files[0].parallel_diff_lines.length).toBeGreaterThan(0);
+ expect(splitParallelDiff.diff_files[0].highlighted_diff_lines.length).toEqual(0);
+ });
+
+ it('merges existing diff files with newly loaded diff files to ensure split diffs are eventually completed', () => {
+ expect(completedDiff.diff_files.length).toEqual(1);
+ expect(completedDiff.diff_files[0].parallel_diff_lines.length).toBeGreaterThan(0);
+ expect(completedDiff.diff_files[0].highlighted_diff_lines.length).toBeGreaterThan(0);
+ });
});
describe('isDiscussionApplicableToLine', () => {
diff --git a/spec/lib/gitlab/background_migration/activate_prometheus_services_for_shared_cluster_applications_spec.rb b/spec/lib/gitlab/background_migration/activate_prometheus_services_for_shared_cluster_applications_spec.rb
deleted file mode 100644
index 9d9281f4e98..00000000000
--- a/spec/lib/gitlab/background_migration/activate_prometheus_services_for_shared_cluster_applications_spec.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::BackgroundMigration::ActivatePrometheusServicesForSharedClusterApplications, :migration, schema: 2019_12_20_102807 do
- include MigrationHelpers::PrometheusServiceHelpers
-
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:services) { table(:services) }
- let(:namespace) { namespaces.create(name: 'user', path: 'user') }
- let(:project) { projects.create(namespace_id: namespace.id) }
-
- let(:columns) do
- %w(project_id active properties type template push_events
- issues_events merge_requests_events tag_push_events
- note_events category default wiki_page_events pipeline_events
- confidential_issues_events commit_events job_events
- confidential_note_events deployment_events)
- end
-
- describe '#perform' do
- it 'is idempotent' do
- expect { subject.perform(project.id) }.to change { services.order(:id).map { |row| row.attributes } }
-
- expect { subject.perform(project.id) }.not_to change { services.order(:id).map { |row| row.attributes } }
- end
-
- context 'non prometheus services' do
- it 'does not change them' do
- other_type = 'SomeOtherService'
- services.create(service_params_for(project.id, active: true, type: other_type))
-
- expect { subject.perform(project.id) }.not_to change { services.where(type: other_type).order(:id).map { |row| row.attributes } }
- end
- end
-
- context 'prometheus services are configured manually ' do
- it 'does not change them' do
- properties = '{"api_url":"http://test.dev","manual_configuration":"1"}'
- services.create(service_params_for(project.id, properties: properties, active: false))
-
- expect { subject.perform(project.id) }.not_to change { services.order(:id).map { |row| row.attributes } }
- end
- end
-
- context 'prometheus integration services do not exist' do
- it 'creates missing services entries' do
- subject.perform(project.id)
-
- rows = services.order(:id).map { |row| row.attributes.slice(*columns).symbolize_keys }
-
- expect([service_params_for(project.id, active: true)]).to eq rows
- end
- end
-
- context 'prometheus integration services exist' do
- context 'in active state' do
- it 'does not change them' do
- services.create(service_params_for(project.id, active: true))
-
- expect { subject.perform(project.id) }.not_to change { services.order(:id).map { |row| row.attributes } }
- end
- end
-
- context 'not in active state' do
- it 'sets active attribute to true' do
- service = services.create(service_params_for(project.id))
-
- expect { subject.perform(project.id) }.to change { service.reload.active? }.from(false).to(true)
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
index ac370433955..24d3beb35b9 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
@@ -76,45 +76,8 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do
end
end
- context 'when pipeline triggered by legacy trigger' do
- let(:user) { nil }
- let(:trigger_request) do
- build_stubbed(:ci_trigger_request, trigger: build_stubbed(:ci_trigger, owner: nil))
- end
-
- context 'when :use_legacy_pipeline_triggers feature flag is enabled' do
- before do
- stub_feature_flags(use_legacy_pipeline_triggers: true)
- step.perform!
- end
-
- it 'allows legacy triggers to create a pipeline' do
- expect(pipeline).to be_valid
- end
-
- it 'does not break the chain' do
- expect(step.break?).to eq false
- end
- end
-
- context 'when :use_legacy_pipeline_triggers feature flag is disabled' do
- before do
- stub_feature_flags(use_legacy_pipeline_triggers: false)
- step.perform!
- end
-
- it 'prevents legacy triggers from creating a pipeline' do
- expect(pipeline.errors.to_a).to include /Trigger token is invalid/
- end
-
- it 'breaks the pipeline builder chain' do
- expect(step.break?).to eq true
- end
- end
- end
-
- describe '#allowed_to_create?' do
- subject { step.allowed_to_create? }
+ describe '#allowed_to_write_ref?' do
+ subject { step.send(:allowed_to_write_ref?) }
context 'when user is a developer' do
before do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 07439880beb..ee4db935b58 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -578,3 +578,30 @@ zoom_meetings:
sentry_issue:
- issue
design_versions: *version
+epic:
+- subscriptions
+- award_emoji
+- description_versions
+- author
+- assignee
+- issues
+- epic_issues
+- milestone
+- notes
+- label_links
+- labels
+- todos
+- metrics
+- group
+- parent
+- children
+- updated_by
+- last_edited_by
+- closed_by
+- start_date_sourcing_milestone
+- due_date_sourcing_milestone
+- start_date_sourcing_epic
+- due_date_sourcing_epic
+- events
+- resource_label_events
+- user_mentions \ No newline at end of file
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index 4d12d05211b..56426bb8110 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -36,10 +36,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
context 'JSON' do
- before do
- stub_feature_flags(use_legacy_pipeline_triggers: false)
- end
-
it 'restores models based on JSON' do
expect(@restored_project_json).to be_truthy
end
@@ -502,7 +498,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
it_behaves_like 'restores project successfully',
- issues: 2,
+ issues: 3,
labels: 2,
label_with_priorities: 'A project label',
milestones: 2,
@@ -515,7 +511,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
it 'restores issue states' do
expect(project.issues.with_state(:closed).count).to eq(1)
- expect(project.issues.with_state(:opened).count).to eq(1)
+ expect(project.issues.with_state(:opened).count).to eq(2)
end
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index e5014eeca09..c921e7cadde 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -766,3 +766,33 @@ ContainerExpirationPolicy:
- older_than
- keep_n
- enabled
+Epic:
+ - id
+ - milestone_id
+ - group_id
+ - author_id
+ - assignee_id
+ - iid
+ - updated_by_id
+ - last_edited_by_id
+ - lock_version
+ - start_date
+ - end_date
+ - last_edited_at
+ - created_at
+ - updated_at
+ - title
+ - description
+ - start_date_sourcing_milestone_id
+ - due_date_sourcing_milestone_id
+ - start_date_fixed
+ - due_date_fixed
+ - start_date_is_fixed
+ - due_date_is_fixed
+ - closed_by_id
+ - closed_at
+ - parent_id
+ - relative_position
+ - state_id
+ - start_date_sourcing_epic_id
+ - due_date_sourcing_epic_id
diff --git a/spec/migrations/20191204114127_delete_legacy_triggers_spec.rb b/spec/migrations/20191204114127_delete_legacy_triggers_spec.rb
new file mode 100644
index 00000000000..c2660d699ca
--- /dev/null
+++ b/spec/migrations/20191204114127_delete_legacy_triggers_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20191204114127_delete_legacy_triggers.rb')
+
+describe DeleteLegacyTriggers, :migration, schema: 2019_11_25_140458 do
+ let(:ci_trigger_table) { table(:ci_triggers) }
+ let(:user) { table(:users).create!(name: 'test', email: 'test@example.com', projects_limit: 1) }
+
+ before do
+ @trigger_with_user = ci_trigger_table.create!(owner_id: user.id)
+ ci_trigger_table.create!(owner_id: nil)
+ ci_trigger_table.create!(owner_id: nil)
+ end
+
+ it 'removes legacy triggers which has null owner_id' do
+ expect do
+ migrate!
+ end.to change(ci_trigger_table, :count).by(-2)
+
+ expect(ci_trigger_table.all).to eq([@trigger_with_user])
+ end
+end
diff --git a/spec/migrations/patch_prometheus_services_for_shared_cluster_applications_spec.rb b/spec/migrations/patch_prometheus_services_for_shared_cluster_applications_spec.rb
deleted file mode 100644
index 74f6d56a2eb..00000000000
--- a/spec/migrations/patch_prometheus_services_for_shared_cluster_applications_spec.rb
+++ /dev/null
@@ -1,134 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20191220102807_patch_prometheus_services_for_shared_cluster_applications.rb')
-
-describe PatchPrometheusServicesForSharedClusterApplications, :migration, :sidekiq do
- include MigrationHelpers::PrometheusServiceHelpers
-
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:services) { table(:services) }
- let(:clusters) { table(:clusters) }
- let(:cluster_groups) { table(:cluster_groups) }
- let(:clusters_applications_prometheus) { table(:clusters_applications_prometheus) }
- let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
-
- let(:application_statuses) do
- {
- errored: -1,
- installed: 3,
- updated: 5
- }
- end
-
- let(:cluster_types) do
- {
- instance_type: 1,
- group_type: 2
- }
- end
-
- describe '#up' do
- let!(:project_with_missing_service) { projects.create!(name: 'gitlab', path: 'gitlab-ce', namespace_id: namespace.id) }
- let(:project_with_inactive_service) { projects.create!(name: 'gitlab', path: 'gitlab-ee', namespace_id: namespace.id) }
- let(:project_with_active_service) { projects.create!(name: 'gitlab', path: 'gitlab-ee', namespace_id: namespace.id) }
- let(:project_with_manual_active_service) { projects.create!(name: 'gitlab', path: 'gitlab-ee', namespace_id: namespace.id) }
- let(:project_with_manual_inactive_service) { projects.create!(name: 'gitlab', path: 'gitlab-ee', namespace_id: namespace.id) }
- let(:project_with_active_not_prometheus_service) { projects.create!(name: 'gitlab', path: 'gitlab-ee', namespace_id: namespace.id) }
- let(:project_with_inactive_not_prometheus_service) { projects.create!(name: 'gitlab', path: 'gitlab-ee', namespace_id: namespace.id) }
-
- before do
- services.create(service_params_for(project_with_inactive_service.id, active: false))
- services.create(service_params_for(project_with_active_service.id, active: true))
- services.create(service_params_for(project_with_active_not_prometheus_service.id, active: true, type: 'other'))
- services.create(service_params_for(project_with_inactive_not_prometheus_service.id, active: false, type: 'other'))
- services.create(service_params_for(project_with_manual_inactive_service.id, active: false, properties: { some: 'data' }.to_json))
- services.create(service_params_for(project_with_manual_active_service.id, active: true, properties: { some: 'data' }.to_json))
- end
-
- shared_examples 'patch prometheus services post migration' do
- context 'prometheus application is installed on the cluster' do
- it 'schedules a background migration' do
- clusters_applications_prometheus.create(cluster_id: cluster.id, status: application_statuses[:installed], version: '123')
-
- Sidekiq::Testing.fake! do
- Timecop.freeze do
- background_migrations = [["ActivatePrometheusServicesForSharedClusterApplications", project_with_missing_service.id],
- ["ActivatePrometheusServicesForSharedClusterApplications", project_with_inactive_service.id],
- ["ActivatePrometheusServicesForSharedClusterApplications", project_with_active_not_prometheus_service.id],
- ["ActivatePrometheusServicesForSharedClusterApplications", project_with_inactive_not_prometheus_service.id]]
-
- migrate!
-
- enqueued_migrations = BackgroundMigrationWorker.jobs.map { |job| job['args'] }
- expect(enqueued_migrations).to match_array(background_migrations)
- end
- end
- end
- end
-
- context 'prometheus application was recently updated on the cluster' do
- it 'schedules a background migration' do
- clusters_applications_prometheus.create(cluster_id: cluster.id, status: application_statuses[:updated], version: '123')
-
- Sidekiq::Testing.fake! do
- Timecop.freeze do
- background_migrations = [["ActivatePrometheusServicesForSharedClusterApplications", project_with_missing_service.id],
- ["ActivatePrometheusServicesForSharedClusterApplications", project_with_inactive_service.id],
- ["ActivatePrometheusServicesForSharedClusterApplications", project_with_active_not_prometheus_service.id],
- ["ActivatePrometheusServicesForSharedClusterApplications", project_with_inactive_not_prometheus_service.id]]
-
- migrate!
-
- enqueued_migrations = BackgroundMigrationWorker.jobs.map { |job| job['args'] }
- expect(enqueued_migrations).to match_array(background_migrations)
- end
- end
- end
- end
-
- context 'prometheus application failed to install on the cluster' do
- it 'does not schedule a background migration' do
- clusters_applications_prometheus.create(cluster_id: cluster.id, status: application_statuses[:errored], version: '123')
-
- Sidekiq::Testing.fake! do
- Timecop.freeze do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq 0
- end
- end
- end
- end
-
- context 'prometheus application is NOT installed on the cluster' do
- it 'does not schedule a background migration' do
- Sidekiq::Testing.fake! do
- Timecop.freeze do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq 0
- end
- end
- end
- end
- end
-
- context 'Cluster is group_type' do
- let(:cluster) { clusters.create(name: 'cluster', cluster_type: cluster_types[:group_type]) }
-
- before do
- cluster_groups.create(group_id: namespace.id, cluster_id: cluster.id)
- end
-
- it_behaves_like 'patch prometheus services post migration'
- end
-
- context 'Cluster is instance_type' do
- let(:cluster) { clusters.create(name: 'cluster', cluster_type: cluster_types[:instance_type]) }
-
- it_behaves_like 'patch prometheus services post migration'
- end
- end
-end
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index 5b5d6f51b33..5b0815f8156 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -11,6 +11,10 @@ describe Ci::Trigger do
it { is_expected.to have_many(:trigger_requests) }
end
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:owner) }
+ end
+
describe 'before_validation' do
it 'sets an random token if none provided' do
trigger = create(:ci_trigger_without_token, project: project)
@@ -35,63 +39,22 @@ describe Ci::Trigger do
end
end
- describe '#legacy?' do
- let(:trigger) { create(:ci_trigger, owner: owner, project: project) }
-
- subject { trigger }
-
- context 'when owner is blank' do
- let(:owner) { nil }
-
- it { is_expected.to be_legacy }
- end
-
- context 'when owner is set' do
- let(:owner) { create(:user) }
-
- it { is_expected.not_to be_legacy }
- end
- end
-
describe '#can_access_project?' do
let(:owner) { create(:user) }
let(:trigger) { create(:ci_trigger, owner: owner, project: project) }
- context 'when owner is blank' do
+ subject { trigger.can_access_project? }
+
+ context 'and is member of the project' do
before do
- stub_feature_flags(use_legacy_pipeline_triggers: false)
- trigger.update_attribute(:owner, nil)
+ project.add_developer(owner)
end
- subject { trigger.can_access_project? }
-
- it { is_expected.to eq(false) }
-
- context 'when :use_legacy_pipeline_triggers feature flag is enabled' do
- before do
- stub_feature_flags(use_legacy_pipeline_triggers: true)
- end
-
- subject { trigger.can_access_project? }
-
- it { is_expected.to eq(true) }
- end
+ it { is_expected.to eq(true) }
end
- context 'when owner is set' do
- subject { trigger.can_access_project? }
-
- context 'and is member of the project' do
- before do
- project.add_developer(owner)
- end
-
- it { is_expected.to eq(true) }
- end
-
- context 'and is not member of the project' do
- it { is_expected.to eq(false) }
- end
+ context 'and is not member of the project' do
+ it { is_expected.to eq(false) }
end
end
end
diff --git a/spec/policies/ci/trigger_policy_spec.rb b/spec/policies/ci/trigger_policy_spec.rb
index e936277a391..28e5a2b2cd6 100644
--- a/spec/policies/ci/trigger_policy_spec.rb
+++ b/spec/policies/ci/trigger_policy_spec.rb
@@ -10,60 +10,6 @@ describe Ci::TriggerPolicy do
subject { described_class.new(user, trigger) }
describe '#rules' do
- context 'when owner is undefined' do
- before do
- stub_feature_flags(use_legacy_pipeline_triggers: false)
- trigger.update_attribute(:owner, nil)
- end
-
- context 'when user is maintainer of the project' do
- before do
- project.add_maintainer(user)
- end
-
- it { is_expected.to be_allowed(:manage_trigger) }
- it { is_expected.not_to be_allowed(:admin_trigger) }
- end
-
- context 'when user is developer of the project' do
- before do
- project.add_developer(user)
- end
-
- it { is_expected.not_to be_allowed(:manage_trigger) }
- it { is_expected.not_to be_allowed(:admin_trigger) }
- end
-
- context 'when :use_legacy_pipeline_triggers feature flag is enabled' do
- before do
- stub_feature_flags(use_legacy_pipeline_triggers: true)
- end
-
- context 'when user is maintainer of the project' do
- before do
- project.add_maintainer(user)
- end
-
- it { is_expected.to be_allowed(:manage_trigger) }
- it { is_expected.to be_allowed(:admin_trigger) }
- end
-
- context 'when user is developer of the project' do
- before do
- project.add_developer(user)
- end
-
- it { is_expected.not_to be_allowed(:manage_trigger) }
- it { is_expected.not_to be_allowed(:admin_trigger) }
- end
-
- context 'when user is not member of the project' do
- it { is_expected.not_to be_allowed(:manage_trigger) }
- it { is_expected.not_to be_allowed(:admin_trigger) }
- end
- end
- end
-
context 'when owner is an user' do
before do
trigger.update!(owner: user)
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index fd1104fa978..d54d112cd9f 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -87,22 +87,6 @@ describe API::Triggers do
expect(pipeline.variables.map { |v| { v.key => v.value } }.last).to eq(variables)
end
end
-
- context 'when legacy trigger' do
- before do
- trigger.update(owner: nil)
- end
-
- it 'creates pipeline' do
- post api("/projects/#{project.id}/trigger/pipeline"), params: options.merge(ref: 'master')
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response).to include('id' => pipeline.id)
- pipeline.builds.reload
- expect(pipeline.builds.pending.size).to eq(2)
- expect(pipeline.builds.size).to eq(5)
- end
- end
end
context 'when triggering a pipeline from a trigger token' do
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index a82bdfe3ce8..93b2c19c74a 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -161,3 +161,17 @@ describe Admin::GroupsController, "routing" do
expect(get("/admin/groups/#{name}/edit")).to route_to('admin/groups#edit', id: name)
end
end
+
+describe Admin::SessionsController, "routing" do
+ it "to #new" do
+ expect(get("/admin/session/new")).to route_to('admin/sessions#new')
+ end
+
+ it "to #create" do
+ expect(post("/admin/session")).to route_to('admin/sessions#create')
+ end
+
+ it "to #destroy" do
+ expect(post("/admin/session/destroy")).to route_to('admin/sessions#destroy')
+ end
+end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 6f67cdb1222..ff002469e3c 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -256,10 +256,8 @@ describe "Authentication", "routing" do
expect(post("/users/sign_in")).to route_to('sessions#create')
end
- # sign_out with GET instead of DELETE facilitates ad-hoc single-sign-out processes
- # (https://gitlab.com/gitlab-org/gitlab-foss/issues/39708)
- it "GET /users/sign_out" do
- expect(get("/users/sign_out")).to route_to('sessions#destroy')
+ it "POST /users/sign_out" do
+ expect(post("/users/sign_out")).to route_to('sessions#destroy')
end
it "POST /users/password" do
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 2876f7b5f59..dc67efe0fbe 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -1123,21 +1123,6 @@ describe Ci::CreatePipelineService do
it_behaves_like 'when ref is protected'
end
- context 'when ref is not protected' do
- context 'when trigger belongs to no one' do
- let(:user) {}
- let(:trigger) { create(:ci_trigger, owner: nil) }
- let(:trigger_request) { create(:ci_trigger_request, trigger: trigger) }
- let(:pipeline) { execute_service(trigger_request: trigger_request) }
-
- it 'creates an unprotected pipeline' do
- expect(pipeline).to be_persisted
- expect(pipeline).not_to be_protected
- expect(Ci::Pipeline.count).to eq(1)
- end
- end
- end
-
context 'when pipeline is running for a tag' do
before do
config = YAML.dump(test: { script: 'test', only: ['branches'] },
diff --git a/spec/support/migrations_helpers/prometheus_service_helpers.rb b/spec/support/migrations_helpers/prometheus_service_helpers.rb
deleted file mode 100644
index 88f2f71ee1e..00000000000
--- a/spec/support/migrations_helpers/prometheus_service_helpers.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module MigrationHelpers
- module PrometheusServiceHelpers
- def service_params_for(project_id, params = {})
- {
- project_id: project_id,
- active: false,
- properties: '{}',
- type: 'PrometheusService',
- template: false,
- push_events: true,
- issues_events: true,
- merge_requests_events: true,
- tag_push_events: true,
- note_events: true,
- category: 'monitoring',
- default: false,
- wiki_page_events: true,
- pipeline_events: true,
- confidential_issues_events: true,
- commit_events: true,
- job_events: true,
- confidential_note_events: true,
- deployment_events: false
- }.merge(params)
- end
-
- def row_attributes(entity)
- entity.attributes.with_indifferent_access.tap do |hash|
- hash.merge!(hash.slice(:created_at, :updated_at).transform_values { |v| v.to_s(:db) })
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/merge_requests_rendering_a_single_diff_version.rb b/spec/support/shared_examples/merge_requests_rendering_a_single_diff_version.rb
deleted file mode 100644
index 18d025a4b07..00000000000
--- a/spec/support/shared_examples/merge_requests_rendering_a_single_diff_version.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-# This pending test can be removed when `single_mr_diff_view` is enabled by default
-# disabling the feature flag above is then not needed anymore.
-RSpec.shared_examples 'rendering a single diff version' do |attribute|
- before do
- stub_feature_flags(diffs_batch_load: false)
- end
-
- pending 'allows editing diff settings single_mr_diff_view is enabled' do
- project = create(:project, :repository)
- user = project.creator
- merge_request = create(:merge_request, source_project: project)
- stub_feature_flags(single_mr_diff_view: true)
- sign_in(user)
-
- visit(diffs_project_merge_request_path(project, merge_request))
-
- expect(page).to have_selector('.js-show-diff-settings')
- end
-end
diff --git a/yarn.lock b/yarn.lock
index 5289e563189..aa18e5d0096 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5640,6 +5640,11 @@ immediate@~3.0.5:
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
+immer@^5.2.1:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/immer/-/immer-5.2.1.tgz#7d4f74c242178e87151d595f48db1b5c51580485"
+ integrity sha512-9U1GEbJuH6nVoyuFRgTQDGMzcBuNBPfXM3M7Pp/sdmYKTKYOBUZGgeUb9H57GfLK/xC1DMLarWX2FrhMBfUJ8g==
+
import-fresh@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"