Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1a9d9cc14e
commit
79d62647bc
|
@ -50,7 +50,6 @@ rules:
|
|||
# all offenses of no-jquery/no-animate-toggle are false positives ( $toast.show() )
|
||||
no-jquery/no-animate-toggle: off
|
||||
no-jquery/no-event-shorthand: off
|
||||
no-jquery/no-fade: off
|
||||
no-jquery/no-serialize: error
|
||||
no-jquery/no-sizzle: off
|
||||
promise/always-return: off
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -458,7 +458,7 @@ gem 'grpc', '~> 1.24.0'
|
|||
|
||||
gem 'google-protobuf', '~> 3.8.0'
|
||||
|
||||
gem 'toml-rb', '~> 1.0.0', require: false
|
||||
gem 'toml-rb', '~> 1.0.0'
|
||||
|
||||
# Feature toggles
|
||||
gem 'flipper', '~> 0.17.1'
|
||||
|
|
|
@ -40,7 +40,10 @@ export default class ImageFile {
|
|||
.removeClass('active')
|
||||
.filter(`.${viewMode}`)
|
||||
.addClass('active');
|
||||
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
return $(`.view:visible:not(.${viewMode})`, this.file).fadeOut(200, () => {
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$(`.view.${viewMode}`, this.file).fadeIn(200);
|
||||
return this.initView(viewMode);
|
||||
});
|
||||
|
|
|
@ -116,11 +116,13 @@ class DueDateSelect {
|
|||
}
|
||||
|
||||
updateIssueBoardIssue() {
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
this.$loading.fadeIn();
|
||||
this.$dropdown.trigger('loading.gl.dropdown');
|
||||
this.$selectbox.hide();
|
||||
this.$value.css('display', '');
|
||||
const fadeOutLoader = () => {
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
this.$loading.fadeOut();
|
||||
};
|
||||
|
||||
|
@ -135,6 +137,7 @@ class DueDateSelect {
|
|||
const hasDueDate = this.displayedDate !== __('None');
|
||||
const displayedDateStyle = hasDueDate ? 'bold' : 'no-value';
|
||||
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
this.$loading.removeClass('hidden').fadeIn();
|
||||
|
||||
if (isDropdown) {
|
||||
|
@ -158,6 +161,7 @@ class DueDateSelect {
|
|||
}
|
||||
this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
|
||||
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
return this.$loading.fadeOut();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -25,10 +25,33 @@ export default {
|
|||
PREV_PAGE: 1,
|
||||
NEXT_PAGE: 2,
|
||||
fields: [
|
||||
{ key: 'error', label: __('Open errors'), thClass: 'w-70p' },
|
||||
{ key: 'events', label: __('Events') },
|
||||
{ key: 'users', label: __('Users') },
|
||||
{ key: 'lastSeen', label: __('Last seen'), thClass: 'w-15p' },
|
||||
{
|
||||
key: 'error',
|
||||
label: __('Error'),
|
||||
thClass: 'w-70p',
|
||||
tdClass: 'table-col d-flex align-items-center d-sm-table-cell',
|
||||
},
|
||||
{
|
||||
key: 'events',
|
||||
label: __('Events'),
|
||||
tdClass: 'table-col d-flex align-items-center d-sm-table-cell',
|
||||
},
|
||||
{
|
||||
key: 'users',
|
||||
label: __('Users'),
|
||||
tdClass: 'table-col d-flex align-items-center d-sm-table-cell',
|
||||
},
|
||||
{
|
||||
key: 'lastSeen',
|
||||
label: __('Last seen'),
|
||||
thClass: 'w-15p',
|
||||
tdClass: 'table-col d-flex align-items-center d-sm-table-cell',
|
||||
},
|
||||
{
|
||||
key: 'details',
|
||||
tdClass: 'table-col d-sm-none d-flex align-items-center',
|
||||
thClass: 'invisible w-0',
|
||||
},
|
||||
],
|
||||
sortFields: {
|
||||
last_seen: __('Last Seen'),
|
||||
|
@ -149,12 +172,13 @@ export default {
|
|||
<div class="error-list">
|
||||
<div v-if="errorTrackingEnabled">
|
||||
<div
|
||||
class="d-flex flex-row justify-content-around align-items-center bg-secondary border mt-2"
|
||||
class="row flex-column flex-sm-row align-items-sm-center row-top m-0 mt-sm-2 mx-sm-1 p-0 p-sm-3"
|
||||
>
|
||||
<div class="filtered-search-box flex-grow-1 my-3 ml-3 mr-2">
|
||||
<div class="search-box flex-fill mr-sm-2 my-3 m-sm-0 p-3 p-sm-0">
|
||||
<div class="filtered-search-box mb-0">
|
||||
<gl-dropdown
|
||||
:text="__('Recent searches')"
|
||||
class="filtered-search-history-dropdown-wrapper d-none d-md-block"
|
||||
class="filtered-search-history-dropdown-wrapper"
|
||||
toggle-class="filtered-search-history-dropdown-toggle-button"
|
||||
:disabled="loading"
|
||||
>
|
||||
|
@ -166,12 +190,12 @@ export default {
|
|||
v-for="searchQuery in recentSearches"
|
||||
:key="searchQuery"
|
||||
@click="setSearchText(searchQuery)"
|
||||
>{{ searchQuery }}</gl-dropdown-item
|
||||
>
|
||||
>{{ searchQuery }}
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-divider />
|
||||
<gl-dropdown-item ref="clearRecentSearches" @click="clearRecentSearches">{{
|
||||
__('Clear recent searches')
|
||||
}}</gl-dropdown-item>
|
||||
<gl-dropdown-item ref="clearRecentSearches" @click="clearRecentSearches"
|
||||
>{{ __('Clear recent searches') }}
|
||||
</gl-dropdown-item>
|
||||
</template>
|
||||
<div v-else class="px-3">{{ __("You don't have any recent searches") }}</div>
|
||||
</gl-dropdown>
|
||||
|
@ -198,12 +222,13 @@ export default {
|
|||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<gl-dropdown
|
||||
class="sort-control"
|
||||
:text="$options.sortFields[sortField]"
|
||||
left
|
||||
:disabled="loading"
|
||||
class="mr-3"
|
||||
menu-class="sort-dropdown"
|
||||
>
|
||||
<gl-dropdown-item
|
||||
|
@ -227,51 +252,65 @@ export default {
|
|||
<gl-loading-icon size="md" />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<h4 class="d-block d-sm-none my-3">{{ __('Open errors') }}</h4>
|
||||
|
||||
<gl-table
|
||||
v-else
|
||||
class="mt-3"
|
||||
:items="errors"
|
||||
:fields="$options.fields"
|
||||
:show-empty="true"
|
||||
fixed
|
||||
stacked="sm"
|
||||
tbody-tr-class="table-row mb-4"
|
||||
>
|
||||
<template slot="HEAD_events" slot-scope="data">
|
||||
<div class="text-md-right">{{ data.label }}</div>
|
||||
<template v-slot:head(error)>
|
||||
<div class="d-none d-sm-block">{{ __('Open errors') }}</div>
|
||||
</template>
|
||||
<template slot="HEAD_users" slot-scope="data">
|
||||
<div class="text-md-right">{{ data.label }}</div>
|
||||
<template v-slot:head(events)="data">
|
||||
<div class="text-sm-right">{{ data.label }}</div>
|
||||
</template>
|
||||
<template slot="error" slot-scope="errors">
|
||||
<template v-slot:head(users)="data">
|
||||
<div class="text-sm-right">{{ data.label }}</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:error="errors">
|
||||
<div class="d-flex flex-column">
|
||||
<gl-link class="d-flex text-dark" :href="getDetailsLink(errors.item.id)">
|
||||
<gl-link class="d-flex mw-100 text-dark" :href="getDetailsLink(errors.item.id)">
|
||||
<strong class="text-truncate">{{ errors.item.title.trim() }}</strong>
|
||||
</gl-link>
|
||||
<span class="text-secondary text-truncate">
|
||||
<span class="text-secondary text-truncate mw-100">
|
||||
{{ errors.item.culprit }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="events" slot-scope="errors">
|
||||
<div class="text-md-right">{{ errors.item.count }}</div>
|
||||
<template v-slot:events="errors">
|
||||
<div class="text-right">{{ errors.item.count }}</div>
|
||||
</template>
|
||||
|
||||
<template slot="users" slot-scope="errors">
|
||||
<div class="text-md-right">{{ errors.item.userCount }}</div>
|
||||
<template v-slot:users="errors">
|
||||
<div class="text-right">{{ errors.item.userCount }}</div>
|
||||
</template>
|
||||
|
||||
<template slot="lastSeen" slot-scope="errors">
|
||||
<div class="d-flex align-items-center">
|
||||
<template v-slot:lastSeen="errors">
|
||||
<div class="text-md-left text-right">
|
||||
<time-ago :time="errors.item.lastSeen" class="text-secondary" />
|
||||
</div>
|
||||
</template>
|
||||
<template slot="empty">
|
||||
<div ref="empty">
|
||||
<template v-slot:details="errors">
|
||||
<gl-button
|
||||
:href="getDetailsLink(errors.item.id)"
|
||||
variant="outline-info"
|
||||
class="d-block"
|
||||
>
|
||||
{{ __('More details') }}
|
||||
</gl-button>
|
||||
</template>
|
||||
<template v-slot:empty>
|
||||
{{ __('No errors to display.') }}
|
||||
<gl-link class="js-try-again" @click="restartPolling">
|
||||
{{ __('Check again') }}
|
||||
</gl-link>
|
||||
</div>
|
||||
</template>
|
||||
</gl-table>
|
||||
<gl-pagination
|
||||
|
@ -283,6 +322,7 @@ export default {
|
|||
align="center"
|
||||
@input="goToPage"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else-if="userCanEnableErrorTracking">
|
||||
<gl-empty-state
|
||||
|
|
|
@ -64,6 +64,7 @@ export default class FilterableList {
|
|||
return false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$(this.listHolderElement).fadeTo(250, 0.5);
|
||||
|
||||
this.isBusy = true;
|
||||
|
@ -98,6 +99,7 @@ export default class FilterableList {
|
|||
|
||||
onFilterComplete() {
|
||||
this.isBusy = false;
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$(this.listHolderElement).fadeTo(250, 1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ export default class LabelsSelect {
|
|||
const $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
|
||||
const $value = $block.find('.value');
|
||||
const $dropdownMenu = $dropdown.parent().find('.dropdown-menu');
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
const $loading = $block.find('.block-loading').fadeOut();
|
||||
const fieldName = $dropdown.data('fieldName');
|
||||
let initialSelected = $selectbox
|
||||
|
@ -84,6 +85,7 @@ export default class LabelsSelect {
|
|||
if (!selected.length) {
|
||||
data[abilityName].label_ids = [''];
|
||||
}
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.removeClass('hidden').fadeIn();
|
||||
$dropdown.trigger('loading.gl.dropdown');
|
||||
axios
|
||||
|
@ -91,6 +93,7 @@ export default class LabelsSelect {
|
|||
.then(({ data }) => {
|
||||
let labelTooltipTitle;
|
||||
let template;
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.fadeOut();
|
||||
$dropdown.trigger('loaded.gl.dropdown');
|
||||
$selectbox.hide();
|
||||
|
@ -361,6 +364,7 @@ export default class LabelsSelect {
|
|||
const label = clickEvent.selectedObj;
|
||||
|
||||
const fadeOutLoader = () => {
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.fadeOut();
|
||||
};
|
||||
|
||||
|
@ -422,6 +426,7 @@ export default class LabelsSelect {
|
|||
boardsStore.detail.issue.labels = labels;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.fadeIn();
|
||||
const oldLabels = boardsStore.detail.issue.labels;
|
||||
|
||||
|
|
|
@ -113,6 +113,7 @@ function deferredInitialisation() {
|
|||
});
|
||||
|
||||
$('.js-remove-tr').on('ajax:success', function removeTRAjaxSuccessCallback() {
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$(this)
|
||||
.closest('tr')
|
||||
.fadeOut();
|
||||
|
|
|
@ -52,6 +52,7 @@ export default class MilestoneSelect {
|
|||
const $block = $selectBox.closest('.block');
|
||||
const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
|
||||
const $value = $block.find('.value');
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
const $loading = $block.find('.block-loading').fadeOut();
|
||||
selectedMilestoneDefault = showAny ? '' : null;
|
||||
selectedMilestoneDefault =
|
||||
|
@ -202,15 +203,18 @@ export default class MilestoneSelect {
|
|||
}
|
||||
|
||||
$dropdown.trigger('loading.gl.dropdown');
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.removeClass('hidden').fadeIn();
|
||||
|
||||
boardsStore.detail.issue
|
||||
.update($dropdown.attr('data-issue-update'))
|
||||
.then(() => {
|
||||
$dropdown.trigger('loaded.gl.dropdown');
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.fadeOut();
|
||||
})
|
||||
.catch(() => {
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.fadeOut();
|
||||
});
|
||||
} else {
|
||||
|
@ -218,12 +222,14 @@ export default class MilestoneSelect {
|
|||
data = {};
|
||||
data[abilityName] = {};
|
||||
data[abilityName].milestone_id = selected != null ? selected : null;
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.removeClass('hidden').fadeIn();
|
||||
$dropdown.trigger('loading.gl.dropdown');
|
||||
return axios
|
||||
.put(issueUpdateURL, data)
|
||||
.then(({ data }) => {
|
||||
$dropdown.trigger('loaded.gl.dropdown');
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.fadeOut();
|
||||
$selectBox.hide();
|
||||
$value.css('display', '');
|
||||
|
@ -247,6 +253,7 @@ export default class MilestoneSelect {
|
|||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.fadeOut();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -336,6 +336,7 @@ export default {
|
|||
|
||||
<markdown-field
|
||||
ref="markdownField"
|
||||
:is-submitting="isSubmitting"
|
||||
:markdown-preview-path="markdownPreviewPath"
|
||||
:markdown-docs-path="markdownDocsPath"
|
||||
:quick-actions-docs-path="quickActionsDocsPath"
|
||||
|
|
|
@ -53,6 +53,7 @@ function UsersSelect(currentUser, els, options = {}) {
|
|||
const abilityName = $dropdown.data('abilityName');
|
||||
let $value = $block.find('.value');
|
||||
const $collapsedSidebar = $block.find('.sidebar-collapsed-user');
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
const $loading = $block.find('.block-loading').fadeOut();
|
||||
const selectedIdDefault = defaultNullUser && showNullUser ? 0 : null;
|
||||
let selectedId = $dropdown.data('selected');
|
||||
|
@ -188,6 +189,7 @@ function UsersSelect(currentUser, els, options = {}) {
|
|||
const data = {};
|
||||
data[abilityName] = {};
|
||||
data[abilityName].assignee_id = selected != null ? selected : null;
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.removeClass('hidden').fadeIn();
|
||||
$dropdown.trigger('loading.gl.dropdown');
|
||||
|
||||
|
@ -195,6 +197,7 @@ function UsersSelect(currentUser, els, options = {}) {
|
|||
let user = {};
|
||||
let tooltipTitle = user.name;
|
||||
$dropdown.trigger('loaded.gl.dropdown');
|
||||
// eslint-disable-next-line no-jquery/no-fade
|
||||
$loading.fadeOut();
|
||||
if (data.assignee) {
|
||||
user = {
|
||||
|
|
|
@ -20,6 +20,11 @@ export default {
|
|||
Suggestions,
|
||||
},
|
||||
props: {
|
||||
isSubmitting: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
markdownPreviewPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -133,6 +138,20 @@ export default {
|
|||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
isSubmitting(isSubmitting) {
|
||||
if (!isSubmitting || !this.$refs['markdown-preview'].querySelectorAll) {
|
||||
return;
|
||||
}
|
||||
const mediaInPreview = this.$refs['markdown-preview'].querySelectorAll('video, audio');
|
||||
|
||||
if (mediaInPreview) {
|
||||
mediaInPreview.forEach(media => {
|
||||
media.pause();
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
/*
|
||||
GLForm class handles all the toolbar buttons
|
||||
|
@ -177,7 +196,6 @@ export default {
|
|||
this.renderMarkdown();
|
||||
}
|
||||
},
|
||||
|
||||
showWriteTab() {
|
||||
this.markdownPreview = '';
|
||||
this.previewMarkdown = false;
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
$gray-border: 1px solid $border-color;
|
||||
|
||||
.error-list {
|
||||
.sort-control {
|
||||
.btn {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
.gl-dropdown-caret {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
.row-top {
|
||||
border: $gray-border;
|
||||
background-color: $gray-50;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
.table-row {
|
||||
border: $gray-border;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
border-top: $gray-border;
|
||||
border-bottom: $gray-border;
|
||||
background-color: $gray-50;
|
||||
}
|
||||
|
||||
.table-col {
|
||||
min-height: 68px;
|
||||
|
||||
&::before {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
div {
|
||||
padding: 0 !important;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
height: 64px;
|
||||
background-color: $gray-normal;
|
||||
|
||||
&::before {
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
div {
|
||||
width: 100% !important;
|
||||
padding: 0 !important;
|
||||
|
||||
a {
|
||||
color: $blue-500;
|
||||
border-color: $blue-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -51,6 +51,7 @@ class Blob < SimpleDelegator
|
|||
BlobViewer::Contributing,
|
||||
BlobViewer::Changelog,
|
||||
|
||||
BlobViewer::CargoToml,
|
||||
BlobViewer::Cartfile,
|
||||
BlobViewer::ComposerJson,
|
||||
BlobViewer::Gemfile,
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module BlobViewer
|
||||
class CargoToml < DependencyManager
|
||||
include Static
|
||||
|
||||
self.file_types = %i(cargo_toml)
|
||||
|
||||
def manager_name
|
||||
'Cargo'
|
||||
end
|
||||
|
||||
def manager_url
|
||||
'https://crates.io/'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -717,8 +717,8 @@ module Ci
|
|||
end
|
||||
end
|
||||
|
||||
def has_expiring_artifacts?
|
||||
artifacts_expire_at.present? && artifacts_expire_at > Time.now
|
||||
def has_expiring_archive_artifacts?
|
||||
has_expiring_artifacts? && artifacts_file&.exists?
|
||||
end
|
||||
|
||||
def keep_artifacts!
|
||||
|
@ -933,6 +933,10 @@ module Ci
|
|||
value.with_indifferent_access
|
||||
end
|
||||
end
|
||||
|
||||
def has_expiring_artifacts?
|
||||
artifacts_expire_at.present? && artifacts_expire_at > Time.now
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -23,6 +23,12 @@ class DiffNote < Note
|
|||
|
||||
before_validation :set_line_code, if: :on_text?, unless: :importing?
|
||||
after_save :keep_around_commits, unless: :importing?
|
||||
|
||||
NoteDiffFileCreationError = Class.new(StandardError)
|
||||
|
||||
DIFF_LINE_NOT_FOUND_MESSAGE = "Failed to find diff line for: %{file_path}, old_line: %{old_line}, new_line: %{new_line}"
|
||||
DIFF_FILE_NOT_FOUND_MESSAGE = "Failed to find diff file"
|
||||
|
||||
after_commit :create_diff_file, on: :create
|
||||
|
||||
def discussion_class(*)
|
||||
|
@ -33,7 +39,16 @@ class DiffNote < Note
|
|||
return unless should_create_diff_file?
|
||||
|
||||
diff_file = fetch_diff_file
|
||||
raise NoteDiffFileCreationError, DIFF_FILE_NOT_FOUND_MESSAGE unless diff_file
|
||||
|
||||
diff_line = diff_file.line_for_position(self.original_position)
|
||||
unless diff_line
|
||||
raise NoteDiffFileCreationError, DIFF_LINE_NOT_FOUND_MESSAGE % {
|
||||
file_path: diff_file.file_path,
|
||||
old_line: original_position.old_line,
|
||||
new_line: original_position.new_line
|
||||
}
|
||||
end
|
||||
|
||||
creation_params = diff_file.diff.to_hash
|
||||
.except(:too_large)
|
||||
|
@ -110,7 +125,6 @@ class DiffNote < Note
|
|||
def fetch_diff_file
|
||||
return note_diff_file.raw_diff_file if note_diff_file
|
||||
|
||||
file =
|
||||
if created_at_diff?(noteable.diff_refs)
|
||||
# We're able to use the already persisted diffs (Postgres) if we're
|
||||
# presenting a "current version" of the MR discussion diff.
|
||||
|
@ -118,11 +132,13 @@ class DiffNote < Note
|
|||
# As an extra benefit, the returned `diff_file` already
|
||||
# has `highlighted_diff_lines` data set from Redis on
|
||||
# `Diff::FileCollection::MergeRequestDiff`.
|
||||
noteable.diffs(original_position.diff_options).diff_files.first
|
||||
else
|
||||
original_position.diff_file(repository)
|
||||
file = noteable.diffs(original_position.diff_options).diff_files.first
|
||||
# if line is not found in persisted diffs, fallback and retrieve file from repository using gitaly
|
||||
# This is required because of https://gitlab.com/gitlab-org/gitlab/issues/42676
|
||||
file = nil if file&.line_for_position(original_position).nil? && importing?
|
||||
end
|
||||
|
||||
file ||= original_position.diff_file(repository)
|
||||
file&.unfold_diff_lines(position)
|
||||
|
||||
file
|
||||
|
|
|
@ -14,7 +14,7 @@ class BuildArtifactEntity < Grape::Entity
|
|||
download_project_job_artifacts_path(project, job)
|
||||
end
|
||||
|
||||
expose :keep_path, if: -> (*) { job.has_expiring_artifacts? } do |job|
|
||||
expose :keep_path, if: -> (*) { job.has_expiring_archive_artifacts? } do |job|
|
||||
keep_project_job_artifacts_path(project, job)
|
||||
end
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ class BuildDetailsEntity < JobEntity
|
|||
browse_project_job_artifacts_path(project, build)
|
||||
end
|
||||
|
||||
expose :keep_path, if: -> (*) { build.has_expiring_artifacts? && can?(current_user, :update_build, build) } do |build|
|
||||
expose :keep_path, if: -> (*) { build.has_expiring_archive_artifacts? && can?(current_user, :update_build, build) } do |build|
|
||||
keep_project_job_artifacts_path(project, build)
|
||||
end
|
||||
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
.js-autodevops-banner.banner-callout.banner-non-empty-state.append-bottom-20.prepend-top-10{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } }
|
||||
.banner-graphic
|
||||
= custom_icon('icon_autodevops')
|
||||
%section.js-autodevops-banner.gl-banner{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } }
|
||||
.gl-banner-illustration
|
||||
= image_tag('illustrations/autodevops.svg')
|
||||
|
||||
.banner-body.prepend-left-10.append-bottom-10
|
||||
%h5.banner-title= s_('AutoDevOps|Auto DevOps')
|
||||
.gl-banner-content
|
||||
%h1.gl-banner-title= s_('AutoDevOps|Auto DevOps')
|
||||
%p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
|
||||
%p
|
||||
- link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
|
||||
= s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
|
||||
.banner-buttons
|
||||
= link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'), class: 'btn js-close-callout'
|
||||
= link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'), class: 'btn btn-md new-gl-button js-close-callout'
|
||||
|
||||
%button.btn-transparent.banner-close.close.js-close-callout{ type: 'button',
|
||||
%button.gl-banner-close.close.js-close-callout{ type: 'button',
|
||||
'aria-label' => 'Dismiss Auto DevOps box' }
|
||||
= icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Added Conan recipe in place of the package name on the package details page.
|
||||
merge_request: 21247
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Adds created_at object to package api response
|
||||
merge_request: 20816
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Improve error list UI on mobile viewports
|
||||
merge_request: 21192
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add fallbacks and proper errors for diff file creation
|
||||
merge_request: 21034
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Don't run Auto DevOps when no dockerfile or matching buildpack exists
|
||||
merge_request: 20267
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove keep button for non archive artifacts
|
||||
merge_request: 21553
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add support for Rust Cargo.toml dependency vizualisation and linking
|
||||
merge_request: 21374
|
||||
author: Fabio Huser
|
||||
type: added
|
|
@ -201,7 +201,7 @@ module.exports = {
|
|||
loader: 'raw-loader',
|
||||
},
|
||||
{
|
||||
test: /\.(gif|png)$/,
|
||||
test: /\.(gif|png|mp4)$/,
|
||||
loader: 'url-loader',
|
||||
options: { limit: 2048 },
|
||||
},
|
||||
|
|
|
@ -31,13 +31,15 @@ Example response:
|
|||
"id": 1,
|
||||
"name": "com/mycompany/my-app",
|
||||
"version": "1.0-SNAPSHOT",
|
||||
"package_type": "maven"
|
||||
"package_type": "maven",
|
||||
"created_at": "2019-11-27T03:37:38.711Z"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "@foo/bar",
|
||||
"version": "1.0.3",
|
||||
"package_type": "npm"
|
||||
"package_type": "npm",
|
||||
"created_at": "2019-11-27T03:37:38.711Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
@ -76,7 +78,8 @@ Example response:
|
|||
"_links": {
|
||||
"web_path": "/namespace1/project1/-/packages/1",
|
||||
"delete_api_path": "/namespace1/project1/-/packages/1"
|
||||
}
|
||||
},
|
||||
"created_at": "2019-11-27T03:37:38.711Z"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
|
@ -86,7 +89,8 @@ Example response:
|
|||
"_links": {
|
||||
"web_path": "/namespace1/project1/-/packages/1",
|
||||
"delete_api_path": "/namespace1/project1/-/packages/1"
|
||||
}
|
||||
},
|
||||
"created_at": "2019-11-27T03:37:38.711Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
@ -128,7 +132,8 @@ Example response:
|
|||
"_links": {
|
||||
"web_path": "/namespace1/project1/-/packages/1",
|
||||
"delete_api_path": "/namespace1/project1/-/packages/1"
|
||||
}
|
||||
},
|
||||
"created_at": "2019-11-27T03:37:38.711Z"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -127,6 +127,7 @@ Its feature set is listed on the table below according to DevOps stages.
|
|||
| [GitLab Pages](../user/project/pages/index.md) | Deploy static websites. |
|
||||
| [GitLab Releases](../user/project/releases/index.md) | Add release notes to Git tags. |
|
||||
| [Review Apps](review_apps/index.md) | Configure GitLab CI/CD to preview code changes. |
|
||||
| [Cloud deployment](cloud_deployment/index.md) | Deploy your application to a main cloud provider. |
|
||||
|---+---|
|
||||
| **Secure** ||
|
||||
| [Container Scanning](../user/application_security/container_scanning/index.md) **(ULTIMATE)** | Check your Docker containers for known vulnerabilities.|
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
type: howto
|
||||
---
|
||||
|
||||
# Cloud deployment
|
||||
|
||||
Interacting with a major cloud provider such as Amazon AWS may have become a much needed task that's
|
||||
part of your delivery process. GitLab is making this process less painful by providing Docker images
|
||||
that come with the needed libraries and tools pre-installed.
|
||||
By referencing them in your CI/CD pipeline, you'll be able to interact with your chosen
|
||||
cloud provider more easily.
|
||||
|
||||
## AWS
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/31167) in GitLab 12.6.
|
||||
|
||||
GitLab's AWS Docker image provides the [AWS Command Line Interface](https://aws.amazon.com/cli/),
|
||||
which enables you to run `aws` commands. As part of your deployment strategy, you can run `aws` commands directly from
|
||||
`.gitlab-ci.yml` by specifying GitLab's AWS Docker image.
|
||||
|
||||
Some credentials are required to be able to run `aws` commands:
|
||||
|
||||
1. Sign up for [an AWS account](https://docs.aws.amazon.com/IAM/latest/UserGuide/getting-set-up.html) if you don't have one yet.
|
||||
1. Log in onto the console and create [a new IAM user](https://console.aws.amazon.com/iam/home#/home).
|
||||
1. Select your newly created user to access its details. Navigate to **Security credentials > Create a new access key**.
|
||||
|
||||
NOTE: **Note:**
|
||||
A new **Access key ID** and **Secret access key** pair will be generated. Please take a note of them right away.
|
||||
|
||||
1. In your GitLab project, go to **Settings > CI / CD**. Set the Access key ID and Secret access key as [environment variables](../variables/README.md#gitlab-cicd-environment-variables), using the following variable names:
|
||||
|
||||
| Env. variable name | Value |
|
||||
|:------------------------|:-------------------------|
|
||||
| `AWS_ACCESS_KEY_ID` | Your "Access key ID" |
|
||||
| `AWS_SECRET_ACCESS_KEY` | Your "Secret access key" |
|
||||
|
||||
1. You can now use `aws` commands in the `.gitlab-ci.yml` file of this project:
|
||||
|
||||
```yml
|
||||
deploy:
|
||||
stage: deploy
|
||||
image: registry.gitlab.com/gitlab-org/cloud-deploy:latest
|
||||
script:
|
||||
- aws s3 ...
|
||||
- aws create-deployment ...
|
||||
```
|
|
@ -19,7 +19,7 @@ Only admin users can access the Admin Area.
|
|||
The Admin Area is made up of the following sections:
|
||||
|
||||
| Section | Description |
|
||||
|:------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
|:--------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [Overview](#overview-section) | View your GitLab [Dashboard](#admin-dashboard), and administer [projects](#administering-projects), [users](#administering-users), [groups](#administering-groups), [jobs](#administering-jobs), [Runners](#administering-runners), and [Gitaly servers](#administering-gitaly-servers). |
|
||||
| Monitoring | View GitLab [system information](#system-info), and information on [background jobs](#background-jobs), [logs](#logs), [health checks](monitoring/health_check.md), [requests profiles](#requests-profiles), and [audit logs](#audit-log-premium-only). |
|
||||
| Messages | Send and manage [broadcast messages](broadcast_messages.md) for your users. |
|
||||
|
@ -30,6 +30,7 @@ The Admin Area is made up of the following sections:
|
|||
| Push Rules **(STARTER)** | Configure pre-defined Git [push rules](../../push_rules/push_rules.md) for projects. |
|
||||
| Geo **(PREMIUM ONLY)** | Configure and maintain [Geo nodes](geo_nodes.md). |
|
||||
| Deploy Keys | Create instance-wide [SSH deploy keys](../../ssh/README.md#deploy-keys). |
|
||||
| Credentials **(ULTIMATE ONLY)** | View [credentials](credentials_inventory.md) that can be used to access your instance. |
|
||||
| Service Templates | Create [service templates](../project/integrations/services_templates.md) for projects. |
|
||||
| Labels | Create and maintain [labels](labels.md) for your GitLab instance. |
|
||||
| Appearance | Customize [GitLab's appearance](appearance.md). |
|
||||
|
|
|
@ -728,7 +728,11 @@ When removing the cluster integration, note:
|
|||
- You need Maintainer [permissions](../../permissions.md) and above to remove a Kubernetes cluster
|
||||
integration.
|
||||
- When you remove a cluster, you only remove its relationship to GitLab, not the cluster itself. To
|
||||
remove the cluster, you can do so by visiting the GKE dashboard or using `kubectl`.
|
||||
remove the cluster, you can do so by visiting the GKE or EKS dashboard, or using `kubectl`.
|
||||
|
||||
[From GitLab 12.6](https://gitlab.com/gitlab-org/gitlab/issues/26815), you can also remove all
|
||||
related GitLab cluster resources (for example, namespaces, roles, and bindings) when removing the
|
||||
integration.
|
||||
|
||||
## Learn more
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ From the image above, we can deduce the following things:
|
|||
- Administrator is the Owner and member of **all** groups and for that reason,
|
||||
there is an indication of an ancestor group and inherited Owner permissions.
|
||||
|
||||
[From](https://gitlab.com/gitlab-org/gitlab/issues/21727), you can filter this list
|
||||
[From GitLab 12.6](https://gitlab.com/gitlab-org/gitlab/issues/21727), you can filter this list
|
||||
using dropdown on the right side:
|
||||
|
||||
![Project members filter](img/project_members_filter_v12_6.png)
|
||||
|
|
|
@ -11,7 +11,7 @@ module Gitlab
|
|||
strong_memoize(:content) do
|
||||
next unless project&.auto_devops_enabled?
|
||||
|
||||
template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps')
|
||||
template = Gitlab::Template::GitlabCiYmlTemplate.find(template_name)
|
||||
YAML.dump('include' => [{ 'template' => template.full_name }])
|
||||
end
|
||||
end
|
||||
|
@ -19,6 +19,22 @@ module Gitlab
|
|||
def source
|
||||
:auto_devops_source
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def template_name
|
||||
if beta_enabled?
|
||||
'Beta/Auto-DevOps'
|
||||
else
|
||||
'Auto-DevOps'
|
||||
end
|
||||
end
|
||||
|
||||
def beta_enabled?
|
||||
Feature.enabled?(:auto_devops_beta, project, default_enabled: true) &&
|
||||
# workflow:rules are required by `Beta/Auto-DevOps.gitlab-ci.yml`
|
||||
Feature.enabled?(:workflow_rules, project, default_enabled: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,7 +11,7 @@ module Gitlab
|
|||
strong_memoize(:content) do
|
||||
next unless project&.auto_devops_enabled?
|
||||
|
||||
template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps')
|
||||
template = Gitlab::Template::GitlabCiYmlTemplate.find(template_name)
|
||||
template.content
|
||||
end
|
||||
end
|
||||
|
@ -19,6 +19,22 @@ module Gitlab
|
|||
def source
|
||||
:auto_devops_source
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def template_name
|
||||
if beta_enabled?
|
||||
'Beta/Auto-DevOps'
|
||||
else
|
||||
'Auto-DevOps'
|
||||
end
|
||||
end
|
||||
|
||||
def beta_enabled?
|
||||
Feature.enabled?(:auto_devops_beta, project, default_enabled: true) &&
|
||||
# workflow:rules are required by `Beta/Auto-DevOps.gitlab-ci.yml`
|
||||
Feature.enabled?(:workflow_rules, project, default_enabled: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
# Auto DevOps - BETA do not use
|
||||
# This CI/CD configuration provides a standard pipeline for
|
||||
# * building a Docker image (using a buildpack if necessary),
|
||||
# * storing the image in the container registry,
|
||||
# * running tests from a buildpack,
|
||||
# * running code quality analysis,
|
||||
# * creating a review app for each topic branch,
|
||||
# * and continuous deployment to production
|
||||
#
|
||||
# Test jobs may be disabled by setting environment variables:
|
||||
# * test: TEST_DISABLED
|
||||
# * code_quality: CODE_QUALITY_DISABLED
|
||||
# * license_management: LICENSE_MANAGEMENT_DISABLED
|
||||
# * performance: PERFORMANCE_DISABLED
|
||||
# * sast: SAST_DISABLED
|
||||
# * dependency_scanning: DEPENDENCY_SCANNING_DISABLED
|
||||
# * container_scanning: CONTAINER_SCANNING_DISABLED
|
||||
# * dast: DAST_DISABLED
|
||||
# * review: REVIEW_DISABLED
|
||||
# * stop_review: REVIEW_DISABLED
|
||||
#
|
||||
# In order to deploy, you must have a Kubernetes cluster configured either
|
||||
# via a project integration, or via group/project variables.
|
||||
# KUBE_INGRESS_BASE_DOMAIN must also be set on the cluster settings,
|
||||
# as a variable at the group or project level, or manually added below.
|
||||
#
|
||||
# Continuous deployment to production is enabled by default.
|
||||
# If you want to deploy to staging first, set STAGING_ENABLED environment variable.
|
||||
# If you want to enable incremental rollout, either manual or time based,
|
||||
# set INCREMENTAL_ROLLOUT_MODE environment variable to "manual" or "timed".
|
||||
# If you want to use canary deployments, set CANARY_ENABLED environment variable.
|
||||
#
|
||||
# If Auto DevOps fails to detect the proper buildpack, or if you want to
|
||||
# specify a custom buildpack, set a project variable `BUILDPACK_URL` to the
|
||||
# repository URL of the buildpack.
|
||||
# e.g. BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-ruby.git#v142
|
||||
# If you need multiple buildpacks, add a file to your project called
|
||||
# `.buildpacks` that contains the URLs, one on each line, in order.
|
||||
# Note: Auto CI does not work with multiple buildpacks yet
|
||||
|
||||
image: alpine:latest
|
||||
|
||||
variables:
|
||||
# KUBE_INGRESS_BASE_DOMAIN is the application deployment domain and should be set as a variable at the group or project level.
|
||||
# KUBE_INGRESS_BASE_DOMAIN: domain.example.com
|
||||
|
||||
POSTGRES_USER: user
|
||||
POSTGRES_PASSWORD: testing-password
|
||||
POSTGRES_ENABLED: "true"
|
||||
POSTGRES_DB: $CI_ENVIRONMENT_SLUG
|
||||
POSTGRES_VERSION: 9.6.2
|
||||
|
||||
DOCKER_DRIVER: overlay2
|
||||
|
||||
ROLLOUT_RESOURCE_TYPE: deployment
|
||||
|
||||
DOCKER_TLS_CERTDIR: "" # https://gitlab.com/gitlab-org/gitlab-runner/issues/4501
|
||||
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
- deploy # dummy stage to follow the template guidelines
|
||||
- review
|
||||
- dast
|
||||
- staging
|
||||
- canary
|
||||
- production
|
||||
- incremental rollout 10%
|
||||
- incremental rollout 25%
|
||||
- incremental rollout 50%
|
||||
- incremental rollout 100%
|
||||
- performance
|
||||
- cleanup
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: '$BUILDPACK_URL || $AUTO_DEVOPS_EXPLICITLY_ENABLED == "1"'
|
||||
|
||||
- exists:
|
||||
- Dockerfile
|
||||
|
||||
# https://github.com/heroku/heroku-buildpack-clojure
|
||||
- exists:
|
||||
- project.clj
|
||||
|
||||
# https://github.com/heroku/heroku-buildpack-go
|
||||
- exists:
|
||||
- go.mod
|
||||
- Gopkg.mod
|
||||
- Godeps/Godeps.json
|
||||
- vendor/vendor.json
|
||||
- glide.yaml
|
||||
- src/**/*.go
|
||||
|
||||
# https://github.com/heroku/heroku-buildpack-gradle
|
||||
- exists:
|
||||
- gradlew
|
||||
- build.gradle
|
||||
- settings.gradle
|
||||
|
||||
# https://github.com/heroku/heroku-buildpack-java
|
||||
- exists:
|
||||
- pom.xml
|
||||
- pom.atom
|
||||
- pom.clj
|
||||
- pom.groovy
|
||||
- pom.rb
|
||||
- pom.scala
|
||||
- pom.yaml
|
||||
- pom.yml
|
||||
|
||||
# https://github.com/heroku/heroku-buildpack-multi
|
||||
- exists:
|
||||
- .buildpacks
|
||||
|
||||
# https://github.com/heroku/heroku-buildpack-nodejs
|
||||
- exists:
|
||||
- package.json
|
||||
|
||||
# https://github.com/heroku/heroku-buildpack-php
|
||||
- exists:
|
||||
- composer.json
|
||||
- index.php
|
||||
|
||||
# https://github.com/heroku/heroku-buildpack-play
|
||||
# TODO: detect script excludes some scala files
|
||||
- exists:
|
||||
- '**/conf/application.conf'
|
||||
|
||||
# https://github.com/heroku/heroku-buildpack-python
|
||||
# TODO: detect script checks that all of these exist, not any
|
||||
- exists:
|
||||
- requirements.txt
|
||||
- setup.py
|
||||
- Pipfile
|
||||
|
||||
# https://github.com/heroku/heroku-buildpack-ruby
|
||||
- exists:
|
||||
- Gemfile
|
||||
|
||||
# https://github.com/heroku/heroku-buildpack-scala
|
||||
- exists:
|
||||
- '*.sbt'
|
||||
- project/*.scala
|
||||
- .sbt/*.scala
|
||||
- project/build.properties
|
||||
|
||||
# https://github.com/dokku/buildpack-nginx
|
||||
- exists:
|
||||
- .static
|
||||
|
||||
include:
|
||||
- template: Jobs/Build.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
|
||||
- template: Jobs/Test.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
|
||||
- template: Jobs/Code-Quality.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
|
||||
- template: Jobs/Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
|
||||
- template: Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
|
||||
- template: Jobs/Browser-Performance-Testing.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
|
||||
- template: Security/DAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
|
||||
- template: Security/Container-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
|
||||
- template: Security/Dependency-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
|
||||
- template: Security/License-Management.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml
|
||||
- template: Security/SAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
|
|
@ -12,7 +12,8 @@ module Gitlab
|
|||
PodspecJsonLinker,
|
||||
CartfileLinker,
|
||||
GodepsJsonLinker,
|
||||
RequirementsTxtLinker
|
||||
RequirementsTxtLinker,
|
||||
CargoTomlLinker
|
||||
].freeze
|
||||
|
||||
def self.linker(blob_name)
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module DependencyLinker
|
||||
class CargoTomlLinker < BaseLinker
|
||||
self.file_type = :cargo_toml
|
||||
|
||||
def link
|
||||
return highlighted_text unless toml
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def link_dependencies
|
||||
link_dependencies_at("dependencies")
|
||||
link_dependencies_at("dev-dependencies")
|
||||
link_dependencies_at("build-dependencies")
|
||||
end
|
||||
|
||||
def link_dependencies_at(type)
|
||||
dependencies = toml[type]
|
||||
return unless dependencies
|
||||
|
||||
dependencies.each do |name, value|
|
||||
link_toml(name, value, type) do |name|
|
||||
"https://crates.io/crates/#{name}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def link_toml(key, value, type, &url_proc)
|
||||
if value.is_a? String
|
||||
link_regex(/^(?<name>#{key})\s*=\s*"#{value}"/, &url_proc)
|
||||
else
|
||||
link_regex(/^\[#{type}\.(?<name>#{key})]/, &url_proc)
|
||||
end
|
||||
end
|
||||
|
||||
def toml
|
||||
@toml ||= TomlRB.parse(plain_text) rescue nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -25,6 +25,7 @@ module Gitlab
|
|||
route_map: '.gitlab/route-map.yml',
|
||||
|
||||
# Dependency files
|
||||
cargo_toml: 'Cargo.toml',
|
||||
cartfile: %r{\ACartfile[^/]*\z},
|
||||
composer_json: 'composer.json',
|
||||
gemfile: /\A(Gemfile|gems\.rb)\z/,
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
module Sentry
|
||||
class Client
|
||||
include Sentry::Client::Projects
|
||||
include Sentry::Client::Issue
|
||||
|
||||
Error = Class.new(StandardError)
|
||||
MissingKeysError = Class.new(StandardError)
|
||||
|
@ -23,12 +24,6 @@ module Sentry
|
|||
@token = token
|
||||
end
|
||||
|
||||
def issue_details(issue_id:)
|
||||
issue = get_issue(issue_id: issue_id)
|
||||
|
||||
map_to_detailed_error(issue)
|
||||
end
|
||||
|
||||
def issue_latest_event(issue_id:)
|
||||
latest_event = get_issue_latest_event(issue_id: issue_id)
|
||||
|
||||
|
@ -107,10 +102,6 @@ module Sentry
|
|||
}.compact
|
||||
end
|
||||
|
||||
def get_issue(issue_id:)
|
||||
http_get(issue_api_url(issue_id))[:body]
|
||||
end
|
||||
|
||||
def get_issue_latest_event(issue_id:)
|
||||
http_get(issue_latest_event_api_url(issue_id))[:body]
|
||||
end
|
||||
|
@ -145,13 +136,6 @@ module Sentry
|
|||
raise Client::Error, message
|
||||
end
|
||||
|
||||
def issue_api_url(issue_id)
|
||||
issue_url = URI(@url)
|
||||
issue_url.path = "/api/0/issues/#{issue_id}/"
|
||||
|
||||
issue_url
|
||||
end
|
||||
|
||||
def issue_latest_event_api_url(issue_id)
|
||||
latest_event_url = URI(@url)
|
||||
latest_event_url.path = "/api/0/issues/#{issue_id}/events/latest/"
|
||||
|
@ -212,42 +196,6 @@ module Sentry
|
|||
stack_trace_entry.dig('stacktrace', 'frames')
|
||||
end
|
||||
|
||||
def parse_gitlab_issue(plugin_issues)
|
||||
return unless plugin_issues
|
||||
|
||||
gitlab_plugin = plugin_issues.detect { |item| item['id'] == 'gitlab' }
|
||||
return unless gitlab_plugin
|
||||
|
||||
gitlab_plugin.dig('issue', 'url')
|
||||
end
|
||||
|
||||
def map_to_detailed_error(issue)
|
||||
Gitlab::ErrorTracking::DetailedError.new(
|
||||
id: issue.fetch('id'),
|
||||
first_seen: issue.fetch('firstSeen', nil),
|
||||
last_seen: issue.fetch('lastSeen', nil),
|
||||
title: issue.fetch('title', nil),
|
||||
type: issue.fetch('type', nil),
|
||||
user_count: issue.fetch('userCount', nil),
|
||||
count: issue.fetch('count', nil),
|
||||
message: issue.dig('metadata', 'value'),
|
||||
culprit: issue.fetch('culprit', nil),
|
||||
external_url: issue_url(issue.fetch('id')),
|
||||
external_base_url: project_url,
|
||||
short_id: issue.fetch('shortId', nil),
|
||||
status: issue.fetch('status', nil),
|
||||
frequency: issue.dig('stats', '24h'),
|
||||
project_id: issue.dig('project', 'id'),
|
||||
project_name: issue.dig('project', 'name'),
|
||||
project_slug: issue.dig('project', 'slug'),
|
||||
gitlab_issue: parse_gitlab_issue(issue.fetch('pluginIssues', nil)),
|
||||
first_release_last_commit: issue.dig('firstRelease', 'lastCommit'),
|
||||
last_release_last_commit: issue.dig('lastRelease', 'lastCommit'),
|
||||
first_release_short_version: issue.dig('firstRelease', 'shortVersion'),
|
||||
last_release_short_version: issue.dig('lastRelease', 'shortVersion')
|
||||
)
|
||||
end
|
||||
|
||||
def map_to_error(issue)
|
||||
Gitlab::ErrorTracking::Error.new(
|
||||
id: issue.fetch('id'),
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Sentry
|
||||
class Client
|
||||
module Issue
|
||||
def issue_details(issue_id:)
|
||||
issue = get_issue(issue_id: issue_id)
|
||||
|
||||
map_to_detailed_error(issue)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_issue(issue_id:)
|
||||
http_get(issue_api_url(issue_id))[:body]
|
||||
end
|
||||
|
||||
def issue_api_url(issue_id)
|
||||
issue_url = URI(url)
|
||||
issue_url.path = "/api/0/issues/#{CGI.escape(issue_id.to_s)}/"
|
||||
|
||||
issue_url
|
||||
end
|
||||
|
||||
def parse_gitlab_issue(plugin_issues)
|
||||
return unless plugin_issues
|
||||
|
||||
gitlab_plugin = plugin_issues.detect { |item| item['id'] == 'gitlab' }
|
||||
return unless gitlab_plugin
|
||||
|
||||
gitlab_plugin.dig('issue', 'url')
|
||||
end
|
||||
|
||||
def map_to_detailed_error(issue)
|
||||
Gitlab::ErrorTracking::DetailedError.new(
|
||||
id: issue.fetch('id'),
|
||||
first_seen: issue.fetch('firstSeen', nil),
|
||||
last_seen: issue.fetch('lastSeen', nil),
|
||||
title: issue.fetch('title', nil),
|
||||
type: issue.fetch('type', nil),
|
||||
user_count: issue.fetch('userCount', nil),
|
||||
count: issue.fetch('count', nil),
|
||||
message: issue.dig('metadata', 'value'),
|
||||
culprit: issue.fetch('culprit', nil),
|
||||
external_url: issue_url(issue.fetch('id')),
|
||||
external_base_url: project_url,
|
||||
short_id: issue.fetch('shortId', nil),
|
||||
status: issue.fetch('status', nil),
|
||||
frequency: issue.dig('stats', '24h'),
|
||||
project_id: issue.dig('project', 'id'),
|
||||
project_name: issue.dig('project', 'name'),
|
||||
project_slug: issue.dig('project', 'slug'),
|
||||
gitlab_issue: parse_gitlab_issue(issue.fetch('pluginIssues', nil)),
|
||||
first_release_last_commit: issue.dig('firstRelease', 'lastCommit'),
|
||||
last_release_last_commit: issue.dig('lastRelease', 'lastCommit'),
|
||||
first_release_short_version: issue.dig('firstRelease', 'shortVersion'),
|
||||
last_release_short_version: issue.dig('lastRelease', 'shortVersion')
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7795,6 +7795,9 @@ msgstr ""
|
|||
msgid "Filter by milestone name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter by name..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter by two-factor authentication"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11548,6 +11551,9 @@ msgstr ""
|
|||
msgid "More actions"
|
||||
msgstr ""
|
||||
|
||||
msgid "More details"
|
||||
msgstr ""
|
||||
|
||||
msgid "More info"
|
||||
msgstr ""
|
||||
|
||||
|
@ -14726,6 +14732,9 @@ msgstr ""
|
|||
msgid "Recent searches"
|
||||
msgstr ""
|
||||
|
||||
msgid "Recipe"
|
||||
msgstr ""
|
||||
|
||||
msgid "Recovery Codes"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -0,0 +1,311 @@
|
|||
{
|
||||
"activity": [
|
||||
{
|
||||
"data": {},
|
||||
"dateCreated": "2018-11-06T21:19:55Z",
|
||||
"id": "0",
|
||||
"type": "first_seen",
|
||||
"user": null
|
||||
}
|
||||
],
|
||||
"annotations": [],
|
||||
"assignedTo": null,
|
||||
"count": "1",
|
||||
"culprit": "raven.scripts.runner in main",
|
||||
"firstRelease": {
|
||||
"authors": [],
|
||||
"commitCount": 0,
|
||||
"data": {},
|
||||
"dateCreated": "2018-11-06T21:19:55.146Z",
|
||||
"dateReleased": null,
|
||||
"deployCount": 0,
|
||||
"firstEvent": "2018-11-06T21:19:55.271Z",
|
||||
"lastCommit": null,
|
||||
"lastDeploy": null,
|
||||
"lastEvent": "2018-11-06T21:19:55.271Z",
|
||||
"newGroups": 0,
|
||||
"owner": null,
|
||||
"projects": [
|
||||
{
|
||||
"name": "Pump Station",
|
||||
"slug": "pump-station"
|
||||
}
|
||||
],
|
||||
"ref": null,
|
||||
"shortVersion": "1764232",
|
||||
"url": null,
|
||||
"version": "17642328ead24b51867165985996d04b29310337"
|
||||
},
|
||||
"firstSeen": "2018-11-06T21:19:55Z",
|
||||
"hasSeen": false,
|
||||
"id": "503504",
|
||||
"isBookmarked": false,
|
||||
"isPublic": false,
|
||||
"isSubscribed": true,
|
||||
"lastRelease": null,
|
||||
"lastSeen": "2018-11-06T21:19:55Z",
|
||||
"level": "error",
|
||||
"logger": null,
|
||||
"metadata": {
|
||||
"title": "This is an example Python exception"
|
||||
},
|
||||
"numComments": 0,
|
||||
"participants": [],
|
||||
"permalink": "https://sentrytest.gitlab.com/sentry-org/sentry-project/issues/503504/",
|
||||
"pluginActions": [],
|
||||
"pluginContexts": [],
|
||||
"pluginIssues": [
|
||||
{
|
||||
"id": "gitlab",
|
||||
"issue": {
|
||||
"url": "https://gitlab.com/gitlab-org/gitlab/issues/1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"project": {
|
||||
"id": "2",
|
||||
"name": "Pump Station",
|
||||
"slug": "pump-station"
|
||||
},
|
||||
"seenBy": [],
|
||||
"shareId": null,
|
||||
"shortId": "PUMP-STATION-1",
|
||||
"stats": {
|
||||
"24h": [
|
||||
[
|
||||
1541451600.0,
|
||||
557
|
||||
],
|
||||
[
|
||||
1541455200.0,
|
||||
473
|
||||
],
|
||||
[
|
||||
1541458800.0,
|
||||
914
|
||||
],
|
||||
[
|
||||
1541462400.0,
|
||||
991
|
||||
],
|
||||
[
|
||||
1541466000.0,
|
||||
925
|
||||
],
|
||||
[
|
||||
1541469600.0,
|
||||
881
|
||||
],
|
||||
[
|
||||
1541473200.0,
|
||||
182
|
||||
],
|
||||
[
|
||||
1541476800.0,
|
||||
490
|
||||
],
|
||||
[
|
||||
1541480400.0,
|
||||
820
|
||||
],
|
||||
[
|
||||
1541484000.0,
|
||||
322
|
||||
],
|
||||
[
|
||||
1541487600.0,
|
||||
836
|
||||
],
|
||||
[
|
||||
1541491200.0,
|
||||
565
|
||||
],
|
||||
[
|
||||
1541494800.0,
|
||||
758
|
||||
],
|
||||
[
|
||||
1541498400.0,
|
||||
880
|
||||
],
|
||||
[
|
||||
1541502000.0,
|
||||
677
|
||||
],
|
||||
[
|
||||
1541505600.0,
|
||||
381
|
||||
],
|
||||
[
|
||||
1541509200.0,
|
||||
814
|
||||
],
|
||||
[
|
||||
1541512800.0,
|
||||
329
|
||||
],
|
||||
[
|
||||
1541516400.0,
|
||||
446
|
||||
],
|
||||
[
|
||||
1541520000.0,
|
||||
731
|
||||
],
|
||||
[
|
||||
1541523600.0,
|
||||
111
|
||||
],
|
||||
[
|
||||
1541527200.0,
|
||||
926
|
||||
],
|
||||
[
|
||||
1541530800.0,
|
||||
772
|
||||
],
|
||||
[
|
||||
1541534400.0,
|
||||
400
|
||||
],
|
||||
[
|
||||
1541538000.0,
|
||||
943
|
||||
]
|
||||
],
|
||||
"30d": [
|
||||
[
|
||||
1538870400.0,
|
||||
565
|
||||
],
|
||||
[
|
||||
1538956800.0,
|
||||
12862
|
||||
],
|
||||
[
|
||||
1539043200.0,
|
||||
15617
|
||||
],
|
||||
[
|
||||
1539129600.0,
|
||||
10809
|
||||
],
|
||||
[
|
||||
1539216000.0,
|
||||
15065
|
||||
],
|
||||
[
|
||||
1539302400.0,
|
||||
12927
|
||||
],
|
||||
[
|
||||
1539388800.0,
|
||||
12994
|
||||
],
|
||||
[
|
||||
1539475200.0,
|
||||
13139
|
||||
],
|
||||
[
|
||||
1539561600.0,
|
||||
11838
|
||||
],
|
||||
[
|
||||
1539648000.0,
|
||||
12088
|
||||
],
|
||||
[
|
||||
1539734400.0,
|
||||
12338
|
||||
],
|
||||
[
|
||||
1539820800.0,
|
||||
12768
|
||||
],
|
||||
[
|
||||
1539907200.0,
|
||||
12816
|
||||
],
|
||||
[
|
||||
1539993600.0,
|
||||
15356
|
||||
],
|
||||
[
|
||||
1540080000.0,
|
||||
10910
|
||||
],
|
||||
[
|
||||
1540166400.0,
|
||||
12306
|
||||
],
|
||||
[
|
||||
1540252800.0,
|
||||
12912
|
||||
],
|
||||
[
|
||||
1540339200.0,
|
||||
14700
|
||||
],
|
||||
[
|
||||
1540425600.0,
|
||||
11890
|
||||
],
|
||||
[
|
||||
1540512000.0,
|
||||
11684
|
||||
],
|
||||
[
|
||||
1540598400.0,
|
||||
13510
|
||||
],
|
||||
[
|
||||
1540684800.0,
|
||||
12625
|
||||
],
|
||||
[
|
||||
1540771200.0,
|
||||
12811
|
||||
],
|
||||
[
|
||||
1540857600.0,
|
||||
13180
|
||||
],
|
||||
[
|
||||
1540944000.0,
|
||||
14651
|
||||
],
|
||||
[
|
||||
1541030400.0,
|
||||
14161
|
||||
],
|
||||
[
|
||||
1541116800.0,
|
||||
12612
|
||||
],
|
||||
[
|
||||
1541203200.0,
|
||||
14316
|
||||
],
|
||||
[
|
||||
1541289600.0,
|
||||
14742
|
||||
],
|
||||
[
|
||||
1541376000.0,
|
||||
12505
|
||||
],
|
||||
[
|
||||
1541462400.0,
|
||||
14180
|
||||
]
|
||||
]
|
||||
},
|
||||
"status": "unresolved",
|
||||
"statusDetails": {},
|
||||
"subscriptionDetails": null,
|
||||
"tags": [],
|
||||
"title": "This is an example Python exception",
|
||||
"type": "default",
|
||||
"userCount": 0,
|
||||
"userReportCount": 0
|
||||
}
|
|
@ -272,6 +272,7 @@ describe('ErrorTrackingList', () => {
|
|||
|
||||
describe('When pagination is not required', () => {
|
||||
beforeEach(() => {
|
||||
store.state.list.loading = false;
|
||||
store.state.list.pagination = {};
|
||||
mountComponent();
|
||||
});
|
||||
|
@ -284,6 +285,7 @@ describe('ErrorTrackingList', () => {
|
|||
describe('When pagination is required', () => {
|
||||
describe('and the user is on the first page', () => {
|
||||
beforeEach(() => {
|
||||
store.state.list.loading = false;
|
||||
mountComponent({ sync: false });
|
||||
});
|
||||
|
||||
|
@ -295,6 +297,7 @@ describe('ErrorTrackingList', () => {
|
|||
describe('and the user is not on the first page', () => {
|
||||
describe('and the previous button is clicked', () => {
|
||||
beforeEach(() => {
|
||||
store.state.list.loading = false;
|
||||
mountComponent({ sync: false });
|
||||
wrapper.setData({ pageValue: 2 });
|
||||
});
|
||||
|
@ -313,6 +316,7 @@ describe('ErrorTrackingList', () => {
|
|||
|
||||
describe('and the next page button is clicked', () => {
|
||||
beforeEach(() => {
|
||||
store.state.list.loading = false;
|
||||
mountComponent({ sync: false });
|
||||
});
|
||||
|
||||
|
|
Binary file not shown.
|
@ -1,9 +1,9 @@
|
|||
import { mount, createLocalVue } from '@vue/test-utils';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import fieldComponent from '~/vue_shared/components/markdown/field.vue';
|
||||
import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants';
|
||||
import AxiosMockAdapter from 'axios-mock-adapter';
|
||||
import $ from 'jquery';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import fieldComponent from '~/vue_shared/components/markdown/field.vue';
|
||||
|
||||
const markdownPreviewPath = `${TEST_HOST}/preview`;
|
||||
const markdownDocsPath = `${TEST_HOST}/docs`;
|
||||
|
@ -19,6 +19,7 @@ function createComponent() {
|
|||
propsData: {
|
||||
markdownDocsPath,
|
||||
markdownPreviewPath,
|
||||
isSubmitting: false,
|
||||
},
|
||||
slots: {
|
||||
textarea: '<textarea>testing\n123</textarea>',
|
||||
|
@ -27,6 +28,7 @@ function createComponent() {
|
|||
<field-component
|
||||
markdown-preview-path="${markdownPreviewPath}"
|
||||
markdown-docs-path="${markdownDocsPath}"
|
||||
:isSubmitting="false"
|
||||
>
|
||||
<textarea
|
||||
slot="textarea"
|
||||
|
@ -44,6 +46,7 @@ const getPreviewLink = wrapper => wrapper.find('.nav-links .js-preview-link');
|
|||
const getWriteLink = wrapper => wrapper.find('.nav-links .js-write-link');
|
||||
const getMarkdownButton = wrapper => wrapper.find('.js-md');
|
||||
const getAllMarkdownButtons = wrapper => wrapper.findAll('.js-md');
|
||||
const getVideo = wrapper => wrapper.find('video');
|
||||
|
||||
describe('Markdown field component', () => {
|
||||
let axiosMock;
|
||||
|
@ -59,7 +62,10 @@ describe('Markdown field component', () => {
|
|||
|
||||
describe('mounted', () => {
|
||||
let wrapper;
|
||||
const previewHTML = '<p>markdown preview</p>';
|
||||
const previewHTML = `
|
||||
<p>markdown preview</p>
|
||||
<video src="${FIXTURES_PATH}/static/mock-video.mp4" muted="muted"></video>
|
||||
`;
|
||||
let previewLink;
|
||||
let writeLink;
|
||||
|
||||
|
@ -112,9 +118,35 @@ describe('Markdown field component', () => {
|
|||
|
||||
previewLink.trigger('click');
|
||||
|
||||
setTimeout(() => {
|
||||
expect($.fn.renderGFM).toHaveBeenCalled();
|
||||
}, 0);
|
||||
return axios.waitFor(markdownPreviewPath).then(() => {
|
||||
expect(wrapper.find('.md-preview-holder').element.innerHTML).toContain(previewHTML);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls video.pause() on comment input when isSubmitting is changed to true', () => {
|
||||
wrapper = createComponent();
|
||||
previewLink = getPreviewLink(wrapper);
|
||||
previewLink.trigger('click');
|
||||
|
||||
let callPause;
|
||||
|
||||
return axios
|
||||
.waitFor(markdownPreviewPath)
|
||||
.then(() => {
|
||||
const video = getVideo(wrapper);
|
||||
callPause = jest.spyOn(video.element, 'pause').mockImplementation(() => true);
|
||||
|
||||
wrapper.setProps({
|
||||
isSubmitting: true,
|
||||
markdownPreviewPath,
|
||||
markdownDocsPath,
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick();
|
||||
})
|
||||
.then(() => {
|
||||
expect(callPause).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('clicking already active write or preview link does nothing', () => {
|
||||
|
|
|
@ -40,7 +40,7 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
|
|||
subject.perform!
|
||||
|
||||
expect(pipeline.config_source).to eq 'auto_devops_source'
|
||||
template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps')
|
||||
template = Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps')
|
||||
expect(command.config_content).to eq(template.content)
|
||||
end
|
||||
end
|
||||
|
@ -52,7 +52,7 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
|
|||
subject.perform!
|
||||
|
||||
expect(pipeline.config_source).to eq 'auto_devops_source'
|
||||
template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps')
|
||||
template = Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps')
|
||||
expect(command.config_content).to eq(template.content)
|
||||
end
|
||||
end
|
||||
|
@ -82,6 +82,25 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
|
|||
expect(project).to receive(:auto_devops_enabled?).and_return(true)
|
||||
end
|
||||
|
||||
context 'when beta is enabled' do
|
||||
before do
|
||||
stub_feature_flags(auto_devops_beta: true)
|
||||
end
|
||||
|
||||
it 'returns the content of AutoDevops template' do
|
||||
subject.perform!
|
||||
|
||||
expect(pipeline.config_source).to eq 'auto_devops_source'
|
||||
template = Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps')
|
||||
expect(command.config_content).to eq(template.content)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when beta is disabled' do
|
||||
before do
|
||||
stub_feature_flags(auto_devops_beta: false)
|
||||
end
|
||||
|
||||
it 'returns the content of AutoDevops template' do
|
||||
subject.perform!
|
||||
|
||||
|
@ -90,6 +109,7 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
|
|||
expect(command.config_content).to eq(template.content)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when config is not defined anywhere' do
|
||||
let(:ci_config_path) { nil }
|
||||
|
@ -190,6 +210,28 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
|
|||
expect(project).to receive(:auto_devops_enabled?).and_return(true)
|
||||
end
|
||||
|
||||
context 'when beta is enabled' do
|
||||
before do
|
||||
stub_feature_flags(auto_devops_beta: true)
|
||||
end
|
||||
|
||||
it 'builds root config including the auto-devops template' do
|
||||
subject.perform!
|
||||
|
||||
expect(pipeline.config_source).to eq 'auto_devops_source'
|
||||
expect(command.config_content).to eq(<<~EOY)
|
||||
---
|
||||
include:
|
||||
- template: Beta/Auto-DevOps.gitlab-ci.yml
|
||||
EOY
|
||||
end
|
||||
end
|
||||
|
||||
context 'when beta is disabled' do
|
||||
before do
|
||||
stub_feature_flags(auto_devops_beta: false)
|
||||
end
|
||||
|
||||
it 'builds root config including the auto-devops template' do
|
||||
subject.perform!
|
||||
|
||||
|
@ -201,6 +243,7 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
|
|||
EOY
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when config is not defined anywhere' do
|
||||
let(:ci_config_path) { nil }
|
||||
|
|
|
@ -9,7 +9,7 @@ describe 'Auto-DevOps.gitlab-ci.yml' do
|
|||
let(:user) { create(:admin) }
|
||||
let(:default_branch) { 'master' }
|
||||
let(:pipeline_branch) { default_branch }
|
||||
let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
|
||||
let(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) }
|
||||
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
|
||||
let(:pipeline) { service.execute!(:push) }
|
||||
let(:build_names) { pipeline.builds.pluck(:name) }
|
||||
|
@ -107,4 +107,52 @@ describe 'Auto-DevOps.gitlab-ci.yml' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'build-pack detection' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:case_name, :files, :variables, :include_build_names, :not_include_build_names) do
|
||||
'No match' | { 'README.md' => '' } | {} | %w() | %w(build test)
|
||||
'Buildpack' | { 'README.md' => '' } | { 'BUILDPACK_URL' => 'http://example.com' } | %w(build test) | %w()
|
||||
'Explicit set' | { 'README.md' => '' } | { 'AUTO_DEVOPS_EXPLICITLY_ENABLED' => '1' } | %w(build test) | %w()
|
||||
'Explicit unset' | { 'README.md' => '' } | { 'AUTO_DEVOPS_EXPLICITLY_ENABLED' => '0' } | %w() | %w(build test)
|
||||
'Dockerfile' | { 'Dockerfile' => '' } | {} | %w(build test) | %w()
|
||||
'Clojure' | { 'project.clj' => '' } | {} | %w(build test) | %w()
|
||||
'Go modules' | { 'go.mod' => '' } | {} | %w(build test) | %w()
|
||||
'Go gb' | { 'src/gitlab.com/gopackage.go' => '' } | {} | %w(build test) | %w()
|
||||
'Gradle' | { 'gradlew' => '' } | {} | %w(build test) | %w()
|
||||
'Java' | { 'pom.xml' => '' } | {} | %w(build test) | %w()
|
||||
'Multi-buildpack' | { '.buildpacks' => '' } | {} | %w(build test) | %w()
|
||||
'NodeJS' | { 'package.json' => '' } | {} | %w(build test) | %w()
|
||||
'PHP' | { 'composer.json' => '' } | {} | %w(build test) | %w()
|
||||
'Play' | { 'conf/application.conf' => '' } | {} | %w(build test) | %w()
|
||||
'Python' | { 'Pipfile' => '' } | {} | %w(build test) | %w()
|
||||
'Ruby' | { 'Gemfile' => '' } | {} | %w(build test) | %w()
|
||||
'Scala' | { 'build.sbt' => '' } | {} | %w(build test) | %w()
|
||||
'Static' | { '.static' => '' } | {} | %w(build test) | %w()
|
||||
end
|
||||
|
||||
with_them do
|
||||
subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps') }
|
||||
|
||||
let(:user) { create(:admin) }
|
||||
let(:project) { create(:project, :custom_repo, files: files) }
|
||||
let(:service) { Ci::CreatePipelineService.new(project, user, ref: 'master' ) }
|
||||
let(:pipeline) { service.execute(:push) }
|
||||
let(:build_names) { pipeline.builds.pluck(:name) }
|
||||
|
||||
before do
|
||||
stub_ci_pipeline_yaml_file(template.content)
|
||||
allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
|
||||
variables.each do |(key, value)|
|
||||
create(:ci_variable, project: project, key: key, value: value)
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates a pipeline with the expected jobs' do
|
||||
expect(build_names).to include(*include_build_names)
|
||||
expect(build_names).not_to include(*not_include_build_names)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::DependencyLinker::CargoTomlLinker do
|
||||
describe '.support?' do
|
||||
it 'supports Cargo.toml' do
|
||||
expect(described_class.support?('Cargo.toml')).to be_truthy
|
||||
end
|
||||
|
||||
it 'does not support other files' do
|
||||
expect(described_class.support?('cargo.yaml')).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe '#link' do
|
||||
let(:file_name) { "Cargo.toml" }
|
||||
|
||||
let(:file_content) do
|
||||
<<-CONTENT.strip_heredoc
|
||||
# See https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[package]
|
||||
# Package shouldn't be matched
|
||||
name = "gitlab-test"
|
||||
version = "0.0.1"
|
||||
authors = ["Some User <some.user@example.org>"]
|
||||
description = "A GitLab test Cargo.toml."
|
||||
keywords = ["gitlab", "test", "rust", "crago"]
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
# Default dependencies format with fixed version and version range
|
||||
chrono = "0.4.7"
|
||||
xml-rs = ">=0.8.0"
|
||||
|
||||
[dependencies.memchr]
|
||||
# Specific dependency with optional info
|
||||
version = "2.2.1"
|
||||
optional = true
|
||||
|
||||
[dev-dependencies]
|
||||
# Dev dependency with version modifier
|
||||
commandspec = "~0.12.2"
|
||||
|
||||
[build-dependencies]
|
||||
# Build dependency with version wildcard
|
||||
thread_local = "0.3.*"
|
||||
CONTENT
|
||||
end
|
||||
|
||||
subject { Gitlab::Highlight.highlight(file_name, file_content) }
|
||||
|
||||
def link(name, url)
|
||||
%{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
|
||||
end
|
||||
|
||||
it 'links dependencies' do
|
||||
expect(subject).to include(link('chrono', 'https://crates.io/crates/chrono'))
|
||||
expect(subject).to include(link('xml-rs', 'https://crates.io/crates/xml-rs'))
|
||||
expect(subject).to include(link('memchr', 'https://crates.io/crates/memchr'))
|
||||
expect(subject).to include(link('commandspec', 'https://crates.io/crates/commandspec'))
|
||||
expect(subject).to include(link('thread_local', 'https://crates.io/crates/thread_local'))
|
||||
end
|
||||
|
||||
it 'does not contain metadata identified as package' do
|
||||
expect(subject).not_to include(link('version', 'https://crates.io/crates/version'))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -83,5 +83,13 @@ describe Gitlab::DependencyLinker do
|
|||
|
||||
described_class.link(blob_name, nil, nil)
|
||||
end
|
||||
|
||||
it 'links using CargoTomlLinker' do
|
||||
blob_name = 'Cargo.toml'
|
||||
|
||||
expect(described_class::CargoTomlLinker).to receive(:link)
|
||||
|
||||
described_class.link(blob_name, nil, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Sentry::Client::Issue do
|
||||
include SentryClientHelpers
|
||||
|
||||
let(:token) { 'test-token' }
|
||||
let(:client) { Sentry::Client.new(sentry_url, token) }
|
||||
|
||||
describe '#issue_details' do
|
||||
let(:issue_sample_response) do
|
||||
Gitlab::Utils.deep_indifferent_access(
|
||||
JSON.parse(fixture_file('sentry/issue_sample_response.json'))
|
||||
)
|
||||
end
|
||||
|
||||
let(:issue_id) { 503504 }
|
||||
let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0' }
|
||||
let(:sentry_request_url) { "#{sentry_url}/issues/#{issue_id}/" }
|
||||
let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: issue_sample_response) }
|
||||
|
||||
subject { client.issue_details(issue_id: issue_id) }
|
||||
|
||||
it_behaves_like 'calls sentry api'
|
||||
|
||||
it 'escapes issue ID' do
|
||||
allow(CGI).to receive(:escape).and_call_original
|
||||
|
||||
subject
|
||||
|
||||
expect(CGI).to have_received(:escape).with(issue_id.to_s)
|
||||
end
|
||||
|
||||
context 'error object created from sentry response' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:error_object, :sentry_response) do
|
||||
:id | :id
|
||||
:first_seen | :firstSeen
|
||||
:last_seen | :lastSeen
|
||||
:title | :title
|
||||
:type | :type
|
||||
:user_count | :userCount
|
||||
:count | :count
|
||||
:message | [:metadata, :value]
|
||||
:culprit | :culprit
|
||||
:short_id | :shortId
|
||||
:status | :status
|
||||
:frequency | [:stats, '24h']
|
||||
:project_id | [:project, :id]
|
||||
:project_name | [:project, :name]
|
||||
:project_slug | [:project, :slug]
|
||||
:first_release_last_commit | [:firstRelease, :lastCommit]
|
||||
:last_release_last_commit | [:lastRelease, :lastCommit]
|
||||
:first_release_short_version | [:firstRelease, :shortVersion]
|
||||
:last_release_short_version | [:lastRelease, :shortVersion]
|
||||
end
|
||||
|
||||
with_them do
|
||||
it do
|
||||
expect(subject.public_send(error_object)).to eq(issue_sample_response.dig(*sentry_response))
|
||||
end
|
||||
end
|
||||
|
||||
it 'has a correct external URL' do
|
||||
expect(subject.external_url).to eq('https://sentrytest.gitlab.com/api/0/issues/503504')
|
||||
end
|
||||
|
||||
it 'issue has a correct external base url' do
|
||||
expect(subject.external_base_url).to eq('https://sentrytest.gitlab.com/api/0')
|
||||
end
|
||||
|
||||
it 'has a correct GitLab issue url' do
|
||||
expect(subject.gitlab_issue).to eq('https://gitlab.com/gitlab-org/gitlab/issues/1')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -14,12 +14,6 @@ describe Sentry::Client do
|
|||
}
|
||||
end
|
||||
|
||||
let(:issues_sample_response) do
|
||||
Gitlab::Utils.deep_indifferent_access(
|
||||
JSON.parse(fixture_file('sentry/issues_sample_response.json'))
|
||||
)
|
||||
end
|
||||
|
||||
subject(:client) { described_class.new(sentry_url, token) }
|
||||
|
||||
shared_examples 'issues has correct return type' do |klass|
|
||||
|
@ -33,6 +27,12 @@ describe Sentry::Client do
|
|||
end
|
||||
|
||||
describe '#list_issues' do
|
||||
let(:issues_sample_response) do
|
||||
Gitlab::Utils.deep_indifferent_access(
|
||||
JSON.parse(fixture_file('sentry/issues_sample_response.json'))
|
||||
)
|
||||
end
|
||||
|
||||
let(:issue_status) { 'unresolved' }
|
||||
let(:limit) { 20 }
|
||||
let(:search_term) { '' }
|
||||
|
|
|
@ -2245,14 +2245,24 @@ describe Ci::Build do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#has_expiring_artifacts?' do
|
||||
describe '#has_expiring_archive_artifacts?' do
|
||||
context 'when artifacts have expiration date set' do
|
||||
before do
|
||||
build.update(artifacts_expire_at: 1.day.from_now)
|
||||
end
|
||||
|
||||
context 'and job artifacts file exists' do
|
||||
let!(:archive) { create(:ci_job_artifact, :archive, job: build) }
|
||||
|
||||
it 'has expiring artifacts' do
|
||||
expect(build).to have_expiring_artifacts
|
||||
expect(build).to have_expiring_archive_artifacts
|
||||
end
|
||||
end
|
||||
|
||||
context 'and job artifacts file does not exist' do
|
||||
it 'does not have expiring artifacts' do
|
||||
expect(build).not_to have_expiring_archive_artifacts
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -2262,7 +2272,7 @@ describe Ci::Build do
|
|||
end
|
||||
|
||||
it 'does not have expiring artifacts' do
|
||||
expect(build).not_to have_expiring_artifacts
|
||||
expect(build).not_to have_expiring_archive_artifacts
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -91,18 +91,124 @@ describe DiffNote do
|
|||
end
|
||||
|
||||
describe '#create_diff_file callback' do
|
||||
let(:noteable) { create(:merge_request) }
|
||||
let(:project) { noteable.project }
|
||||
|
||||
context 'merge request' do
|
||||
let!(:diff_note) { create(:diff_note_on_merge_request, project: project, noteable: noteable) }
|
||||
let(:position) do
|
||||
Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb",
|
||||
new_path: "files/ruby/popen.rb",
|
||||
old_line: nil,
|
||||
new_line: 9,
|
||||
diff_refs: merge_request.diff_refs)
|
||||
end
|
||||
|
||||
subject { build(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) }
|
||||
|
||||
let(:diff_file_from_repository) do
|
||||
position.diff_file(project.repository)
|
||||
end
|
||||
|
||||
let(:diff_file) do
|
||||
diffs = merge_request.diffs
|
||||
raw_diff = diffs.diffable.raw_diffs(diffs.diff_options.merge(paths: ['files/ruby/popen.rb'])).first
|
||||
Gitlab::Diff::File.new(raw_diff,
|
||||
repository: diffs.project.repository,
|
||||
diff_refs: diffs.diff_refs,
|
||||
fallback_diff_refs: diffs.fallback_diff_refs)
|
||||
end
|
||||
|
||||
let(:diff_line) { diff_file.diff_lines.first }
|
||||
|
||||
let(:line_code) { '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14' }
|
||||
|
||||
before do
|
||||
allow(subject.position).to receive(:line_code).and_return('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14')
|
||||
end
|
||||
|
||||
context 'when diffs are already created' do
|
||||
before do
|
||||
allow(subject).to receive(:created_at_diff?).and_return(true)
|
||||
end
|
||||
|
||||
context 'when diff_file is found in persisted diffs' do
|
||||
before do
|
||||
allow(merge_request).to receive_message_chain(:diffs, :diff_files, :first).and_return(diff_file)
|
||||
end
|
||||
|
||||
context 'when importing' do
|
||||
before do
|
||||
subject.importing = true
|
||||
subject.line_code = line_code
|
||||
end
|
||||
|
||||
context 'when diff_line is found in persisted diff_file' do
|
||||
before do
|
||||
allow(diff_file).to receive(:line_for_position).with(position).and_return(diff_line)
|
||||
end
|
||||
|
||||
it 'creates a diff note file' do
|
||||
expect(diff_note.reload.note_diff_file).to be_present
|
||||
subject.save
|
||||
expect(subject.note_diff_file).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'when diff_line is not found in persisted diff_file' do
|
||||
before do
|
||||
allow(diff_file).to receive(:line_for_position).and_return(nil)
|
||||
end
|
||||
|
||||
it_behaves_like 'a valid diff note with after commit callback'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not importing' do
|
||||
context 'when diff_line is not found' do
|
||||
before do
|
||||
allow(diff_file).to receive(:line_for_position).with(position).and_return(nil)
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject.save }.to raise_error(::DiffNote::NoteDiffFileCreationError,
|
||||
"Failed to find diff line for: #{diff_file.file_path}, "\
|
||||
"old_line: #{position.old_line}"\
|
||||
", new_line: #{position.new_line}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when diff_line is found' do
|
||||
before do
|
||||
allow(diff_file).to receive(:line_for_position).with(position).and_return(diff_line)
|
||||
end
|
||||
|
||||
it 'creates a diff note file' do
|
||||
subject.save
|
||||
expect(subject.reload.note_diff_file).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when diff file is not found in persisted diffs' do
|
||||
before do
|
||||
allow_next_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiff) do |merge_request_diff|
|
||||
allow(merge_request_diff).to receive(:diff_files).and_return([])
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'a valid diff note with after commit callback'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when diffs are not already created' do
|
||||
before do
|
||||
allow(subject).to receive(:created_at_diff?).and_return(false)
|
||||
end
|
||||
|
||||
it_behaves_like 'a valid diff note with after commit callback'
|
||||
end
|
||||
|
||||
it 'does not create diff note file if it is a reply' do
|
||||
expect { create(:diff_note_on_merge_request, noteable: noteable, in_reply_to: diff_note) }
|
||||
diff_note = create(:diff_note_on_merge_request, project: project, noteable: merge_request)
|
||||
|
||||
expect { create(:diff_note_on_merge_request, noteable: merge_request, in_reply_to: diff_note) }
|
||||
.not_to change(NoteDiffFile, :count)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,6 +4,8 @@ require 'spec_helper'
|
|||
|
||||
describe BuildArtifactEntity do
|
||||
let(:job) { create(:ci_build, name: 'test:job', artifacts_expire_at: 1.hour.from_now) }
|
||||
let!(:archive) { create(:ci_job_artifact, :archive, job: job) }
|
||||
let!(:metadata) { create(:ci_job_artifact, :metadata, job: job) }
|
||||
|
||||
let(:entity) do
|
||||
described_class.new(job, request: double)
|
||||
|
|
|
@ -176,5 +176,27 @@ describe BuildDetailsEntity do
|
|||
expect(subject[:reports].first[:file_type]).to eq('codequality')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the build has no archive type artifacts' do
|
||||
let!(:report) { create(:ci_job_artifact, :codequality, job: build) }
|
||||
|
||||
it 'does not expose any artifact actions path' do
|
||||
expect(subject[:artifact].keys).not_to include(:download_path, :browse_path, :keep_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the build has archive type artifacts' do
|
||||
let!(:report) { create(:ci_job_artifact, :codequality, job: build) }
|
||||
let!(:archive) { create(:ci_job_artifact, :archive, job: build) }
|
||||
let!(:metadata) { create(:ci_job_artifact, :metadata, job: build) }
|
||||
|
||||
before do
|
||||
build.update(artifacts_expire_at: 7.days.from_now)
|
||||
end
|
||||
|
||||
it 'exposes artifact details' do
|
||||
expect(subject[:artifact].keys).to include(:download_path, :browse_path, :keep_path, :expire_at, :expired)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -493,6 +493,7 @@ describe Ci::CreatePipelineService do
|
|||
before do
|
||||
stub_ci_pipeline_yaml_file(nil)
|
||||
allow_any_instance_of(Project).to receive(:auto_devops_enabled?).and_return(true)
|
||||
create(:project_auto_devops, project: project)
|
||||
end
|
||||
|
||||
it 'pull it from Auto-DevOps' do
|
||||
|
|
|
@ -4,7 +4,7 @@ require 'spec_helper'
|
|||
|
||||
describe ExternalPullRequests::CreatePipelineService do
|
||||
describe '#execute' do
|
||||
set(:project) { create(:project, :repository) }
|
||||
set(:project) { create(:project, :auto_devops, :repository) }
|
||||
set(:user) { create(:user) }
|
||||
let(:pull_request) { create(:external_pull_request, project: project) }
|
||||
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
shared_examples 'a valid diff note with after commit callback' do
|
||||
context 'when diff file is fetched from repository' do
|
||||
before do
|
||||
allow_any_instance_of(::Gitlab::Diff::Position).to receive(:diff_file).with(project.repository).and_return(diff_file_from_repository)
|
||||
end
|
||||
|
||||
context 'when diff_line is not found' do
|
||||
it 'raises an error' do
|
||||
allow(diff_file_from_repository).to receive(:line_for_position).with(position).and_return(nil)
|
||||
|
||||
expect { subject.save }.to raise_error(::DiffNote::NoteDiffFileCreationError,
|
||||
"Failed to find diff line for: #{diff_file_from_repository.file_path}, "\
|
||||
"old_line: #{position.old_line}"\
|
||||
", new_line: #{position.new_line}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when diff_line is found' do
|
||||
before do
|
||||
allow(diff_file_from_repository).to receive(:line_for_position).with(position).and_return(diff_line)
|
||||
end
|
||||
|
||||
it 'fallback to fetch file from repository' do
|
||||
expect_any_instance_of(::Gitlab::Diff::Position).to receive(:diff_file).with(project.repository)
|
||||
|
||||
subject.save
|
||||
end
|
||||
|
||||
it 'creates a diff note file' do
|
||||
subject.save
|
||||
|
||||
expect(subject.reload.note_diff_file).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when diff file is not found in repository' do
|
||||
it 'raises an error' do
|
||||
allow_any_instance_of(::Gitlab::Diff::Position).to receive(:diff_file).with(project.repository).and_return(nil)
|
||||
|
||||
expect { subject.save }.to raise_error(::DiffNote::NoteDiffFileCreationError, 'Failed to find diff file')
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue