Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-08 21:09:09 +00:00
parent 1d3086ebb4
commit 83fc2f3dc8
69 changed files with 734 additions and 311 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
---
title: Track last activity for Personal Access Token
merge_request: 35471
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Allow prefixing with Draft to mark MR as WIP
merge_request: 35940
author:
type: added

View File

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

View File

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

View File

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

View File

@ -12879,6 +12879,7 @@ type TestReportEdge {
State of a test report
"""
enum TestReportState {
FAILED
PASSED
}

View File

@ -37984,6 +37984,12 @@
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "FAILED",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13260,6 +13260,9 @@ msgstr ""
msgid "Last Seen"
msgstr ""
msgid "Last Used"
msgstr ""
msgid "Last accessed on"
msgstr ""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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