提交 f3e7bc80 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 eea1fbf9
......@@ -355,7 +355,7 @@ rspec foss-impact:
- run_timed_command "scripts/gitaly-test-spawn"
- source scripts/rspec_helpers.sh
- tooling/bin/find_foss_tests tmp/matching_foss_tests.txt
- rspec_simple_job "--tag ~quarantine --tag ~geo --tag ~level:migration $(cat tmp/matching_foss_tests.txt)"
- 'if [[ -n "$(cat tmp/matching_foss_tests.txt)" ]]; then rspec_simple_job "--tag ~quarantine --tag ~geo --tag ~level:migration $(cat tmp/matching_foss_tests.txt)"; fi'
artifacts:
expire_in: 7d
paths:
......
......@@ -46,6 +46,7 @@
"Docker",
"Elasticsearch",
"Facebook",
"fastlane",
"GDK",
"Geo",
"Git LFS",
......
......@@ -15,6 +15,16 @@ export default {
type: Object,
required: true,
},
diffFile: {
type: Object,
required: false,
default: () => ({}),
},
line: {
type: Object,
required: false,
default: null,
},
},
data() {
return {
......@@ -61,6 +71,8 @@ export default {
<ul class="notes draft-notes">
<noteable-note
:note="draft"
:diff-lines="diffFile.highlighted_diff_lines"
:line="line"
class="draft-note"
@handleEdit="handleEditing"
@cancelForm="handleNotEditing"
......
......@@ -10,6 +10,15 @@ export default {
type: Object,
required: true,
},
diffFile: {
type: Object,
required: true,
},
line: {
type: Object,
required: false,
default: null,
},
},
};
</script>
......@@ -17,7 +26,7 @@ export default {
<template>
<tr class="notes_holder js-temp-notes-holder">
<td class="notes-content" colspan="4">
<div class="content"><draft-note :draft="draft" /></div>
<div class="content"><draft-note :draft="draft" :diff-file="diffFile" :line="line" /></div>
</td>
</tr>
</template>
......@@ -4,12 +4,20 @@ import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
import { sprintf, __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import resolvedStatusMixin from '../mixins/resolved_status';
import { GlSprintf } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
getStartLineNumber,
getEndLineNumber,
getLineClasses,
} from '~/notes/components/multiline_comment_utils';
export default {
components: {
Icon,
GlSprintf,
},
mixins: [resolvedStatusMixin],
mixins: [resolvedStatusMixin, glFeatureFlagsMixin()],
props: {
draft: {
type: Object,
......@@ -51,7 +59,7 @@ export default {
const position = this.discussion ? this.discussion.position : this.draft.position;
return position.new_line || position.old_line;
return position?.new_line || position?.old_line;
},
content() {
const el = document.createElement('div');
......@@ -62,9 +70,18 @@ export default {
showLinePosition() {
return this.draft.file_hash || this.isDiffDiscussion;
},
startLineNumber() {
return getStartLineNumber(this.draft.position?.line_range);
},
endLineNumber() {
return getEndLineNumber(this.draft.position?.line_range);
},
},
methods: {
...mapActions('batchComments', ['scrollToDraft']),
getLineClasses(lineNumber) {
return getLineClasses(lineNumber);
},
},
showStaysResolved: false,
};
......@@ -83,11 +100,33 @@ export default {
@click="scrollToDraft(draft)"
>
<span class="review-preview-item-header">
<icon class="gl-mr-3 flex-shrink-0" :name="iconName" />
<span class="bold text-nowrap">
<span class="review-preview-item-header-text block-truncated"> {{ titleText }} </span>
<icon class="flex-shrink-0" :name="iconName" />
<span
class="bold text-nowrap"
:class="{ 'gl-align-items-center': glFeatures.multilineComments }"
>
<span class="review-preview-item-header-text block-truncated">
{{ titleText }}
</span>
<template v-if="showLinePosition">
:{{ linePosition }}
<template v-if="!glFeatures.multilineComments"
>:{{ linePosition }}</template
>
<template v-else-if="startLineNumber === endLineNumber">
:<span :class="getLineClasses(startLineNumber)">{{ startLineNumber }}</span>
</template>
<gl-sprintf v-else :message="__(':%{startLine} to %{endLine}')">
<template #startLine>
<span class="gl-mr-2" :class="getLineClasses(startLineNumber)">{{
startLineNumber
}}</span>
</template>
<template #endLine>
<span class="gl-ml-2" :class="getLineClasses(endLineNumber)">{{
endLineNumber
}}</span>
</template>
</gl-sprintf>
</template>
</span>
</span>
......
......@@ -25,9 +25,9 @@ export default {
discard(endpoint) {
return axios.delete(endpoint);
},
update(endpoint, { draftId, note, resolveDiscussion }) {
update(endpoint, { draftId, note, resolveDiscussion, position }) {
return axios.put(`${endpoint}/${draftId}`, {
draft_note: { note, resolve_discussion: resolveDiscussion },
draft_note: { note, resolve_discussion: resolveDiscussion, position },
});
},
};
......@@ -84,12 +84,16 @@ export const discardReview = ({ commit, getters }) => {
.catch(() => commit(types.RECEIVE_DISCARD_REVIEW_ERROR));
};
export const updateDraft = ({ commit, getters }, { note, noteText, resolveDiscussion, callback }) =>
export const updateDraft = (
{ commit, getters },
{ note, noteText, resolveDiscussion, position, callback },
) =>
service
.update(getters.getNotesData.draftsPath, {
draftId: note.id,
note: noteText,
resolveDiscussion,
position: JSON.stringify(position),
})
.then(res => res.data)
.then(data => commit(types.RECEIVE_DRAFT_UPDATE_SUCCESS, data))
......
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__ } from '~/locale';
import noteForm from '../../notes/components/note_form.vue';
import MultilineCommentForm from '../../notes/components/multiline_comment_form.vue';
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';
export default {
components: {
noteForm,
userAvatarLink,
MultilineCommentForm,
},
mixins: [autosave, diffLineNoteFormMixin],
mixins: [autosave, diffLineNoteFormMixin, glFeatureFlagsMixin()],
props: {
diffFileHash: {
type: String,
......@@ -37,6 +41,14 @@ export default {
default: '',
},
},
data() {
return {
commentLineStart: {
lineCode: this.line.line_code,
type: this.line.type,
},
};
},
computed: {
...mapState({
noteableData: state => state.notes.noteableData,
......@@ -62,11 +74,20 @@ 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,
},
};
},
diffFile() {
return this.getDiffFileByHash(this.diffFileHash);
},
commentLineOptions() {
return commentLineOptions(this.diffFile.highlighted_diff_lines, this.line.line_code);
},
},
mounted() {
if (this.isLoggedIn) {
......@@ -83,7 +104,6 @@ export default {
methods: {
...mapActions('diffs', [
'cancelCommentForm',
'assignDiscussionsToDiff',
'saveDiffDiscussion',
'setSuggestPopoverDismissed',
]),
......@@ -116,6 +136,16 @@ 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"
>
<multiline-comment-form
v-model="commentLineStart"
:line="line"
:comment-line-options="commentLineOptions"
/>
</div>
<user-avatar-link
v-if="author"
:link-href="author.path"
......@@ -133,7 +163,7 @@ export default {
:diff-file="diffFile"
:show-suggest-popover="showSuggestPopover"
save-button-title="Comment"
class="diff-comment-form"
class="diff-comment-form prepend-top-10"
@handleFormUpdateAddToReview="addToReview"
@cancelForm="handleCancelCommentForm"
@handleFormUpdate="handleSaveNote"
......
......@@ -80,6 +80,8 @@ export default {
v-if="shouldRenderDraftRow(diffFile.file_hash, line)"
:key="`draft_${index}`"
:draft="draftForLine(diffFile.file_hash, line)"
:diff-file="diffFile"
:line="line"
/>
</template>
</tbody>
......
......@@ -40,6 +40,7 @@ export function getFormData(params) {
diffViewType,
linePosition,
positionType,
lineRange,
} = params;
const position = JSON.stringify({
......@@ -55,6 +56,7 @@ export function getFormData(params) {
y: params.y,
width: params.width,
height: params.height,
line_range: lineRange,
});
const postData = {
......
<script>
import { GlFormSelect, GlSprintf } from '@gitlab/ui';
import { getSymbol, getLineClasses } from './multiline_comment_utils';
export default {
components: { GlFormSelect, GlSprintf },
props: {
lineRange: {
type: Object,
required: false,
default: null,
},
line: {
type: Object,
required: true,
},
commentLineOptions: {
type: Array,
required: true,
},
},
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,
},
};
},
methods: {
getSymbol({ type }) {
return getSymbol(type);
},
getLineClasses(line) {
return getLineClasses(line);
},
},
};
</script>
<template>
<div>
<gl-sprintf
:message="
s__('MergeRequestDiffs|Commenting on lines %{selectStart}start%{selectEnd} to %{end}')
"
>
<template #select>
<label for="comment-line-start" class="sr-only">{{
s__('MergeRequestDiffs|Select comment starting line')
}}</label>
<gl-form-select
id="comment-line-start"
:value="commentLineStart"
:options="commentLineOptions"
size="sm"
class="gl-w-auto gl-vertical-align-baseline"
@change="$emit('input', $event)"
/>
</template>
<template #end>
<span :class="getLineClasses(line)">
{{ getSymbol(line) + (line.new_line || line.old_line) }}
</span>
</template>
</gl-sprintf>
</div>
</template>
import { takeRightWhile } from 'lodash';
export function getSymbol(type) {
if (type === 'new') return '+';
if (type === 'old') return '-';
return '';
}
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];
return (lineNumber && getSymbol(lineType) + lineNumber) || '';
}
export function getStartLineNumber(lineRange) {
return getLineNumber(lineRange, 'start');
}
export function getEndLineNumber(lineRange) {
return getLineNumber(lineRange, 'end');
}
export function getLineClasses(line) {
const symbol = typeof line === 'string' ? line.charAt(0) : getSymbol(line?.type);
if (symbol !== '+' && symbol !== '-') return '';
return [
'gl-px-1 gl-rounded-small gl-border-solid gl-border-1 gl-border-white',
{
'gl-bg-green-100 gl-text-green-800': symbol === '+',
'gl-bg-red-100 gl-text-red-800': symbol === '-',
},
];
}
export function commentLineOptions(diffLines, lineCode) {
const selectedIndex = diffLines.findIndex(line => line.line_code === lineCode);
const notMatchType = l => l.type !== 'match';
// 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);
// 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}`,
}));
}
......@@ -2,6 +2,8 @@
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { escape } from 'lodash';
import { GlSprintf } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import { __, s__, sprintf } from '../../locale';
......@@ -14,17 +16,26 @@ import eventHub from '../event_hub';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
import httpStatusCodes from '~/lib/utils/http_status';
import {
getStartLineNumber,
getEndLineNumber,
getLineClasses,
commentLineOptions,
} from './multiline_comment_utils';
import MultilineCommentForm from './multiline_comment_form.vue';
export default {
name: 'NoteableNote',
components: {
GlSprintf,
userAvatarLink,
noteHeader,
noteActions,
NoteBody,
TimelineEntryItem,
MultilineCommentForm,
},
mixins: [noteable, resolvable],
mixins: [noteable, resolvable, glFeatureFlagsMixin()],
props: {
note: {
type: Object,
......@@ -50,6 +61,11 @@ export default {
required: false,
default: false,
},
diffLines: {
type: Object,
required: false,
default: null,
},
},
data() {
return {
......@@ -57,9 +73,14 @@ export default {
isDeleting: false,
isRequesting: false,
isResolving: false,
commentLineStart: {
line_code: this.line?.line_code,
type: this.line?.type,
},
};
},
computed: {
...mapGetters('diffs', ['getDiffFileByHash']),
...mapGetters(['targetNoteHash', 'getNoteableData', 'getUserData', 'commentsDisabled']),
author() {
return this.note.author;
......@@ -113,6 +134,32 @@ export default {
(this.note.isDraft && this.note.discussion_id !== null)
);
},
lineRange() {
return this.note.position?.line_range;
},
startLineNumber() {
return getStartLineNumber(this.lineRange);
},
endLineNumber() {
return getEndLineNumber(this.lineRange);
},
showMultiLineComment() {
return (
this.glFeatures.multilineComments &&
this.startLineNumber &&
this.endLineNumber &&
(this.startLineNumber !== this.endLineNumber || this.isEditing)
);
},
commentLineOptions() {
if (this.diffLines) {
return commentLineOptions(this.diffLines, this.line.line_code);
}
const diffFile = this.diffFile || this.getDiffFileByHash(this.targetNoteHash);
if (!diffFile) return null;
return commentLineOptions(diffFile.highlighted_diff_lines, this.line.line_code);
},
},
created() {
......@@ -174,10 +221,20 @@ export default {
this.$emit('updateSuccess');
},
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,
},
};
this.$emit('handleUpdateNote', {
note: this.note,
noteText,
resolveDiscussion,
position,
callback: () => this.updateSuccess(),
});
......@@ -239,6 +296,9 @@ export default {
noteBody.note.note = noteText;
}
},
getLineClasses(lineNumber) {
return getLineClasses(lineNumber);
},
},
};
</script>
......@@ -251,6 +311,26 @@ export default {
:data-note-id="note.id"
class="note note-wrapper qa-noteable-note-item"
>
<div v-if="showMultiLineComment" data-testid="multiline-comment">
<multiline-comment-form
v-if="isEditing && commentLineOptions && line"
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"
/>
<div v-else class="gl-mb-3 gl-text-gray-700">
<gl-sprintf :message="__('Comment on lines %{startLine} to %{endLine}')">
<template #startLine>
<span :class="getLineClasses(startLineNumber)">{{ startLineNumber }}</span>
</template>
<template #endLine>
<span :class="getLineClasses(endLineNumber)">{{ endLineNumber }}</span>
</template>
</gl-sprintf>
</div>
</div>
<div v-once class="timeline-icon">
<user-avatar-link
:link-href="author.path"
......
......@@ -82,7 +82,8 @@ export default {
})
.catch(() => createFlash(__('Failed to load emoji list.')));
},
showEmojiMenu() {
showEmojiMenu(e) {
e.stopPropagation();
this.isEmojiMenuVisible = true;
this.emojiMenu.showEmojiMenu($(this.$refs.toggleEmojiMenuButton));
},
......
......@@ -33,6 +33,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:widget_visibility_polling, @project, default_enabled: true)
push_frontend_feature_flag(:merge_ref_head_comments, @project)
push_frontend_feature_flag(:mr_commit_neighbor_nav, @project, default_enabled: true)
push_frontend_feature_flag(:multiline_comments, @project)
end
before_action do
......
......@@ -57,7 +57,7 @@ module Resolvers
def jira_projects(name:, start_at:, limit:)
args = { query: name, start_at: start_at, limit: limit }.compact
response = jira_service&.jira_projects(args)
response = Jira::Requests::Projects.new(project.jira_service, args).execute
projects = response.payload[:projects]
start_cursor = start_at == 0 ? nil : Base64.encode64((start_at - 1).to_s)
end_cursor = Base64.encode64((start_at + projects.size - 1).to_s)
......
......@@ -226,26 +226,8 @@ class JiraService < IssueTrackerService
true
end
def jira_projects(query: '', limit: PROJECTS_PER_PAGE, start_at: 0)
return ServiceResponse.success(payload: { projects: [], is_last: true }) if limit.to_i <= 0
response = jira_request { client.get(projects_url(query: query, limit: limit.to_i, start_at: start_at.to_i)) }
return ServiceResponse.error(message: @error.message) if @error.present?
return ServiceResponse.success(payload: { projects: [] }) unless response['values'].present?
projects = response['values'].map { |v| JIRA::Resource::Project.build(client, v) }
ServiceResponse.success(payload: { projects: projects, is_last: response['isLast'] })
end
private
def projects_url(query:, limit:, start_at:)
'/rest/api/2/project/search?query=%{query}&maxResults=%{limit}&startAt=%{start_at}' %
{ query: CGI.escape(query.to_s), limit: limit, start_at: start_at }
end
def test_settings
return unless client_url.present?
......
......@@ -6,8 +6,6 @@ class Releases::Evidence < ApplicationRecord
belongs_to :release, inverse_of: :evidences
before_validation :generate_summary_and_sha
default_scope { order(created_at: :asc) }
sha_attribute :summary_sha
......@@ -31,14 +29,4 @@ class Releases::Evidence < ApplicationRecord
safe_summary
end
private
def generate_summary_and_sha
summary = Evidences::EvidenceSerializer.new.represent(self) # rubocop: disable CodeReuse/Serializer
return unless summary
self.summary = summary
self.summary_sha = Gitlab::CryptoHelper.sha256(summary)
end
end
# frozen_string_literal: true
module Jira
module Requests
class Base
include ProjectServicesLoggable
PER_PAGE = 50
attr_reader :jira_service, :project, :limit, :start_at, :query
def initialize(jira_service, limit: PER_PAGE, start_at: 0, query: nil)
@project = jira_service&.project
@jira_service = jira_service
@limit = limit
@start_at = start_at
@query = query
end
def execute
return ServiceResponse.error(message: _('Jira service not configured.')) unless jira_service&.active?
return ServiceResponse.success(payload: empty_payload) if limit.to_i <= 0
request
end
private
def client
@client ||= jira_service.client
end
def request
response = client.get(url)
build_service_response(response)
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => error
error_message = error.message
log_error("Error sending message", client_url: client.options[:site], error: error_message)
ServiceResponse.error(message: error_message)
end
def url
raise NotImplementedError
end
def build_service_response(response)
raise NotImplementedError
end
end
end
end
# frozen_string_literal: true
module Jira
module Requests
class Projects < Base
extend ::Gitlab::Utils::Override
private
override :url
def url
'/rest/api/2/project/search?query=%{query}&maxResults=%{limit}&startAt=%{start_at}' %
{ query: CGI.escape(query.to_s), limit: limit.to_i, start_at: start_at.to_i }
end
override :build_service_response
def build_service_response(response)
return ServiceResponse.success(payload: empty_payload) unless response['values'].present?
ServiceResponse.success(payload: { projects: map_projects(response), is_last: response['isLast'] })
end
def map_projects(response)
response['values'].map { |v| JIRA::Resource::Project.build(client, v) }
end
def empty_payload
{ projects: [], is_last: true }
end
end
end
end
# frozen_string_literal: true
module Releases
class CreateEvidenceService
def initialize(release)
@release = release
end
def execute
evidence = release.evidences.build
summary = Evidences::EvidenceSerializer.new.represent(evidence) # rubocop: disable CodeReuse/Serializer
evidence.summary = summary
# TODO: fix the sha generating https://gitlab.com/gitlab-org/gitlab/-/issues/209000
evidence.summary_sha = Gitlab::CryptoHelper.sha256(summary)
evidence.save!
end
private
attr_reader :release
end
end
......@@ -19,7 +19,8 @@ class UserProjectAccessChangedService
if priority == HIGH_PRIORITY
AuthorizedProjectsWorker.bulk_perform_async(bulk_args) # rubocop:disable Scalability/BulkPerformWithContext
else
AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker.bulk_perform_in(DELAY, bulk_args) # rubocop:disable Scalability/BulkPerformWithContext
AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker.bulk_perform_in( # rubocop:disable Scalability/BulkPerformWithContext
DELAY, bulk_args, batch_size: 100, batch_delay: 30.seconds)
end
end
end
......
......@@ -84,7 +84,7 @@ module ApplicationWorker
Sidekiq::Client.push_bulk('class' => self, 'args' => args_list)
end
def bulk_perform_in(delay, args_list)
def bulk_perform_in(delay, args_list, batch_size: nil, batch_delay: nil)
now = Time.now.to_i
schedule = now + delay.to_i
......@@ -92,7 +92,14 @@ module ApplicationWorker
raise ArgumentError, _('The schedule time must be in the future!')
end
Sidekiq::Client.push_bulk('class' => self, 'args' => args_list, 'at' => schedule)
if batch_size && batch_delay
args_list.each_slice(batch_size.to_i).with_index do |args_batch, idx|
batch_schedule = schedule + idx * batch_delay.to_i
Sidekiq::Client.push_bulk('class' => self, 'args' => args_batch, 'at' => batch_schedule)
end
else
Sidekiq::Client.push_bulk('class' => self, 'args' => args_list, 'at' => schedule)
end
end
end
end
......@@ -10,6 +10,6 @@ class CreateEvidenceWorker # rubocop:disable Scalability/IdempotentWorker
release = Release.find_by_id(release_id)
return unless release
Releases::Evidence.create!(release: release)
::Releases::CreateEvidenceService.new(release).execute
end
end
---
title: Fix invisible emoji modal on Set Status form when clicked the second time
merge_request: 33398
author:
type: fixed
---
title: Fix snippet repository import fail with older export files
merge_request: 33584
author:
type: fixed
---
title: Add frontend support for multiline comments
merge_request: 29516
author:
type: added
......@@ -9,6 +9,7 @@ architected
Artifactory
Asana
Asciidoctor
Assembla
Atlassian
Auth0
Authentiq
......@@ -75,6 +76,7 @@ crosslinking
crosslinks
Crossplane
CrowdIn
datetime
Debian
deduplicate
deduplicated
......@@ -97,11 +99,13 @@ Dreamweaver
Elasticsearch
enablement
enqueued
Excon
expirable
Facebook
failover
failovers
failsafe
fastlane
favicon
firewalled
Flawfinder
......@@ -172,6 +176,7 @@ Laravel
LDAP
Libravatar
Lograge
Logstash
lookahead
lookaheads
lookbehind
......@@ -202,6 +207,7 @@ mitigations
mockup
mockups
ModSecurity
mutex
nameserver
nameservers
namespace
......@@ -209,6 +215,7 @@ namespaced
namespaces
Nanoc
NGINX
Nokogiri
npm
Nurtch
OAuth
......@@ -273,6 +280,7 @@ relicensing
remediations
Repmgr
Repmgrd
repurposing
requeue
requeued
requeues
......@@ -313,8 +321,11 @@ serializing
Sitespeed
Slack
Slony
smartcard
smartcards
SMTP
Sobelow
Sourcegraph
spidering
Splunk
SpotBugs
......@@ -398,6 +409,7 @@ unpublished
unpublishes
unpublishing
unreferenced
unreplicated
unresolve
unresolved
unresolving
......@@ -423,6 +435,7 @@ upvotes
validator
validators
vendored
versionless
virtualized
virtualizing
Vue
......@@ -430,6 +443,7 @@ Vuex
walkthrough
walkthroughs
WebdriverIO
Webex
webpack
webserver
whitepaper
......
......@@ -66,7 +66,7 @@ One risk of using a single bucket would be that if your organisation decided to
migrate GitLab to the Helm deployment in the future. GitLab would run, but the situation with
backups might not be realised until the organisation had a critical requirement for the backups to work.
### S3 API compatability issues
### S3 API compatibility issues
Not all S3 providers [are fully compatible](../raketasks/backup_restore.md#other-s3-providers)
with the Fog library that GitLab uses. Symptoms include:
......
......@@ -189,7 +189,7 @@ Moving past that, it is best to attempt the same search using the [Elasticsearch
If the results:
- Sync up, then there is not a technical "issue" per se. Instead, it might be a problem
- Sync up, then there is not a technical "issue." Instead, it might be a problem
with the Elasticsearch filters we are using. This can be complicated, so it is best to
escalate to GitLab support to check these and guide you on the potential on whether or
not a feature request is needed.
......
......@@ -263,7 +263,7 @@ p.import_state.mark_as_failed("Failed manually through console.")
In a specific situation, an imported repository needed to be renamed. The Support
Team was informed of a backup restore that failed on a single repository, which created
the project with an empty repository. The project was successfully restored to a dev
the project with an empty repository. The project was successfully restored to a development
instance, then exported, and imported into a new project under a different name.
The Support Team was able to transfer the incorrectly named imported project into the
......@@ -652,7 +652,7 @@ License.current.feature_available?(:jira_dev_panel_integration)
### Check if a project feature is available in a project
Features listed in <https://gitlab.com/gitlab-org/gitlab/blob/master/ee/app/models/license.rb>.
Features listed in [`license.rb`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/app/models/license.rb).
```ruby
p = Project.find_by_full_path('<group>/<project>')
......
......@@ -55,8 +55,8 @@ This section is for links to information elsewhere in the GitLab documentation.
- [GitLab database requirements](../../install/requirements.md#database) including
- Support for MySQL was removed in GitLab 12.1; [migrate to PostgreSQL](../../update/mysql_to_postgresql.md)
- required extension pg_trgm
- required extension postgres_fdw for Geo
- required extension `pg_trgm`
- required extension `postgres_fdw` for Geo
- Errors like this in the `production/sidekiq` log; see: [Set default_transaction_isolation into read committed](https://docs.gitlab.com/omnibus/settings/database.html#set-default_transaction_isolation-into-read-committed):
......
......@@ -86,13 +86,13 @@ sudo apt-get install linux-tools-common linux-tools-generic linux-tools-`uname -
sudo yum install perf
```
Run perf against the Sidekiq PID:
Run `perf` against the Sidekiq PID:
```shell
sudo perf record -p <sidekiq_pid>
```
Let this run for 30-60 seconds and then press Ctrl-C. Then view the perf report:
Let this run for 30-60 seconds and then press Ctrl-C. Then view the `perf` report:
```shell
$ sudo perf report
......@@ -105,13 +105,13 @@ Samples: 348K of event 'cycles', Event count (approx.): 280908431073
0.10% ruby libc-2.12.so [.] _int_free
```
Above you see sample output from a perf report. It shows that 97% of the CPU is
Above you see sample output from a `perf` report. It shows that 97% of the CPU is
being spent inside Nokogiri and `xmlXPathNodeSetMergeAndClear`. For something
this obvious you should then go investigate what job in GitLab would use
Nokogiri and XPath. Combine with `TTIN` or `gdb` output to show the
corresponding Ruby code where this is happening.
## The GNU Project Debugger (gdb)
## The GNU Project Debugger (`gdb`)
`gdb` can be another effective tool for debugging Sidekiq. It gives you a little
more interactive way to look at each thread and see what's causing problems.
......
......@@ -168,8 +168,8 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
| `provider` | Always `OpenStack` for compatible hosts | `OpenStack` |
| `openstack_username` | OpenStack username | |
| `openstack_api_key` | OpenStack API key | |
| `openstack_temp_url_key` | OpenStack key for generating temporary urls | |
| `openstack_auth_url` | OpenStack authentication endpont | |
| `openstack_temp_url_key` | OpenStack key for generating temporary URLs | |
| `openstack_auth_url` | OpenStack authentication endpoint | |
| `openstack_region` | OpenStack region | |
| `openstack_tenant` | OpenStack tenant ID |
......
......@@ -50,7 +50,7 @@ PUT /application/appearance
| `description` | string | no | Markdown text shown on the sign in / sign up page
| `logo` | mixed | no | Instance image used on the sign in / sign up page
| `header_logo` | mixed | no | Instance image used for the main navigation bar
| `favicon` | mixed | no | Instance favicon in .ico/.png format
| `favicon` | mixed | no | Instance favicon in `.ico` or `.png` format
| `new_project_guidelines` | string | no | Markdown text shown on the new project page
| `profile_image_guidelines` | string | no | Markdown text shown on the profile page below Public Avatar
| `header_message` | string | no | Message within the system header bar
......
......@@ -783,7 +783,9 @@ Diff comments also contain position:
"new_line": 27,
"line_range": {
"start_line_code": "588440f66559714280628a4f9799f0c4eb880a4a_10_10",
"end_line_code": "588440f66559714280628a4f9799f0c4eb880a4a_11_11"
"start_line_type": "new",
"end_line_code": "588440f66559714280628a4f9799f0c4eb880a4a_11_11",
"end_line_type": "old"
}
},
"resolved": false,
......@@ -848,6 +850,8 @@ Parameters:
| `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) |
......
......@@ -141,7 +141,7 @@ Example of response
## Create a new environment
Creates a new environment with the given name and external_url.
Creates a new environment with the given name and `external_url`.
It returns `201` if the environment was successfully created, `400` for wrong parameters.
......@@ -173,7 +173,7 @@ Example response:
## Edit an existing environment
Updates an existing environment's name and/or external_url.
Updates an existing environment's name and/or `external_url`.
It returns `200` if the environment was successfully updated. In case of an error, a status code `400` is returned.
......@@ -186,7 +186,7 @@ PUT /projects/:id/environments/:environments_id
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `environment_id` | integer | yes | The ID of the environment |
| `name` | string | no | The new name of the environment |
| `external_url` | string | no | The new external_url |
| `external_url` | string | no | The new `external_url` |
```shell
curl --request PUT --data "name=staging&external_url=https://staging.example.gitlab.com" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/environments/1"
......
......@@ -29,11 +29,11 @@ allows clients to request exactly the data they need, making it
possible to get all required data in a limited number of requests.
The GraphQL data (fields) can be described in the form of types,
allowing clients to use [clientside GraphQL
allowing clients to use [client-side GraphQL
libraries](https://graphql.org/code/#graphql-clients) to consume the
API and avoid manual parsing.
Since there's no fixed endpoints and datamodel, new abilities can be
Since there's no fixed endpoints and data model, new abilities can be
added to the API without creating breaking changes. This allows us to
have a versionless API as described in [the GraphQL
documentation](https://graphql.org/learn/best-practices/#versioning).
......
......@@ -2,7 +2,7 @@
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29089) in GitLab 12.10 behind a disabled feature flag.
Metrics dashboard annotations allow you to indicate events on your graphs at a single point in time or over a timespan.
Metrics dashboard annotations allow you to indicate events on your graphs at a single point in time or over a time span.
## Create a new annotation
......@@ -12,7 +12,7 @@ POST /clusters/:id/metrics_dashboard/annotations/
```
NOTE: **Note:**
The value of `dashboard_path` will be treated as a CGI-escaped path, and automatically unescaped.
The value of `dashboard_path` will be treated as a CGI-escaped path, and automatically un-escaped.
Parameters:
......
......@@ -4,7 +4,7 @@ This API is a project-specific version of these endpoints:
- [Dockerfile templates](templates/dockerfiles.md)
- [Gitignore templates](templates/gitignores.md)
- [GitLab CI/CD Config templates](templates/gitlab_ci_ymls.md)
- [GitLab CI/CD Configuration templates](templates/gitlab_ci_ymls.md)
- [Open source license templates](templates/licenses.md)
It deprecates these endpoints, which will be removed for API version 5.
......
......@@ -51,7 +51,7 @@ Parameters:
- `ref` (required) - The name of branch, tag or commit
NOTE: **Note:**
`blob_id` is the blob sha, see [repositories - Get a blob from repository](repositories.md#get-a-blob-from-repository)
`blob_id` is the blob SHA, see [repositories - Get a blob from repository](repositories.md#get-a-blob-from-repository)
In addition to the `GET` method, you can also use `HEAD` to get just file metadata.
......
......@@ -714,7 +714,7 @@ Parameters:
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `recipients` | string | true | Recipients/channels separated by whitespaces |
| `default_irc_uri` | string | false | irc://irc.network.net:6697/ |
| `default_irc_uri` | string | false | `irc://irc.network.net:6697/` |
| `server_host` | string | false | localhost |
| `server_port` | integer | false | 6659 |
| `colorize_messages` | boolean | false | Colorize messages |
......@@ -1277,7 +1277,7 @@ A continuous integration and build server
Set JetBrains TeamCity CI service for a project.
> The build configuration in Teamcity must use the build format number %build.vcs.number% you will also want to configure monitoring of all branches so merge requests build, that setting is in the vsc root advanced settings.
> The build configuration in TeamCity must use the build format number `%build.vcs.number%` you will also want to configure monitoring of all branches so merge requests build, that setting is in the VSC root advanced settings.
```plaintext
PUT /projects/:id/services/teamcity
......
......@@ -71,7 +71,7 @@ available through the UI. You can use them by creating a new file,
choosing a template that suits your application, and adjusting it
to your needs:
![Use a .gitlab-ci.yml template](img/add_file_template_11_10.png)
![Use a `.gitlab-ci.yml` template](img/add_file_template_11_10.png)
For a broader overview, see the [CI/CD getting started](quick_start/README.md) guide.
......@@ -108,7 +108,7 @@ GitLab CI/CD supports numerous configuration options:
| [Pipeline triggers](triggers/README.md) | Trigger pipelines through the API. |
| [Pipelines for Merge Requests](merge_request_pipelines/index.md) | Design a pipeline structure for running a pipeline in merge requests. |
| [Integrate with Kubernetes clusters](../user/project/clusters/index.md) | Connect your project to Google Kubernetes Engine (GKE) or an existing Kubernetes cluster. |
| [Optimize GitLab and Runner for large repositories](large_repositories/index.md) | Recommended strategies for handling large repos. |
| [Optimize GitLab and Runner for large repositories](large_repositories/index.md) | Recommended strategies for handling large repositories. |
| [`.gitlab-ci.yml` full reference](yaml/README.md) | All the attributes you can use with GitLab CI/CD. |
Note that certain operations can only be performed according to the
......
......@@ -13,9 +13,8 @@ implement [GitLab CI/CD](../README.md) for your specific use case.
Examples are available in several forms. As a collection of:
- `.gitlab-ci.yml` [template files](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/lib/gitlab/ci/templates) maintained in GitLab. When you create a new file via the UI,
GitLab will give you the option to choose one of these templates. This will allow you to start using CI/CD with your project quickly.
If your favorite programming language or framework are missing, we would love your help by sending a merge request with a new `.gitlab-ci.yml` to this project.
- `.gitlab-ci.yml` [template files](#cicd-templates) maintained in GitLab, for many
common frameworks and programming languages.
- Repositories with [example projects](https://gitlab.com/gitlab-examples) for various languages. You can fork and adjust them to your own needs. Projects include demonstrations of [multi-project pipelines](https://gitlab.com/gitlab-examples/multi-project-pipelines) and using [Review Apps with a static site served by NGINX](https://gitlab.com/gitlab-examples/review-apps-nginx/).
- Examples and [other resources](#other-resources) listed below.
......@@ -50,9 +49,55 @@ language users and GitLab by sending a merge request with a guide for that langu
You may want to apply for the [GitLab Community Writers Program](https://about.gitlab.com/community/writers/)
to get paid for writing complete articles for GitLab.
## Adding templates to your GitLab installation **(PREMIUM ONLY)**
If you want to have customized examples and templates for your own self-managed GitLab instance available to your team, your GitLab administrator can [designate an instance template repository](../../user/admin_area/settings/instance_template_repository.md) that contains examples and templates specific to your enterprise.
## CI/CD templates
Get started with GitLab CI/CD and your favorite programming language or framework by using a
`.gitlab-ci.yml` [template](https://gitlab.com/gitlab-org/gitlab/tree/master/lib/gitlab/ci/templates).
When you create a `gitlab-ci.yml` file in the UI, you can
choose one of these templates:
- [Android (`Android.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Android.gitlab-ci.yml)
- [Android with fastlane (`Android-Fastlane.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml)
- [Bash (`Bash.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Bash.gitlab-ci.yml)
- [C++ (`C++.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/C++.gitlab-ci.yml)
- [Chef (`Chef.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Chef.gitlab-ci.yml)
- [Clojure (`Clojure.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Clojure.gitlab-ci.yml)
- [Crystal (`Crystal.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Crystal.gitlab-ci.yml)
- [Django (`Django.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Django.gitlab-ci.yml)
- [Docker (`Docker.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Docker.gitlab-ci.yml)
- [dotNET (`dotNET.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/dotNET.gitlab-ci.yml)
- [dotNET Core (`dotNET-Core.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/dotNET-Core.yml)
- [Elixir (`Elixir.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Elixir.gitlab-ci.yml)
- [goLang (`Go.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Go.gitlab-ci.yml)
- [Gradle (`Gradle.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Gradle.gitlab-ci.yml)
- [Grails (`Grails.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Grails.gitlab-ci.yml)
- [iOS with fastlane (`iOS-Fastlane.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/iOS-Fastlane.gitlab-ci.yml)
- [Julia (`Julia.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Julia.gitlab-ci.yml)
- [Laravel (`Laravel.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml)
- [LaTeX (`LaTeX.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/LaTeX.gitlab-ci.yml)
- [Maven (`Maven.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Maven.gitlab-ci.yml)
- [Mono (`Mono.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Mono.gitlab-ci.yml)
- [Node.js (`Nodejs.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml)
- [OpenShift (`OpenShift.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/OpenShift.gitlab-ci.yml)
- [Packer (`Packer.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Packer.gitlab-ci.yml)
- [PHP (`PHP.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/PHP.gitlab-ci.yml)
- [Python (`Python.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Python.gitlab-ci.yml)
- [Ruby (`Ruby.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml)
- [Rust (`Rust.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Rust.gitlab-ci.yml)
- [Scala (`Scala.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Scala.gitlab-ci.yml)
- [Swift (`Swift.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Swift.gitlab-ci.yml)
- [Terraform (`Terraform.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml)
If a programming language or framework template is not in this list, you can contribute
one. To create a template, submit a merge request
to <https://gitlab.com/gitlab-org/gitlab/tree/master/lib/gitlab/ci/templates>.
### Adding templates to your GitLab installation **(PREMIUM ONLY)**
You can add custom examples and templates to your self-managed GitLab instance.
Your GitLab administrator can [designate an instance template repository](../../user/admin_area/settings/instance_template_repository.md)
that contains examples and templates specific to your organization.
## Other resources
......
......@@ -920,7 +920,7 @@ can use one of the [`workflow: rules` templates](#workflowrules-templates) to ge
This will ensure that the behavior is more stable as you start adding additional `rules`
blocks, and will avoid issues like creating a duplicate, merge request (detached) pipeline.
We don't recomment mixing `only/except` jobs with `rules` jobs in the same pipeline.
We don't recommend mixing `only/except` jobs with `rules` jobs in the same pipeline.
It may not cause YAML errors, but debugging the exact execution behavior can be complex
due to the different default behaviors of `only/except` and `rules`.
......@@ -1048,6 +1048,10 @@ You can use [`allow_failure: true`](#allow_failure) within `rules:` to allow a j
wait for action, without stopping the pipeline itself. All jobs using `rules:` default to `allow_failure: false`
if `allow_failure:` is not defined.
The rule-level `rules:allow_failure` option overrides the job-level
[`allow_failure`](#allow_failure) option, and is only applied when the job is
triggered by the particular rule.
```yaml
job:
script: "echo Hello, Rules!"
......
......@@ -35,11 +35,18 @@ Here are some things to keep in mind regarding test performance:
To run RSpec tests:
```shell
# run all tests
# run test for a file
bin/rspec spec/models/project_spec.rb
# run test for the example on line 10 on that file
bin/rspec spec/models/project_spec.rb:10
# run tests matching the example name has that string
bin/rspec spec/models/project_spec.rb -e associations
# run all tests, will take hours for GitLab codebase!
bin/rspec
# run test for path
bin/rspec spec/[path]/[to]/[spec].rb
```
Use [Guard](https://github.com/guard/guard) to continuously monitor for changes and only run matching tests:
......
......@@ -1433,7 +1433,9 @@ Example:
Additionally, you can choose the alignment of text within columns by adding colons (`:`)
to the sides of the "dash" lines in the second row. This will affect every cell in the column.
> Note that the headers are always right aligned [within GitLab itself](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#tables).
NOTE: **Note:**
[Within GitLab itself](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#tables),
the headers are always left-aligned in Chrome and Firefox, and centered in Safari.
```markdown
| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
......
......@@ -150,9 +150,12 @@ The following table depicts the various user permission levels in a project.
| Manage [project access tokens](./project/settings/project_access_tokens.md) **(CORE ONLY)** | | | | ✓ | ✓ |
| Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ |
| Rename project | | | | | ✓ |
| Remove fork relationship | | | | | ✓ |
| Remove project | | | | | ✓ |
| Archive project | | | | | ✓ |
| Delete issues | | | | | ✓ |
| Delete merge request | | | | | ✓ |
| Disable notification emails | | | | | ✓ |
| Force push to protected branches (*4*) | | | | | |
| Remove protected branches (*4*) | | | | | |
......
......@@ -129,9 +129,8 @@ allowing access only to trusted actors.
CAUTION: **Caution:**
Before GitLab 11.5, Code Quality job and artifact had to be named specifically to
automatically extract report data and show it in the merge request widget. While these
old job definitions are still maintained they have been deprecated and may be removed
in the next major release, GitLab 12.0. You are advised to update your current `.gitlab-ci.yml`
configuration to reflect that change.
old job definitions are still maintained they have been deprecated and are no longer supported on GitLab 12.0 or higher.
You are advised to update your `.gitlab-ci.yml` configuration to reflect that change.
For GitLab 11.5 and later, the job should look like:
......
......@@ -78,6 +78,8 @@ module API
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'
end
end
end
......
......@@ -10,8 +10,6 @@ module Gitlab
end
def restore
return true unless Dir.exist?(snippets_repo_bundle_path)
@project.snippets.find_each.all? do |snippet|
Gitlab::ImportExport::SnippetRepoRestorer.new(snippet: snippet,
user: @user,
......
......@@ -11,7 +11,13 @@ namespace :gitlab do
warn_user_is_not_gitlab
url = registry_config.api_url
client = ContainerRegistry::Client.new(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!(
......
......@@ -833,6 +833,9 @@ msgstr ""
msgid "8 hours"
msgstr ""
msgid ":%{startLine} to %{endLine}"
msgstr ""
msgid "< 1 hour"
msgstr ""
......@@ -5568,6 +5571,9 @@ msgstr ""
msgid "Comment is being updated"
msgstr ""
msgid "Comment on lines %{startLine} to %{endLine}"
msgstr ""
msgid "Comment/Reply (quoting selected text)"
msgstr ""
......@@ -12340,6 +12346,9 @@ msgstr ""
msgid "Jira project: %{importProject}"
msgstr ""
msgid "Jira service not configured."
msgstr ""
msgid "JiraService| on branch %{branch_link}"
msgstr ""
......@@ -13621,6 +13630,12 @@ msgstr ""
msgid "MergeConflict|origin//their changes"
msgstr ""
msgid "MergeRequestDiffs|Commenting on lines %{selectStart}start%{selectEnd} to %{end}"
msgstr ""
msgid "MergeRequestDiffs|Select comment starting line"
msgstr ""
msgid "MergeRequests|Add a reply"
msgstr ""
......
......@@ -3,6 +3,8 @@
module QA
context 'Plan', :smoke do
describe 'Issue creation' do
let(:closed_issue) { Resource::Issue.fabricate_via_api! }
before do
Flow::Login.sign_in
end
......@@ -17,6 +19,25 @@ module QA
end
end
it 'closes an issue' do
closed_issue.visit!
Page::Project::Issue::Show.perform do |issue_page|
issue_page.click_close_issue_button
expect(issue_page).to have_element(:reopen_issue_button)
end
Page::Project::Menu.perform(&:click_issues)
Page::Project::Issue::Index.perform do |index|
expect(index).not_to have_issue(closed_issue)
index.click_closed_issues_link
expect(index).to have_issue(closed_issue)
end
end
context 'when using attachments in comments', :object_storage do
let(:gif_file_name) { 'banana_sample.gif' }
let(:file_to_attach) do
......
......@@ -31,8 +31,8 @@ describe Projects::Releases::EvidencesController do
end
describe 'GET #show' do
let_it_be(:tag_name) { "v1.1.0-evidence" }
let!(:release) { create(:release, :with_evidence, project: project, tag: tag_name) }
let(:tag_name) { "v1.1.0-evidence" }
let!(:release) { create(:release, project: project, tag: tag_name) }
let(:evidence) { release.evidences.first }
let(:tag) { CGI.escape(release.tag) }
let(:format) { :json }
......@@ -48,6 +48,8 @@ describe Projects::Releases::EvidencesController do
end
before do
::Releases::CreateEvidenceService.new(release).execute
sign_in(user)
end
......@@ -84,14 +86,9 @@ describe Projects::Releases::EvidencesController do
end
context 'when release is associated to a milestone which includes an issue' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:milestone) { create(:milestone, project: project, issues: [issue]) }
let_it_be(:release) { create(:release, project: project, tag: tag_name, milestones: [milestone]) }
before do
create(:evidence, release: release)
end
let(:issue) { create(:issue, project: project) }
let(:milestone) { create(:milestone, project: project, issues: [issue]) }
let(:release) { create(:release, project: project, tag: tag_name, milestones: [milestone]) }
shared_examples_for 'does not show the issue in evidence' do
it do
......@@ -111,7 +108,9 @@ describe Projects::Releases::EvidencesController do
end
end
shared_examples_for 'safely expose evidence' do
context 'when user is non-project member' do
let(:user) { create(:user) }
it_behaves_like 'does not show the issue in evidence'
context 'when the issue is confidential' do
......@@ -127,28 +126,50 @@ describe Projects::Releases::EvidencesController do
end
context 'when project is private' do
let!(:project) { create(:project, :repository, :private) }
let(:project) { create(:project, :repository, :private) }
it_behaves_like 'evidence not found'
end
context 'when project restricts the visibility of issues to project members only' do
let!(:project) { create(:project, :repository, :issues_private) }
let(:project) { create(:project, :repository, :issues_private) }
it_behaves_like 'evidence not found'
end
end
context 'when user is non-project member' do
let(:user) { create(:user) }
it_behaves_like 'safely expose evidence'
end
context 'when user is auditor', if: Gitlab.ee? do
let(:user) { create(:user, :auditor) }
it_behaves_like 'safely expose evidence'
it_behaves_like 'does not show the issue in evidence'
context 'when the issue is confidential' do
let(:issue) { create(:issue, :confidential, project: project) }
it_behaves_like 'does not show the issue in evidence'
end
context 'when the user is the author of the confidential issue' do
let(:issue) { create(:issue, :confidential, project: project, author: user) }
it_behaves_like 'does not show the issue in evidence'
end
context 'when project is private' do
let(:project) { create(:project, :repository, :private) }
it 'returns evidence ' do
subject
expect(json_response).to eq(evidence.summary)
end
end
context 'when project restricts the visibility of issues to project members only' do
let(:project) { create(:project, :repository, :issues_private) }
it_behaves_like 'evidence not found'
end
end
context 'when external authorization control is enabled' do
......
......@@ -114,6 +114,40 @@ describe 'User comments on a diff', :js do
include_examples 'comment on merge request file'
end
context 'when adding multiline comments' do
it 'saves a multiline comment' do
click_diff_line(find("[id='#{sample_commit.line_code}']"))
page.within('.discussion-form') do
find('#comment-line-start option', text: '-13').select_option
end
page.within('.js-discussion-note-form') do
fill_in(:note_note, with: 'Line is wrong')
click_button('Add comment now')
end
wait_for_requests
page.within('.notes_holder') do
expect(page).to have_content('Line is wrong')
expect(page).to have_content('Comment on lines -13 to +14')
end
visit(merge_request_path(merge_request))
page.within('.notes .discussion') do
expect(page).to have_content("#{user.name} #{user.to_reference} started a thread")
expect(page).to have_content(sample_commit.line_code_path)
expect(page).to have_content('Line is wrong')
end
page.within('.notes-tab .badge') do
expect(page).to have_content('1')
end
end
end
context 'when editing comments' do
it 'edits a comment' do
click_diff_line(find("[id='#{sample_commit.line_code}']"))
......
......@@ -245,6 +245,15 @@ describe 'User edit profile' do
end
end
it 'opens the emoji modal again after closing it' do
open_user_status_modal
select_emoji('biohazard', true)
find('.js-toggle-emoji-menu').click
expect(page).to have_selector('.emoji-menu')
end
it 'does not update the awards panel emoji' do
project.add_maintainer(user)
visit(project_issue_path(project, issue))
......
......@@ -80,7 +80,7 @@ describe 'User views releases', :js do
context 'with a tag containing a slash' do
it 'sees the release' do
release = create :release, :with_evidence, project: project, tag: 'debian/2.4.0-1'
release = create :release, project: project, tag: 'debian/2.4.0-1'
visit project_releases_path(project)
expect(page).to have_content(release.name)
......
......@@ -4,7 +4,6 @@
"id",
"title",
"description",
"author",
"state",
"iid",
"confidential",
......
......@@ -7,7 +7,8 @@
"state",
"iid",
"created_at",
"due_date"
"due_date",
"issues"
],
"properties": {
"id": { "type": "integer" },
......@@ -16,7 +17,11 @@
"state": { "type": "string" },
"iid": { "type": "integer" },
"created_at": { "type": "date" },
"due_date": { "type": ["date", "null"] }
"due_date": { "type": ["date", "null"] },
"issues": {
"type": "array",
"items": { "$ref": "issue.json" }
}
},
"additionalProperties": false
}
......@@ -77,12 +77,24 @@ describe('DiffLineNoteForm', () => {
.spyOn(wrapper.vm, 'saveDiffDiscussion')
.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,
};
const formData = {
...wrapper.vm.formData,
lineRange,
};
wrapper.vm
.handleSaveNote('note body')
.then(() => {
expect(saveDiffDiscussionSpy).toHaveBeenCalledWith({
note: 'note body',
formData: wrapper.vm.formData,
formData,
});
})
.then(done)
......
......@@ -543,9 +543,9 @@ describe('DiffsStoreActions', () => {
new_path: 'file1',
old_line: 5,
old_path: 'file2',
line_range: null,
line_code: 'ABC_1_1',
position_type: 'text',
line_range: null,
},
},
hash: 'ABC_123',
......
......@@ -187,6 +187,7 @@ describe('DiffsStoreUtils', () => {
},
diffViewType: PARALLEL_DIFF_VIEW_TYPE,
linePosition: LINE_POSITION_LEFT,
lineRange: { start_line_code: 'abc_1_1', end_line_code: 'abc_2_2' },
};
const position = JSON.stringify({
......@@ -198,6 +199,7 @@ describe('DiffsStoreUtils', () => {
position_type: TEXT_DIFF_POSITION_TYPE,
old_line: options.noteTargetLine.old_line,
new_line: options.noteTargetLine.new_line,
line_range: options.lineRange,
});
const postData = {
......
import {
getSymbol,
getStartLineNumber,
getEndLineNumber,
} from '~/notes/components/multiline_comment_utils';
describe('Multiline comment utilities', () => {
describe('getStartLineNumber', () => {
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);
});
});
describe('getSymbol', () => {
it.each`
type | result
${'new'} | ${'+'}
${'old'} | ${'-'}
${'unused'} | ${''}
${''} | ${''}
${null} | ${''}
${undefined} | ${''}
`('`$type` returns `$result`', ({ type, result }) => {
expect(getSymbol(type)).toEqual(result);
});
});
});
import { escape } from 'lodash';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { mount, createLocalVue } from '@vue/test-utils';
import createStore from '~/notes/stores';
import issueNote from '~/notes/components/noteable_note.vue';
import NoteHeader from '~/notes/components/note_header.vue';
......@@ -8,9 +8,19 @@ import NoteActions from '~/notes/components/note_actions.vue';
import NoteBody from '~/notes/components/note_body.vue';
import { noteableDataMock, notesDataMock, note } from '../mock_data';
jest.mock('~/vue_shared/mixins/gl_feature_flags_mixin', () => () => ({
inject: {
glFeatures: {
from: 'glFeatures',
default: () => ({ multilineComments: true }),
},
},
}));
describe('issue_note', () => {
let store;
let wrapper;
const findMultilineComment = () => wrapper.find('[data-testid="multiline-comment"]');
beforeEach(() => {
store = createStore();
......@@ -18,12 +28,13 @@ describe('issue_note', () => {
store.dispatch('setNotesData', notesDataMock);
const localVue = createLocalVue();
wrapper = shallowMount(localVue.extend(issueNote), {
wrapper = mount(localVue.extend(issueNote), {
store,
propsData: {
note,
},
localVue,
stubs: ['note-header', 'user-avatar-link', 'note-actions', 'note-body'],
});
});
......@@ -31,6 +42,44 @@ describe('issue_note', () => {
wrapper.destroy();
});
describe('mutiline comments', () => {
it('should render if has multiline comment', () => {
const position = {
line_range: {
start_line_code: 'abc_1_1',
end_line_code: 'abc_2_2',
},
};
wrapper.setProps({
note: { ...note, position },
});
return wrapper.vm.$nextTick().then(() => {
expect(findMultilineComment().text()).toEqual('Comment on lines 1 to 2');
});
});
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',
},
};
wrapper.setProps({
note: { ...note, position },
});
return wrapper.vm.$nextTick().then(() => {
expect(findMultilineComment().exists()).toBe(false);
});
});
it('should not render if `line_range` is unavailable', () => {
expect(findMultilineComment().exists()).toBe(false);
});
});
it('should render user information', () => {
const { author } = note;
const avatar = wrapper.find(UserAvatarLink);
......
......@@ -4,11 +4,15 @@ require 'spec_helper'
describe API::Entities::Release do
let_it_be(:project) { create(:project) }
let_it_be(:release) { create(:release, :with_evidence, project: project) }
let(:release) { create(:release, project: project) }
let(:evidence) { release.evidences.first }
let(:user) { create(:user) }
let(:entity) { described_class.new(release, current_user: user).as_json }
before do
::Releases::CreateEvidenceService.new(release).execute
end
describe 'evidences' do
context 'when the current user can download code' do
let(:entity_evidence) { entity[:evidences].first }
......
......@@ -8,42 +8,90 @@ describe Gitlab::ImportExport::SnippetsRepoRestorer do
describe 'bundle a snippet Git repo' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let_it_be(:snippet_with_repo) { create(:project_snippet, :repository, project: project, author: user) }
let_it_be(:snippet_without_repo) { create(:project_snippet, project: project, author: user) }
let!(:snippet1) { create(:project_snippet, project: project, author: user) }
let!(:snippet2) { create(:project_snippet, project: project, author: user) }
let(:shared) { project.import_export_shared }
let(:exporter) { Gitlab::ImportExport::SnippetsRepoSaver.new(current_user: user, project: project, shared: shared) }
let(:bundle_dir) { ::Gitlab::ImportExport.snippets_repo_bundle_path(shared.export_path) }
let(:service) { instance_double(Gitlab::ImportExport::SnippetRepoRestorer) }
let(:restorer) do
described_class.new(user: user,
shared: shared,
project: project)
end
let(:service) { instance_double(Gitlab::ImportExport::SnippetRepoRestorer) }
before do
exporter.save
end
after do
FileUtils.rm_rf(shared.export_path)
end
it 'calls SnippetRepoRestorer per each snippet with the bundle path' do
allow(service).to receive(:restore).and_return(true)
shared_examples 'imports snippet repositories' do
before do
snippet1.snippet_repository&.delete
snippet1.repository.remove
snippet2.snippet_repository&.delete
snippet2.repository.remove
end
specify do
expect(snippet1.repository_exists?).to be false
expect(snippet2.repository_exists?).to be false
expect(Gitlab::ImportExport::SnippetRepoRestorer).to receive(:new).with(hash_including(snippet: snippet_with_repo, path_to_bundle: bundle_path(snippet_with_repo))).and_return(service)
expect(Gitlab::ImportExport::SnippetRepoRestorer).to receive(:new).with(hash_including(snippet: snippet_without_repo, path_to_bundle: bundle_path(snippet_without_repo))).and_return(service)
expect(Gitlab::ImportExport::SnippetRepoRestorer).to receive(:new).with(hash_including(snippet: snippet1, path_to_bundle: bundle_path(snippet1))).and_call_original
expect(Gitlab::ImportExport::SnippetRepoRestorer).to receive(:new).with(hash_including(snippet: snippet2, path_to_bundle: bundle_path(snippet2))).and_call_original
expect(restorer.restore).to be_truthy
snippet1.repository.expire_exists_cache
snippet2.repository.expire_exists_cache
expect(snippet1.blobs).not_to be_empty
expect(snippet2.blobs).not_to be_empty
end
end
context 'when export has no snippet repository bundle' do
before do
expect(Dir.exist?(bundle_dir)).to be false
end
it_behaves_like 'imports snippet repositories'
end
context 'when export has snippet repository bundles and snippets without them' do
let!(:snippet1) { create(:project_snippet, :repository, project: project, author: user) }
let!(:snippet2) { create(:project_snippet, project: project, author: user) }
before do
exporter.save
expect(File.exist?(bundle_path(snippet1))).to be true
expect(File.exist?(bundle_path(snippet2))).to be false
end
it_behaves_like 'imports snippet repositories'
end
context 'when export has only snippet bundles' do
let!(:snippet1) { create(:project_snippet, :repository, project: project, author: user) }
let!(:snippet2) { create(:project_snippet, :repository, project: project, author: user) }
before do
exporter.save
expect(File.exist?(bundle_path(snippet1))).to be true
expect(File.exist?(bundle_path(snippet2))).to be true
end
expect(restorer.restore).to be_truthy
it_behaves_like 'imports snippet repositories'
end
context 'when one snippet cannot be saved' do
it 'returns false and do not process other snippets' do
allow(Gitlab::ImportExport::SnippetRepoRestorer).to receive(:new).with(hash_including(snippet: snippet_with_repo)).and_return(service)
allow(Gitlab::ImportExport::SnippetRepoRestorer).to receive(:new).with(hash_including(snippet: snippet1)).and_return(service)
allow(service).to receive(:restore).and_return(false)
expect(Gitlab::ImportExport::SnippetRepoRestorer).not_to receive(:new).with(hash_including(snippet: snippet_without_repo))
expect(Gitlab::ImportExport::SnippetRepoRestorer).not_to receive(:new).with(hash_including(snippet: snippet2))
expect(restorer.restore).to be_falsey
end
end
......
......@@ -812,85 +812,4 @@ describe JiraService do
end
end
end
describe '#jira_projects' do
let(:project) { create(:project) }
let(:jira_service) do
described_class.new(
project: project,
url: url,
username: username,
password: password
)
end
context 'when request to the jira server fails' do
it 'returns error' do
test_url = "#{url}/rest/api/2/project/search?maxResults=50&query=&startAt=0"
WebMock.stub_request(:get, test_url).with(basic_auth: [username, password])
.to_raise(JIRA::HTTPError.new(double(message: 'random error')))
response = jira_service.jira_projects
expect(response.error?).to be true
expect(response.message).to eq('random error')
end
end
context 'with invalid params' do
it 'escapes params' do
escaped_url = "#{url}/rest/api/2/project/search?query=Test%26maxResults%3D3&maxResults=10&startAt=0"
WebMock.stub_request(:get, escaped_url).with(basic_auth: [username, password])
.to_return(body: {}.to_json, headers: { "Content-Type": "application/json" })
response = jira_service.jira_projects(query: 'Test&maxResults=3', limit: 10, start_at: 'zero')
expect(response.error?).to be false
end
end
context 'when no jira_projects are returned' do
let(:jira_projects_json) do
'{
"self": "https://your-domain.atlassian.net/rest/api/2/project/search?startAt=0&maxResults=2",
"nextPage": "https://your-domain.atlassian.net/rest/api/2/project/search?startAt=2&maxResults=2",
"maxResults": 2,
"startAt": 0,
"total": 7,
"isLast": false,
"values": []
}'
end
it 'returns empty array of jira projects' do
test_url = "#{url}/rest/api/2/project/search?maxResults=50&query=&startAt=0"
WebMock.stub_request(:get, test_url).with(basic_auth: [username, password])
.to_return(body: jira_projects_json, headers: { "Content-Type": "application/json" })
response = jira_service.jira_projects
expect(response.success?).to be true
expect(response.payload).not_to be nil
end
end
context 'when jira_projects are returned' do
include_context 'jira projects request context'
it 'returns array of jira projects' do
response = jira_service.jira_projects
projects = response.payload[:projects]
project_keys = projects.map(&:key)
project_names = projects.map(&:name)
project_ids = projects.map(&:id)
expect(response.success?).to be true
expect(projects.size).to eq(2)
expect(project_keys).to eq(%w(EX ABC))
expect(project_names).to eq(%w(Example Alphabetical))
expect(project_ids).to eq(%w(10000 10001))
end
end
end
end
......@@ -94,14 +94,6 @@ RSpec.describe Release do
describe 'evidence' do
let(:release_with_evidence) { create(:release, :with_evidence, project: project) }
describe '#create_evidence!' do
context 'when a release is created' do
it 'creates one Evidence object too' do
expect { release_with_evidence }.to change(Releases::Evidence, :count).by(1)
end
end
end
context 'when a release is deleted' do
it 'also deletes the associated evidence' do
release_with_evidence
......
......@@ -5,83 +5,21 @@ require 'spec_helper'
describe Releases::Evidence do
let_it_be(:project) { create(:project) }
let(:release) { create(:release, project: project) }
let(:schema_file) { 'evidences/evidence' }
let(:summary_json) { described_class.last.summary.to_json }
describe 'associations' do
it { is_expected.to belong_to(:release) }
end
describe 'summary_sha' do
it 'returns nil if summary is nil' do
expect(build(:evidence, summary: nil).summary_sha).to be_nil
end
end
describe '#generate_summary_and_sha' do
before do
described_class.create!(release: release)
end
context 'when a release name is not provided' do
let(:release) { create(:release, project: project, name: nil) }
it 'creates a valid JSON object' do
expect(release.name).to eq(release.tag)
expect(summary_json).to match_schema(schema_file)
end
end
context 'when a release is associated to a milestone' do
let(:milestone) { create(:milestone, project: project) }
let(:release) { create(:release, project: project, milestones: [milestone]) }
context 'when a milestone has no issue associated with it' do
it 'creates a valid JSON object' do
expect(milestone.issues).to be_empty
expect(summary_json).to match_schema(schema_file)
end
end
context 'when a milestone has no description' do
let(:milestone) { create(:milestone, project: project, description: nil) }
it 'creates a valid JSON object' do
expect(milestone.description).to be_nil
expect(summary_json).to match_schema(schema_file)
end
end
context 'when a milestone has no due_date' do
let(:milestone) { create(:milestone, project: project, due_date: nil) }
it 'creates a valid JSON object' do
expect(milestone.due_date).to be_nil
expect(summary_json).to match_schema(schema_file)
end
end
context 'when a milestone has an issue' do
context 'when the issue has no description' do
let(:issue) { create(:issue, project: project, description: nil, state: 'closed') }
before do
milestone.issues << issue
end
it 'filters out issues from summary json' do
milestone = create(:milestone, project: project, due_date: nil)
issue = create(:issue, project: project, description: nil, state: 'closed')
milestone.issues << issue
release.milestones << milestone
it 'creates a valid JSON object' do
expect(milestone.issues.first.description).to be_nil
expect(summary_json).to match_schema(schema_file)
end
end
end
end
::Releases::CreateEvidenceService.new(release).execute
evidence = release.evidences.last
context 'when a release is not associated to any milestone' do
it 'creates a valid JSON object' do
expect(release.milestones).to be_empty
expect(summary_json).to match_schema(schema_file)
end
end
expect(evidence.read_attribute(:summary)["release"]["milestones"].first["issues"].first["title"]).to be_present
expect(evidence.summary["release"]["milestones"].first["issues"]).to be_nil
end
end
......@@ -3,12 +3,66 @@
require 'spec_helper'
describe Evidences::EvidenceEntity do
let(:evidence) { build(:evidence) }
let(:entity) { described_class.new(evidence) }
let_it_be(:project) { create(:project) }
let(:release) { create(:release, project: project) }
let(:evidence) { build(:evidence, release: release) }
let(:schema_file) { 'evidences/evidence' }
subject { entity.as_json }
subject { described_class.new(evidence).as_json }
it 'exposes the expected fields' do
expect(subject.keys).to contain_exactly(:release)
end
context 'when a release is associated to a milestone' do
let(:milestone) { create(:milestone, project: project) }
let(:release) { create(:release, project: project, milestones: [milestone]) }
context 'when a milestone has no issue associated with it' do
it 'creates a valid JSON object' do
expect(subject[:release][:milestones].first[:issues]).to be_empty
expect(subject.to_json).to match_schema(schema_file)
end
end
context 'when a milestone has no description' do
let(:milestone) { create(:milestone, project: project, description: nil) }
it 'creates a valid JSON object' do
expect(subject[:release][:milestones].first[:description]).to be_nil
expect(subject.to_json).to match_schema(schema_file)
end
end
context 'when a milestone has no due_date' do
let(:milestone) { create(:milestone, project: project, due_date: nil) }
it 'creates a valid JSON object' do
expect(subject[:release][:milestones].first[:due_date]).to be_nil
expect(subject.to_json).to match_schema(schema_file)
end
end
context 'when a milestone has an issue' do
context 'when the issue has no description' do
let(:issue) { create(:issue, project: project, description: nil, state: 'closed') }
before do
milestone.issues << issue
end
it 'creates a valid JSON object' do
expect(subject[:release][:milestones].first[:issues].first[:title]).to be_present
expect(subject.to_json).to match_schema(schema_file)
end
end
end
end
context 'when a release is not associated to any milestone' do
it 'creates a valid JSON object' do
expect(subject[:release][:milestones]).to be_empty
expect(subject.to_json).to match_schema(schema_file)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Jira::Requests::Projects do
let(:jira_service) { create(:jira_service) }
let(:params) { {} }
describe '#execute' do
let(:service) { described_class.new(jira_service, params) }
subject { service.execute }
context 'without jira_service' do
before do
jira_service.update!(active: false)
end
it 'returns an error response' do
expect(subject.error?).to be_truthy
expect(subject.message).to eq('Jira service not configured.')
end
end
context 'when jira_service is nil' do
let(:jira_service) { nil }
it 'returns an error response' do
expect(subject.error?).to be_truthy
expect(subject.message).to eq('Jira service not configured.')
end
end
context 'with jira_service' do
context 'when limit is invalid' do
let(:params) { { limit: 0 } }
it 'returns a paylod with no projects returned' do
expect(subject.payload[:projects]).to be_empty
end
end
context 'when validations and params are ok' do
let(:client) { double(options: { site: 'https://jira.example.com' }) }
before do
expect(service).to receive(:client).at_least(:once).and_return(client)
end
context 'when the request to Jira returns an error' do
before do
expect(client).to receive(:get).and_raise(Timeout::Error)
end
it 'returns an error response' do
expect(subject.error?).to be_truthy
expect(subject.message).to eq('Timeout::Error')
end
end
context 'when the request does not return any values' do
before do
expect(client).to receive(:get).and_return({ 'someKey' => 'value' })
end
it 'returns a paylod with no projects returned' do
payload = subject.payload
expect(subject.success?).to be_truthy
expect(payload[:projects]).to be_empty
expect(payload[:is_last]).to be_truthy
end
end
context 'when the request returns values' do
before do
expect(client).to receive(:get).and_return(
{ 'values' => %w(project1 project2), 'isLast' => false }
)
expect(JIRA::Resource::Project).to receive(:build).with(client, 'project1').and_return('jira_project1')
expect(JIRA::Resource::Project).to receive(:build).with(client, 'project2').and_return('jira_project2')
end
it 'returns a paylod with jira projets' do
payload = subject.payload
expect(subject.success?).to be_truthy
expect(payload[:projects]).to eq(%w(jira_project1 jira_project2))
expect(payload[:is_last]).to be_falsey
end
end
end
end
end
end
......@@ -558,7 +558,9 @@ describe Projects::CreateService, '#execute' do
)
expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).to(
receive(:bulk_perform_in)
.with(1.hour, array_including([user.id], [other_user.id]))
.with(1.hour,
array_including([user.id], [other_user.id]),
batch_delay: 30.seconds, batch_size: 100)
.and_call_original
)
......
# frozen_string_literal: true
require 'spec_helper'
describe Releases::CreateEvidenceService do
let_it_be(:project) { create(:project) }
let(:release) { create(:release, project: project) }
let(:service) { described_class.new(release) }
it 'creates evidence' do
expect { service.execute }.to change { release.reload.evidences.count }.by(1)
end
it 'saves evidence summary' do
service.execute
evidence = Releases::Evidence.last
expect(release.tag).not_to be_nil
expect(evidence.summary["release"]["tag_name"]).to eq(release.tag)
end
it 'saves sha' do
service.execute
evidence = Releases::Evidence.last
expect(evidence.summary_sha).not_to be_nil
end
end
......@@ -20,7 +20,11 @@ describe UserProjectAccessChangedService do
it 'permits low-priority operation' do
expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).to(
receive(:bulk_perform_in).with(described_class::DELAY, [[1], [2]])
receive(:bulk_perform_in).with(
described_class::DELAY,
[[1], [2]],
{ batch_delay: 30.seconds, batch_size: 100 }
)
)
described_class.new([1, 2]).execute(blocking: false,
......
......@@ -13,6 +13,7 @@ RSpec.shared_examples 'comment on merge request file' do
page.within('.notes_holder') do
expect(page).to have_content('Line is wrong')
expect(page).not_to have_content('Comment on lines')
end
visit(merge_request_path(merge_request))
......
......@@ -30,7 +30,9 @@ RSpec.shared_examples 'diff discussions API' do |parent_type, noteable_type, id_
it "creates a new diff note" do
line_range = {
"start_line_code" => Gitlab::Git.diff_line_code(diff_note.position.file_path, 1, 1),
"end_line_code" => Gitlab::Git.diff_line_code(diff_note.position.file_path, 2, 2)
"end_line_code" => Gitlab::Git.diff_line_code(diff_note.position.file_path, 2, 2),
"start_line_type" => diff_note.position.type,
"end_line_type" => diff_note.position.type
}
position = diff_note.position.to_h.merge({ line_range: line_range })
......
......@@ -4,6 +4,7 @@ require 'rake_helper'
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'
......@@ -11,7 +12,8 @@ describe 'gitlab:container_registry namespace rake tasks' do
describe 'configure' do
before do
stub_container_registry_config(enabled: true, api_url: 'http://registry.gitlab')
stub_access_token
stub_container_registry_config(enabled: true, api_url: api_url)
end
shared_examples 'invalid config' do
......@@ -37,6 +39,24 @@ 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) }
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)
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({})
......@@ -79,6 +99,11 @@ describe 'gitlab:container_registry namespace rake tasks' do
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)
......
......@@ -118,5 +118,45 @@ describe ApplicationWorker do
.to raise_error(ArgumentError)
end
end
context 'with batches' do
let(:batch_delay) { 1.minute }
it 'correctly schedules jobs' do
expect(Sidekiq::Client).to(
receive(:push_bulk).with(hash_including('args' => [['Foo', [1]], ['Foo', [2]]]))
.ordered
.and_call_original)
expect(Sidekiq::Client).to(
receive(:push_bulk).with(hash_including('args' => [['Foo', [3]], ['Foo', [4]]]))
.ordered
.and_call_original)
expect(Sidekiq::Client).to(
receive(:push_bulk).with(hash_including('args' => [['Foo', [5]]]))
.ordered
.and_call_original)
worker.bulk_perform_in(
1.minute,
[['Foo', [1]], ['Foo', [2]], ['Foo', [3]], ['Foo', [4]], ['Foo', [5]]],
batch_size: 2, batch_delay: batch_delay)
expect(worker.jobs.count).to eq 5
expect(worker.jobs[0]['at']).to eq(worker.jobs[1]['at'])
expect(worker.jobs[2]['at']).to eq(worker.jobs[3]['at'])
expect(worker.jobs[2]['at'] - worker.jobs[1]['at']).to eq(batch_delay)
expect(worker.jobs[4]['at'] - worker.jobs[3]['at']).to eq(batch_delay)
end
context 'when batch_size is invalid' do
it 'raises an ArgumentError exception' do
expect do
worker.bulk_perform_in(1.minute,
[['Foo']],
batch_size: -1, batch_delay: batch_delay)
end.to raise_error(ArgumentError)
end
end
end
end
end
......@@ -3,9 +3,13 @@
require 'spec_helper'
describe CreateEvidenceWorker do
let!(:release) { create(:release) }
let(:release) { create(:release) }
it 'creates a new Evidence record' do
expect_next_instance_of(::Releases::CreateEvidenceService, release) do |service|
expect(service).to receive(:execute).and_call_original
end
expect { described_class.new.perform(release.id) }.to change(Releases::Evidence, :count).by(1)
end
end
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册