Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
141902c049
commit
254ec28f54
47 changed files with 915 additions and 548 deletions
|
@ -112,7 +112,6 @@ export default {
|
||||||
mergeRequestDiffs: state => state.diffs.mergeRequestDiffs,
|
mergeRequestDiffs: state => state.diffs.mergeRequestDiffs,
|
||||||
mergeRequestDiff: state => state.diffs.mergeRequestDiff,
|
mergeRequestDiff: state => state.diffs.mergeRequestDiff,
|
||||||
commit: state => state.diffs.commit,
|
commit: state => state.diffs.commit,
|
||||||
targetBranchName: state => state.diffs.targetBranchName,
|
|
||||||
renderOverflowWarning: state => state.diffs.renderOverflowWarning,
|
renderOverflowWarning: state => state.diffs.renderOverflowWarning,
|
||||||
numTotalFiles: state => state.diffs.realSize,
|
numTotalFiles: state => state.diffs.realSize,
|
||||||
numVisibleFiles: state => state.diffs.size,
|
numVisibleFiles: state => state.diffs.size,
|
||||||
|
@ -123,19 +122,9 @@ export default {
|
||||||
...mapState('diffs', ['showTreeList', 'isLoading', 'startVersion']),
|
...mapState('diffs', ['showTreeList', 'isLoading', 'startVersion']),
|
||||||
...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']),
|
...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']),
|
||||||
...mapGetters(['isNotesFetched', 'getNoteableData']),
|
...mapGetters(['isNotesFetched', 'getNoteableData']),
|
||||||
targetBranch() {
|
|
||||||
return {
|
|
||||||
branchName: this.targetBranchName,
|
|
||||||
versionIndex: -1,
|
|
||||||
path: '',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
canCurrentUserFork() {
|
canCurrentUserFork() {
|
||||||
return this.currentUser.can_fork === true && this.currentUser.can_create_merge_request;
|
return this.currentUser.can_fork === true && this.currentUser.can_create_merge_request;
|
||||||
},
|
},
|
||||||
showCompareVersions() {
|
|
||||||
return this.mergeRequestDiffs && this.mergeRequestDiff;
|
|
||||||
},
|
|
||||||
renderDiffFiles() {
|
renderDiffFiles() {
|
||||||
return (
|
return (
|
||||||
this.diffFiles.length > 0 ||
|
this.diffFiles.length > 0 ||
|
||||||
|
@ -369,8 +358,6 @@ export default {
|
||||||
<div v-else id="diffs" :class="{ active: shouldShow }" class="diffs tab-pane">
|
<div v-else id="diffs" :class="{ active: shouldShow }" class="diffs tab-pane">
|
||||||
<compare-versions
|
<compare-versions
|
||||||
:merge-request-diffs="mergeRequestDiffs"
|
:merge-request-diffs="mergeRequestDiffs"
|
||||||
:merge-request-diff="mergeRequestDiff"
|
|
||||||
:target-branch="targetBranch"
|
|
||||||
:is-limited-container="isLimitedContainer"
|
:is-limited-container="isLimitedContainer"
|
||||||
:diff-files-length="diffFilesLength"
|
:diff-files-length="diffFilesLength"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
<script>
|
||||||
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
|
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Icon,
|
||||||
|
TimeAgo,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
versions: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selectedVersionName() {
|
||||||
|
return this.versions.find(x => x.selected)?.versionName || '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span class="dropdown inline">
|
||||||
|
<a
|
||||||
|
class="dropdown-menu-toggle btn btn-default w-100"
|
||||||
|
data-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span> {{ selectedVersionName }} </span>
|
||||||
|
<icon :size="12" name="angle-down" class="position-absolute" />
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-select dropdown-menu-selectable">
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<ul>
|
||||||
|
<li v-for="version in versions" :key="version.id">
|
||||||
|
<a :class="{ 'is-active': version.selected }" :href="version.href">
|
||||||
|
<div>
|
||||||
|
<strong>
|
||||||
|
{{ version.versionName }}
|
||||||
|
<template v-if="version.isHead">{{
|
||||||
|
s__('DiffsCompareBaseBranch|(HEAD)')
|
||||||
|
}}</template>
|
||||||
|
<template v-else-if="version.isBase">{{
|
||||||
|
s__('DiffsCompareBaseBranch|(base)')
|
||||||
|
}}</template>
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<small class="commit-sha"> {{ version.short_commit_sha }} </small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<small>
|
||||||
|
<template v-if="version.commitsText">
|
||||||
|
{{ version.commitsText }}
|
||||||
|
</template>
|
||||||
|
<time-ago
|
||||||
|
v-if="version.created_at"
|
||||||
|
:time="version.created_at"
|
||||||
|
class="js-timeago"
|
||||||
|
/>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dropdown {
|
||||||
|
min-width: 0;
|
||||||
|
max-height: 170px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -4,14 +4,14 @@ import { GlTooltipDirective, GlLink, GlDeprecatedButton, GlSprintf } from '@gitl
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import { polyfillSticky } from '~/lib/utils/sticky';
|
import { polyfillSticky } from '~/lib/utils/sticky';
|
||||||
import Icon from '~/vue_shared/components/icon.vue';
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
import CompareVersionsDropdown from './compare_versions_dropdown.vue';
|
import CompareDropdownLayout from './compare_dropdown_layout.vue';
|
||||||
import SettingsDropdown from './settings_dropdown.vue';
|
import SettingsDropdown from './settings_dropdown.vue';
|
||||||
import DiffStats from './diff_stats.vue';
|
import DiffStats from './diff_stats.vue';
|
||||||
import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants';
|
import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
CompareVersionsDropdown,
|
CompareDropdownLayout,
|
||||||
Icon,
|
Icon,
|
||||||
GlLink,
|
GlLink,
|
||||||
GlDeprecatedButton,
|
GlDeprecatedButton,
|
||||||
|
@ -27,16 +27,6 @@ export default {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
mergeRequestDiff: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
targetBranch: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
isLimitedContainer: {
|
isLimitedContainer: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
@ -48,7 +38,11 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('diffs', ['hasCollapsedFile']),
|
...mapGetters('diffs', [
|
||||||
|
'hasCollapsedFile',
|
||||||
|
'diffCompareDropdownTargetVersions',
|
||||||
|
'diffCompareDropdownSourceVersions',
|
||||||
|
]),
|
||||||
...mapState('diffs', [
|
...mapState('diffs', [
|
||||||
'commit',
|
'commit',
|
||||||
'showTreeList',
|
'showTreeList',
|
||||||
|
@ -57,18 +51,12 @@ export default {
|
||||||
'addedLines',
|
'addedLines',
|
||||||
'removedLines',
|
'removedLines',
|
||||||
]),
|
]),
|
||||||
comparableDiffs() {
|
|
||||||
return this.mergeRequestDiffs.slice(1);
|
|
||||||
},
|
|
||||||
showDropdowns() {
|
showDropdowns() {
|
||||||
return !this.commit && this.mergeRequestDiffs.length;
|
return !this.commit && this.mergeRequestDiffs.length;
|
||||||
},
|
},
|
||||||
toggleFileBrowserTitle() {
|
toggleFileBrowserTitle() {
|
||||||
return this.showTreeList ? __('Hide file browser') : __('Show file browser');
|
return this.showTreeList ? __('Hide file browser') : __('Show file browser');
|
||||||
},
|
},
|
||||||
baseVersionPath() {
|
|
||||||
return this.mergeRequestDiff.base_version_path;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES;
|
this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES;
|
||||||
|
@ -113,19 +101,14 @@ export default {
|
||||||
:message="s__('MergeRequest|Compare %{source} and %{target}')"
|
:message="s__('MergeRequest|Compare %{source} and %{target}')"
|
||||||
>
|
>
|
||||||
<template #source>
|
<template #source>
|
||||||
<compare-versions-dropdown
|
<compare-dropdown-layout
|
||||||
:other-versions="mergeRequestDiffs"
|
:versions="diffCompareDropdownSourceVersions"
|
||||||
:merge-request-version="mergeRequestDiff"
|
|
||||||
:show-commit-count="true"
|
|
||||||
class="mr-version-dropdown"
|
class="mr-version-dropdown"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #target>
|
<template #target>
|
||||||
<compare-versions-dropdown
|
<compare-dropdown-layout
|
||||||
:other-versions="comparableDiffs"
|
:versions="diffCompareDropdownTargetVersions"
|
||||||
:base-version-path="baseVersionPath"
|
|
||||||
:start-version="startVersion"
|
|
||||||
:target-branch="targetBranch"
|
|
||||||
class="mr-version-compare-dropdown"
|
class="mr-version-compare-dropdown"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,162 +0,0 @@
|
||||||
<script>
|
|
||||||
import Icon from '~/vue_shared/components/icon.vue';
|
|
||||||
import { n__, __, sprintf } from '~/locale';
|
|
||||||
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
|
|
||||||
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Icon,
|
|
||||||
TimeAgo,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
otherVersions: {
|
|
||||||
type: Array,
|
|
||||||
required: false,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
mergeRequestVersion: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
startVersion: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
targetBranch: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
showCommitCount: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
baseVersionPath: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
targetVersions() {
|
|
||||||
if (this.mergeRequestVersion) {
|
|
||||||
return this.otherVersions;
|
|
||||||
}
|
|
||||||
return [...this.otherVersions, this.targetBranch];
|
|
||||||
},
|
|
||||||
selectedVersionName() {
|
|
||||||
const selectedVersion = this.startVersion || this.targetBranch || this.mergeRequestVersion;
|
|
||||||
return this.versionName(selectedVersion);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
commitsText(version) {
|
|
||||||
return n__(`%d commit,`, `%d commits,`, version.commits_count);
|
|
||||||
},
|
|
||||||
href(version) {
|
|
||||||
if (this.isBase(version)) {
|
|
||||||
return this.baseVersionPath;
|
|
||||||
}
|
|
||||||
if (this.showCommitCount) {
|
|
||||||
return version.version_path;
|
|
||||||
}
|
|
||||||
return version.compare_path;
|
|
||||||
},
|
|
||||||
versionName(version) {
|
|
||||||
if (this.isLatest(version)) {
|
|
||||||
return __('latest version');
|
|
||||||
}
|
|
||||||
if (this.targetBranch && (this.isBase(version) || !version)) {
|
|
||||||
return this.targetBranch.branchName;
|
|
||||||
}
|
|
||||||
return sprintf(__(`version %{versionIndex}`), { versionIndex: version.version_index });
|
|
||||||
},
|
|
||||||
isActive(version) {
|
|
||||||
if (!version) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.targetBranch) {
|
|
||||||
return (
|
|
||||||
(this.isBase(version) && !this.startVersion) ||
|
|
||||||
(this.startVersion && this.startVersion.version_index === version.version_index)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return version.version_index === this.mergeRequestVersion.version_index;
|
|
||||||
},
|
|
||||||
isBase(version) {
|
|
||||||
if (!version || !this.targetBranch) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return version.versionIndex === -1;
|
|
||||||
},
|
|
||||||
isHead() {
|
|
||||||
return parseBoolean(getParameterByName('diff_head'));
|
|
||||||
},
|
|
||||||
isLatest(version) {
|
|
||||||
return (
|
|
||||||
this.mergeRequestVersion && version.version_index === this.targetVersions[0].version_index
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<span class="dropdown inline">
|
|
||||||
<a
|
|
||||||
class="dropdown-menu-toggle btn btn-default w-100"
|
|
||||||
data-toggle="dropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
<span> {{ selectedVersionName }} </span>
|
|
||||||
<icon :size="12" name="angle-down" class="position-absolute" />
|
|
||||||
</a>
|
|
||||||
<div class="dropdown-menu dropdown-select dropdown-menu-selectable">
|
|
||||||
<div class="dropdown-content">
|
|
||||||
<ul>
|
|
||||||
<li v-for="version in targetVersions" :key="version.id">
|
|
||||||
<a :class="{ 'is-active': isActive(version) }" :href="href(version)">
|
|
||||||
<div>
|
|
||||||
<strong>
|
|
||||||
{{ versionName(version) }}
|
|
||||||
<template v-if="isHead()">{{ s__('DiffsCompareBaseBranch|(HEAD)') }}</template>
|
|
||||||
<template v-else-if="isBase(version)">{{
|
|
||||||
s__('DiffsCompareBaseBranch|(base)')
|
|
||||||
}}</template>
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<small class="commit-sha"> {{ version.short_commit_sha }} </small>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<small>
|
|
||||||
<template v-if="showCommitCount">
|
|
||||||
{{ commitsText(version) }}
|
|
||||||
</template>
|
|
||||||
<time-ago
|
|
||||||
v-if="version.created_at"
|
|
||||||
:time="version.created_at"
|
|
||||||
class="js-timeago"
|
|
||||||
/>
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.dropdown {
|
|
||||||
min-width: 0;
|
|
||||||
max-height: 170px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -58,3 +58,5 @@ export const START_RENDERING_INDEX = 200;
|
||||||
export const INLINE_DIFF_LINES_KEY = 'highlighted_diff_lines';
|
export const INLINE_DIFF_LINES_KEY = 'highlighted_diff_lines';
|
||||||
export const PARALLEL_DIFF_LINES_KEY = 'parallel_diff_lines';
|
export const PARALLEL_DIFF_LINES_KEY = 'parallel_diff_lines';
|
||||||
export const DIFFS_PER_PAGE = 20;
|
export const DIFFS_PER_PAGE = 20;
|
||||||
|
|
||||||
|
export const DIFF_COMPARE_BASE_VERSION_INDEX = -1;
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { __, n__ } from '~/locale';
|
import { __, n__ } from '~/locale';
|
||||||
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
|
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
|
||||||
|
|
||||||
|
export * from './getters_versions_dropdowns';
|
||||||
|
|
||||||
export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE;
|
export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE;
|
||||||
|
|
||||||
export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE;
|
export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE;
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { __, n__, sprintf } from '~/locale';
|
||||||
|
import { DIFF_COMPARE_BASE_VERSION_INDEX } from '../constants';
|
||||||
|
|
||||||
|
export const selectedTargetIndex = state =>
|
||||||
|
state.startVersion?.version_index || DIFF_COMPARE_BASE_VERSION_INDEX;
|
||||||
|
|
||||||
|
export const selectedSourceIndex = state => state.mergeRequestDiff.version_index;
|
||||||
|
|
||||||
|
export const diffCompareDropdownTargetVersions = (state, getters) => {
|
||||||
|
// startVersion only exists if the user has selected a version other
|
||||||
|
// than "base" so if startVersion is null then base must be selected
|
||||||
|
const baseVersion = {
|
||||||
|
versionName: state.targetBranchName,
|
||||||
|
version_index: DIFF_COMPARE_BASE_VERSION_INDEX,
|
||||||
|
href: state.mergeRequestDiff.base_version_path,
|
||||||
|
isBase: true,
|
||||||
|
selected: !state.startVersion,
|
||||||
|
};
|
||||||
|
// Appended properties here are to make the compare_dropdown_layout easier to reason about
|
||||||
|
const formatVersion = v => {
|
||||||
|
return {
|
||||||
|
href: v.compare_path,
|
||||||
|
versionName: sprintf(__(`version %{versionIndex}`), { versionIndex: v.version_index }),
|
||||||
|
selected: v.version_index === getters.selectedTargetIndex,
|
||||||
|
...v,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return [...state.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const diffCompareDropdownSourceVersions = (state, getters) => {
|
||||||
|
// Appended properties here are to make the compare_dropdown_layout easier to reason about
|
||||||
|
return state.mergeRequestDiffs.map((v, i) => ({
|
||||||
|
...v,
|
||||||
|
href: v.version_path,
|
||||||
|
commitsText: n__(`%d commit,`, `%d commits,`, v.commits_count),
|
||||||
|
versionName:
|
||||||
|
i === 0
|
||||||
|
? __('latest version')
|
||||||
|
: sprintf(__(`version %{versionIndex}`), { versionIndex: v.version_index }),
|
||||||
|
selected: v.version_index === getters.selectedSourceIndex,
|
||||||
|
}));
|
||||||
|
};
|
|
@ -15,7 +15,7 @@ export default () => ({
|
||||||
endpoint: '',
|
endpoint: '',
|
||||||
basePath: '',
|
basePath: '',
|
||||||
commit: null,
|
commit: null,
|
||||||
startVersion: null,
|
startVersion: null, // Null unless a target diff is selected for comparison that is not the "base" diff
|
||||||
diffFiles: [],
|
diffFiles: [],
|
||||||
coverageFiles: {},
|
coverageFiles: {},
|
||||||
mergeRequestDiffs: [],
|
mergeRequestDiffs: [],
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapActions } from 'vuex';
|
import { mapActions } from 'vuex';
|
||||||
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
|
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||||
|
import GitlabTeamMemberBadge from '~/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
timeAgoTooltip,
|
timeAgoTooltip,
|
||||||
|
GitlabTeamMemberBadge,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
author: {
|
author: {
|
||||||
|
@ -48,6 +50,9 @@ export default {
|
||||||
hasAuthor() {
|
hasAuthor() {
|
||||||
return this.author && Object.keys(this.author).length;
|
return this.author && Object.keys(this.author).length;
|
||||||
},
|
},
|
||||||
|
showGitlabTeamMemberBadge() {
|
||||||
|
return this.author?.is_gitlab_employee;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['setTargetNoteHash']),
|
...mapActions(['setTargetNoteHash']),
|
||||||
|
@ -73,19 +78,21 @@ export default {
|
||||||
{{ __('Toggle thread') }}
|
{{ __('Toggle thread') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<template v-if="hasAuthor">
|
||||||
v-if="hasAuthor"
|
<a
|
||||||
v-once
|
v-once
|
||||||
:href="author.path"
|
:href="author.path"
|
||||||
class="js-user-link"
|
class="js-user-link"
|
||||||
:data-user-id="author.id"
|
:data-user-id="author.id"
|
||||||
:data-username="author.username"
|
:data-username="author.username"
|
||||||
>
|
>
|
||||||
<slot name="note-header-info"></slot>
|
<slot name="note-header-info"></slot>
|
||||||
<span class="note-header-author-name bold">{{ author.name }}</span>
|
<span class="note-header-author-name bold">{{ author.name }}</span>
|
||||||
<span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
|
<span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
|
||||||
<span class="note-headline-light">@{{ author.username }}</span>
|
<span class="note-headline-light">@{{ author.username }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
<gitlab-team-member-badge v-if="showGitlabTeamMemberBadge" />
|
||||||
|
</template>
|
||||||
<span v-else>{{ __('A deleted user') }}</span>
|
<span v-else>{{ __('A deleted user') }}</span>
|
||||||
<span class="note-headline-light note-headline-meta">
|
<span class="note-headline-light note-headline-meta">
|
||||||
<span class="system-note-message"> <slot></slot> </span>
|
<span class="system-note-message"> <slot></slot> </span>
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
|
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
|
||||||
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
|
import PrometheusMetrics from 'ee_else_ce/prometheus_metrics/prometheus_metrics';
|
||||||
import PrometheusAlerts from '~/prometheus_alerts';
|
import PrometheusAlerts from '~/prometheus_alerts';
|
||||||
import initAlertsSettings from '~/alerts_service_settings';
|
import initAlertsSettings from '~/alerts_service_settings';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const prometheusSettingsWrapper = document.querySelector('.js-prometheus-metrics-monitoring');
|
|
||||||
const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
|
const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
|
||||||
integrationSettingsForm.init();
|
integrationSettingsForm.init();
|
||||||
|
|
||||||
|
const prometheusSettingsSelector = '.js-prometheus-metrics-monitoring';
|
||||||
|
const prometheusSettingsWrapper = document.querySelector(prometheusSettingsSelector);
|
||||||
if (prometheusSettingsWrapper) {
|
if (prometheusSettingsWrapper) {
|
||||||
const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
|
const prometheusMetrics = new PrometheusMetrics(prometheusSettingsSelector);
|
||||||
prometheusMetrics.loadActiveMetrics();
|
prometheusMetrics.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
PrometheusAlerts();
|
PrometheusAlerts();
|
||||||
|
|
|
@ -28,6 +28,10 @@ export default class PrometheusMetrics {
|
||||||
this.$panelToggle.on('click', e => this.handlePanelToggle(e));
|
this.$panelToggle.on('click', e => this.handlePanelToggle(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.loadActiveMetrics();
|
||||||
|
}
|
||||||
|
|
||||||
/* eslint-disable class-methods-use-this */
|
/* eslint-disable class-methods-use-this */
|
||||||
handlePanelToggle(e) {
|
handlePanelToggle(e) {
|
||||||
const $toggleBtn = $(e.currentTarget);
|
const $toggleBtn = $(e.currentTarget);
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script>
|
||||||
|
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
|
||||||
|
import { __ } from '~/locale';
|
||||||
|
|
||||||
|
const GITLAB_TEAM_MEMBER_LABEL = __('GitLab Team Member');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GitlabTeamMemberBadge',
|
||||||
|
directives: {
|
||||||
|
GlTooltip: GlTooltipDirective,
|
||||||
|
},
|
||||||
|
components: { GlIcon },
|
||||||
|
gitlabTeamMemberLabel: GITLAB_TEAM_MEMBER_LABEL,
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
v-gl-tooltip.hover
|
||||||
|
:title="$options.gitlabTeamMemberLabel"
|
||||||
|
role="img"
|
||||||
|
:aria-label="$options.gitlabTeamMemberLabel"
|
||||||
|
class="d-inline-block align-middle"
|
||||||
|
>
|
||||||
|
<gl-icon name="tanuki-verified" class="gl-text-purple d-block" />
|
||||||
|
</span>
|
||||||
|
</template>
|
|
@ -180,7 +180,7 @@ class Projects::PipelinesController < Projects::ApplicationController
|
||||||
|
|
||||||
render json: TestReportSerializer
|
render json: TestReportSerializer
|
||||||
.new(current_user: @current_user)
|
.new(current_user: @current_user)
|
||||||
.represent(test_reports)
|
.represent(test_reports, project: project)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,11 +3,6 @@
|
||||||
class SearchService
|
class SearchService
|
||||||
include Gitlab::Allowable
|
include Gitlab::Allowable
|
||||||
|
|
||||||
REDACTABLE_RESULTS = [
|
|
||||||
ActiveRecord::Relation,
|
|
||||||
Gitlab::Search::FoundBlob
|
|
||||||
].freeze
|
|
||||||
|
|
||||||
SEARCH_TERM_LIMIT = 64
|
SEARCH_TERM_LIMIT = 64
|
||||||
SEARCH_CHAR_LIMIT = 4096
|
SEARCH_CHAR_LIMIT = 4096
|
||||||
|
|
||||||
|
@ -68,10 +63,6 @@ class SearchService
|
||||||
@search_objects ||= redact_unauthorized_results(search_results.objects(scope, params[:page]))
|
@search_objects ||= redact_unauthorized_results(search_results.objects(scope, params[:page]))
|
||||||
end
|
end
|
||||||
|
|
||||||
def redactable_results
|
|
||||||
REDACTABLE_RESULTS
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def visible_result?(object)
|
def visible_result?(object)
|
||||||
|
@ -80,12 +71,9 @@ class SearchService
|
||||||
Ability.allowed?(current_user, :"read_#{object.to_ability_name}", object)
|
Ability.allowed?(current_user, :"read_#{object.to_ability_name}", object)
|
||||||
end
|
end
|
||||||
|
|
||||||
def redact_unauthorized_results(results)
|
def redact_unauthorized_results(results_collection)
|
||||||
return results unless redactable_results.any? { |redactable| results.is_a?(redactable) }
|
results = results_collection.to_a
|
||||||
|
permitted_results = results.select { |object| visible_result?(object) }
|
||||||
permitted_results = results.select do |object|
|
|
||||||
visible_result?(object)
|
|
||||||
end
|
|
||||||
|
|
||||||
filtered_results = (results - permitted_results).each_with_object({}) do |object, memo|
|
filtered_results = (results - permitted_results).each_with_object({}) do |object, memo|
|
||||||
memo[object.id] = { ability: :"read_#{object.to_ability_name}", id: object.id, class_name: object.class.name }
|
memo[object.id] = { ability: :"read_#{object.to_ability_name}", id: object.id, class_name: object.class.name }
|
||||||
|
@ -93,13 +81,13 @@ class SearchService
|
||||||
|
|
||||||
log_redacted_search_results(filtered_results.values) if filtered_results.any?
|
log_redacted_search_results(filtered_results.values) if filtered_results.any?
|
||||||
|
|
||||||
return results.id_not_in(filtered_results.keys) if results.is_a?(ActiveRecord::Relation)
|
return results_collection.id_not_in(filtered_results.keys) if results_collection.is_a?(ActiveRecord::Relation)
|
||||||
|
|
||||||
Kaminari.paginate_array(
|
Kaminari.paginate_array(
|
||||||
permitted_results,
|
permitted_results,
|
||||||
total_count: results.total_count,
|
total_count: results_collection.total_count,
|
||||||
limit: results.limit_value,
|
limit: results_collection.limit_value,
|
||||||
offset: results.offset_value
|
offset: results_collection.offset_value
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
.avatar-container.rect-avatar.s40
|
.avatar-container.rect-avatar.s40
|
||||||
= link_to group do
|
= link_to group do
|
||||||
= group_icon(group, class: "avatar s40 d-none d-sm-block")
|
= group_icon(group, class: "avatar s40")
|
||||||
.title
|
.title
|
||||||
= link_to group.full_name, group, class: 'group-name'
|
= link_to group.full_name, group, class: 'group-name'
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add Nginx error percentage metric
|
||||||
|
merge_request: 28983
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Make search redaction more robust
|
||||||
|
merge_request: 29166
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix missing group icons on profile page when screen < 576px
|
||||||
|
merge_request: 28973
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add index for created_at of resource_milestone_events
|
||||||
|
merge_request: 28929
|
||||||
|
author:
|
||||||
|
type: performance
|
5
changelogs/unreleased/jc-replicas-rake.yml
Normal file
5
changelogs/unreleased/jc-replicas-rake.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add Praefect rake task to print out replica checksums
|
||||||
|
merge_request: 28369
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -218,3 +218,13 @@ panel_groups:
|
||||||
query_range: 'sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m]))'
|
query_range: 'sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m]))'
|
||||||
label: HTTP Errors
|
label: HTTP Errors
|
||||||
unit: "errors / sec"
|
unit: "errors / sec"
|
||||||
|
- title: "HTTP Error Rate"
|
||||||
|
type: "area-chart"
|
||||||
|
y_label: "HTTP Errors (%)"
|
||||||
|
weight: 1
|
||||||
|
metrics:
|
||||||
|
- id: response_metrics_nginx_http_error_percentage
|
||||||
|
query_range: 'sum(rate(nginx_server_requests{code=~"5.*", host="*", %{environment_filter}}[2m])) / sum(rate(nginx_server_requests{code="total", host="*", %{environment_filter}}[2m])) * 100'
|
||||||
|
label: 5xx Errors (%)
|
||||||
|
unit: "%"
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddIndexToCreatedAtOnResourceMilestoneEvents < ActiveRecord::Migration[6.0]
|
||||||
|
include Gitlab::Database::MigrationHelpers
|
||||||
|
|
||||||
|
INDEX_NAME = 'index_resource_milestone_events_created_at'
|
||||||
|
|
||||||
|
DOWNTIME = false
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_concurrent_index :resource_milestone_events, :created_at, name: INDEX_NAME
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_concurrent_index_by_name :resource_milestone_events, INDEX_NAME
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddNginxFiveHundredPercentageMetric < ActiveRecord::Migration[6.0]
|
||||||
|
DOWNTIME = false
|
||||||
|
|
||||||
|
def up
|
||||||
|
::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
# no-op
|
||||||
|
end
|
||||||
|
end
|
|
@ -10036,6 +10036,8 @@ CREATE INDEX index_resource_label_events_on_merge_request_id ON public.resource_
|
||||||
|
|
||||||
CREATE INDEX index_resource_label_events_on_user_id ON public.resource_label_events USING btree (user_id);
|
CREATE INDEX index_resource_label_events_on_user_id ON public.resource_label_events USING btree (user_id);
|
||||||
|
|
||||||
|
CREATE INDEX index_resource_milestone_events_created_at ON public.resource_milestone_events USING btree (created_at);
|
||||||
|
|
||||||
CREATE INDEX index_resource_milestone_events_on_issue_id ON public.resource_milestone_events USING btree (issue_id);
|
CREATE INDEX index_resource_milestone_events_on_issue_id ON public.resource_milestone_events USING btree (issue_id);
|
||||||
|
|
||||||
CREATE INDEX index_resource_milestone_events_on_merge_request_id ON public.resource_milestone_events USING btree (merge_request_id);
|
CREATE INDEX index_resource_milestone_events_on_merge_request_id ON public.resource_milestone_events USING btree (merge_request_id);
|
||||||
|
@ -13070,7 +13072,9 @@ COPY "schema_migrations" (version) FROM STDIN;
|
||||||
20200403184110
|
20200403184110
|
||||||
20200403185127
|
20200403185127
|
||||||
20200403185422
|
20200403185422
|
||||||
|
20200406135648
|
||||||
20200407094005
|
20200407094005
|
||||||
20200407094923
|
20200407094923
|
||||||
|
20200408110856
|
||||||
\.
|
\.
|
||||||
|
|
||||||
|
|
|
@ -60,12 +60,18 @@ GitLab](https://about.gitlab.com/install/).
|
||||||
|
|
||||||
- 1 Praefect node (minimal storage required)
|
- 1 Praefect node (minimal storage required)
|
||||||
- 3 Gitaly nodes (high CPU, high memory, fast storage)
|
- 3 Gitaly nodes (high CPU, high memory, fast storage)
|
||||||
|
- 1 GitLab server
|
||||||
|
|
||||||
You will need the IP/host address for each node.
|
You will need the IP/host address for each node.
|
||||||
|
|
||||||
1. `POSTGRESQL_SERVER_ADDRESS`: the IP/host address of the PostgreSQL server
|
1. `POSTGRESQL_SERVER_ADDRESS`: the IP/host address of the PostgreSQL server
|
||||||
1. `PRAEFECT_SERVER_ADDRESS`: the IP/host address of the Praefect server
|
1. `PRAEFECT_HOST`: the IP/host address of the Praefect server
|
||||||
1. `GITALY_SERVER_ADDRESS`: the IP/host address of each Gitaly node
|
1. `GITALY_HOST`: the IP/host address of each Gitaly server
|
||||||
|
1. `GITLAB_HOST`: the IP/host address of the GitLab server
|
||||||
|
|
||||||
|
If you are using a cloud provider, you can look up the addresses for each server through your cloud provider's management console.
|
||||||
|
|
||||||
|
If you are using Google Cloud Platform, SoftLayer, or any other vendor that provides a virtual private cloud (VPC) you can use the private addresses for each cloud instance (corresponds to “internal address” for Google Cloud Platform) for `PRAEFECT_HOST`, `GITALY_HOST`, and `GITLAB_HOST`.
|
||||||
|
|
||||||
#### Secrets
|
#### Secrets
|
||||||
|
|
||||||
|
@ -183,14 +189,18 @@ application server, or a Gitaly node.
|
||||||
1. Configure **Praefect** to listen on network interfaces by editing
|
1. Configure **Praefect** to listen on network interfaces by editing
|
||||||
`/etc/gitlab/gitlab.rb`:
|
`/etc/gitlab/gitlab.rb`:
|
||||||
|
|
||||||
|
You will need to replace:
|
||||||
|
|
||||||
|
- `PRAEFECT_HOST` with the IP address or hostname of the Praefect node
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
# Make Praefect accept connections on all network interfaces.
|
# Make Praefect accept connections on all network interfaces.
|
||||||
# Use firewalls to restrict access to this address/port.
|
# Use firewalls to restrict access to this address/port.
|
||||||
praefect['listen_addr'] = '0.0.0.0:2305'
|
praefect['listen_addr'] = 'PRAEFECT_HOST:2305'
|
||||||
|
|
||||||
# Enable Prometheus metrics access to Praefect. You must use firewalls
|
# Enable Prometheus metrics access to Praefect. You must use firewalls
|
||||||
# to restrict access to this address/port.
|
# to restrict access to this address/port.
|
||||||
praefect['prometheus_listen_addr'] = '0.0.0.0:9652'
|
praefect['prometheus_listen_addr'] = 'PRAEFECT_HOST:9652'
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Configure a strong `auth_token` for **Praefect** by editing
|
1. Configure a strong `auth_token` for **Praefect** by editing
|
||||||
|
@ -357,14 +367,18 @@ documentation](index.md#3-gitaly-server-configuration).
|
||||||
1. Configure **Gitaly** to listen on network interfaces by editing
|
1. Configure **Gitaly** to listen on network interfaces by editing
|
||||||
`/etc/gitlab/gitlab.rb`:
|
`/etc/gitlab/gitlab.rb`:
|
||||||
|
|
||||||
|
You will need to replace:
|
||||||
|
|
||||||
|
- `GITALY_HOST` with the IP address or hostname of the Gitaly node
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
# Make Gitaly accept connections on all network interfaces.
|
# Make Gitaly accept connections on all network interfaces.
|
||||||
# Use firewalls to restrict access to this address/port.
|
# Use firewalls to restrict access to this address/port.
|
||||||
gitaly['listen_addr'] = '0.0.0.0:8075'
|
gitaly['listen_addr'] = 'GITALY_HOST:8075'
|
||||||
|
|
||||||
# Enable Prometheus metrics access to Gitaly. You must use firewalls
|
# Enable Prometheus metrics access to Gitaly. You must use firewalls
|
||||||
# to restrict access to this address/port.
|
# to restrict access to this address/port.
|
||||||
gitaly['prometheus_listen_addr'] = '0.0.0.0:9236'
|
gitaly['prometheus_listen_addr'] = 'GITALY_HOST:9236'
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Configure a strong `auth_token` for **Gitaly** by editing
|
1. Configure a strong `auth_token` for **Gitaly** by editing
|
||||||
|
@ -387,7 +401,7 @@ documentation](index.md#3-gitaly-server-configuration).
|
||||||
# Configure the gitlab-shell API callback URL. Without this, `git push` will
|
# Configure the gitlab-shell API callback URL. Without this, `git push` will
|
||||||
# fail. This can be your front door GitLab URL or an internal load balancer.
|
# fail. This can be your front door GitLab URL or an internal load balancer.
|
||||||
# Examples: 'https://example.gitlab.com', 'http://1.2.3.4'
|
# Examples: 'https://example.gitlab.com', 'http://1.2.3.4'
|
||||||
gitlab_rails['internal_api_url'] = 'GITLAB_SERVER_URL'
|
gitlab_rails['internal_api_url'] = 'http://GITLAB_HOST'
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Configure the storage location for Git data by setting `git_data_dirs` in
|
1. Configure the storage location for Git data by setting `git_data_dirs` in
|
||||||
|
@ -499,12 +513,13 @@ Particular attention should be shown to:
|
||||||
You will need to replace:
|
You will need to replace:
|
||||||
|
|
||||||
- `PRAEFECT_HOST` with the IP address or hostname of the Praefect node
|
- `PRAEFECT_HOST` with the IP address or hostname of the Praefect node
|
||||||
|
- `GITLAB_HOST` with the IP address or hostname of the GitLab server
|
||||||
- `PRAEFECT_EXTERNAL_TOKEN` with the real secret
|
- `PRAEFECT_EXTERNAL_TOKEN` with the real secret
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
git_data_dirs({
|
git_data_dirs({
|
||||||
"default" => {
|
"default" => {
|
||||||
"path" => "/var/opt/gitlab/git-data"
|
"gitaly_address" => "tcp://GITLAB_HOST:8075"
|
||||||
},
|
},
|
||||||
"praefect" => {
|
"praefect" => {
|
||||||
"gitaly_address" => "tcp://PRAEFECT_HOST:2305",
|
"gitaly_address" => "tcp://PRAEFECT_HOST:2305",
|
||||||
|
@ -513,6 +528,13 @@ Particular attention should be shown to:
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
1. Allow Gitaly to listen on a tcp port by editing
|
||||||
|
`/etc/gitlab/gitlab.rb`
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
gitaly['listen_addr'] = 'tcp://GITLAB_HOST:8075'
|
||||||
|
```
|
||||||
|
|
||||||
1. Configure the `gitlab_shell['secret_token']` so that callbacks from Gitaly
|
1. Configure the `gitlab_shell['secret_token']` so that callbacks from Gitaly
|
||||||
nodes during a `git push` are properly authenticated by editing
|
nodes during a `git push` are properly authenticated by editing
|
||||||
`/etc/gitlab/gitlab.rb`:
|
`/etc/gitlab/gitlab.rb`:
|
||||||
|
@ -526,7 +548,7 @@ Particular attention should be shown to:
|
||||||
1. Configure the `external_url` so that files could be served by GitLab
|
1. Configure the `external_url` so that files could be served by GitLab
|
||||||
by proper endpoint access by editing `/etc/gitlab/gitlab.rb`:
|
by proper endpoint access by editing `/etc/gitlab/gitlab.rb`:
|
||||||
|
|
||||||
You will need to replace `GITLAB_SERVER_URL` with the real URL on which
|
You will need to replace `GITLAB_SERVER_URL` with the real external facing URL on which
|
||||||
current GitLab instance is serving:
|
current GitLab instance is serving:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
|
|
|
@ -635,6 +635,43 @@ Each line contains a JSON line that can be ingested by Elasticsearch. For exampl
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `geo.log`
|
||||||
|
|
||||||
|
> Introduced in 9.5.
|
||||||
|
|
||||||
|
Geo stores structured log messages in a `geo.log` file. For Omnibus installations, this file is at `/var/log/gitlab/gitlab-rails/geo.log`.
|
||||||
|
|
||||||
|
This file contains information about when Geo attempts to sync repositories and files. Each line in the file contains a separate JSON entry that can be ingested into. For example, Elasticsearch or Splunk.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"severity":"INFO","time":"2017-08-06T05:40:16.104Z","message":"Repository update","project_id":1,"source":"repository","resync_repository":true,"resync_wiki":true,"class":"Gitlab::Geo::LogCursor::Daemon","cursor_delay_s":0.038}
|
||||||
|
```
|
||||||
|
|
||||||
|
This message shows that Geo detected that a repository update was needed for project `1`.
|
||||||
|
|
||||||
|
## Registry Logs
|
||||||
|
|
||||||
|
For Omnibus installations, Container Registry logs reside in `/var/log/gitlab/registry/current`.
|
||||||
|
|
||||||
|
## NGINX Logs
|
||||||
|
|
||||||
|
For Omnibus installations, NGINX logs reside in:
|
||||||
|
|
||||||
|
- `/var/log/gitlab/nginx/gitlab_access.log` contains a log of requests made to GitLab.
|
||||||
|
- `/var/log/gitlab/nginx/gitlab_error.log` contains a log of NGINX errors for GitLab.
|
||||||
|
- `/var/log/gitlab/nginx/gitlab_pages_access.log` contains a log of requests made to Pages static sites.
|
||||||
|
- `/var/log/gitlab/nginx/gitlab_pages_error.log` contains a log of NGINX errors for Pages static sites.
|
||||||
|
- `/var/log/gitlab/nginx/gitlab_registry_access.log` contains a log of requests made to the Container Registry.
|
||||||
|
- `/var/log/gitlab/nginx/gitlab_registry_error.log` contains a log of NGINX errors for the Container Regsitry.
|
||||||
|
|
||||||
|
Below is the default GitLab NGINX access log format:
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"
|
||||||
|
```
|
||||||
|
|
||||||
[repocheck]: repository_checks.md
|
[repocheck]: repository_checks.md
|
||||||
[Rack Attack]: ../security/rack_attack.md
|
[Rack Attack]: ../security/rack_attack.md
|
||||||
[Rate Limit]: ../user/admin_area/settings/rate_limits_on_raw_endpoints.md
|
[Rate Limit]: ../user/admin_area/settings/rate_limits_on_raw_endpoints.md
|
||||||
|
|
|
@ -81,6 +81,7 @@ Example response:
|
||||||
"title": "Accusamus iste et ullam ratione voluptatem omnis debitis dolor est.",
|
"title": "Accusamus iste et ullam ratione voluptatem omnis debitis dolor est.",
|
||||||
"description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.",
|
"description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.",
|
||||||
"state": "opened",
|
"state": "opened",
|
||||||
|
"confidential": "false",
|
||||||
"web_url": "http://localhost:3001/groups/test/-/epics/4",
|
"web_url": "http://localhost:3001/groups/test/-/epics/4",
|
||||||
"reference": "&4",
|
"reference": "&4",
|
||||||
"references": {
|
"references": {
|
||||||
|
@ -240,6 +241,7 @@ POST /groups/:id/epics
|
||||||
| `title` | string | yes | The title of the epic |
|
| `title` | string | yes | The title of the epic |
|
||||||
| `labels` | string | no | The comma separated list of labels |
|
| `labels` | string | no | The comma separated list of labels |
|
||||||
| `description` | string | no | The description of the epic. Limited to 1,048,576 characters. |
|
| `description` | string | no | The description of the epic. Limited to 1,048,576 characters. |
|
||||||
|
| `confidential` | boolean | no | Whether the epic should be confidential. Will be ignored if `confidential_epics` feature flag is disabled. |
|
||||||
| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
|
| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
|
||||||
| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) |
|
| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) |
|
||||||
| `due_date_is_fixed` | boolean | no | Whether due date should be sourced from `due_date_fixed` or from milestones (since 11.3) |
|
| `due_date_is_fixed` | boolean | no | Whether due date should be sourced from `due_date_fixed` or from milestones (since 11.3) |
|
||||||
|
@ -260,6 +262,7 @@ Example response:
|
||||||
"title": "Epic",
|
"title": "Epic",
|
||||||
"description": "Epic description",
|
"description": "Epic description",
|
||||||
"state": "opened",
|
"state": "opened",
|
||||||
|
"confidential": "false",
|
||||||
"web_url": "http://localhost:3001/groups/test/-/epics/6",
|
"web_url": "http://localhost:3001/groups/test/-/epics/6",
|
||||||
"reference": "&6",
|
"reference": "&6",
|
||||||
"references": {
|
"references": {
|
||||||
|
@ -314,6 +317,7 @@ PUT /groups/:id/epics/:epic_iid
|
||||||
| `epic_iid` | integer/string | yes | The internal ID of the epic |
|
| `epic_iid` | integer/string | yes | The internal ID of the epic |
|
||||||
| `title` | string | no | The title of an epic |
|
| `title` | string | no | The title of an epic |
|
||||||
| `description` | string | no | The description of an epic. Limited to 1,048,576 characters. |
|
| `description` | string | no | The description of an epic. Limited to 1,048,576 characters. |
|
||||||
|
| `confidential` | boolean | no | Whether the epic should be confidential. Will be ignored if `confidential_epics` feature flag is disabled. |
|
||||||
| `labels` | string | no | The comma separated list of labels |
|
| `labels` | string | no | The comma separated list of labels |
|
||||||
| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
|
| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
|
||||||
| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) |
|
| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) |
|
||||||
|
@ -335,6 +339,7 @@ Example response:
|
||||||
"title": "New Title",
|
"title": "New Title",
|
||||||
"description": "Epic description",
|
"description": "Epic description",
|
||||||
"state": "opened",
|
"state": "opened",
|
||||||
|
"confidential": "false",
|
||||||
"web_url": "http://localhost:3001/groups/test/-/epics/6",
|
"web_url": "http://localhost:3001/groups/test/-/epics/6",
|
||||||
"reference": "&6",
|
"reference": "&6",
|
||||||
"references": {
|
"references": {
|
||||||
|
|
|
@ -149,6 +149,18 @@ otherwise.
|
||||||
If you want a running pipeline to finish even if you push new commits to a merge
|
If you want a running pipeline to finish even if you push new commits to a merge
|
||||||
request, be sure to start the `dont-interrupt-me` job before pushing.
|
request, be sure to start the `dont-interrupt-me` job before pushing.
|
||||||
|
|
||||||
|
## PostgreSQL versions testing
|
||||||
|
|
||||||
|
We follow [the PostgreSQL versions Omnibus support policy](https://gitlab.com/groups/gitlab-org/-/epics/2184#proposal):
|
||||||
|
|
||||||
|
| | 12.10 (April 2020) | 13.0 (May 2020) | 13.1 (June 2020) | 13.2 (July 2020) | 13.3 (August 2020) | 13.4, 13.5 | 13.6 (November 2020) | 14.0 (May 2021?) |
|
||||||
|
| ------ | ------------------ | --------------- | ---------------- | ---------------- | ------------------ | ------------ | -------------------- | -------------------- |
|
||||||
|
| PG9.6 | nightly | - | - | - | - | - | - | - |
|
||||||
|
| PG10 | `master` | - | - | - | - | - | - | - |
|
||||||
|
| PG11 | MRs/`master` | MRs/`master` | MRs/`master` | MRs/`master` | MRs/`master` | MRs/`master` | nightly | - |
|
||||||
|
| PG12 | - | - | - | - | `master` | `master` | MRs/`master` | `master` |
|
||||||
|
| PG13 | - | - | - | - | - | - | - | MRs/`master` |
|
||||||
|
|
||||||
## Directed acyclic graph
|
## Directed acyclic graph
|
||||||
|
|
||||||
We're using the [`needs:`](../ci/yaml/README.md#needs) keyword to
|
We're using the [`needs:`](../ci/yaml/README.md#needs) keyword to
|
||||||
|
|
|
@ -303,10 +303,10 @@ it highlighted:
|
||||||
"version": "2.3",
|
"version": "2.3",
|
||||||
"vulnerabilities": [
|
"vulnerabilities": [
|
||||||
{
|
{
|
||||||
|
"id": "ac0997ad-1006-4c81-81fb-ee2bbe6e78e3",
|
||||||
"category": "container_scanning",
|
"category": "container_scanning",
|
||||||
"message": "CVE-2019-3462 in apt",
|
"message": "CVE-2019-3462 in apt",
|
||||||
"description": "Incorrect sanitation of the 302 redirect field in HTTP transport method of apt versions 1.4.8 and earlier can lead to content injection by a MITM attacker, potentially leading to remote code execution on the target machine.",
|
"description": "Incorrect sanitation of the 302 redirect field in HTTP transport method of apt versions 1.4.8 and earlier can lead to content injection by a MITM attacker, potentially leading to remote code execution on the target machine.",
|
||||||
"cve": "debian:9:apt:CVE-2019-3462",
|
|
||||||
"severity": "High",
|
"severity": "High",
|
||||||
"confidence": "Unknown",
|
"confidence": "Unknown",
|
||||||
"solution": "Upgrade apt from 1.4.8 to 1.4.9",
|
"solution": "Upgrade apt from 1.4.8 to 1.4.9",
|
||||||
|
@ -343,7 +343,7 @@ it highlighted:
|
||||||
{
|
{
|
||||||
"fixes": [
|
"fixes": [
|
||||||
{
|
{
|
||||||
"cve": "debian:9:apt:CVE-2019-3462"
|
"id": "c0997ad-1006-4c81-81fb-ee2bbe6e78e3"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"summary": "Upgrade apt from 1.4.8 to 1.4.9",
|
"summary": "Upgrade apt from 1.4.8 to 1.4.9",
|
||||||
|
@ -363,10 +363,11 @@ the report JSON unless stated otherwise. Presence of optional fields depends on
|
||||||
|------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `version` | Report syntax version used to generate this JSON. |
|
| `version` | Report syntax version used to generate this JSON. |
|
||||||
| `vulnerabilities` | Array of vulnerability objects. |
|
| `vulnerabilities` | Array of vulnerability objects. |
|
||||||
|
| `vulnerabilities[].id` | Unique identifier of the vulnerability. |
|
||||||
| `vulnerabilities[].category` | Where this vulnerability belongs (for example, SAST or Container Scanning). For Container Scanning, it will always be `container_scanning`. |
|
| `vulnerabilities[].category` | Where this vulnerability belongs (for example, SAST or Container Scanning). For Container Scanning, it will always be `container_scanning`. |
|
||||||
| `vulnerabilities[].message` | A short text that describes the vulnerability, it may include occurrence's specific information. Optional. |
|
| `vulnerabilities[].message` | A short text that describes the vulnerability, it may include occurrence's specific information. Optional. |
|
||||||
| `vulnerabilities[].description` | A long text that describes the vulnerability. Optional. |
|
| `vulnerabilities[].description` | A long text that describes the vulnerability. Optional. |
|
||||||
| `vulnerabilities[].cve` | A fingerprint string value that represents a concrete occurrence of the vulnerability. It's used to determine whether two vulnerability occurrences are same or different. May not be 100% accurate. **This is NOT a [CVE](https://cve.mitre.org/)**. |
|
| `vulnerabilities[].cve` | (**DEPRECATED - use `vulnerabilities[].id` instead**) A fingerprint string value that represents a concrete occurrence of the vulnerability. It's used to determine whether two vulnerability occurrences are same or different. May not be 100% accurate. **This is NOT a [CVE](https://cve.mitre.org/)**. |
|
||||||
| `vulnerabilities[].severity` | How much the vulnerability impacts the software. Possible values: `Undefined` (an analyzer has not provided this information), `Info`, `Unknown`, `Low`, `Medium`, `High`, `Critical`. **Note:** Our current container scanning tool based on [klar](https://github.com/optiopay/klar) only provides the following levels: `Unknown`, `Low`, `Medium`, `High`, `Critical`. |
|
| `vulnerabilities[].severity` | How much the vulnerability impacts the software. Possible values: `Undefined` (an analyzer has not provided this information), `Info`, `Unknown`, `Low`, `Medium`, `High`, `Critical`. **Note:** Our current container scanning tool based on [klar](https://github.com/optiopay/klar) only provides the following levels: `Unknown`, `Low`, `Medium`, `High`, `Critical`. |
|
||||||
| `vulnerabilities[].confidence` | How reliable the vulnerability's assessment is. Possible values: `Undefined` (an analyzer has not provided this information), `Ignore`, `Unknown`, `Experimental`, `Low`, `Medium`, `High`, `Confirmed`. **Note:** Our current container scanning tool based on [klar](https://github.com/optiopay/klar) does not provide a confidence level, so this value is currently hardcoded to `Unknown`. |
|
| `vulnerabilities[].confidence` | How reliable the vulnerability's assessment is. Possible values: `Undefined` (an analyzer has not provided this information), `Ignore`, `Unknown`, `Experimental`, `Low`, `Medium`, `High`, `Confirmed`. **Note:** Our current container scanning tool based on [klar](https://github.com/optiopay/klar) does not provide a confidence level, so this value is currently hardcoded to `Unknown`. |
|
||||||
| `vulnerabilities[].solution` | Explanation of how to fix the vulnerability. Optional. |
|
| `vulnerabilities[].solution` | Explanation of how to fix the vulnerability. Optional. |
|
||||||
|
@ -390,7 +391,8 @@ the report JSON unless stated otherwise. Presence of optional fields depends on
|
||||||
| `vulnerabilities[].links[].url` | URL of the vulnerability details document. Optional. |
|
| `vulnerabilities[].links[].url` | URL of the vulnerability details document. Optional. |
|
||||||
| `remediations` | An array of objects containing information on cured vulnerabilities along with patch diffs to apply. Empty if no remediations provided by an underlying analyzer. |
|
| `remediations` | An array of objects containing information on cured vulnerabilities along with patch diffs to apply. Empty if no remediations provided by an underlying analyzer. |
|
||||||
| `remediations[].fixes` | An array of strings that represent references to vulnerabilities fixed by this particular remediation. |
|
| `remediations[].fixes` | An array of strings that represent references to vulnerabilities fixed by this particular remediation. |
|
||||||
| `remediations[].fixes[].cve` | A string value that describes a fixed vulnerability occurrence in the same format as `vulnerabilities[].cve`. |
|
| `remediations[].fixes[].id` | The id of a fixed vulnerability. |
|
||||||
|
| `remediations[].fixes[].cve` | (**DEPRECATED - use `remediations[].fixes[].id` instead**) A string value that describes a fixed vulnerability in the same format as `vulnerabilities[].cve`. |
|
||||||
| `remediations[].summary` | Overview of how the vulnerabilities have been fixed. |
|
| `remediations[].summary` | Overview of how the vulnerabilities have been fixed. |
|
||||||
| `remediations[].diff` | base64-encoded remediation code diff, compatible with [`git apply`](https://git-scm.com/docs/git-format-patch#_discussion). |
|
| `remediations[].diff` | base64-encoded remediation code diff, compatible with [`git apply`](https://git-scm.com/docs/git-format-patch#_discussion). |
|
||||||
|
|
||||||
|
@ -414,7 +416,8 @@ Some vulnerabilities can be fixed by applying the solution that GitLab
|
||||||
automatically generates.
|
automatically generates.
|
||||||
|
|
||||||
To enable remediation support, the scanning tool _must_ have access to the `Dockerfile` specified by
|
To enable remediation support, the scanning tool _must_ have access to the `Dockerfile` specified by
|
||||||
the `DOCKERFILE_PATH` environment variable. To ensure that the scanning tool has access to this
|
the [`DOCKERFILE_PATH`](#available-variables) environment variable. To ensure that the scanning tool
|
||||||
|
has access to this
|
||||||
file, it's necessary to set [`GIT_STRATEGY: fetch`](../../../ci/yaml/README.md#git-strategy) in
|
file, it's necessary to set [`GIT_STRATEGY: fetch`](../../../ci/yaml/README.md#git-strategy) in
|
||||||
your `.gitlab-ci.yml` file by following the instructions described in this document's
|
your `.gitlab-ci.yml` file by following the instructions described in this document's
|
||||||
[overriding the Container Scanning template](#overriding-the-container-scanning-template) section.
|
[overriding the Container Scanning template](#overriding-the-container-scanning-template) section.
|
||||||
|
|
|
@ -83,8 +83,11 @@ That's needed when one totally relies on [custom analyzers](#custom-analyzers).
|
||||||
|
|
||||||
## Custom analyzers
|
## Custom analyzers
|
||||||
|
|
||||||
You can provide your own analyzers as a comma separated list of Docker images.
|
### Custom analyzers with Docker-in-Docker
|
||||||
Here's how to add `analyzers/nugget` and `analyzers/perl` to the default images.
|
|
||||||
|
When Docker-in-Docker for Dependency Scanning is enabled,
|
||||||
|
you can provide your own analyzers as a comma-separated list of Docker images.
|
||||||
|
Here's how to add `analyzers/nuget` and `analyzers/perl` to the default images.
|
||||||
In `.gitlab-ci.yml` define:
|
In `.gitlab-ci.yml` define:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
@ -92,7 +95,7 @@ include:
|
||||||
template: Dependency-Scanning.gitlab-ci.yml
|
template: Dependency-Scanning.gitlab-ci.yml
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
DS_ANALYZER_IMAGES: "my-docker-registry/analyzers/nugget,amy-docker-registry/nalyzers/perl"
|
DS_ANALYZER_IMAGES: "my-docker-registry/analyzers/nuget,amy-docker-registry/analyzers/perl"
|
||||||
```
|
```
|
||||||
|
|
||||||
The values must be the full path to the container registry images,
|
The values must be the full path to the container registry images,
|
||||||
|
@ -103,6 +106,28 @@ This configuration doesn't benefit from the integrated detection step. Dependenc
|
||||||
Scanning has to fetch and spawn each Docker image to establish whether the
|
Scanning has to fetch and spawn each Docker image to establish whether the
|
||||||
custom analyzer can scan the source code.
|
custom analyzer can scan the source code.
|
||||||
|
|
||||||
|
### Custom analyzers without Docker-in-Docker
|
||||||
|
|
||||||
|
When Docker-in-Docker for Dependency Scanning is disabled, you can provide your own analyzers by
|
||||||
|
defining CI jobs in your CI configuration. For consistency, you should suffix your custom Dependency
|
||||||
|
Scanning jobs with `-dependency_scanning`. Here's how to add a scanning job that's based on the
|
||||||
|
Docker image `my-docker-registry/analyzers/nuget` and generates a Dependency Scanning report
|
||||||
|
`gl-dependency-scanning-report.json` when `/analyzer run` is executed. Define the following in
|
||||||
|
`.gitlab-ci.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
nuget-dependency_scanning:
|
||||||
|
image:
|
||||||
|
name: "my-docker-registry/analyzers/nuget"
|
||||||
|
script:
|
||||||
|
- /analyzer run
|
||||||
|
artifacts:
|
||||||
|
reports:
|
||||||
|
dependency_scanning: gl-dependency-scanning-report.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The [Security Scanner Integration](../../../development/integrations/secure.md) documentation explains how to integrate custom security scanners into GitLab.
|
||||||
|
|
||||||
## Analyzers data
|
## Analyzers data
|
||||||
|
|
||||||
The following table lists the data available for each official analyzer.
|
The following table lists the data available for each official analyzer.
|
||||||
|
|
|
@ -259,11 +259,11 @@ it highlighted:
|
||||||
"version": "2.0",
|
"version": "2.0",
|
||||||
"vulnerabilities": [
|
"vulnerabilities": [
|
||||||
{
|
{
|
||||||
|
"id": "51e83874-0ff6-4677-a4c5-249060554eae",
|
||||||
"category": "dependency_scanning",
|
"category": "dependency_scanning",
|
||||||
"name": "Regular Expression Denial of Service",
|
"name": "Regular Expression Denial of Service",
|
||||||
"message": "Regular Expression Denial of Service in debug",
|
"message": "Regular Expression Denial of Service in debug",
|
||||||
"description": "The debug module is vulnerable to regular expression denial of service when untrusted user input is passed into the `o` formatter. It takes around 50k characters to block for 2 seconds making this a low severity issue.",
|
"description": "The debug module is vulnerable to regular expression denial of service when untrusted user input is passed into the `o` formatter. It takes around 50k characters to block for 2 seconds making this a low severity issue.",
|
||||||
"cve": "yarn.lock:debug:gemnasium:37283ed4-0380-40d7-ada7-2d994afcc62a",
|
|
||||||
"severity": "Unknown",
|
"severity": "Unknown",
|
||||||
"solution": "Upgrade to latest versions.",
|
"solution": "Upgrade to latest versions.",
|
||||||
"scanner": {
|
"scanner": {
|
||||||
|
@ -300,11 +300,11 @@ it highlighted:
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "5d681b13-e8fa-4668-957e-8d88f932ddc7",
|
||||||
"category": "dependency_scanning",
|
"category": "dependency_scanning",
|
||||||
"name": "Authentication bypass via incorrect DOM traversal and canonicalization",
|
"name": "Authentication bypass via incorrect DOM traversal and canonicalization",
|
||||||
"message": "Authentication bypass via incorrect DOM traversal and canonicalization in saml2-js",
|
"message": "Authentication bypass via incorrect DOM traversal and canonicalization in saml2-js",
|
||||||
"description": "Some XML DOM traversal and canonicalization APIs may be inconsistent in handling of comments within XML nodes. Incorrect use of these APIs by some SAML libraries results in incorrect parsing of the inner text of XML nodes such that any inner text after the comment is lost prior to cryptographically signing the SAML message. Text after the comment therefore has no impact on the signature on the SAML message.\r\n\r\nA remote attacker can modify SAML content for a SAML service provider without invalidating the cryptographic signature, which may allow attackers to bypass primary authentication for the affected SAML service provider.",
|
"description": "Some XML DOM traversal and canonicalization APIs may be inconsistent in handling of comments within XML nodes. Incorrect use of these APIs by some SAML libraries results in incorrect parsing of the inner text of XML nodes such that any inner text after the comment is lost prior to cryptographically signing the SAML message. Text after the comment therefore has no impact on the signature on the SAML message.\r\n\r\nA remote attacker can modify SAML content for a SAML service provider without invalidating the cryptographic signature, which may allow attackers to bypass primary authentication for the affected SAML service provider.",
|
||||||
"cve": "yarn.lock:saml2-js:gemnasium:9952e574-7b5b-46fa-a270-aeb694198a98",
|
|
||||||
"severity": "Unknown",
|
"severity": "Unknown",
|
||||||
"solution": "Upgrade to fixed version.\r\n",
|
"solution": "Upgrade to fixed version.\r\n",
|
||||||
"scanner": {
|
"scanner": {
|
||||||
|
@ -351,7 +351,7 @@ it highlighted:
|
||||||
{
|
{
|
||||||
"fixes": [
|
"fixes": [
|
||||||
{
|
{
|
||||||
"cve": "yarn.lock:saml2-js:gemnasium:9952e574-7b5b-46fa-a270-aeb694198a98"
|
"id": "5d681b13-e8fa-4668-957e-8d88f932ddc7",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"summary": "Upgrade saml2-js",
|
"summary": "Upgrade saml2-js",
|
||||||
|
@ -371,11 +371,12 @@ the report JSON unless stated otherwise. Presence of optional fields depends on
|
||||||
|------------------------------------------------------|-------------|
|
|------------------------------------------------------|-------------|
|
||||||
| `version` | Report syntax version used to generate this JSON. |
|
| `version` | Report syntax version used to generate this JSON. |
|
||||||
| `vulnerabilities` | Array of vulnerability objects. |
|
| `vulnerabilities` | Array of vulnerability objects. |
|
||||||
|
| `vulnerabilities[].id` | Unique identifier of the vulnerability. |
|
||||||
| `vulnerabilities[].category` | Where this vulnerability belongs (SAST, Dependency Scanning etc.). For Dependency Scanning, it will always be `dependency_scanning`. |
|
| `vulnerabilities[].category` | Where this vulnerability belongs (SAST, Dependency Scanning etc.). For Dependency Scanning, it will always be `dependency_scanning`. |
|
||||||
| `vulnerabilities[].name` | Name of the vulnerability, this must not include the occurrence's specific information. Optional. |
|
| `vulnerabilities[].name` | Name of the vulnerability, this must not include the occurrence's specific information. Optional. |
|
||||||
| `vulnerabilities[].message` | A short text that describes the vulnerability, it may include occurrence's specific information. Optional. |
|
| `vulnerabilities[].message` | A short text that describes the vulnerability, it may include occurrence's specific information. Optional. |
|
||||||
| `vulnerabilities[].description` | A long text that describes the vulnerability. Optional. |
|
| `vulnerabilities[].description` | A long text that describes the vulnerability. Optional. |
|
||||||
| `vulnerabilities[].cve` | A fingerprint string value that represents a concrete occurrence of the vulnerability. It's used to determine whether two vulnerability occurrences are same or different. May not be 100% accurate. **This is NOT a [CVE](https://cve.mitre.org/)**. |
|
| `vulnerabilities[].cve` | (**DEPRECATED - use `vulnerabilities[].id` instead**) A fingerprint string value that represents a concrete occurrence of the vulnerability. It's used to determine whether two vulnerability occurrences are same or different. May not be 100% accurate. **This is NOT a [CVE](https://cve.mitre.org/)**. |
|
||||||
| `vulnerabilities[].severity` | How much the vulnerability impacts the software. Possible values: `Undefined` (an analyzer has not provided this information), `Info`, `Unknown`, `Low`, `Medium`, `High`, `Critical`. |
|
| `vulnerabilities[].severity` | How much the vulnerability impacts the software. Possible values: `Undefined` (an analyzer has not provided this information), `Info`, `Unknown`, `Low`, `Medium`, `High`, `Critical`. |
|
||||||
| `vulnerabilities[].confidence` | How reliable the vulnerability's assessment is. Possible values: `Undefined` (an analyzer has not provided this information), `Ignore`, `Unknown`, `Experimental`, `Low`, `Medium`, `High`, `Confirmed`. |
|
| `vulnerabilities[].confidence` | How reliable the vulnerability's assessment is. Possible values: `Undefined` (an analyzer has not provided this information), `Ignore`, `Unknown`, `Experimental`, `Low`, `Medium`, `High`, `Confirmed`. |
|
||||||
| `vulnerabilities[].solution` | Explanation of how to fix the vulnerability. Optional. |
|
| `vulnerabilities[].solution` | Explanation of how to fix the vulnerability. Optional. |
|
||||||
|
@ -398,7 +399,8 @@ the report JSON unless stated otherwise. Presence of optional fields depends on
|
||||||
| `vulnerabilities[].links[].url` | URL of the vulnerability details document. Optional. |
|
| `vulnerabilities[].links[].url` | URL of the vulnerability details document. Optional. |
|
||||||
| `remediations` | An array of objects containing information on cured vulnerabilities along with patch diffs to apply. Empty if no remediations provided by an underlying analyzer. |
|
| `remediations` | An array of objects containing information on cured vulnerabilities along with patch diffs to apply. Empty if no remediations provided by an underlying analyzer. |
|
||||||
| `remediations[].fixes` | An array of strings that represent references to vulnerabilities fixed by this particular remediation. |
|
| `remediations[].fixes` | An array of strings that represent references to vulnerabilities fixed by this particular remediation. |
|
||||||
| `remediations[].fixes[].cve` | A string value that describes a fixed vulnerability occurrence in the same format as `vulnerabilities[].cve`. |
|
| `remediations[].fixes[].id` | The id of a fixed vulnerability. |
|
||||||
|
| `remediations[].fixes[].cve` | (**DEPRECATED - use `remediations[].fixes[].id` instead**) A string value that describes a fixed vulnerability in the same format as `vulnerabilities[].cve`. |
|
||||||
| `remediations[].summary` | Overview of how the vulnerabilities have been fixed. |
|
| `remediations[].summary` | Overview of how the vulnerabilities have been fixed. |
|
||||||
| `remediations[].diff` | base64-encoded remediation code diff, compatible with [`git apply`](https://git-scm.com/docs/git-format-patch#_discussion). |
|
| `remediations[].diff` | base64-encoded remediation code diff, compatible with [`git apply`](https://git-scm.com/docs/git-format-patch#_discussion). |
|
||||||
|
|
||||||
|
|
|
@ -92,7 +92,10 @@ That's needed when one totally relies on [custom analyzers](#custom-analyzers).
|
||||||
|
|
||||||
## Custom Analyzers
|
## Custom Analyzers
|
||||||
|
|
||||||
You can provide your own analyzers as a comma separated list of Docker images.
|
### Custom analyzers with Docker-in-Docker
|
||||||
|
|
||||||
|
When Docker-in-Docker for SAST is enabled,
|
||||||
|
you can provide your own analyzers as a comma-separated list of Docker images.
|
||||||
Here's how to add `analyzers/csharp` and `analyzers/perl` to the default images:
|
Here's how to add `analyzers/csharp` and `analyzers/perl` to the default images:
|
||||||
In `.gitlab-ci.yml` define:
|
In `.gitlab-ci.yml` define:
|
||||||
|
|
||||||
|
@ -112,8 +115,27 @@ This configuration doesn't benefit from the integrated detection step.
|
||||||
SAST has to fetch and spawn each Docker image to establish whether the
|
SAST has to fetch and spawn each Docker image to establish whether the
|
||||||
custom analyzer can scan the source code.
|
custom analyzer can scan the source code.
|
||||||
|
|
||||||
CAUTION: **Caution:**
|
### Custom analyzers without Docker-in-Docker
|
||||||
Custom analyzers are not spawned automatically when [Docker In Docker](index.md#disabling-docker-in-docker-for-sast) is disabled.
|
|
||||||
|
When Docker-in-Docker for SAST is disabled, you can provide your own analyzers by
|
||||||
|
defining CI jobs in your CI configuration. For consistency, you should suffix your custom
|
||||||
|
SAST jobs with `-sast`. Here's how to add a scanning job that's based on the
|
||||||
|
Docker image `my-docker-registry/analyzers/csharp` and generates a SAST report
|
||||||
|
`gl-sast-report.json` when `/analyzer run` is executed. Define the following in
|
||||||
|
`.gitlab-ci.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
csharp-sast:
|
||||||
|
image:
|
||||||
|
name: "my-docker-registry/analyzers/csharp"
|
||||||
|
script:
|
||||||
|
- /analyzer run
|
||||||
|
artifacts:
|
||||||
|
reports:
|
||||||
|
sast: gl-sast-report.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The [Security Scanner Integration](../../../development/integrations/secure.md) documentation explains how to integrate custom security scanners into GitLab.
|
||||||
|
|
||||||
## Analyzers Data
|
## Analyzers Data
|
||||||
|
|
||||||
|
|
|
@ -368,11 +368,11 @@ it highlighted:
|
||||||
"version": "2.0",
|
"version": "2.0",
|
||||||
"vulnerabilities": [
|
"vulnerabilities": [
|
||||||
{
|
{
|
||||||
|
"id": "9e96e0ab-23da-4d7d-a09e-0acbaa5e83ca",
|
||||||
"category": "sast",
|
"category": "sast",
|
||||||
"name": "Predictable pseudorandom number generator",
|
"name": "Predictable pseudorandom number generator",
|
||||||
"message": "Predictable pseudorandom number generator",
|
"message": "Predictable pseudorandom number generator",
|
||||||
"description": "The use of java.util.Random is predictable",
|
"description": "The use of java.util.Random is predictable",
|
||||||
"cve": "818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM",
|
|
||||||
"severity": "Medium",
|
"severity": "Medium",
|
||||||
"confidence": "Medium",
|
"confidence": "Medium",
|
||||||
"scanner": {
|
"scanner": {
|
||||||
|
@ -405,9 +405,9 @@ it highlighted:
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": "e6dbf91f-4c07-46f7-a365-0169489c27d1",
|
||||||
"category": "sast",
|
"category": "sast",
|
||||||
"message": "Probable insecure usage of temp file/directory.",
|
"message": "Probable insecure usage of temp file/directory.",
|
||||||
"cve": "python/hardcoded/hardcoded-tmp.py:4ad6d4c40a8c263fc265f3384724014e0a4f8dd6200af83e51ff120420038031:B108",
|
|
||||||
"severity": "Medium",
|
"severity": "Medium",
|
||||||
"confidence": "Medium",
|
"confidence": "Medium",
|
||||||
"scanner": {
|
"scanner": {
|
||||||
|
@ -446,11 +446,12 @@ the report JSON unless stated otherwise. Presence of optional fields depends on
|
||||||
|-----------------------------------------|----------|
|
|-----------------------------------------|----------|
|
||||||
| `version` | Report syntax version used to generate this JSON. |
|
| `version` | Report syntax version used to generate this JSON. |
|
||||||
| `vulnerabilities` | Array of vulnerability objects. |
|
| `vulnerabilities` | Array of vulnerability objects. |
|
||||||
|
| `vulnerabilities[].id` | Unique identifier of the vulnerability. |
|
||||||
| `vulnerabilities[].category` | Where this vulnerability belongs (SAST, Dependency Scanning etc.). For SAST, it will always be `sast`. |
|
| `vulnerabilities[].category` | Where this vulnerability belongs (SAST, Dependency Scanning etc.). For SAST, it will always be `sast`. |
|
||||||
| `vulnerabilities[].name` | Name of the vulnerability, this must not include the occurrence's specific information. Optional. |
|
| `vulnerabilities[].name` | Name of the vulnerability, this must not include the occurrence's specific information. Optional. |
|
||||||
| `vulnerabilities[].message` | A short text that describes the vulnerability, it may include the occurrence's specific information. Optional. |
|
| `vulnerabilities[].message` | A short text that describes the vulnerability, it may include the occurrence's specific information. Optional. |
|
||||||
| `vulnerabilities[].description` | A long text that describes the vulnerability. Optional. |
|
| `vulnerabilities[].description` | A long text that describes the vulnerability. Optional. |
|
||||||
| `vulnerabilities[].cve` | A fingerprint string value that represents a concrete occurrence of the vulnerability. Is used to determine whether two vulnerability occurrences are same or different. May not be 100% accurate. **This is NOT a [CVE](https://cve.mitre.org/)**. |
|
| `vulnerabilities[].cve` | (**DEPRECATED - use `vulnerabilities[].id` instead**) A fingerprint string value that represents a concrete occurrence of the vulnerability. It's used to determine whether two vulnerability occurrences are same or different. May not be 100% accurate. **This is NOT a [CVE](https://cve.mitre.org/)**. |
|
||||||
| `vulnerabilities[].severity` | How much the vulnerability impacts the software. Possible values: `Undefined` (an analyzer has not provided this information), `Info`, `Unknown`, `Low`, `Medium`, `High`, `Critical`. |
|
| `vulnerabilities[].severity` | How much the vulnerability impacts the software. Possible values: `Undefined` (an analyzer has not provided this information), `Info`, `Unknown`, `Low`, `Medium`, `High`, `Critical`. |
|
||||||
| `vulnerabilities[].confidence` | How reliable the vulnerability's assessment is. Possible values: `Undefined` (an analyzer has not provided this information), `Ignore`, `Unknown`, `Experimental`, `Low`, `Medium`, `High`, `Confirmed`. |
|
| `vulnerabilities[].confidence` | How reliable the vulnerability's assessment is. Possible values: `Undefined` (an analyzer has not provided this information), `Ignore`, `Unknown`, `Experimental`, `Low`, `Medium`, `High`, `Confirmed`. |
|
||||||
| `vulnerabilities[].solution` | Explanation of how to fix the vulnerability. Optional. |
|
| `vulnerabilities[].solution` | Explanation of how to fix the vulnerability. Optional. |
|
||||||
|
|
|
@ -312,7 +312,7 @@ From there, you can see tracked over time:
|
||||||
|
|
||||||
If a significant percentage of traffic is anomalous, it should be investigated
|
If a significant percentage of traffic is anomalous, it should be investigated
|
||||||
for potential threats, which can be done by
|
for potential threats, which can be done by
|
||||||
[examining the application logs](#web-application-firewall-modsecurity).
|
[examining the Web Application Firewall logs](#web-application-firewall-modsecurity).
|
||||||
|
|
||||||
![Threat Monitoring](img/threat_monitoring_v12_9.png)
|
![Threat Monitoring](img/threat_monitoring_v12_9.png)
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ NGINX server metrics are detected, which tracks the pages and content directly s
|
||||||
| Throughput (req/sec) | `sum(rate(nginx_server_requests{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (code)` |
|
| Throughput (req/sec) | `sum(rate(nginx_server_requests{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (code)` |
|
||||||
| Latency (ms) | `avg(nginx_server_requestMsec{%{environment_filter}})` |
|
| Latency (ms) | `avg(nginx_server_requestMsec{%{environment_filter}})` |
|
||||||
| HTTP Error Rate (HTTP Errors / sec) | `sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m]))` |
|
| HTTP Error Rate (HTTP Errors / sec) | `sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m]))` |
|
||||||
|
| HTTP Error (%)| `sum(rate(nginx_server_requests{code=~"5.*", host="*", %{environment_filter}}[2m])) / sum(rate(nginx_server_requests{code="total", host="*", %{environment_filter}}[2m])) * 100` |
|
||||||
|
|
||||||
## Configuring Prometheus to monitor for NGINX metrics
|
## Configuring Prometheus to monitor for NGINX metrics
|
||||||
|
|
||||||
|
|
54
lib/tasks/gitlab/praefect.rake
Normal file
54
lib/tasks/gitlab/praefect.rake
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
namespace :gitlab do
|
||||||
|
namespace :praefect do
|
||||||
|
def int?(string)
|
||||||
|
true if Integer(string) rescue false
|
||||||
|
end
|
||||||
|
|
||||||
|
def print_checksums(header, row)
|
||||||
|
header.each_with_index do |val, i|
|
||||||
|
width = [val.length, row[i].length].max
|
||||||
|
header[i] = header[i].ljust(width)
|
||||||
|
row[i] = row[i].ljust(width)
|
||||||
|
end
|
||||||
|
|
||||||
|
header_str = header.join(' | ')
|
||||||
|
puts header_str
|
||||||
|
puts '-' * header_str.length
|
||||||
|
puts row.join(' | ')
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'GitLab | Praefect | Check replicas'
|
||||||
|
task :replicas, [:project_id] => :gitlab_environment do |t, args|
|
||||||
|
warn_user_is_not_gitlab
|
||||||
|
|
||||||
|
unless int?(args.project_id)
|
||||||
|
puts 'argument must be a valid project_id'
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
project = Project.find_by_id(args.project_id)
|
||||||
|
if project.nil?
|
||||||
|
puts 'No project was found with that id'
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
replicas_resp = project.repository.replicas
|
||||||
|
|
||||||
|
sorted_replicas = replicas_resp.replicas.sort_by { |r| r.repository.storage_name }
|
||||||
|
|
||||||
|
header = ['Project name'] << "#{replicas_resp.primary.repository.storage_name} (primary)"
|
||||||
|
header.concat(sorted_replicas.map { |r| r.repository.storage_name })
|
||||||
|
|
||||||
|
row = [project.name] << replicas_resp.primary.checksum
|
||||||
|
row.concat(sorted_replicas.map {|r| r.checksum})
|
||||||
|
rescue
|
||||||
|
puts 'Something went wrong when getting replicas.'
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "\n"
|
||||||
|
print_checksums(header, row)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -17951,6 +17951,9 @@ msgstr ""
|
||||||
msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
|
msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "SecurityDashboard|There was an error while generating the report."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "SecurityDashboard|Unable to add %{invalidProjects}"
|
msgid "SecurityDashboard|Unable to add %{invalidProjects}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -23402,6 +23405,9 @@ msgstr ""
|
||||||
msgid "You can move around the graph by using the arrow keys."
|
msgid "You can move around the graph by using the arrow keys."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "You can now export your security dashboard to a CSV report."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "You can now submit a merge request to get this change into the original branch."
|
msgid "You can now submit a merge request to get this change into the original branch."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -789,25 +789,49 @@ describe Projects::PipelinesController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when test_report contains attachment and scope is with_attachment as a URL param' do
|
context 'when junit_pipeline_screenshots_view is enabled' do
|
||||||
let(:pipeline) { create(:ci_pipeline, :with_test_reports_attachment, project: project) }
|
before do
|
||||||
|
stub_feature_flags(junit_pipeline_screenshots_view: { enabled: true, thing: project })
|
||||||
|
end
|
||||||
|
|
||||||
it 'returns a test reports with attachment' do
|
context 'when test_report contains attachment and scope is with_attachment as a URL param' do
|
||||||
get_test_report_json(scope: 'with_attachment')
|
let(:pipeline) { create(:ci_pipeline, :with_test_reports_attachment, project: project) }
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
it 'returns a test reports with attachment' do
|
||||||
expect(json_response["test_suites"]).to be_present
|
get_test_report_json(scope: 'with_attachment')
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(json_response["test_suites"]).to be_present
|
||||||
|
expect(json_response["test_suites"].first["test_cases"].first).to include("attachment_url")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when test_report does not contain attachment and scope is with_attachment as a URL param' do
|
||||||
|
let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
|
||||||
|
|
||||||
|
it 'returns a test reports with empty values' do
|
||||||
|
get_test_report_json(scope: 'with_attachment')
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(json_response["test_suites"]).to be_empty
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when test_report does not contain attachment and scope is with_attachment as a URL param' do
|
context 'when junit_pipeline_screenshots_view is disabled' do
|
||||||
let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
|
before do
|
||||||
|
stub_feature_flags(junit_pipeline_screenshots_view: { enabled: false, thing: project })
|
||||||
|
end
|
||||||
|
|
||||||
it 'returns a test reports with empty values' do
|
context 'when test_report contains attachment and scope is with_attachment as a URL param' do
|
||||||
get_test_report_json(scope: 'with_attachment')
|
let(:pipeline) { create(:ci_pipeline, :with_test_reports_attachment, project: project) }
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
it 'returns a test reports without attachment_url' do
|
||||||
expect(json_response["test_suites"]).to be_empty
|
get_test_report_json(scope: 'with_attachment')
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(json_response["test_suites"].first["test_cases"].first).not_to include("attachment_url")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -613,13 +613,7 @@ describe('diffs/components/app', () => {
|
||||||
expect(wrapper.contains(CompareVersions)).toBe(true);
|
expect(wrapper.contains(CompareVersions)).toBe(true);
|
||||||
expect(wrapper.find(CompareVersions).props()).toEqual(
|
expect(wrapper.find(CompareVersions).props()).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
targetBranch: {
|
|
||||||
branchName: 'target-branch',
|
|
||||||
versionIndex: -1,
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
mergeRequestDiffs: diffsMockData,
|
mergeRequestDiffs: diffsMockData,
|
||||||
mergeRequestDiff,
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { trimText } from 'helpers/text_helper';
|
||||||
|
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||||
|
import CompareDropdownLayout from '~/diffs/components/compare_dropdown_layout.vue';
|
||||||
|
|
||||||
|
const TEST_COMMIT_TEXT = '1 commit';
|
||||||
|
const TEST_CREATED_AT = '2018-10-23T11:49:16.611Z';
|
||||||
|
|
||||||
|
describe('CompareDropdownLayout', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
const createVersion = ({ id, isHead, isBase, selected, commitsText, createdAt }) => ({
|
||||||
|
id,
|
||||||
|
href: `version/${id}`,
|
||||||
|
versionName: `version ${id}`,
|
||||||
|
isHead,
|
||||||
|
isBase,
|
||||||
|
short_commit_sha: `abcdef${id}`,
|
||||||
|
commitsText,
|
||||||
|
created_at: createdAt,
|
||||||
|
selected,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createComponent = (propsData = {}) => {
|
||||||
|
wrapper = shallowMount(CompareDropdownLayout, {
|
||||||
|
propsData: {
|
||||||
|
...propsData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const findListItems = () => wrapper.findAll('li');
|
||||||
|
const findListItemsData = () =>
|
||||||
|
findListItems().wrappers.map(listItem => ({
|
||||||
|
href: listItem.find('a').attributes('href'),
|
||||||
|
text: trimText(listItem.text()),
|
||||||
|
createdAt: listItem.findAll(TimeAgo).wrappers[0]?.props('time'),
|
||||||
|
isActive: listItem.find('a.is-active').exists(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
wrapper = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with versions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const versions = [
|
||||||
|
createVersion({
|
||||||
|
id: 1,
|
||||||
|
isHead: false,
|
||||||
|
isBase: true,
|
||||||
|
selected: true,
|
||||||
|
commitsText: TEST_COMMIT_TEXT,
|
||||||
|
createdAt: TEST_CREATED_AT,
|
||||||
|
}),
|
||||||
|
createVersion({ id: 2, isHead: true, isBase: false, selected: false }),
|
||||||
|
createVersion({ id: 3, isHead: false, isBase: false, selected: false }),
|
||||||
|
];
|
||||||
|
|
||||||
|
createComponent({ versions });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the selected version name', () => {
|
||||||
|
expect(wrapper.text()).toContain('version 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders versions in order', () => {
|
||||||
|
expect(findListItemsData()).toEqual([
|
||||||
|
{
|
||||||
|
href: 'version/1',
|
||||||
|
text: 'version 1 (base) abcdef1 1 commit',
|
||||||
|
createdAt: TEST_CREATED_AT,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'version/2',
|
||||||
|
text: 'version 2 (HEAD) abcdef2',
|
||||||
|
createdAt: undefined,
|
||||||
|
isActive: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'version/3',
|
||||||
|
text: 'version 3 abcdef3',
|
||||||
|
createdAt: undefined,
|
||||||
|
isActive: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,179 +0,0 @@
|
||||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
|
||||||
import CompareVersionsDropdown from '~/diffs/components/compare_versions_dropdown.vue';
|
|
||||||
import diffsMockData from '../mock_data/merge_request_diffs';
|
|
||||||
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
|
||||||
import { TEST_HOST } from 'helpers/test_constants';
|
|
||||||
|
|
||||||
const localVue = createLocalVue();
|
|
||||||
const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 };
|
|
||||||
const startVersion = { version_index: 4 };
|
|
||||||
const mergeRequestVersion = {
|
|
||||||
version_path: '123',
|
|
||||||
};
|
|
||||||
const baseVersionPath = '/gnuwget/wget2/-/merge_requests/6/diffs?diff_id=37';
|
|
||||||
|
|
||||||
describe('CompareVersionsDropdown', () => {
|
|
||||||
let wrapper;
|
|
||||||
|
|
||||||
const findSelectedVersion = () => wrapper.find('.dropdown-menu-toggle');
|
|
||||||
const findVersionsListElements = () => wrapper.findAll('li');
|
|
||||||
const findLinkElement = index =>
|
|
||||||
findVersionsListElements()
|
|
||||||
.at(index)
|
|
||||||
.find('a');
|
|
||||||
const findLastLink = () => findLinkElement(findVersionsListElements().length - 1);
|
|
||||||
|
|
||||||
const createComponent = (props = {}) => {
|
|
||||||
wrapper = shallowMount(localVue.extend(CompareVersionsDropdown), {
|
|
||||||
localVue,
|
|
||||||
propsData: { ...props },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
wrapper.destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('selected version name', () => {
|
|
||||||
it('shows latest version when latest is selected', () => {
|
|
||||||
createComponent({
|
|
||||||
mergeRequestVersion,
|
|
||||||
startVersion,
|
|
||||||
otherVersions: diffsMockData,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(findSelectedVersion().text()).toBe('latest version');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows target branch name for base branch', () => {
|
|
||||||
createComponent({
|
|
||||||
targetBranch,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(findSelectedVersion().text()).toBe('tmp-wine-dev');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows correct version for non-base and non-latest branches', () => {
|
|
||||||
createComponent({
|
|
||||||
startVersion,
|
|
||||||
targetBranch,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(findSelectedVersion().text()).toBe(`version ${startVersion.version_index}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('target versions list', () => {
|
|
||||||
it('should have the same length as otherVersions if merge request version is present', () => {
|
|
||||||
createComponent({
|
|
||||||
mergeRequestVersion,
|
|
||||||
otherVersions: diffsMockData,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(findVersionsListElements().length).toEqual(diffsMockData.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have an otherVersions length plus 1 if no merge request version is present', () => {
|
|
||||||
createComponent({
|
|
||||||
targetBranch,
|
|
||||||
otherVersions: diffsMockData,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(findVersionsListElements().length).toEqual(diffsMockData.length + 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have base branch link as active on base branch', () => {
|
|
||||||
createComponent({
|
|
||||||
targetBranch,
|
|
||||||
otherVersions: diffsMockData,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(findLastLink().classes()).toContain('is-active');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have correct branch link as active if start version present', () => {
|
|
||||||
createComponent({
|
|
||||||
targetBranch,
|
|
||||||
startVersion,
|
|
||||||
otherVersions: diffsMockData,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(findLinkElement(0).classes()).toContain('is-active');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render a correct base version link', () => {
|
|
||||||
createComponent({
|
|
||||||
baseVersionPath,
|
|
||||||
otherVersions: diffsMockData.slice(1),
|
|
||||||
targetBranch,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(findLastLink().attributes('href')).toEqual(baseVersionPath);
|
|
||||||
expect(findLastLink().text()).toContain('(base)');
|
|
||||||
expect(findLastLink().text()).not.toContain('(HEAD)');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render a correct head version link', () => {
|
|
||||||
Object.defineProperty(window, 'location', {
|
|
||||||
writable: true,
|
|
||||||
value: { href: `${TEST_HOST}?diff_head=true` },
|
|
||||||
});
|
|
||||||
|
|
||||||
createComponent({
|
|
||||||
baseVersionPath,
|
|
||||||
otherVersions: diffsMockData.slice(1),
|
|
||||||
targetBranch,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(findLastLink().attributes('href')).toEqual(baseVersionPath);
|
|
||||||
expect(findLastLink().text()).not.toContain('(base)');
|
|
||||||
expect(findLastLink().text()).toContain('(HEAD)');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not render commits count if no showCommitsCount is passed', () => {
|
|
||||||
createComponent({
|
|
||||||
otherVersions: diffsMockData,
|
|
||||||
targetBranch,
|
|
||||||
});
|
|
||||||
|
|
||||||
const commitsCount = diffsMockData[0].commits_count;
|
|
||||||
|
|
||||||
expect(findLinkElement(0).text()).not.toContain(`${commitsCount} commit`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render correct commits count if showCommitsCount is passed', () => {
|
|
||||||
createComponent({
|
|
||||||
otherVersions: diffsMockData,
|
|
||||||
targetBranch,
|
|
||||||
showCommitCount: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const commitsCount = diffsMockData[0].commits_count;
|
|
||||||
|
|
||||||
expect(findLinkElement(0).text()).toContain(`${commitsCount} commit`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render correct commit sha', () => {
|
|
||||||
createComponent({
|
|
||||||
otherVersions: diffsMockData,
|
|
||||||
targetBranch,
|
|
||||||
});
|
|
||||||
|
|
||||||
const commitShaElement = findLinkElement(0).find('.commit-sha');
|
|
||||||
|
|
||||||
expect(commitShaElement.text()).toBe(diffsMockData[0].short_commit_sha);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render correct time-ago ', () => {
|
|
||||||
createComponent({
|
|
||||||
otherVersions: diffsMockData,
|
|
||||||
targetBranch,
|
|
||||||
});
|
|
||||||
|
|
||||||
const timeAgoElement = findLinkElement(0).find(TimeAgo);
|
|
||||||
|
|
||||||
expect(timeAgoElement.exists()).toBe(true);
|
|
||||||
expect(timeAgoElement.props('time')).toBe(diffsMockData[0].created_at);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -12,23 +12,25 @@ localVue.use(Vuex);
|
||||||
|
|
||||||
describe('CompareVersions', () => {
|
describe('CompareVersions', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 };
|
const targetBranchName = 'tmp-wine-dev';
|
||||||
|
|
||||||
const createWrapper = props => {
|
const createWrapper = props => {
|
||||||
const store = createStore();
|
const store = createStore();
|
||||||
|
const mergeRequestDiff = diffsMockData[0];
|
||||||
|
|
||||||
store.state.diffs.addedLines = 10;
|
store.state.diffs.addedLines = 10;
|
||||||
store.state.diffs.removedLines = 20;
|
store.state.diffs.removedLines = 20;
|
||||||
store.state.diffs.diffFiles.push('test');
|
store.state.diffs.diffFiles.push('test');
|
||||||
|
store.state.diffs.targetBranchName = targetBranchName;
|
||||||
|
store.state.diffs.mergeRequestDiff = mergeRequestDiff;
|
||||||
|
store.state.diffs.mergeRequestDiffs = diffsMockData;
|
||||||
|
|
||||||
wrapper = mount(CompareVersionsComponent, {
|
wrapper = mount(CompareVersionsComponent, {
|
||||||
localVue,
|
localVue,
|
||||||
store,
|
store,
|
||||||
propsData: {
|
propsData: {
|
||||||
mergeRequestDiffs: diffsMockData,
|
mergeRequestDiffs: diffsMockData,
|
||||||
mergeRequestDiff: diffsMockData[0],
|
|
||||||
diffFilesLength: 0,
|
diffFilesLength: 0,
|
||||||
targetBranch,
|
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -59,7 +61,7 @@ describe('CompareVersions', () => {
|
||||||
expect(sourceDropdown.exists()).toBe(true);
|
expect(sourceDropdown.exists()).toBe(true);
|
||||||
expect(targetDropdown.exists()).toBe(true);
|
expect(targetDropdown.exists()).toBe(true);
|
||||||
expect(sourceDropdown.find('a span').html()).toContain('latest version');
|
expect(sourceDropdown.find('a span').html()).toContain('latest version');
|
||||||
expect(targetDropdown.find('a span').html()).toContain(targetBranch.branchName);
|
expect(targetDropdown.find('a span').html()).toContain(targetBranchName);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not render comparison dropdowns if no mergeRequestDiffs are specified', () => {
|
it('should not render comparison dropdowns if no mergeRequestDiffs are specified', () => {
|
||||||
|
@ -119,21 +121,6 @@ describe('CompareVersions', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('comparableDiffs', () => {
|
|
||||||
it('should not contain the first item in the mergeRequestDiffs property', () => {
|
|
||||||
const { comparableDiffs } = wrapper.vm;
|
|
||||||
const comparableDiffsMock = diffsMockData.slice(1);
|
|
||||||
|
|
||||||
expect(comparableDiffs).toEqual(comparableDiffsMock);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('baseVersionPath', () => {
|
|
||||||
it('should be set correctly from mergeRequestDiff', () => {
|
|
||||||
expect(wrapper.vm.baseVersionPath).toEqual(wrapper.vm.mergeRequestDiff.base_version_path);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('commit', () => {
|
describe('commit', () => {
|
||||||
beforeEach(done => {
|
beforeEach(done => {
|
||||||
wrapper.vm.$store.state.diffs.commit = getDiffWithCommit().commit;
|
wrapper.vm.$store.state.diffs.commit = getDiffWithCommit().commit;
|
||||||
|
|
100
spec/frontend/diffs/store/getters_versions_dropdowns_spec.js
Normal file
100
spec/frontend/diffs/store/getters_versions_dropdowns_spec.js
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import * as getters from '~/diffs/store/getters';
|
||||||
|
import state from '~/diffs/store/modules/diff_state';
|
||||||
|
import { DIFF_COMPARE_BASE_VERSION_INDEX } from '~/diffs/constants';
|
||||||
|
import diffsMockData from '../mock_data/merge_request_diffs';
|
||||||
|
|
||||||
|
describe('Compare diff version dropdowns', () => {
|
||||||
|
let localState;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
localState = state();
|
||||||
|
localState.mergeRequestDiff = {
|
||||||
|
base_version_path: 'basePath',
|
||||||
|
head_version_path: 'headPath',
|
||||||
|
version_index: 1,
|
||||||
|
};
|
||||||
|
localState.targetBranchName = 'baseVersion';
|
||||||
|
localState.mergeRequestDiffs = diffsMockData;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('selectedTargetIndex', () => {
|
||||||
|
it('without startVersion', () => {
|
||||||
|
expect(getters.selectedTargetIndex(localState)).toEqual(DIFF_COMPARE_BASE_VERSION_INDEX);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with startVersion', () => {
|
||||||
|
const startVersion = { version_index: 1 };
|
||||||
|
localState.startVersion = startVersion;
|
||||||
|
expect(getters.selectedTargetIndex(localState)).toEqual(startVersion.version_index);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selectedSourceIndex', () => {
|
||||||
|
expect(getters.selectedSourceIndex(localState)).toEqual(
|
||||||
|
localState.mergeRequestDiff.version_index,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('diffCompareDropdownTargetVersions', () => {
|
||||||
|
// diffCompareDropdownTargetVersions slices the array at the first position
|
||||||
|
// and appends a "base" version which is why we use diffsMockData[1] below
|
||||||
|
// This is to display "base" at the end of the target dropdown
|
||||||
|
const expectedFirstVersion = {
|
||||||
|
...diffsMockData[1],
|
||||||
|
href: expect.any(String),
|
||||||
|
versionName: expect.any(String),
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedBaseVersion = {
|
||||||
|
versionName: 'baseVersion',
|
||||||
|
version_index: DIFF_COMPARE_BASE_VERSION_INDEX,
|
||||||
|
href: 'basePath',
|
||||||
|
isBase: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('base version selected', () => {
|
||||||
|
expectedFirstVersion.selected = false;
|
||||||
|
expectedBaseVersion.selected = true;
|
||||||
|
|
||||||
|
const targetVersions = getters.diffCompareDropdownTargetVersions(localState, {
|
||||||
|
selectedTargetIndex: DIFF_COMPARE_BASE_VERSION_INDEX,
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastVersion = targetVersions[targetVersions.length - 1];
|
||||||
|
expect(targetVersions[0]).toEqual(expectedFirstVersion);
|
||||||
|
expect(lastVersion).toEqual(expectedBaseVersion);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('first version selected', () => {
|
||||||
|
expectedFirstVersion.selected = true;
|
||||||
|
expectedBaseVersion.selected = false;
|
||||||
|
|
||||||
|
localState.startVersion = expectedFirstVersion;
|
||||||
|
|
||||||
|
const targetVersions = getters.diffCompareDropdownTargetVersions(localState, {
|
||||||
|
selectedTargetIndex: expectedFirstVersion.version_index,
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastVersion = targetVersions[targetVersions.length - 1];
|
||||||
|
expect(targetVersions[0]).toEqual(expectedFirstVersion);
|
||||||
|
expect(lastVersion).toEqual(expectedBaseVersion);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('diffCompareDropdownSourceVersions', () => {
|
||||||
|
const firstDiff = localState.mergeRequestDiffs[0];
|
||||||
|
const expectedShape = {
|
||||||
|
...firstDiff,
|
||||||
|
href: firstDiff.version_path,
|
||||||
|
commitsText: `${firstDiff.commits_count} commits,`,
|
||||||
|
versionName: 'latest version',
|
||||||
|
selected: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sourceVersions = getters.diffCompareDropdownSourceVersions(localState, {
|
||||||
|
selectedSourceIndex: expectedShape.version_index,
|
||||||
|
});
|
||||||
|
expect(sourceVersions[0]).toEqual(expectedShape);
|
||||||
|
expect(sourceVersions[1].selected).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,6 +1,7 @@
|
||||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
import NoteHeader from '~/notes/components/note_header.vue';
|
import NoteHeader from '~/notes/components/note_header.vue';
|
||||||
|
import GitlabTeamMemberBadge from '~/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue';
|
||||||
|
|
||||||
const localVue = createLocalVue();
|
const localVue = createLocalVue();
|
||||||
localVue.use(Vuex);
|
localVue.use(Vuex);
|
||||||
|
@ -17,6 +18,15 @@ describe('NoteHeader component', () => {
|
||||||
const findActionText = () => wrapper.find({ ref: 'actionText' });
|
const findActionText = () => wrapper.find({ ref: 'actionText' });
|
||||||
const findTimestamp = () => wrapper.find({ ref: 'noteTimestamp' });
|
const findTimestamp = () => wrapper.find({ ref: 'noteTimestamp' });
|
||||||
|
|
||||||
|
const author = {
|
||||||
|
avatar_url: null,
|
||||||
|
id: 1,
|
||||||
|
name: 'Root',
|
||||||
|
path: '/root',
|
||||||
|
state: 'active',
|
||||||
|
username: 'root',
|
||||||
|
};
|
||||||
|
|
||||||
const createComponent = props => {
|
const createComponent = props => {
|
||||||
wrapper = shallowMount(NoteHeader, {
|
wrapper = shallowMount(NoteHeader, {
|
||||||
localVue,
|
localVue,
|
||||||
|
@ -83,16 +93,7 @@ describe('NoteHeader component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders an author link if author is passed to props', () => {
|
it('renders an author link if author is passed to props', () => {
|
||||||
createComponent({
|
createComponent({ author });
|
||||||
author: {
|
|
||||||
avatar_url: null,
|
|
||||||
id: 1,
|
|
||||||
name: 'Root',
|
|
||||||
path: '/root',
|
|
||||||
state: 'active',
|
|
||||||
username: 'root',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(wrapper.find('.js-user-link').exists()).toBe(true);
|
expect(wrapper.find('.js-user-link').exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
@ -138,4 +139,18 @@ describe('NoteHeader component', () => {
|
||||||
expect(actions.setTargetNoteHash).toHaveBeenCalled();
|
expect(actions.setTargetNoteHash).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.each`
|
||||||
|
props | expected | message1 | message2
|
||||||
|
${{ author: { ...author, is_gitlab_employee: true } }} | ${true} | ${'renders'} | ${'true'}
|
||||||
|
${{ author: { ...author, is_gitlab_employee: false } }} | ${false} | ${"doesn't render"} | ${'false'}
|
||||||
|
${{ author }} | ${false} | ${"doesn't render"} | ${'undefined'}
|
||||||
|
`(
|
||||||
|
'$message1 GitLab team member badge when `is_gitlab_employee` is $message2',
|
||||||
|
({ props, expected }) => {
|
||||||
|
createComponent(props);
|
||||||
|
|
||||||
|
expect(wrapper.find(GitlabTeamMemberBadge).exists()).toBe(expected);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@ def match_mr1_note(content_regex)
|
||||||
MergeRequest.find_by(title: 'MR1').notes.select { |n| n.note.match(/#{content_regex}/)}.first
|
MergeRequest.find_by(title: 'MR1').notes.select { |n| n.note.match(/#{content_regex}/)}.first
|
||||||
end
|
end
|
||||||
|
|
||||||
describe Gitlab::ImportExport::Project::TreeRestorer do
|
describe Gitlab::ImportExport::Project::TreeRestorer, quarantine: { flaky: 'https://gitlab.com/gitlab-org/gitlab/-/issues/213793' } do
|
||||||
include ImportExport::CommonUtil
|
include ImportExport::CommonUtil
|
||||||
|
|
||||||
let(:shared) { project.import_export_shared }
|
let(:shared) { project.import_export_shared }
|
||||||
|
|
|
@ -3,16 +3,16 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe SearchService do
|
describe SearchService do
|
||||||
let(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
let(:accessible_group) { create(:group, :private) }
|
let_it_be(:accessible_group) { create(:group, :private) }
|
||||||
let(:inaccessible_group) { create(:group, :private) }
|
let_it_be(:inaccessible_group) { create(:group, :private) }
|
||||||
let!(:group_member) { create(:group_member, group: accessible_group, user: user) }
|
let_it_be(:group_member) { create(:group_member, group: accessible_group, user: user) }
|
||||||
|
|
||||||
let!(:accessible_project) { create(:project, :private, name: 'accessible_project') }
|
let_it_be(:accessible_project) { create(:project, :repository, :private, name: 'accessible_project') }
|
||||||
let(:note) { create(:note_on_issue, project: accessible_project) }
|
let_it_be(:note) { create(:note_on_issue, project: accessible_project) }
|
||||||
|
|
||||||
let!(:inaccessible_project) { create(:project, :private, name: 'inaccessible_project') }
|
let_it_be(:inaccessible_project) { create(:project, :repository, :private, name: 'inaccessible_project') }
|
||||||
|
|
||||||
let(:snippet) { create(:snippet, author: user) }
|
let(:snippet) { create(:snippet, author: user) }
|
||||||
let(:group_project) { create(:project, group: accessible_group, name: 'group_project') }
|
let(:group_project) { create(:project, group: accessible_group, name: 'group_project') }
|
||||||
|
@ -298,67 +298,129 @@ describe SearchService do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'redacting search results' do
|
context 'redacting search results' do
|
||||||
shared_examples 'it redacts incorrect results' do
|
let(:search) { 'anything' }
|
||||||
before do
|
|
||||||
allow(Ability).to receive(:allowed?).and_return(allowed)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when allowed' do
|
subject(:result) { search_service.search_objects }
|
||||||
let(:allowed) { true }
|
|
||||||
|
|
||||||
it 'does nothing' do
|
def found_blob(project)
|
||||||
expect(results).not_to be_empty
|
Gitlab::Search::FoundBlob.new(project: project)
|
||||||
expect(results).to all(be_an(model_class))
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when disallowed' do
|
def found_wiki_page(project)
|
||||||
let(:allowed) { false }
|
Gitlab::Search::FoundWikiPage.new(found_blob(project))
|
||||||
|
end
|
||||||
|
|
||||||
it 'does nothing' do
|
before do
|
||||||
expect(results).to be_empty
|
expect(search_service)
|
||||||
end
|
.to receive(:search_results)
|
||||||
end
|
.and_return(double('search results', objects: unredacted_results))
|
||||||
|
end
|
||||||
|
|
||||||
|
def ar_relation(klass, *objects)
|
||||||
|
klass.id_in(objects.map(&:id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def kaminari_array(*objects)
|
||||||
|
Kaminari.paginate_array(objects).page(1).per(20)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'issues' do
|
context 'issues' do
|
||||||
let(:issue) { create(:issue, project: accessible_project) }
|
let(:readable) { create(:issue, project: accessible_project) }
|
||||||
|
let(:unreadable) { create(:issue, project: inaccessible_project) }
|
||||||
|
let(:unredacted_results) { ar_relation(Issue, readable, unreadable) }
|
||||||
let(:scope) { 'issues' }
|
let(:scope) { 'issues' }
|
||||||
let(:model_class) { Issue }
|
|
||||||
let(:ability) { :read_issue }
|
|
||||||
let(:search) { issue.title }
|
|
||||||
let(:results) { subject.search_objects }
|
|
||||||
|
|
||||||
it_behaves_like 'it redacts incorrect results'
|
it 'redacts the inaccessible issue' do
|
||||||
|
expect(result).to contain_exactly(readable)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'notes' do
|
context 'notes' do
|
||||||
let(:note) { create(:note_on_commit, project: accessible_project) }
|
let(:readable) { create(:note_on_commit, project: accessible_project) }
|
||||||
|
let(:unreadable) { create(:note_on_commit, project: inaccessible_project) }
|
||||||
|
let(:unredacted_results) { ar_relation(Note, readable, unreadable) }
|
||||||
let(:scope) { 'notes' }
|
let(:scope) { 'notes' }
|
||||||
let(:model_class) { Note }
|
|
||||||
let(:ability) { :read_note }
|
|
||||||
let(:search) { note.note }
|
|
||||||
let(:results) do
|
|
||||||
described_class.new(
|
|
||||||
user,
|
|
||||||
project_id: accessible_project.id,
|
|
||||||
scope: scope,
|
|
||||||
search: note.note
|
|
||||||
).search_objects
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'it redacts incorrect results'
|
it 'redacts the inaccessible note' do
|
||||||
|
expect(result).to contain_exactly(readable)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'merge_requests' do
|
context 'merge_requests' do
|
||||||
|
let(:readable) { create(:merge_request, source_project: accessible_project, author: user) }
|
||||||
|
let(:unreadable) { create(:merge_request, source_project: inaccessible_project) }
|
||||||
|
let(:unredacted_results) { ar_relation(MergeRequest, readable, unreadable) }
|
||||||
let(:scope) { 'merge_requests' }
|
let(:scope) { 'merge_requests' }
|
||||||
let(:model_class) { MergeRequest }
|
|
||||||
let(:ability) { :read_merge_request }
|
|
||||||
let(:merge_request) { create(:merge_request, source_project: accessible_project, author: user) }
|
|
||||||
let(:search) { merge_request.title }
|
|
||||||
let(:results) { subject.search_objects }
|
|
||||||
|
|
||||||
it_behaves_like 'it redacts incorrect results'
|
it 'redacts the inaccessible merge request' do
|
||||||
|
expect(result).to contain_exactly(readable)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'project repository blobs' do
|
||||||
|
let(:readable) { found_blob(accessible_project) }
|
||||||
|
let(:unreadable) { found_blob(inaccessible_project) }
|
||||||
|
let(:unredacted_results) { kaminari_array(readable, unreadable) }
|
||||||
|
let(:scope) { 'blobs' }
|
||||||
|
|
||||||
|
it 'redacts the inaccessible blob' do
|
||||||
|
expect(result).to contain_exactly(readable)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'project wiki blobs' do
|
||||||
|
let(:readable) { found_wiki_page(accessible_project) }
|
||||||
|
let(:unreadable) { found_wiki_page(inaccessible_project) }
|
||||||
|
let(:unredacted_results) { kaminari_array(readable, unreadable) }
|
||||||
|
let(:scope) { 'wiki_blobs' }
|
||||||
|
|
||||||
|
it 'redacts the inaccessible blob' do
|
||||||
|
expect(result).to contain_exactly(readable)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'project snippets' do
|
||||||
|
let(:readable) { create(:project_snippet, project: accessible_project) }
|
||||||
|
let(:unreadable) { create(:project_snippet, project: inaccessible_project) }
|
||||||
|
let(:unredacted_results) { ar_relation(ProjectSnippet, readable, unreadable) }
|
||||||
|
let(:scope) { 'snippet_blobs' }
|
||||||
|
|
||||||
|
it 'redacts the inaccessible snippet' do
|
||||||
|
expect(result).to contain_exactly(readable)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'personal snippets' do
|
||||||
|
let(:readable) { create(:personal_snippet, :private, author: user) }
|
||||||
|
let(:unreadable) { create(:personal_snippet, :private) }
|
||||||
|
let(:unredacted_results) { ar_relation(PersonalSnippet, readable, unreadable) }
|
||||||
|
let(:scope) { 'snippet_blobs' }
|
||||||
|
|
||||||
|
it 'redacts the inaccessible snippet' do
|
||||||
|
expect(result).to contain_exactly(readable)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'commits' do
|
||||||
|
let(:readable) { accessible_project.commit }
|
||||||
|
let(:unreadable) { inaccessible_project.commit }
|
||||||
|
let(:unredacted_results) { kaminari_array(readable, unreadable) }
|
||||||
|
let(:scope) { 'commits' }
|
||||||
|
|
||||||
|
it 'redacts the inaccessible commit' do
|
||||||
|
expect(result).to contain_exactly(readable)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'users' do
|
||||||
|
let(:other_user) { create(:user) }
|
||||||
|
let(:unredacted_results) { ar_relation(User, user, other_user) }
|
||||||
|
let(:scope) { 'users' }
|
||||||
|
|
||||||
|
it 'passes the users through' do
|
||||||
|
# Users are always visible to everyone
|
||||||
|
expect(result).to contain_exactly(user, other_user)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
52
spec/tasks/gitlab/praefect_rake_spec.rb
Normal file
52
spec/tasks/gitlab/praefect_rake_spec.rb
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rake_helper'
|
||||||
|
|
||||||
|
describe 'gitlab:praefect:replicas' do
|
||||||
|
before do
|
||||||
|
Rake.application.rake_require 'tasks/gitlab/praefect'
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:project) { create(:project, :repository) }
|
||||||
|
let(:repository) { project.repository }
|
||||||
|
|
||||||
|
describe 'replicas', :praefect do
|
||||||
|
context 'when a valid project id is used as the argument' do
|
||||||
|
let(:project_arg) { project.id }
|
||||||
|
|
||||||
|
it "calls praefect info service's replicas method" do
|
||||||
|
expect_any_instance_of(Gitlab::GitalyClient::PraefectInfoService).to receive(:replicas).and_call_original
|
||||||
|
|
||||||
|
run_rake_task('gitlab:praefect:replicas', project_arg)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'prints out the expected row' do
|
||||||
|
row = /#{project.name}\s+\| #{project.repository.checksum}/
|
||||||
|
|
||||||
|
expect { run_rake_task('gitlab:praefect:replicas', project_arg) }.to output(row).to_stdout
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a non existent project id is used as the argument' do
|
||||||
|
let(:project_arg) { '2' }
|
||||||
|
|
||||||
|
it "does not call praefect info service's replicas method" do
|
||||||
|
expect_any_instance_of(Gitlab::GitalyClient::PraefectInfoService).not_to receive(:replicas)
|
||||||
|
|
||||||
|
run_rake_task('gitlab:praefect:replicas', project_arg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when replicas throws an exception' do
|
||||||
|
before do
|
||||||
|
allow_next_instance_of(Gitlab::GitalyClient::PraefectInfoService) do |instance|
|
||||||
|
expect(instance).to receive(:replicas).and_raise("error")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'aborts with the correct error message' do
|
||||||
|
expect { run_rake_task('gitlab:praefect:replicas', project.id) }.to output("Something went wrong when getting replicas.\n").to_stdout
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue