Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1d3086ebb4
commit
83fc2f3dc8
|
@ -71,8 +71,8 @@ export default {
|
|||
<ul class="notes draft-notes">
|
||||
<noteable-note
|
||||
:note="draft"
|
||||
:diff-lines="diffFile.highlighted_diff_lines"
|
||||
:line="line"
|
||||
:discussion-root="true"
|
||||
class="draft-note"
|
||||
@handleEdit="handleEditing"
|
||||
@cancelForm="handleNotEditing"
|
||||
|
|
|
@ -35,11 +35,15 @@ export default {
|
|||
<tr :class="className" class="notes_holder">
|
||||
<td class="notes_line old"></td>
|
||||
<td class="notes-content parallel old" colspan="2">
|
||||
<div v-if="leftDraft.isDraft" class="content"><draft-note :draft="leftDraft" /></div>
|
||||
<div v-if="leftDraft.isDraft" class="content">
|
||||
<draft-note :draft="leftDraft" :line="line.left" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="notes_line new"></td>
|
||||
<td class="notes-content parallel new" colspan="2">
|
||||
<div v-if="rightDraft.isDraft" class="content"><draft-note :draft="rightDraft" /></div>
|
||||
<div v-if="rightDraft.isDraft" class="content">
|
||||
<draft-note :draft="rightDraft" :line="line.right" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
|
|
@ -52,14 +52,12 @@ export default {
|
|||
});
|
||||
},
|
||||
linePosition() {
|
||||
if (this.draft.position && this.draft.position.position_type === IMAGE_DIFF_POSITION_TYPE) {
|
||||
if (this.position?.position_type === IMAGE_DIFF_POSITION_TYPE) {
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
return `${this.draft.position.x}x ${this.draft.position.y}y`;
|
||||
return `${this.position.x}x ${this.position.y}y`;
|
||||
}
|
||||
|
||||
const position = this.discussion ? this.discussion.position : this.draft.position;
|
||||
|
||||
return position?.new_line || position?.old_line;
|
||||
return this.position?.new_line || this.position?.old_line;
|
||||
},
|
||||
content() {
|
||||
const el = document.createElement('div');
|
||||
|
@ -70,11 +68,14 @@ export default {
|
|||
showLinePosition() {
|
||||
return this.draft.file_hash || this.isDiffDiscussion;
|
||||
},
|
||||
position() {
|
||||
return this.draft.position || this.discussion.position;
|
||||
},
|
||||
startLineNumber() {
|
||||
return getStartLineNumber(this.draft.position?.line_range);
|
||||
return getStartLineNumber(this.position?.line_range);
|
||||
},
|
||||
endLineNumber() {
|
||||
return getEndLineNumber(this.draft.position?.line_range);
|
||||
return getEndLineNumber(this.position?.line_range);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -15,14 +15,14 @@ export function createHeader(childElementCount, mergeRequestCount) {
|
|||
const headerText = getHeaderText(childElementCount, mergeRequestCount);
|
||||
|
||||
return $('<span />', {
|
||||
class: 'append-right-5',
|
||||
class: 'gl-mr-2',
|
||||
text: headerText,
|
||||
});
|
||||
}
|
||||
|
||||
export function createLink(mergeRequest) {
|
||||
return $('<a />', {
|
||||
class: 'append-right-5',
|
||||
class: 'gl-mr-2',
|
||||
href: mergeRequest.path,
|
||||
text: `!${mergeRequest.iid}`,
|
||||
});
|
||||
|
|
|
@ -163,16 +163,11 @@ export default {
|
|||
:name="collapseIcon"
|
||||
:size="16"
|
||||
aria-hidden="true"
|
||||
class="diff-toggle-caret append-right-5"
|
||||
class="diff-toggle-caret gl-mr-2"
|
||||
@click.stop="handleToggleFile"
|
||||
/>
|
||||
<a v-once ref="titleWrapper" class="gl-mr-2" :href="titleLink" @click="handleFileNameClick">
|
||||
<file-icon
|
||||
:file-name="filePath"
|
||||
:size="18"
|
||||
aria-hidden="true"
|
||||
css-classes="append-right-5"
|
||||
/>
|
||||
<file-icon :file-name="filePath" :size="18" aria-hidden="true" css-classes="gl-mr-2" />
|
||||
<span v-if="isFileRenamed">
|
||||
<strong
|
||||
v-gl-tooltip
|
||||
|
@ -208,7 +203,7 @@ export default {
|
|||
{{ diffFile.a_mode }} → {{ diffFile.b_mode }}
|
||||
</small>
|
||||
|
||||
<span v-if="isUsingLfs" class="label label-lfs append-right-5"> {{ __('LFS') }} </span>
|
||||
<span v-if="isUsingLfs" class="label label-lfs gl-mr-2"> {{ __('LFS') }} </span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
|
@ -8,7 +8,10 @@ import MultilineCommentForm from '../../notes/components/multiline_comment_form.
|
|||
import autosave from '../../notes/mixins/autosave';
|
||||
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import { DIFF_NOTE_TYPE } from '../constants';
|
||||
import { commentLineOptions } from '../../notes/components/multiline_comment_utils';
|
||||
import {
|
||||
commentLineOptions,
|
||||
formatLineRange,
|
||||
} from '../../notes/components/multiline_comment_utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -44,8 +47,10 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
commentLineStart: {
|
||||
lineCode: this.line.line_code,
|
||||
line_code: this.line.line_code,
|
||||
type: this.line.type,
|
||||
old_line: this.line.old_line,
|
||||
new_line: this.line.new_line,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -74,19 +79,26 @@ export default {
|
|||
diffViewType: this.diffViewType,
|
||||
diffFile: this.diffFile,
|
||||
linePosition: this.linePosition,
|
||||
lineRange: {
|
||||
start_line_code: this.commentLineStart.lineCode,
|
||||
start_line_type: this.commentLineStart.type,
|
||||
end_line_code: this.line.line_code,
|
||||
end_line_type: this.line.type,
|
||||
},
|
||||
lineRange: formatLineRange(this.commentLineStart, this.line),
|
||||
};
|
||||
},
|
||||
diffFile() {
|
||||
return this.getDiffFileByHash(this.diffFileHash);
|
||||
},
|
||||
commentLineOptions() {
|
||||
return commentLineOptions(this.diffFile.highlighted_diff_lines, this.line.line_code);
|
||||
const combineSides = (acc, { left, right }) => {
|
||||
// ignore null values match lines
|
||||
if (left && left.type !== 'match') acc.push(left);
|
||||
// if the line_codes are identically, return to avoid duplicates
|
||||
if (left?.line_code === right?.line_code) return acc;
|
||||
if (right && right.type !== 'match') acc.push(right);
|
||||
return acc;
|
||||
};
|
||||
const side = this.line.type === 'new' ? 'right' : 'left';
|
||||
const lines = this.diffFile.highlighted_diff_lines.length
|
||||
? this.diffFile.highlighted_diff_lines
|
||||
: this.diffFile.parallel_diff_lines.reduce(combineSides, []);
|
||||
return commentLineOptions(lines, this.line, this.line.line_code, side);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
@ -136,10 +148,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<div class="content discussion-form discussion-form-container discussion-notes">
|
||||
<div
|
||||
v-if="glFeatures.multilineComments"
|
||||
class="gl-mb-3 gl-text-gray-700 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-pb-3"
|
||||
>
|
||||
<div v-if="glFeatures.multilineComments" class="gl-mb-3 gl-text-gray-700">
|
||||
<multiline-comment-form
|
||||
v-model="commentLineStart"
|
||||
:line="line"
|
||||
|
|
|
@ -80,14 +80,9 @@ export default {
|
|||
<div ref="header" class="file-title file-title-flex-parent">
|
||||
<div class="file-header-content d-flex align-content-center">
|
||||
<div v-if="hasCode" class="d-inline-block cursor-pointer" @click="toggle()">
|
||||
<icon :name="collapseIcon" :size="16" aria-hidden="true" class="append-right-5" />
|
||||
<icon :name="collapseIcon" :size="16" aria-hidden="true" class="gl-mr-2" />
|
||||
</div>
|
||||
<file-icon
|
||||
:file-name="filePath"
|
||||
:size="18"
|
||||
aria-hidden="true"
|
||||
css-classes="append-right-5"
|
||||
/>
|
||||
<file-icon :file-name="filePath" :size="18" aria-hidden="true" css-classes="gl-mr-2" />
|
||||
<strong
|
||||
v-gl-tooltip
|
||||
:title="filePath"
|
||||
|
|
|
@ -108,6 +108,7 @@ export default {
|
|||
:commit="commit"
|
||||
:help-page-path="helpPagePath"
|
||||
:show-reply-button="userCanReply"
|
||||
:discussion-root="true"
|
||||
@handleDeleteNote="$emit('deleteNote')"
|
||||
@startReplying="$emit('startReplying')"
|
||||
>
|
||||
|
@ -151,6 +152,7 @@ export default {
|
|||
:note="componentData(note)"
|
||||
:help-page-path="helpPagePath"
|
||||
:line="diffLine"
|
||||
:discussion-root="index === 0"
|
||||
@handleDeleteNote="$emit('deleteNote')"
|
||||
>
|
||||
<slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
|
||||
|
|
|
@ -21,10 +21,23 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
commentLineStart: {
|
||||
lineCode: this.lineRange ? this.lineRange.start_line_code : this.line.line_code,
|
||||
type: this.lineRange ? this.lineRange.start_line_type : this.line.type,
|
||||
},
|
||||
commentLineStart: {},
|
||||
commentLineEndType: this.lineRange?.end?.line_type || this.line.type,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
lineNumber() {
|
||||
return this.commentLineOptions[this.commentLineOptions.length - 1].text;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
const line = this.lineRange?.start || this.line;
|
||||
|
||||
this.commentLineStart = {
|
||||
line_code: line.line_code,
|
||||
type: line.type,
|
||||
old_line: line.old_line,
|
||||
new_line: line.new_line,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
@ -34,6 +47,10 @@ export default {
|
|||
getLineClasses(line) {
|
||||
return getLineClasses(line);
|
||||
},
|
||||
updateCommentLineStart(value) {
|
||||
this.commentLineStart = value;
|
||||
this.$emit('input', value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -55,12 +72,12 @@ export default {
|
|||
:options="commentLineOptions"
|
||||
size="sm"
|
||||
class="gl-w-auto gl-vertical-align-baseline"
|
||||
@change="$emit('input', $event)"
|
||||
@change="updateCommentLineStart"
|
||||
/>
|
||||
</template>
|
||||
<template #end>
|
||||
<span :class="getLineClasses(line)">
|
||||
{{ getSymbol(line) + (line.new_line || line.old_line) }}
|
||||
{{ lineNumber }}
|
||||
</span>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
|
|
|
@ -7,11 +7,19 @@ export function getSymbol(type) {
|
|||
}
|
||||
|
||||
function getLineNumber(lineRange, key) {
|
||||
if (!lineRange || !key) return '';
|
||||
const lineCode = lineRange[`${key}_line_code`] || '';
|
||||
const lineType = lineRange[`${key}_line_type`] || '';
|
||||
const lines = lineCode.split('_') || [];
|
||||
const lineNumber = lineType === 'old' ? lines[1] : lines[2];
|
||||
if (!lineRange || !key || !lineRange[key]) return '';
|
||||
const { new_line: newLine, old_line: oldLine, type } = lineRange[key];
|
||||
const otherKey = key === 'start' ? 'end' : 'start';
|
||||
|
||||
// By default we want to see the "old" or "left side" line number
|
||||
// The exception is if the "end" line is on the "right" side
|
||||
// `otherLineType` is only used if `type` is null to make sure the line
|
||||
// number relfects the "right" side number, if that is the side
|
||||
// the comment form is located on
|
||||
const otherLineType = !type ? lineRange[otherKey]?.type : null;
|
||||
const lineType = type || '';
|
||||
let lineNumber = oldLine;
|
||||
if (lineType === 'new' || otherLineType === 'new') lineNumber = newLine;
|
||||
return (lineNumber && getSymbol(lineType) + lineNumber) || '';
|
||||
}
|
||||
|
||||
|
@ -37,21 +45,48 @@ export function getLineClasses(line) {
|
|||
];
|
||||
}
|
||||
|
||||
export function commentLineOptions(diffLines, lineCode) {
|
||||
const selectedIndex = diffLines.findIndex(line => line.line_code === lineCode);
|
||||
export function commentLineOptions(diffLines, startingLine, lineCode, side = 'left') {
|
||||
const preferredSide = side === 'left' ? 'old_line' : 'new_line';
|
||||
const fallbackSide = preferredSide === 'new_line' ? 'old_line' : 'new_line';
|
||||
const notMatchType = l => l.type !== 'match';
|
||||
const linesCopy = [...diffLines]; // don't mutate the argument
|
||||
const startingLineCode = startingLine.line_code;
|
||||
|
||||
const currentIndex = linesCopy.findIndex(line => line.line_code === lineCode);
|
||||
|
||||
// We're limiting adding comments to only lines above the current line
|
||||
// to make rendering simpler. Future interations will use a more
|
||||
// intuitive dragging interface that will make this unnecessary
|
||||
const upToSelected = diffLines.slice(0, selectedIndex + 1);
|
||||
const upToSelected = linesCopy.slice(0, currentIndex + 1);
|
||||
|
||||
// Only include the lines up to the first "Show unchanged lines" block
|
||||
// i.e. not a "match" type
|
||||
const lines = takeRightWhile(upToSelected, notMatchType);
|
||||
|
||||
return lines.map(l => ({
|
||||
value: { lineCode: l.line_code, type: l.type },
|
||||
text: `${getSymbol(l.type)}${l.new_line || l.old_line}`,
|
||||
}));
|
||||
// If the selected line is "hidden" in an unchanged line block
|
||||
// or "above" the current group of lines add it to the array so
|
||||
// that the drop down is not defaulted to empty
|
||||
const selectedIndex = lines.findIndex(line => line.line_code === startingLineCode);
|
||||
if (selectedIndex < 0) lines.unshift(startingLine);
|
||||
|
||||
return lines.map(l => {
|
||||
const { line_code, type, old_line, new_line } = l;
|
||||
return {
|
||||
value: { line_code, type, old_line, new_line },
|
||||
text: `${getSymbol(type)}${l[preferredSide] || l[fallbackSide]}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function formatLineRange(start, end) {
|
||||
const extractProps = ({ line_code, type, old_line, new_line }) => ({
|
||||
line_code,
|
||||
type,
|
||||
old_line,
|
||||
new_line,
|
||||
});
|
||||
return {
|
||||
start: extractProps(start),
|
||||
end: extractProps(end),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
getEndLineNumber,
|
||||
getLineClasses,
|
||||
commentLineOptions,
|
||||
formatLineRange,
|
||||
} from './multiline_comment_utils';
|
||||
import MultilineCommentForm from './multiline_comment_form.vue';
|
||||
|
||||
|
@ -62,10 +63,15 @@ export default {
|
|||
default: false,
|
||||
},
|
||||
diffLines: {
|
||||
type: Object,
|
||||
type: Array,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
discussionRoot: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -73,10 +79,7 @@ export default {
|
|||
isDeleting: false,
|
||||
isRequesting: false,
|
||||
isResolving: false,
|
||||
commentLineStart: {
|
||||
line_code: this.line?.line_code,
|
||||
type: this.line?.type,
|
||||
},
|
||||
commentLineStart: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -144,25 +147,42 @@ export default {
|
|||
return getEndLineNumber(this.lineRange);
|
||||
},
|
||||
showMultiLineComment() {
|
||||
return (
|
||||
this.glFeatures.multilineComments &&
|
||||
this.startLineNumber &&
|
||||
this.endLineNumber &&
|
||||
(this.startLineNumber !== this.endLineNumber || this.isEditing)
|
||||
);
|
||||
if (!this.glFeatures.multilineComments) return false;
|
||||
if (this.isEditing) return true;
|
||||
|
||||
return this.line && this.discussionRoot && this.startLineNumber !== this.endLineNumber;
|
||||
},
|
||||
commentLineOptions() {
|
||||
if (this.diffLines) {
|
||||
return commentLineOptions(this.diffLines, this.line.line_code);
|
||||
if (!this.diffFile || !this.line) return [];
|
||||
|
||||
const sideA = this.line.type === 'new' ? 'right' : 'left';
|
||||
const sideB = sideA === 'left' ? 'right' : 'left';
|
||||
const lines = this.diffFile.highlighted_diff_lines.length
|
||||
? this.diffFile.highlighted_diff_lines
|
||||
: this.diffFile.parallel_diff_lines.map(l => l[sideA] || l[sideB]);
|
||||
return commentLineOptions(lines, this.commentLineStart, this.line.line_code, sideA);
|
||||
},
|
||||
diffFile() {
|
||||
if (this.commentLineStart.line_code) {
|
||||
const lineCode = this.commentLineStart.line_code.split('_')[0];
|
||||
return this.getDiffFileByHash(lineCode);
|
||||
}
|
||||
|
||||
const diffFile = this.diffFile || this.getDiffFileByHash(this.targetNoteHash);
|
||||
if (!diffFile) return null;
|
||||
return commentLineOptions(diffFile.highlighted_diff_lines, this.line.line_code);
|
||||
return null;
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
const line = this.note.position?.line_range?.start || this.line;
|
||||
|
||||
this.commentLineStart = line
|
||||
? {
|
||||
line_code: line.line_code,
|
||||
type: line.type,
|
||||
old_line: line.old_line,
|
||||
new_line: line.new_line,
|
||||
}
|
||||
: {};
|
||||
|
||||
eventHub.$on('enterEditMode', ({ noteId }) => {
|
||||
if (noteId === this.note.id) {
|
||||
this.isEditing = true;
|
||||
|
@ -224,13 +244,11 @@ export default {
|
|||
formUpdateHandler(noteText, parentElement, callback, resolveDiscussion) {
|
||||
const position = {
|
||||
...this.note.position,
|
||||
line_range: {
|
||||
start_line_code: this.commentLineStart?.lineCode,
|
||||
start_line_type: this.commentLineStart?.type,
|
||||
end_line_code: this.line?.line_code,
|
||||
end_line_type: this.line?.type,
|
||||
},
|
||||
};
|
||||
|
||||
if (this.commentLineStart && this.line)
|
||||
position.line_range = formatLineRange(this.commentLineStart, this.line);
|
||||
|
||||
this.$emit('handleUpdateNote', {
|
||||
note: this.note,
|
||||
noteText,
|
||||
|
@ -246,7 +264,7 @@ export default {
|
|||
note: {
|
||||
target_type: this.getNoteableData.targetType,
|
||||
target_id: this.note.noteable_id,
|
||||
note: { note: noteText },
|
||||
note: { note: noteText, position: JSON.stringify(position) },
|
||||
},
|
||||
};
|
||||
this.isRequesting = true;
|
||||
|
@ -317,14 +335,17 @@ export default {
|
|||
>
|
||||
<div v-if="showMultiLineComment" data-testid="multiline-comment">
|
||||
<multiline-comment-form
|
||||
v-if="isEditing && commentLineOptions && line"
|
||||
v-if="isEditing && note.position"
|
||||
v-model="commentLineStart"
|
||||
:line="line"
|
||||
:comment-line-options="commentLineOptions"
|
||||
:line-range="note.position.line_range"
|
||||
class="gl-mb-3 gl-text-gray-700 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-pb-3"
|
||||
class="gl-mb-3 gl-text-gray-700"
|
||||
/>
|
||||
<div v-else class="gl-mb-3 gl-text-gray-700">
|
||||
<div
|
||||
v-else
|
||||
class="gl-mb-3 gl-text-gray-700 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-pb-3"
|
||||
>
|
||||
<gl-sprintf :message="__('Comment on lines %{startLine} to %{endLine}')">
|
||||
<template #startLine>
|
||||
<span :class="getLineClasses(startLineNumber)">{{ startLineNumber }}</span>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { TEXT_DIFF_POSITION_TYPE, IMAGE_DIFF_POSITION_TYPE } from '~/diffs/const
|
|||
import createFlash from '~/flash';
|
||||
import { s__ } from '~/locale';
|
||||
import { clearDraft } from '~/lib/utils/autosave';
|
||||
import { formatLineRange } from '~/notes/components/multiline_comment_utils';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
|
@ -45,6 +46,9 @@ export default {
|
|||
});
|
||||
},
|
||||
addToReview(note) {
|
||||
const lineRange =
|
||||
(this.line && this.commentLineStart && formatLineRange(this.commentLineStart, this.line)) ||
|
||||
{};
|
||||
const positionType = this.diffFileCommentForm
|
||||
? IMAGE_DIFF_POSITION_TYPE
|
||||
: TEXT_DIFF_POSITION_TYPE;
|
||||
|
@ -60,6 +64,7 @@ export default {
|
|||
linePosition: this.position,
|
||||
positionType,
|
||||
...this.diffFileCommentForm,
|
||||
lineRange,
|
||||
});
|
||||
|
||||
const diffFileHeadSha = this.commit && this?.diffFile?.diff_refs?.head_sha;
|
||||
|
|
|
@ -38,11 +38,7 @@ export default {
|
|||
<template>
|
||||
<div class="report-block-list-issue-description prepend-top-5 gl-mb-2">
|
||||
<div ref="accessibility-issue-description" class="report-block-list-issue-description-text">
|
||||
<div
|
||||
v-if="isNew"
|
||||
ref="accessibility-issue-is-new-badge"
|
||||
class="badge badge-danger append-right-5"
|
||||
>
|
||||
<div v-if="isNew" ref="accessibility-issue-is-new-badge" class="badge badge-danger gl-mr-2">
|
||||
{{ s__('AccessibilityReport|New') }}
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
@ -32,7 +32,7 @@ export default {
|
|||
class="btn-link btn-blank text-left break-link vulnerability-name-button"
|
||||
@click="openModal({ issue })"
|
||||
>
|
||||
<div v-if="isNew" class="badge badge-danger append-right-5">{{ s__('New') }}</div>
|
||||
<div v-if="isNew" class="badge badge-danger gl-mr-2">{{ s__('New') }}</div>
|
||||
{{ issue.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -166,7 +166,7 @@ export default {
|
|||
<div class="detail-page-header">
|
||||
<div class="detail-page-header-body">
|
||||
<div
|
||||
class="snippet-box has-tooltip d-flex align-items-center append-right-5 mb-1"
|
||||
class="snippet-box has-tooltip d-flex align-items-center gl-mr-2 mb-1"
|
||||
data-qa-selector="snippet_container"
|
||||
:title="snippetVisibilityLevelDescription"
|
||||
data-container="body"
|
||||
|
|
|
@ -47,7 +47,7 @@ export default {
|
|||
v-if="loading"
|
||||
:inline="true"
|
||||
:class="{
|
||||
'append-right-5': label,
|
||||
'gl-mr-2': label,
|
||||
}"
|
||||
class="js-loading-button-icon"
|
||||
/>
|
||||
|
|
|
@ -122,7 +122,7 @@ export default {
|
|||
></div>
|
||||
<div v-if="hasMoreCommits" class="flex-list">
|
||||
<div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded">
|
||||
<icon :name="toggleIcon" :size="8" class="append-right-5" />
|
||||
<icon :name="toggleIcon" :size="8" class="gl-mr-2" />
|
||||
<span>{{ __('Toggle commit list') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -405,7 +405,6 @@ img.emoji {
|
|||
.prepend-left-15 { margin-left: 15px; }
|
||||
.prepend-left-20 { margin-left: 20px; }
|
||||
.prepend-left-64 { margin-left: 64px; }
|
||||
.append-right-5 { margin-right: 5px; }
|
||||
.append-right-10 { margin-right: 10px; }
|
||||
.append-right-15 { margin-right: 15px; }
|
||||
.append-right-20 { margin-right: 20px; }
|
||||
|
|
|
@ -226,7 +226,7 @@ module NotesActions
|
|||
end
|
||||
|
||||
def update_note_params
|
||||
params.require(:note).permit(:note)
|
||||
params.require(:note).permit(:note, :position)
|
||||
end
|
||||
|
||||
def set_polling_interval_header
|
||||
|
|
|
@ -79,7 +79,7 @@ module CommitsHelper
|
|||
# Returns a link formatted as a commit tag link
|
||||
def commit_tag_link(url, text)
|
||||
link_to(url, class: 'badge badge-gray ref-name') do
|
||||
sprite_icon('tag', size: 12, css_class: 'append-right-5 vertical-align-middle') + "#{text}"
|
||||
sprite_icon('tag', size: 12, css_class: 'gl-mr-2 vertical-align-middle') + "#{text}"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -469,10 +469,12 @@ class Commit
|
|||
# We don't want to do anything for `Commit` model, so this is empty.
|
||||
end
|
||||
|
||||
WIP_REGEX = /\A\s*(((?i)(\[WIP\]|WIP:|WIP)\s|WIP$))|(fixup!|squash!)\s/.freeze
|
||||
# WIP is deprecated in favor of Draft. Currently both options are supported
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/227426
|
||||
DRAFT_REGEX = /\A\s*#{Regexp.union(Gitlab::Regex.merge_request_wip, Gitlab::Regex.merge_request_draft)}|(fixup!|squash!)\s/.freeze
|
||||
|
||||
def work_in_progress?
|
||||
!!(title =~ WIP_REGEX)
|
||||
!!(title =~ DRAFT_REGEX)
|
||||
end
|
||||
|
||||
def merged_merge_request?(user)
|
||||
|
|
|
@ -391,25 +391,27 @@ class MergeRequest < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
WIP_REGEX = /\A*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
|
||||
# WIP is deprecated in favor of Draft. Currently both options are supported
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/227426
|
||||
DRAFT_REGEX = /\A*#{Regexp.union(Gitlab::Regex.merge_request_wip, Gitlab::Regex.merge_request_draft)}+\s*/i.freeze
|
||||
|
||||
def self.work_in_progress?(title)
|
||||
!!(title =~ WIP_REGEX)
|
||||
!!(title =~ DRAFT_REGEX)
|
||||
end
|
||||
|
||||
def self.wipless_title(title)
|
||||
title.sub(WIP_REGEX, "")
|
||||
title.sub(DRAFT_REGEX, "")
|
||||
end
|
||||
|
||||
def self.wip_title(title)
|
||||
work_in_progress?(title) ? title : "WIP: #{title}"
|
||||
work_in_progress?(title) ? title : "Draft: #{title}"
|
||||
end
|
||||
|
||||
def committers
|
||||
@committers ||= commits.committers
|
||||
end
|
||||
|
||||
# Verifies if title has changed not taking into account WIP prefix
|
||||
# Verifies if title has changed not taking into account Draft prefix
|
||||
# for merge requests.
|
||||
def wipless_title_changed(old_title)
|
||||
self.class.wipless_title(old_title) != self.wipless_title
|
||||
|
|
|
@ -13,15 +13,16 @@ module Jira
|
|||
|
||||
@jql = params[:jql].to_s
|
||||
@page = params[:page].to_i || 1
|
||||
@per_page = params[:per_page].to_i || PER_PAGE
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :jql, :page
|
||||
attr_reader :jql, :page, :per_page
|
||||
|
||||
override :url
|
||||
def url
|
||||
"#{base_api_url}/search?jql=#{CGI.escape(jql)}&startAt=#{start_at}&maxResults=#{PER_PAGE}&fields=*all"
|
||||
"#{base_api_url}/search?jql=#{CGI.escape(jql)}&startAt=#{start_at}&maxResults=#{per_page}&fields=*all"
|
||||
end
|
||||
|
||||
override :build_service_response
|
||||
|
@ -48,7 +49,7 @@ module Jira
|
|||
end
|
||||
|
||||
def start_at
|
||||
(page - 1) * PER_PAGE
|
||||
(page - 1) * per_page
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PersonalAccessTokens
|
||||
class LastUsedService
|
||||
def initialize(personal_access_token)
|
||||
@personal_access_token = personal_access_token
|
||||
end
|
||||
|
||||
def execute
|
||||
# Needed to avoid calling service on Oauth tokens
|
||||
return unless @personal_access_token.has_attribute?(:last_used_at)
|
||||
|
||||
# We _only_ want to update last_used_at and not also updated_at (which
|
||||
# would be updated when using #touch).
|
||||
@personal_access_token.update_column(:last_used_at, Time.zone.now) if update?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update?
|
||||
return false if ::Gitlab::Database.read_only?
|
||||
|
||||
last_used = @personal_access_token.last_used_at
|
||||
|
||||
last_used.nil? || (last_used <= 1.day.ago)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UpdateContainerRegistryInfoService
|
||||
def execute
|
||||
registry_config = Gitlab.config.registry
|
||||
return unless registry_config.enabled && registry_config.api_url.presence
|
||||
|
||||
# registry_info will query the /v2 route of the registry API. This route
|
||||
# requires authentication, but not authorization (the response has no body,
|
||||
# only headers that show the version of the registry). There might be no
|
||||
# associated user when running this (e.g. from a rake task or a cron job),
|
||||
# so we need to generate a valid JWT token with no access permissions to
|
||||
# authenticate as a trusted client.
|
||||
token = Auth::ContainerRegistryAuthenticationService.access_token([], [])
|
||||
client = ContainerRegistry::Client.new(registry_config.api_url, token: token)
|
||||
info = client.registry_info
|
||||
|
||||
Gitlab::CurrentSettings.update!(
|
||||
container_registry_vendor: info[:vendor] || '',
|
||||
container_registry_version: info[:version] || '',
|
||||
container_registry_features: info[:features] || []
|
||||
)
|
||||
end
|
||||
end
|
|
@ -41,7 +41,7 @@
|
|||
%div= uri
|
||||
%td= application.access_tokens.count
|
||||
%td
|
||||
= link_to edit_oauth_application_path(application), class: "btn btn-transparent append-right-5" do
|
||||
= link_to edit_oauth_application_path(application), class: "btn btn-transparent gl-mr-2" do
|
||||
%span.sr-only
|
||||
= _('Edit')
|
||||
= icon('pencil')
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
- events.each do |event|
|
||||
%li
|
||||
%span.description
|
||||
= audit_icon(event.details[:with], class: "append-right-5")
|
||||
= audit_icon(event.details[:with], class: "gl-mr-2")
|
||||
= _('Signed in with %{authentication} authentication') % { authentication: event.details[:with]}
|
||||
%span.float-right= time_ago_with_tooltip(event.created_at)
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
.gl-responsive-table-row.notification-list-item
|
||||
.table-section.section-40
|
||||
%span.notification.fa.fa-holder.append-right-5
|
||||
%span.notification.fa.fa-holder.gl-mr-2
|
||||
= notification_icon(notification_icon_level(setting, emails_disabled))
|
||||
|
||||
%span.str-truncated
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
- emails_disabled = project.emails_disabled?
|
||||
|
||||
%li.notification-list-item
|
||||
%span.notification.fa.fa-holder.append-right-5
|
||||
%span.notification.fa.fa-holder.gl-mr-2
|
||||
= notification_icon(notification_icon_level(setting, emails_disabled))
|
||||
|
||||
%span.str-truncated
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
= sprite_icon('tag', size: 16, css_class: 'icon gl-mr-2')
|
||||
|
||||
- @project.topics_to_show.each do |topic|
|
||||
- project_topics_classes = "badge badge-pill badge-secondary append-right-5"
|
||||
- project_topics_classes = "badge badge-pill badge-secondary gl-mr-2"
|
||||
- explore_project_topic_path = explore_projects_path(tag: topic)
|
||||
- if topic.length > max_project_topic_length
|
||||
%a{ class: "#{ project_topics_classes } str-truncated-30 has-tooltip", data: { container: "body" }, title: topic, href: explore_project_topic_path }
|
||||
|
|
|
@ -10,4 +10,4 @@
|
|||
= number_to_human_size(blob.raw_size)
|
||||
|
||||
- if blob.stored_externally? && blob.external_storage == :lfs
|
||||
%span.badge.label-lfs.append-right-5 LFS
|
||||
%span.badge.label-lfs.gl-mr-2 LFS
|
||||
|
|
|
@ -37,4 +37,4 @@
|
|||
#{diff_file.a_mode} → #{diff_file.b_mode}
|
||||
|
||||
- if diff_file.stored_externally? && diff_file.external_storage == :lfs
|
||||
%span.badge.label-lfs.append-right-5 LFS
|
||||
%span.badge.label-lfs.gl-mr-2 LFS
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
%tr
|
||||
%th= _('Name')
|
||||
%th= s_('AccessTokens|Created')
|
||||
%th
|
||||
= _('Last Used')
|
||||
= link_to icon('question-circle'), help_page_path('user/profile/personal_access_tokens.md', anchor: 'token-activity'), target: '_blank'
|
||||
%th= _('Expires')
|
||||
%th= _('Scopes')
|
||||
%th
|
||||
|
@ -24,6 +27,11 @@
|
|||
%tr
|
||||
%td= token.name
|
||||
%td= token.created_at.to_date.to_s(:medium)
|
||||
%td
|
||||
- if token.last_used_at?
|
||||
%span.token-last-used-label= _(time_ago_with_tooltip(token.last_used_at))
|
||||
- else
|
||||
%span.token-never-used-label= _('Never')
|
||||
%td
|
||||
- if token.expires?
|
||||
- if token.expires_at.past? || token.expires_at.today?
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.detail-page-header
|
||||
.detail-page-header-body
|
||||
.snippet-box.has-tooltip.inline.append-right-5{ title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: "body" } }
|
||||
.snippet-box.has-tooltip.inline.gl-mr-2{ title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: "body" } }
|
||||
%span.sr-only
|
||||
= visibility_level_label(@snippet.visibility_level)
|
||||
= visibility_level_icon(@snippet.visibility_level, fw: false)
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
- git_access_url = wiki_path(@wiki, action: :git_access)
|
||||
= link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '', data: { qa_selector: 'clone_repository_link' } do
|
||||
= sprite_icon('download', size: 16, css_class: 'append-right-5')
|
||||
= sprite_icon('download', size: 16, css_class: 'gl-mr-2')
|
||||
%span= _("Clone repository")
|
||||
|
||||
.blocks-container
|
||||
|
|
|
@ -71,14 +71,14 @@ module WorkerAttributes
|
|||
|
||||
# Set this attribute on a job when it will call to services outside of the
|
||||
# application, such as 3rd party applications, other k8s clusters etc See
|
||||
# doc/development/sidekiq_style_guide.md#Jobs-with-External-Dependencies for
|
||||
# doc/development/sidekiq_style_guide.md#jobs-with-external-dependencies for
|
||||
# details
|
||||
def worker_has_external_dependencies!
|
||||
class_attributes[:external_dependencies] = true
|
||||
end
|
||||
|
||||
# Returns a truthy value if the worker has external dependencies.
|
||||
# See doc/development/sidekiq_style_guide.md#Jobs-with-External-Dependencies
|
||||
# See doc/development/sidekiq_style_guide.md#jobs-with-external-dependencies
|
||||
# for details
|
||||
def worker_has_external_dependencies?
|
||||
class_attributes[:external_dependencies]
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Track last activity for Personal Access Token
|
||||
merge_request: 35471
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow prefixing with Draft to mark MR as WIP
|
||||
merge_request: 35940
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddLastUsedToPersonalAccessTokens < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
add_column :personal_access_tokens, :last_used_at, :datetime_with_timezone
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_column :personal_access_tokens, :last_used_at, :datetime_with_timezone
|
||||
end
|
||||
end
|
||||
end
|
|
@ -13748,7 +13748,8 @@ CREATE TABLE public.personal_access_tokens (
|
|||
'::character varying NOT NULL,
|
||||
impersonation boolean DEFAULT false NOT NULL,
|
||||
token_digest character varying,
|
||||
expire_notification_delivered boolean DEFAULT false NOT NULL
|
||||
expire_notification_delivered boolean DEFAULT false NOT NULL,
|
||||
last_used_at timestamp with time zone
|
||||
);
|
||||
|
||||
CREATE SEQUENCE public.personal_access_tokens_id_seq
|
||||
|
@ -23584,6 +23585,7 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200624222443
|
||||
20200625045442
|
||||
20200625082258
|
||||
20200625113337
|
||||
20200625190458
|
||||
20200626060151
|
||||
20200626130220
|
||||
|
|
|
@ -782,10 +782,14 @@ Diff comments also contain position:
|
|||
"old_line": 27,
|
||||
"new_line": 27,
|
||||
"line_range": {
|
||||
"start_line_code": "588440f66559714280628a4f9799f0c4eb880a4a_10_10",
|
||||
"start_line_type": "new",
|
||||
"end_line_code": "588440f66559714280628a4f9799f0c4eb880a4a_11_11",
|
||||
"end_line_type": "old"
|
||||
"start": {
|
||||
"line_code": "588440f66559714280628a4f9799f0c4eb880a4a_10_10",
|
||||
"type": "new",
|
||||
},
|
||||
"end": {
|
||||
"line_code": "588440f66559714280628a4f9799f0c4eb880a4a_11_11",
|
||||
"type": "old"
|
||||
},
|
||||
}
|
||||
},
|
||||
"resolved": false,
|
||||
|
@ -832,30 +836,32 @@ POST /projects/:id/merge_requests/:merge_request_iid/discussions
|
|||
|
||||
Parameters:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------------------------------------- | -------------- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
|
||||
| `merge_request_iid` | integer | yes | The IID of a merge request |
|
||||
| `body` | string | yes | The content of the thread |
|
||||
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights) |
|
||||
| `position` | hash | no | Position when creating a diff note |
|
||||
| `position[base_sha]` | string | yes | Base commit SHA in the source branch |
|
||||
| `position[start_sha]` | string | yes | SHA referencing commit in target branch |
|
||||
| `position[head_sha]` | string | yes | SHA referencing HEAD of this merge request |
|
||||
| `position[position_type]` | string | yes | Type of the position reference', allowed values: 'text' or 'image' |
|
||||
| `position[new_path]` | string | no | File path after change |
|
||||
| `position[new_line]` | integer | no | Line number after change (for 'text' diff notes) |
|
||||
| `position[old_path]` | string | no | File path before change |
|
||||
| `position[old_line]` | integer | no | Line number before change (for 'text' diff notes) |
|
||||
| `position[line_range]` | hash | no | Line range for a multi-line diff note |
|
||||
| `position[line_range][start_line_code]` | string | yes | Line code for the start line |
|
||||
| `position[line_range][end_line_code]` | string | yes | Line code for the end line |
|
||||
| `position[line_range][start_line_type]` | string | yes | Line type for the start line |
|
||||
| `position[line_range][end_line_type]` | string | yes | Line type for the end line |
|
||||
| `position[width]` | integer | no | Width of the image (for 'image' diff notes) |
|
||||
| `position[height]` | integer | no | Height of the image (for 'image' diff notes) |
|
||||
| `position[x]` | integer | no | X coordinate (for 'image' diff notes) |
|
||||
| `position[y]` | integer | no | Y coordinate (for 'image' diff notes) |
|
||||
| Attribute | Type | Required | Description |
|
||||
| ---------------------------------------- | -------------- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
|
||||
| `merge_request_iid` | integer | yes | The IID of a merge request |
|
||||
| `body` | string | yes | The content of the thread |
|
||||
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights) |
|
||||
| `position` | hash | no | Position when creating a diff note |
|
||||
| `position[base_sha]` | string | yes | Base commit SHA in the source branch |
|
||||
| `position[start_sha]` | string | yes | SHA referencing commit in target branch |
|
||||
| `position[head_sha]` | string | yes | SHA referencing HEAD of this merge request |
|
||||
| `position[position_type]` | string | yes | Type of the position reference', allowed values: 'text' or 'image' |
|
||||
| `position[new_path]` | string | no | File path after change |
|
||||
| `position[new_line]` | integer | no | Line number after change (for 'text' diff notes) |
|
||||
| `position[old_path]` | string | no | File path before change |
|
||||
| `position[old_line]` | integer | no | Line number before change (for 'text' diff notes) |
|
||||
| `position[line_range]` | hash | no | Line range for a multi-line diff note |
|
||||
| `position[line_range][start]` | hash | no | Multiline note starting line |
|
||||
| `position[line_range][start][line_code]` | string | yes | Line code for the start line |
|
||||
| `position[line_range][start][type]` | string | yes | Line type for the start line |
|
||||
| `position[line_range][end]` | hash | no | Multiline note ending line |
|
||||
| `position[line_range][end][line_code]` | string | yes | Line code for the end line |
|
||||
| `position[line_range][end][type]` | string | yes | Line type for the end line |
|
||||
| `position[width]` | integer | no | Width of the image (for 'image' diff notes) |
|
||||
| `position[height]` | integer | no | Height of the image (for 'image' diff notes) |
|
||||
| `position[x]` | integer | no | X coordinate (for 'image' diff notes) |
|
||||
| `position[y]` | integer | no | Y coordinate (for 'image' diff notes) |
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions?body=comment"
|
||||
|
|
|
@ -12879,6 +12879,7 @@ type TestReportEdge {
|
|||
State of a test report
|
||||
"""
|
||||
enum TestReportState {
|
||||
FAILED
|
||||
PASSED
|
||||
}
|
||||
|
||||
|
|
|
@ -37984,6 +37984,12 @@
|
|||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "FAILED",
|
||||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
|
|
|
@ -567,7 +567,7 @@ For monitoring deployed apps, see the [Sentry integration docs](../user/project/
|
|||
- [GDK](https://gitlab.com/gitlab-org/gitlab/blob/master/config/gitlab.yml.example)
|
||||
- Layer: Core Service (Processor)
|
||||
- Process: `sidekiq`
|
||||
- GitLab.com: [Sidekiq](../user/gitlab_com/index.md#Sidekiq)
|
||||
- GitLab.com: [Sidekiq](../user/gitlab_com/index.md#sidekiq)
|
||||
|
||||
Sidekiq is a Ruby background job processor that pulls jobs from the Redis queue and processes them. Background jobs allow GitLab to provide a faster request/response cycle by moving work into the background.
|
||||
|
||||
|
|
|
@ -115,6 +115,6 @@ There are multiple ways to find the source of queries.
|
|||
|
||||
## See also
|
||||
|
||||
- [Bullet](profiling.md#Bullet) For finding `N+1` query problems
|
||||
- [Bullet](profiling.md#bullet) For finding `N+1` query problems
|
||||
- [Performance guidelines](performance.md)
|
||||
- [Merge request performance guidelines](merge_request_performance_guidelines.md#query-counts)
|
||||
|
|
|
@ -741,7 +741,7 @@ GitLab uses [factory_bot](https://github.com/thoughtbot/factory_bot) as a test f
|
|||
- There should be only one top-level factory definition per file.
|
||||
- FactoryBot methods are mixed in to all RSpec groups. This means you can (and
|
||||
should) call `create(...)` instead of `FactoryBot.create(...)`.
|
||||
- Make use of [traits](https://www.rubydoc.info/gems/factory_bot/file/GETTING_STARTED.md#Traits) to clean up definitions and usages.
|
||||
- Make use of [traits](https://www.rubydoc.info/gems/factory_bot/file/GETTING_STARTED.md#traits) to clean up definitions and usages.
|
||||
- When defining a factory, don't define attributes that are not required for the
|
||||
resulting record to pass validation.
|
||||
- When instantiating from a factory, don't supply attributes that aren't
|
||||
|
|
|
@ -17,7 +17,7 @@ These instructions will also work for a self-managed GitLab instance. However, y
|
|||
need to ensure your own [Runners are configured](../../ci/runners/README.md) and
|
||||
[Google OAuth is enabled](../../integration/google.md).
|
||||
|
||||
**Note**: GitLab's Web Application Firewall is deployed with [Ingress](../../user/clusters/applications.md#Ingress),
|
||||
**Note**: GitLab's Web Application Firewall is deployed with [Ingress](../../user/clusters/applications.md#ingress),
|
||||
so it will be available to your applications no matter how you deploy them to Kubernetes.
|
||||
|
||||
## Configuring your Google account
|
||||
|
|
|
@ -44,6 +44,10 @@ profile.
|
|||
At any time, you can revoke any personal access token by clicking the
|
||||
respective **Revoke** button under the **Active Personal Access Token** area.
|
||||
|
||||
### Token activity
|
||||
|
||||
You can see when a token was last used from the **Personal Access Tokens** page. Updates to the token usage is fixed at once per 24 hours. Requests to [API resources](../../api/api_resources.md) and the [GraphQL API](../../api/graphql/index.md) will update a token's usage.
|
||||
|
||||
## Limiting scopes of a personal access token
|
||||
|
||||
Personal access tokens can be created with one or more scopes that allow various
|
||||
|
|
|
@ -76,10 +76,18 @@ module API
|
|||
optional :y, type: Integer, desc: 'Y coordinate in the image'
|
||||
|
||||
optional :line_range, type: Hash, desc: 'Multi-line start and end' do
|
||||
requires :start_line_code, type: String, desc: 'Start line code for multi-line note'
|
||||
requires :end_line_code, type: String, desc: 'End line code for multi-line note'
|
||||
requires :start_line_type, type: String, desc: 'Start line type for multi-line note'
|
||||
requires :end_line_type, type: String, desc: 'End line type for multi-line note'
|
||||
optional :start, type: Hash do
|
||||
optional :line_code, type: String, desc: 'Start line code for multi-line note'
|
||||
optional :type, type: String, desc: 'Start line type for multi-line note'
|
||||
optional :old_line, type: String, desc: 'Start old_line line number'
|
||||
optional :new_line, type: String, desc: 'Start new_line line number'
|
||||
end
|
||||
optional :end, type: Hash do
|
||||
optional :line_code, type: String, desc: 'End line code for multi-line note'
|
||||
optional :type, type: String, desc: 'End line type for multi-line note'
|
||||
optional :old_line, type: String, desc: 'End old_line line number'
|
||||
optional :new_line, type: String, desc: 'End new_line line number'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -92,6 +92,8 @@ module Gitlab
|
|||
|
||||
validate_access_token!(scopes: [:api])
|
||||
|
||||
::PersonalAccessTokens::LastUsedService.new(access_token).execute
|
||||
|
||||
access_token.user || raise(UnauthorizedError)
|
||||
end
|
||||
|
||||
|
@ -100,6 +102,8 @@ module Gitlab
|
|||
|
||||
validate_access_token!
|
||||
|
||||
::PersonalAccessTokens::LastUsedService.new(access_token).execute
|
||||
|
||||
access_token.user || raise(UnauthorizedError)
|
||||
end
|
||||
|
||||
|
|
|
@ -250,6 +250,14 @@ module Gitlab
|
|||
@utc_date_regex ||= /\A[0-9]{4}-[0-9]{2}-[0-9]{2}\z/.freeze
|
||||
end
|
||||
|
||||
def merge_request_wip
|
||||
/(?i)(\[WIP\]\s*|WIP:\s*|WIP\s+|WIP$)/
|
||||
end
|
||||
|
||||
def merge_request_draft
|
||||
/(?i)(\[draft\]|\(draft\)|draft:|draft\s\-\s|draft\s|draft$)/
|
||||
end
|
||||
|
||||
def issue
|
||||
@issue ||= /(?<issue>\d+\b)/
|
||||
end
|
||||
|
|
|
@ -15,21 +15,7 @@ namespace :gitlab do
|
|||
|
||||
warn_user_is_not_gitlab
|
||||
|
||||
url = registry_config.api_url
|
||||
# registry_info will query the /v2 route of the registry API. This route
|
||||
# requires authentication, but not authorization (the response has no body,
|
||||
# only headers that show the version of the registry). There is no
|
||||
# associated user when running this rake, so we need to generate a valid
|
||||
# JWT token with no access permissions to authenticate as a trusted client.
|
||||
token = Auth::ContainerRegistryAuthenticationService.access_token([], [])
|
||||
client = ContainerRegistry::Client.new(url, token: token)
|
||||
info = client.registry_info
|
||||
|
||||
Gitlab::CurrentSettings.update!(
|
||||
container_registry_vendor: info[:vendor] || '',
|
||||
container_registry_version: info[:version] || '',
|
||||
container_registry_features: info[:features] || []
|
||||
)
|
||||
UpdateContainerRegistryInfoService.new.execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13260,6 +13260,9 @@ msgstr ""
|
|||
msgid "Last Seen"
|
||||
msgstr ""
|
||||
|
||||
msgid "Last Used"
|
||||
msgstr ""
|
||||
|
||||
msgid "Last accessed on"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ RSpec.describe 'User creates branch and merge request on issue page', :js do
|
|||
perform_enqueued_jobs do
|
||||
select_dropdown_option('create-mr')
|
||||
|
||||
expect(page).to have_content('WIP: Resolve "Cherry-Coloured Funk"')
|
||||
expect(page).to have_content('Draft: Resolve "Cherry-Coloured Funk"')
|
||||
expect(current_path).to eq(project_merge_request_path(project, MergeRequest.first))
|
||||
|
||||
wait_for_requests
|
||||
|
@ -100,7 +100,7 @@ RSpec.describe 'User creates branch and merge request on issue page', :js do
|
|||
perform_enqueued_jobs do
|
||||
select_dropdown_option('create-mr', branch_name)
|
||||
|
||||
expect(page).to have_content('WIP: Resolve "Cherry-Coloured Funk"')
|
||||
expect(page).to have_content('Draft: Resolve "Cherry-Coloured Funk"')
|
||||
expect(page).to have_content('Request to merge custom-branch-name into')
|
||||
expect(current_path).to eq(project_merge_request_path(project, MergeRequest.first))
|
||||
|
||||
|
|
|
@ -117,9 +117,58 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
context 'when adding multiline comments' do
|
||||
it 'saves a multiline comment' do
|
||||
click_diff_line(find("[id='#{sample_commit.line_code}']"))
|
||||
add_comment('-13', '+14')
|
||||
end
|
||||
|
||||
context 'when in side-by-side view' do
|
||||
before do
|
||||
visit(diffs_project_merge_request_path(project, merge_request, view: 'parallel'))
|
||||
end
|
||||
|
||||
# In `files/ruby/popen.rb`
|
||||
it 'allows comments for changes involving both sides' do
|
||||
# click +15, select -13 add and verify comment
|
||||
click_diff_line(find('div[data-path="files/ruby/popen.rb"] .new_line a[data-linenumber="15"]').find(:xpath, '../..'), 'right')
|
||||
add_comment('-13', '+15')
|
||||
end
|
||||
|
||||
it 'allows comments to start above hidden lines and end below' do
|
||||
# click +28, select 21 add and verify comment
|
||||
click_diff_line(find('div[data-path="files/ruby/popen.rb"] .new_line a[data-linenumber="28"]').find(:xpath, '../..'), 'right')
|
||||
add_comment('21', '+28')
|
||||
end
|
||||
|
||||
it 'allows comments on previously hidden lines at the top of a file' do
|
||||
# Click -9, expand up, select 1 add and verify comment
|
||||
page.within('[data-path="files/ruby/popen.rb"]') do
|
||||
all('.js-unfold-all')[0].click
|
||||
end
|
||||
click_diff_line(find('div[data-path="files/ruby/popen.rb"] .old_line a[data-linenumber="9"]').find(:xpath, '../..'), 'left')
|
||||
add_comment('1', '-9')
|
||||
end
|
||||
|
||||
it 'allows comments on previously hidden lines the middle of a file' do
|
||||
# Click 27, expand up, select 18, add and verify comment
|
||||
page.within('[data-path="files/ruby/popen.rb"]') do
|
||||
all('.js-unfold-all')[1].click
|
||||
end
|
||||
click_diff_line(find('div[data-path="files/ruby/popen.rb"] .old_line a[data-linenumber="21"]').find(:xpath, '../..'), 'left')
|
||||
add_comment('18', '21')
|
||||
end
|
||||
|
||||
it 'allows comments on previously hidden lines at the bottom of a file' do
|
||||
# Click +28, expand down, select 37 add and verify comment
|
||||
page.within('[data-path="files/ruby/popen.rb"]') do
|
||||
all('.js-unfold-down')[1].click
|
||||
end
|
||||
click_diff_line(find('div[data-path="files/ruby/popen.rb"] .old_line a[data-linenumber="30"]').find(:xpath, '../..'), 'left')
|
||||
add_comment('+28', '37')
|
||||
end
|
||||
end
|
||||
|
||||
def add_comment(start_line, end_line)
|
||||
page.within('.discussion-form') do
|
||||
find('#comment-line-start option', text: '-13').select_option
|
||||
find('#comment-line-start option', exact_text: start_line).select_option
|
||||
end
|
||||
|
||||
page.within('.js-discussion-note-form') do
|
||||
|
@ -131,7 +180,7 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
|
||||
page.within('.notes_holder') do
|
||||
expect(page).to have_content('Line is wrong')
|
||||
expect(page).to have_content('Comment on lines -13 to +14')
|
||||
expect(page).to have_content("Comment on lines #{start_line} to #{end_line}")
|
||||
end
|
||||
|
||||
visit(merge_request_path(merge_request))
|
||||
|
|
|
@ -78,10 +78,18 @@ describe('DiffLineNoteForm', () => {
|
|||
.mockReturnValue(Promise.resolve());
|
||||
|
||||
const lineRange = {
|
||||
start_line_code: wrapper.vm.commentLineStart.lineCode,
|
||||
start_line_type: wrapper.vm.commentLineStart.type,
|
||||
end_line_code: wrapper.vm.line.line_code,
|
||||
end_line_type: wrapper.vm.line.type,
|
||||
start: {
|
||||
line_code: wrapper.vm.commentLineStart.line_code,
|
||||
type: wrapper.vm.commentLineStart.type,
|
||||
new_line: 1,
|
||||
old_line: null,
|
||||
},
|
||||
end: {
|
||||
line_code: wrapper.vm.line.line_code,
|
||||
type: wrapper.vm.line.type,
|
||||
new_line: 1,
|
||||
old_line: null,
|
||||
},
|
||||
};
|
||||
|
||||
const formData = {
|
||||
|
|
|
@ -5,32 +5,19 @@ import {
|
|||
} from '~/notes/components/multiline_comment_utils';
|
||||
|
||||
describe('Multiline comment utilities', () => {
|
||||
describe('getStartLineNumber', () => {
|
||||
describe('get start & end line numbers', () => {
|
||||
const lineRanges = ['old', 'new', null].map(type => ({
|
||||
start: { new_line: 1, old_line: 1, type },
|
||||
end: { new_line: 2, old_line: 2, type },
|
||||
}));
|
||||
it.each`
|
||||
lineCode | type | result
|
||||
${'abcdef_1_1'} | ${'old'} | ${'-1'}
|
||||
${'abcdef_1_1'} | ${'new'} | ${'+1'}
|
||||
${'abcdef_1_1'} | ${null} | ${'1'}
|
||||
${'abcdef'} | ${'new'} | ${''}
|
||||
${'abcdef'} | ${'old'} | ${''}
|
||||
${'abcdef'} | ${null} | ${''}
|
||||
`('returns line number', ({ lineCode, type, result }) => {
|
||||
expect(getStartLineNumber({ start_line_code: lineCode, start_line_type: type })).toEqual(
|
||||
result,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('getEndLineNumber', () => {
|
||||
it.each`
|
||||
lineCode | type | result
|
||||
${'abcdef_1_1'} | ${'old'} | ${'-1'}
|
||||
${'abcdef_1_1'} | ${'new'} | ${'+1'}
|
||||
${'abcdef_1_1'} | ${null} | ${'1'}
|
||||
${'abcdef'} | ${'new'} | ${''}
|
||||
${'abcdef'} | ${'old'} | ${''}
|
||||
${'abcdef'} | ${null} | ${''}
|
||||
`('returns line number', ({ lineCode, type, result }) => {
|
||||
expect(getEndLineNumber({ end_line_code: lineCode, end_line_type: type })).toEqual(result);
|
||||
lineRange | start | end
|
||||
${lineRanges[0]} | ${'-1'} | ${'-2'}
|
||||
${lineRanges[1]} | ${'+1'} | ${'+2'}
|
||||
${lineRanges[2]} | ${'1'} | ${'2'}
|
||||
`('returns line numbers `$start` & `$end`', ({ lineRange, start, end }) => {
|
||||
expect(getStartLineNumber(lineRange)).toEqual(start);
|
||||
expect(getEndLineNumber(lineRange)).toEqual(end);
|
||||
});
|
||||
});
|
||||
describe('getSymbol', () => {
|
||||
|
|
|
@ -46,12 +46,30 @@ describe('issue_note', () => {
|
|||
it('should render if has multiline comment', () => {
|
||||
const position = {
|
||||
line_range: {
|
||||
start_line_code: 'abc_1_1',
|
||||
end_line_code: 'abc_2_2',
|
||||
start: {
|
||||
line_code: 'abc_1_1',
|
||||
type: null,
|
||||
old_line: '1',
|
||||
new_line: '1',
|
||||
},
|
||||
end: {
|
||||
line_code: 'abc_2_2',
|
||||
type: null,
|
||||
old_line: '2',
|
||||
new_line: '2',
|
||||
},
|
||||
},
|
||||
};
|
||||
const line = {
|
||||
line_code: 'abc_1_1',
|
||||
type: null,
|
||||
old_line: '1',
|
||||
new_line: '1',
|
||||
};
|
||||
wrapper.setProps({
|
||||
note: { ...note, position },
|
||||
discussionRoot: true,
|
||||
line,
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
|
@ -62,12 +80,30 @@ describe('issue_note', () => {
|
|||
it('should not render if has single line comment', () => {
|
||||
const position = {
|
||||
line_range: {
|
||||
start_line_code: 'abc_1_1',
|
||||
end_line_code: 'abc_1_1',
|
||||
start: {
|
||||
line_code: 'abc_1_1',
|
||||
type: null,
|
||||
old_line: '1',
|
||||
new_line: '1',
|
||||
},
|
||||
end: {
|
||||
line_code: 'abc_1_1',
|
||||
type: null,
|
||||
old_line: '1',
|
||||
new_line: '1',
|
||||
},
|
||||
},
|
||||
};
|
||||
const line = {
|
||||
line_code: 'abc_1_1',
|
||||
type: null,
|
||||
old_line: '1',
|
||||
new_line: '1',
|
||||
};
|
||||
wrapper.setProps({
|
||||
note: { ...note, position },
|
||||
discussionRoot: true,
|
||||
line,
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
|
|
|
@ -675,7 +675,10 @@ eos
|
|||
end
|
||||
|
||||
describe '#work_in_progress?' do
|
||||
['squash! ', 'fixup! ', 'wip: ', 'WIP: ', '[WIP] '].each do |wip_prefix|
|
||||
[
|
||||
'squash! ', 'fixup! ', 'wip: ', 'WIP: ', '[WIP] ',
|
||||
'draft: ', 'Draft - ', '[Draft] ', '(draft) ', 'Draft: '
|
||||
].each do |wip_prefix|
|
||||
it "detects the '#{wip_prefix}' prefix" do
|
||||
commit.message = "#{wip_prefix}#{commit.message}"
|
||||
|
||||
|
@ -689,6 +692,12 @@ eos
|
|||
expect(commit).to be_work_in_progress
|
||||
end
|
||||
|
||||
it "detects WIP for a commit just saying 'draft'" do
|
||||
commit.message = "draft"
|
||||
|
||||
expect(commit).to be_work_in_progress
|
||||
end
|
||||
|
||||
it "doesn't detect WIP for a commit that begins with 'FIXUP! '" do
|
||||
commit.message = "FIXUP! #{commit.message}"
|
||||
|
||||
|
|
|
@ -1109,13 +1109,31 @@ RSpec.describe MergeRequest do
|
|||
end
|
||||
|
||||
describe "#work_in_progress?" do
|
||||
['WIP ', 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', ' [WIP] WIP [WIP] WIP: WIP '].each do |wip_prefix|
|
||||
subject { build_stubbed(:merge_request) }
|
||||
|
||||
[
|
||||
'WIP ', 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', ' [WIP] WIP [WIP] WIP: WIP ',
|
||||
'Draft ', 'draft:', 'Draft: ', '[Draft]', '[DRAFT] ', 'Draft - '
|
||||
].each do |wip_prefix|
|
||||
it "detects the '#{wip_prefix}' prefix" do
|
||||
subject.title = "#{wip_prefix}#{subject.title}"
|
||||
|
||||
expect(subject.work_in_progress?).to eq true
|
||||
end
|
||||
end
|
||||
|
||||
it "detects merge request title just saying 'wip'" do
|
||||
subject.title = "wip"
|
||||
|
||||
expect(subject.work_in_progress?).to eq true
|
||||
end
|
||||
|
||||
it "detects merge request title just saying 'draft'" do
|
||||
subject.title = "draft"
|
||||
|
||||
expect(subject.work_in_progress?).to eq true
|
||||
end
|
||||
|
||||
it "doesn't detect WIP for words starting with WIP" do
|
||||
subject.title = "Wipwap #{subject.title}"
|
||||
expect(subject.work_in_progress?).to eq false
|
||||
|
@ -1132,7 +1150,12 @@ RSpec.describe MergeRequest do
|
|||
end
|
||||
|
||||
describe "#wipless_title" do
|
||||
['WIP ', 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', '[WIP] WIP [WIP] WIP: WIP '].each do |wip_prefix|
|
||||
subject { build_stubbed(:merge_request) }
|
||||
|
||||
[
|
||||
'WIP ', 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', '[WIP] WIP [WIP] WIP: WIP ',
|
||||
'Draft ', 'draft:', 'Draft: ', '[Draft]', '[DRAFT] ', 'Draft - '
|
||||
].each do |wip_prefix|
|
||||
it "removes the '#{wip_prefix}' prefix" do
|
||||
wipless_title = subject.title
|
||||
subject.title = "#{wip_prefix}#{subject.title}"
|
||||
|
@ -1150,14 +1173,14 @@ RSpec.describe MergeRequest do
|
|||
end
|
||||
|
||||
describe "#wip_title" do
|
||||
it "adds the WIP: prefix to the title" do
|
||||
wip_title = "WIP: #{subject.title}"
|
||||
it "adds the Draft: prefix to the title" do
|
||||
wip_title = "Draft: #{subject.title}"
|
||||
|
||||
expect(subject.wip_title).to eq wip_title
|
||||
end
|
||||
|
||||
it "does not add the WIP: prefix multiple times" do
|
||||
wip_title = "WIP: #{subject.title}"
|
||||
it "does not add the Draft: prefix multiple times" do
|
||||
wip_title = "Draft: #{subject.title}"
|
||||
subject.title = subject.wip_title
|
||||
subject.title = subject.wip_title
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Setting WIP status of a merge request' do
|
||||
RSpec.describe 'Setting Draft status of a merge request' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let(:current_user) { create(:user) }
|
||||
|
@ -41,39 +41,39 @@ RSpec.describe 'Setting WIP status of a merge request' do
|
|||
expect(graphql_errors).not_to be_empty
|
||||
end
|
||||
|
||||
it 'marks the merge request as WIP' do
|
||||
it 'marks the merge request as Draft' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response['mergeRequest']['title']).to start_with('WIP:')
|
||||
expect(mutation_response['mergeRequest']['title']).to start_with('Draft:')
|
||||
end
|
||||
|
||||
it 'does not do anything if the merge request was already marked `WIP`' do
|
||||
merge_request.update!(title: 'wip: hello world')
|
||||
it 'does not do anything if the merge request was already marked `Draft`' do
|
||||
merge_request.update!(title: 'draft: hello world')
|
||||
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response['mergeRequest']['title']).to start_with('wip:')
|
||||
expect(mutation_response['mergeRequest']['title']).to start_with('draft:')
|
||||
end
|
||||
|
||||
context 'when passing WIP false as input' do
|
||||
context 'when passing Draft false as input' do
|
||||
let(:input) { { wip: false } }
|
||||
|
||||
it 'does not do anything if the merge reqeust was not marked wip' do
|
||||
it 'does not do anything if the merge reqeust was not marked draft' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response['mergeRequest']['title']).not_to start_with(/wip\:/)
|
||||
expect(mutation_response['mergeRequest']['title']).not_to start_with(/draft\:/)
|
||||
end
|
||||
|
||||
it 'unmarks the merge request as `WIP`' do
|
||||
merge_request.update!(title: 'wip: hello world')
|
||||
it 'unmarks the merge request as `Draft`' do
|
||||
merge_request.update!(title: 'draft: hello world')
|
||||
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response['mergeRequest']['title']).not_to start_with('/wip\:/')
|
||||
expect(mutation_response['mergeRequest']['title']).not_to start_with('/draft\:/')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -55,7 +55,7 @@ RSpec.describe Jira::Requests::Issues::ListService do
|
|||
expect(client).to receive(:get).and_return([])
|
||||
end
|
||||
|
||||
it 'returns a paylod with no issues' do
|
||||
it 'returns a payload with no issues' do
|
||||
payload = subject.payload
|
||||
|
||||
expect(subject.success?).to be_truthy
|
||||
|
@ -75,7 +75,7 @@ RSpec.describe Jira::Requests::Issues::ListService do
|
|||
)
|
||||
end
|
||||
|
||||
it 'returns a paylod with jira issues' do
|
||||
it 'returns a payload with jira issues' do
|
||||
payload = subject.payload
|
||||
|
||||
expect(subject.success?).to be_truthy
|
||||
|
@ -83,6 +83,16 @@ RSpec.describe Jira::Requests::Issues::ListService do
|
|||
expect(payload[:is_last]).to be_falsy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when using pagination parameters' do
|
||||
let(:params) { { page: 3, per_page: 20 } }
|
||||
|
||||
it 'honors page and per_page' do
|
||||
expect(client).to receive(:get).with(include('startAt=40&maxResults=20')).and_return([])
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -189,8 +189,8 @@ RSpec.describe MergeRequests::BuildService do
|
|||
|
||||
it_behaves_like 'allows the merge request to be created'
|
||||
|
||||
it 'adds a WIP prefix to the merge request title' do
|
||||
expect(merge_request.title).to eq('WIP: Feature branch')
|
||||
it 'adds a Draft prefix to the merge request title' do
|
||||
expect(merge_request.title).to eq('Draft: Feature branch')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -163,10 +163,10 @@ RSpec.describe MergeRequests::CreateFromIssueService do
|
|||
expect(result[:merge_request].milestone_id).to eq(milestone_id)
|
||||
end
|
||||
|
||||
it 'sets the merge request title to: "WIP: Resolves "$issue-title"' do
|
||||
it 'sets the merge request title to: "Draft: Resolves "$issue-title"' do
|
||||
result = service.execute
|
||||
|
||||
expect(result[:merge_request].title).to eq("WIP: Resolve \"#{issue.title}\"")
|
||||
expect(result[:merge_request].title).to eq("Draft: Resolve \"#{issue.title}\"")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -193,10 +193,10 @@ RSpec.describe MergeRequests::CreateFromIssueService do
|
|||
|
||||
it_behaves_like 'a service that creates a merge request from an issue'
|
||||
|
||||
it 'sets the merge request title to: "WIP: $issue-branch-name', :sidekiq_might_not_need_inline do
|
||||
it 'sets the merge request title to: "Draft: $issue-branch-name', :sidekiq_might_not_need_inline do
|
||||
result = service.execute
|
||||
|
||||
expect(result[:merge_request].title).to eq("WIP: #{issue.to_branch_name.titleize.humanize}")
|
||||
expect(result[:merge_request].title).to eq("Draft: #{issue.to_branch_name.titleize.humanize}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe PersonalAccessTokens::LastUsedService do
|
||||
describe '#execute' do
|
||||
subject { described_class.new(personal_access_token).execute }
|
||||
|
||||
context 'when the personal access token has not been used recently' do
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token, last_used_at: 1.year.ago) }
|
||||
|
||||
it 'updates the last_used_at timestamp' do
|
||||
expect { subject }.to change { personal_access_token.last_used_at }
|
||||
end
|
||||
|
||||
it 'does not run on read-only GitLab instances' do
|
||||
allow(::Gitlab::Database).to receive(:read_only?).and_return(true)
|
||||
|
||||
expect { subject }.not_to change { personal_access_token.last_used_at }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the personal access token has been used recently' do
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token, last_used_at: 1.minute.ago) }
|
||||
|
||||
it 'does not update the last_used_at timestamp' do
|
||||
expect { subject }.not_to change { personal_access_token.last_used_at }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the last_used_at timestamp is nil' do
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token, last_used_at: nil) }
|
||||
|
||||
it 'updates the last_used_at timestamp' do
|
||||
expect { subject }.to change { personal_access_token.last_used_at }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not a personal access token' do
|
||||
let_it_be(:personal_access_token) { create(:oauth_access_token) }
|
||||
|
||||
it 'does not execute' do
|
||||
expect(subject).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,115 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe UpdateContainerRegistryInfoService do
|
||||
let_it_be(:application_settings) { Gitlab::CurrentSettings }
|
||||
let_it_be(:api_url) { 'http://registry.gitlab' }
|
||||
|
||||
describe '#execute' do
|
||||
before do
|
||||
stub_access_token
|
||||
stub_container_registry_config(enabled: true, api_url: api_url)
|
||||
end
|
||||
|
||||
subject { described_class.new.execute }
|
||||
|
||||
shared_examples 'invalid config' do
|
||||
it 'does not update the application settings' do
|
||||
expect(application_settings).not_to receive(:update!)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'does not raise an error' do
|
||||
expect { subject }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when container registry is disabled' do
|
||||
before do
|
||||
stub_container_registry_config(enabled: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'invalid config'
|
||||
end
|
||||
|
||||
context 'when container registry api_url is blank' do
|
||||
before do
|
||||
stub_container_registry_config(api_url: '')
|
||||
end
|
||||
|
||||
it_behaves_like 'invalid config'
|
||||
end
|
||||
|
||||
context 'when creating a registry client instance' do
|
||||
let(:token) { 'foo' }
|
||||
let(:client) { ContainerRegistry::Client.new(api_url, token: token) }
|
||||
|
||||
before do
|
||||
stub_registry_info({})
|
||||
end
|
||||
|
||||
it 'uses a token with no access permissions' do
|
||||
expect(Auth::ContainerRegistryAuthenticationService)
|
||||
.to receive(:access_token).with([], []).and_return(token)
|
||||
expect(ContainerRegistry::Client)
|
||||
.to receive(:new).with(api_url, token: token).and_return(client)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'when unabled to detect the container registry type' do
|
||||
it 'sets the application settings to their defaults' do
|
||||
stub_registry_info({})
|
||||
|
||||
subject
|
||||
|
||||
application_settings.reload
|
||||
expect(application_settings.container_registry_vendor).to be_blank
|
||||
expect(application_settings.container_registry_version).to be_blank
|
||||
expect(application_settings.container_registry_features).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when able to detect the container registry type' do
|
||||
context 'when using the GitLab container registry' do
|
||||
it 'updates application settings accordingly' do
|
||||
stub_registry_info(vendor: 'gitlab', version: '2.9.1-gitlab', features: %w[a,b,c])
|
||||
|
||||
subject
|
||||
|
||||
application_settings.reload
|
||||
expect(application_settings.container_registry_vendor).to eq('gitlab')
|
||||
expect(application_settings.container_registry_version).to eq('2.9.1-gitlab')
|
||||
expect(application_settings.container_registry_features).to eq(%w[a,b,c])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when using a third-party container registry' do
|
||||
it 'updates application settings accordingly' do
|
||||
stub_registry_info(vendor: 'other', version: nil, features: nil)
|
||||
|
||||
subject
|
||||
|
||||
application_settings.reload
|
||||
expect(application_settings.container_registry_vendor).to eq('other')
|
||||
expect(application_settings.container_registry_version).to be_blank
|
||||
expect(application_settings.container_registry_features).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def stub_access_token
|
||||
allow(Auth::ContainerRegistryAuthenticationService)
|
||||
.to receive(:access_token).with([], []).and_return('foo')
|
||||
end
|
||||
|
||||
def stub_registry_info(output)
|
||||
allow_next_instance_of(ContainerRegistry::Client) do |client|
|
||||
allow(client).to receive(:registry_info).and_return(output)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -47,7 +47,7 @@ RSpec.shared_examples 'create_merge_request quick action' do
|
|||
expect(created_mr.source_branch).to eq(issue.to_branch_name)
|
||||
|
||||
visit project_merge_request_path(project, created_mr)
|
||||
expect(page).to have_content %{WIP: Resolve "#{issue.title}"}
|
||||
expect(page).to have_content %{Draft: Resolve "#{issue.title}"}
|
||||
end
|
||||
|
||||
it 'creates a merge request using the given branch name' do
|
||||
|
@ -60,7 +60,7 @@ RSpec.shared_examples 'create_merge_request quick action' do
|
|||
expect(created_mr.source_branch).to eq(branch_name)
|
||||
|
||||
visit project_merge_request_path(project, created_mr)
|
||||
expect(page).to have_content %{WIP: Resolve "#{issue.title}"}
|
||||
expect(page).to have_content %{Draft: Resolve "#{issue.title}"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,24 +3,18 @@
|
|||
require 'rake_helper'
|
||||
|
||||
RSpec.describe 'gitlab:container_registry namespace rake tasks' do
|
||||
let_it_be(:application_settings) { Gitlab::CurrentSettings }
|
||||
let_it_be(:api_url) { 'http://registry.gitlab' }
|
||||
|
||||
before :all do
|
||||
Rake.application.rake_require 'tasks/gitlab/container_registry'
|
||||
end
|
||||
|
||||
describe 'configure' do
|
||||
before do
|
||||
stub_access_token
|
||||
stub_container_registry_config(enabled: true, api_url: api_url)
|
||||
end
|
||||
|
||||
describe '#configure' do
|
||||
subject { run_rake_task('gitlab:container_registry:configure') }
|
||||
|
||||
shared_examples 'invalid config' do
|
||||
it 'does not update the application settings' do
|
||||
expect(application_settings).not_to receive(:update!)
|
||||
it 'does not call UpdateContainerRegistryInfoService' do
|
||||
expect_any_instance_of(UpdateContainerRegistryInfoService).not_to receive(:execute)
|
||||
|
||||
subject
|
||||
end
|
||||
|
@ -30,7 +24,7 @@ RSpec.describe 'gitlab:container_registry namespace rake tasks' do
|
|||
end
|
||||
|
||||
it 'prints a warning message' do
|
||||
expect { subject }.to output(/Registry is not enabled or registry api url is not present./).to_stdout
|
||||
expect { subject }.to output("Registry is not enabled or registry api url is not present.\n").to_stdout
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -50,74 +44,18 @@ RSpec.describe 'gitlab:container_registry namespace rake tasks' do
|
|||
it_behaves_like 'invalid config'
|
||||
end
|
||||
|
||||
context 'when creating a registry client instance' do
|
||||
let(:token) { 'foo' }
|
||||
let(:client) { ContainerRegistry::Client.new(api_url, token: token) }
|
||||
|
||||
context 'when container registry is enabled and api_url is not blank' do
|
||||
before do
|
||||
stub_registry_info({})
|
||||
stub_container_registry_config(enabled: true, api_url: api_url)
|
||||
end
|
||||
|
||||
it 'uses a token with no access permissions' do
|
||||
expect(Auth::ContainerRegistryAuthenticationService)
|
||||
.to receive(:access_token).with([], []).and_return(token)
|
||||
expect(ContainerRegistry::Client)
|
||||
.to receive(:new).with(api_url, token: token).and_return(client)
|
||||
|
||||
run_rake_task('gitlab:container_registry:configure')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when unabled to detect the container registry type' do
|
||||
it 'fails and raises an error message' do
|
||||
stub_registry_info({})
|
||||
|
||||
run_rake_task('gitlab:container_registry:configure')
|
||||
|
||||
application_settings.reload
|
||||
expect(application_settings.container_registry_vendor).to be_blank
|
||||
expect(application_settings.container_registry_version).to be_blank
|
||||
expect(application_settings.container_registry_features).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when able to detect the container registry type' do
|
||||
context 'when using the GitLab container registry' do
|
||||
it 'updates application settings accordingly' do
|
||||
stub_registry_info(vendor: 'gitlab', version: '2.9.1-gitlab', features: %w[a,b,c])
|
||||
|
||||
run_rake_task('gitlab:container_registry:configure')
|
||||
|
||||
application_settings.reload
|
||||
expect(application_settings.container_registry_vendor).to eq('gitlab')
|
||||
expect(application_settings.container_registry_version).to eq('2.9.1-gitlab')
|
||||
expect(application_settings.container_registry_features).to eq(%w[a,b,c])
|
||||
it 'calls UpdateContainerRegistryInfoService' do
|
||||
expect_next_instance_of(UpdateContainerRegistryInfoService) do |service|
|
||||
expect(service).to receive(:execute)
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
context 'when using a third-party container registry' do
|
||||
it 'updates application settings accordingly' do
|
||||
stub_registry_info(vendor: 'other', version: nil, features: nil)
|
||||
|
||||
run_rake_task('gitlab:container_registry:configure')
|
||||
|
||||
application_settings.reload
|
||||
expect(application_settings.container_registry_vendor).to eq('other')
|
||||
expect(application_settings.container_registry_version).to be_blank
|
||||
expect(application_settings.container_registry_features).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def stub_access_token
|
||||
allow(Auth::ContainerRegistryAuthenticationService)
|
||||
.to receive(:access_token).with([], []).and_return('foo')
|
||||
end
|
||||
|
||||
def stub_registry_info(output)
|
||||
allow_next_instance_of(ContainerRegistry::Client) do |client|
|
||||
allow(client).to receive(:registry_info).and_return(output)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -53,7 +53,7 @@ RSpec.describe 'Every Sidekiq worker' do
|
|||
|
||||
# All Sidekiq worker classes should declare a valid `feature_category`
|
||||
# or explicitly be excluded with the `feature_category_not_owned!` annotation.
|
||||
# Please see doc/development/sidekiq_style_guide.md#Feature-Categorization for more details.
|
||||
# Please see doc/development/sidekiq_style_guide.md#feature-categorization for more details.
|
||||
it 'has a feature_category or feature_category_not_owned! attribute', :aggregate_failures do
|
||||
workers_without_defaults.each do |worker|
|
||||
expect(worker.get_feature_category).to be_a(Symbol), "expected #{worker.inspect} to declare a feature_category or feature_category_not_owned!"
|
||||
|
@ -62,7 +62,7 @@ RSpec.describe 'Every Sidekiq worker' do
|
|||
|
||||
# All Sidekiq worker classes should declare a valid `feature_category`.
|
||||
# The category should match a value in `config/feature_categories.yml`.
|
||||
# Please see doc/development/sidekiq_style_guide.md#Feature-Categorization for more details.
|
||||
# Please see doc/development/sidekiq_style_guide.md#feature-categorization for more details.
|
||||
it 'has a feature_category that maps to a value in feature_categories.yml', :aggregate_failures do
|
||||
workers_with_feature_categories = workers_without_defaults
|
||||
.select(&:get_feature_category)
|
||||
|
|
Loading…
Reference in New Issue