Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-17 12:09:37 +00:00
parent afed1a4be1
commit c6283014fe
64 changed files with 1516 additions and 516 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 387 KiB

After

Width:  |  Height:  |  Size: 387 KiB

View File

@ -18,6 +18,7 @@ import TreeList from './tree_list.vue';
import HiddenFilesWarning from './hidden_files_warning.vue';
import MergeConflictWarning from './merge_conflict_warning.vue';
import CollapsedFilesWarning from './collapsed_files_warning.vue';
import {
TREE_LIST_WIDTH_STORAGE_KEY,
@ -27,6 +28,9 @@ import {
TREE_HIDE_STATS_WIDTH,
MR_TREE_SHOW_KEY,
CENTERED_LIMITED_CONTAINER_CLASSES,
ALERT_OVERFLOW_HIDDEN,
ALERT_MERGE_CONFLICT,
ALERT_COLLAPSED_FILES,
} from '../constants';
export default {
@ -37,6 +41,7 @@ export default {
NoChanges,
HiddenFilesWarning,
MergeConflictWarning,
CollapsedFilesWarning,
CommitWidget,
TreeList,
GlLoadingIcon,
@ -45,6 +50,11 @@ export default {
GlSprintf,
},
mixins: [glFeatureFlagsMixin()],
alerts: {
ALERT_OVERFLOW_HIDDEN,
ALERT_MERGE_CONFLICT,
ALERT_COLLAPSED_FILES,
},
props: {
endpoint: {
type: String,
@ -114,6 +124,7 @@ export default {
return {
treeWidth,
diffFilesLength: 0,
collapsedWarningDismissed: false,
};
},
computed: {
@ -142,7 +153,7 @@ export default {
'canMerge',
'hasConflicts',
]),
...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']),
...mapGetters('diffs', ['hasCollapsedFile', 'isParallelView', 'currentDiffIndex']),
...mapGetters(['isNotesFetched', 'getNoteableData']),
diffs() {
if (!this.viewDiffsFileByFile) {
@ -188,6 +199,23 @@ export default {
return currentFileNumber < diffFiles.length ? currentFileNumber + 1 : null;
},
visibleWarning() {
let visible = false;
if (this.renderOverflowWarning) {
visible = this.$options.alerts.ALERT_OVERFLOW_HIDDEN;
} else if (this.isDiffHead && this.hasConflicts) {
visible = this.$options.alerts.ALERT_MERGE_CONFLICT;
} else if (
this.hasCollapsedFile &&
!this.collapsedWarningDismissed &&
!this.viewDiffsFileByFile
) {
visible = this.$options.alerts.ALERT_COLLAPSED_FILES;
}
return visible;
},
},
watch: {
commit(newCommit, oldCommit) {
@ -401,6 +429,9 @@ export default {
this.toggleShowTreeList(false);
}
},
dismissCollapsedWarning() {
this.collapsedWarningDismissed = true;
},
},
minTreeWidth: MIN_TREE_WIDTH,
maxTreeWidth: MAX_TREE_WIDTH,
@ -418,18 +449,23 @@ export default {
/>
<hidden-files-warning
v-if="renderOverflowWarning"
v-if="visibleWarning == $options.alerts.ALERT_OVERFLOW_HIDDEN"
:visible="numVisibleFiles"
:total="numTotalFiles"
:plain-diff-path="plainDiffPath"
:email-patch-path="emailPatchPath"
/>
<merge-conflict-warning
v-if="isDiffHead && hasConflicts"
v-if="visibleWarning == $options.alerts.ALERT_MERGE_CONFLICT"
:limited="isLimitedContainer"
:resolution-path="conflictResolutionPath"
:mergeable="canMerge"
/>
<collapsed-files-warning
v-if="visibleWarning == $options.alerts.ALERT_COLLAPSED_FILES"
:limited="isLimitedContainer"
@dismiss="dismissCollapsedWarning"
/>
<div
:data-can-create-note="getNoteableData.current_user.can_create_note"

View File

@ -0,0 +1,71 @@
<script>
import { mapActions } from 'vuex';
import { GlAlert, GlButton } from '@gitlab/ui';
import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants';
export default {
components: {
GlAlert,
GlButton,
},
props: {
limited: {
type: Boolean,
required: false,
default: false,
},
dismissed: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
isDismissed: this.dismissed,
};
},
computed: {
containerClasses() {
return {
[CENTERED_LIMITED_CONTAINER_CLASSES]: this.limited,
};
},
},
methods: {
...mapActions('diffs', ['expandAllFiles']),
dismiss() {
this.isDismissed = true;
this.$emit('dismiss');
},
expand() {
this.expandAllFiles();
this.dismiss();
},
},
};
</script>
<template>
<div v-if="!isDismissed" data-testid="root" :class="containerClasses">
<gl-alert
:dismissible="true"
:title="__('Some changes are not shown')"
variant="warning"
class="gl-mb-5"
@dismiss="dismiss"
>
<p class="gl-mb-0">
{{ __('For a faster browsing experience, some files are collapsed by default.') }}
</p>
<template #actions>
<gl-button category="secondary" variant="warning" class="gl-alert-action" @click="expand">
{{ __('Expand all files') }}
</gl-button>
</template>
</gl-alert>
</div>
</template>

View File

@ -49,8 +49,12 @@ export default {
...mapState({
projectPath: state => state.diffs.projectPath,
}),
...mapGetters('diffs', ['isInlineView', 'isParallelView']),
...mapGetters('diffs', ['getCommentFormForDiffFile']),
...mapGetters('diffs', [
'isInlineView',
'isParallelView',
'getCommentFormForDiffFile',
'diffLines',
]),
...mapGetters(['getNoteableData', 'noteableType', 'getUserData']),
diffMode() {
return getDiffMode(this.diffFile);
@ -115,17 +119,15 @@ export default {
<inline-diff-view
v-if="isInlineView"
:diff-file="diffFile"
:diff-lines="
glFeatures.unifiedDiffLines
? diffFile.parallel_diff_lines
: diffFile.highlighted_diff_lines || []
"
:diff-lines="diffFile.highlighted_diff_lines"
:help-page-path="helpPagePath"
/>
<parallel-diff-view
v-else-if="isParallelView"
:diff-file="diffFile"
:diff-lines="diffFile.parallel_diff_lines || []"
:diff-lines="
glFeatures.unifiedDiffLines ? diffLines(diffFile) : diffFile.parallel_diff_lines || []
"
:help-page-path="helpPagePath"
/>
<gl-loading-icon v-if="diffFile.renderingLines" size="md" class="mt-3" />

View File

@ -67,9 +67,7 @@ export default {
computed: {
...mapState({
diffViewType(state) {
return this.glFeatures.unifiedDiffLines
? PARALLEL_DIFF_VIEW_TYPE
: state.diffs.diffViewType;
return this.glFeatures.unifiedDiffLines ? INLINE_DIFF_VIEW_TYPE : state.diffs.diffViewType;
},
diffFiles: state => state.diffs.diffFiles,
}),

View File

@ -64,99 +64,37 @@ export default {
<col />
</colgroup>
<tbody>
<template v-if="glFeatures.unifiedDiffLines">
<template v-for="({ left, right }, index) in diffLines">
<inline-diff-expansion-row
:key="`expand-${index}`"
:file-hash="diffFile.file_hash"
:context-lines-path="diffFile.context_lines_path"
:line="left || right"
:is-top="index === 0"
:is-bottom="index + 1 === diffLinesLength"
/>
<template v-if="left">
<inline-diff-table-row
:key="`${left.line_code || index}`"
:file-hash="diffFile.file_hash"
:file-path="diffFile.file_path"
:line="left"
:is-bottom="index + 1 === diffLinesLength"
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
/>
<inline-diff-comment-row
:key="`icr-${left.line_code || index}`"
:diff-file-hash="diffFile.file_hash"
:line="left"
:help-page-path="helpPagePath"
:has-draft="shouldRenderDraftRow(diffFile.file_hash, left) || false"
/>
<inline-draft-comment-row
v-if="shouldRenderDraftRow(diffFile.file_hash, left)"
:key="`draft_${index}`"
:draft="draftForLine(diffFile.file_hash, left)"
:diff-file="diffFile"
:line="left"
/>
</template>
<template v-if="right && right.type === 'new'">
<inline-diff-table-row
:key="`new-${right.line_code || index}`"
:file-hash="diffFile.file_hash"
:file-path="diffFile.file_path"
:line="right"
:is-bottom="index + 1 === diffLinesLength"
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
/>
<inline-diff-comment-row
:key="`new-icr-${right.line_code || index}`"
:diff-file-hash="diffFile.file_hash"
:line="right"
:help-page-path="helpPagePath"
:has-draft="shouldRenderDraftRow(diffFile.file_hash, right) || false"
/>
<inline-draft-comment-row
v-if="shouldRenderDraftRow(diffFile.file_hash, right)"
:key="`new-draft_${index}`"
:draft="draftForLine(diffFile.file_hash, right)"
:diff-file="diffFile"
:line="right"
/>
</template>
</template>
</template>
<template v-else>
<template v-for="(line, index) in diffLines">
<inline-diff-expansion-row
:key="`expand-${index}`"
:file-hash="diffFile.file_hash"
:context-lines-path="diffFile.context_lines_path"
:line="line"
:is-top="index === 0"
:is-bottom="index + 1 === diffLinesLength"
/>
<inline-diff-table-row
:key="`${line.line_code || index}`"
:file-hash="diffFile.file_hash"
:file-path="diffFile.file_path"
:line="line"
:is-bottom="index + 1 === diffLinesLength"
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
/>
<inline-diff-comment-row
:key="`icr-${line.line_code || index}`"
:diff-file-hash="diffFile.file_hash"
:line="line"
:help-page-path="helpPagePath"
:has-draft="shouldRenderDraftRow(diffFile.file_hash, line) || false"
/>
<inline-draft-comment-row
v-if="shouldRenderDraftRow(diffFile.file_hash, line)"
:key="`draft_${index}`"
:draft="draftForLine(diffFile.file_hash, line)"
:diff-file="diffFile"
:line="line"
/>
</template>
<template v-for="(line, index) in diffLines">
<inline-diff-expansion-row
:key="`expand-${index}`"
:file-hash="diffFile.file_hash"
:context-lines-path="diffFile.context_lines_path"
:line="line"
:is-top="index === 0"
:is-bottom="index + 1 === diffLinesLength"
/>
<inline-diff-table-row
:key="`${line.line_code || index}`"
:file-hash="diffFile.file_hash"
:file-path="diffFile.file_path"
:line="line"
:is-bottom="index + 1 === diffLinesLength"
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
/>
<inline-diff-comment-row
:key="`icr-${line.line_code || index}`"
:diff-file-hash="diffFile.file_hash"
:line="line"
:help-page-path="helpPagePath"
:has-draft="shouldRenderDraftRow(diffFile.file_hash, line) || false"
/>
<inline-draft-comment-row
v-if="shouldRenderDraftRow(diffFile.file_hash, line)"
:key="`draft_${index}`"
:draft="draftForLine(diffFile.file_hash, line)"
:diff-file="diffFile"
:line="line"
/>
</template>
</tbody>
</table>

View File

@ -68,6 +68,11 @@ export const DIFFS_PER_PAGE = 20;
export const DIFF_COMPARE_BASE_VERSION_INDEX = -1;
export const DIFF_COMPARE_HEAD_VERSION_INDEX = -2;
// Diff View Alerts
export const ALERT_OVERFLOW_HIDDEN = 'overflow';
export const ALERT_MERGE_CONFLICT = 'merge-conflict';
export const ALERT_COLLAPSED_FILES = 'collapsed';
// State machine states
export const STATE_IDLING = 'idle';
export const STATE_LOADING = 'loading';

View File

@ -1,4 +1,5 @@
import { __, n__ } from '~/locale';
import { parallelizeDiffLines } from './utils';
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
export * from './getters_versions_dropdowns';
@ -129,3 +130,11 @@ export const fileLineCoverage = state => (file, line) => {
*/
export const currentDiffIndex = state =>
Math.max(0, state.diffFiles.findIndex(diff => diff.file_hash === state.currentDiffFileId));
export const diffLines = state => file => {
if (state.diffViewType === INLINE_DIFF_VIEW_TYPE) {
return null;
}
return parallelizeDiffLines(file.highlighted_diff_lines || []);
};

View File

@ -1,6 +1,6 @@
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { PARALLEL_DIFF_VIEW_TYPE } from '../constants';
import { INLINE_DIFF_VIEW_TYPE } from '../constants';
import {
findDiffFile,
addLineReferences,
@ -152,7 +152,7 @@ export default {
inlineLines: diffFile.highlighted_diff_lines,
parallelLines: diffFile.parallel_diff_lines,
diffViewType: window.gon?.features?.unifiedDiffLines
? PARALLEL_DIFF_VIEW_TYPE
? INLINE_DIFF_VIEW_TYPE
: state.diffViewType,
contextLines: lines,
bottom,

View File

@ -350,15 +350,8 @@ function mergeTwoFiles(target, source) {
}
function ensureBasicDiffFileLines(file) {
if (window.gon?.features?.unifiedDiffLines) {
return Object.assign(file, {
highlighted_diff_lines: [],
parallel_diff_lines: parallelizeDiffLines(file.highlighted_diff_lines || []),
});
}
const missingInline = !file.highlighted_diff_lines;
const missingParallel = !file.parallel_diff_lines;
const missingParallel = !file.parallel_diff_lines || window.gon?.features?.unifiedDiffLines;
Object.assign(file, {
highlighted_diff_lines: missingInline ? [] : file.highlighted_diff_lines,

View File

@ -6,6 +6,7 @@ import UsersSelect from './users_select';
export default class IssuableContext {
constructor(currentUser) {
this.userSelect = new UsersSelect(currentUser);
this.reviewersSelect = new UsersSelect(currentUser, '.js-reviewer-search');
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {

View File

@ -40,6 +40,17 @@ export default class SidebarMediator {
return this.service.update(field, data);
}
saveReviewers(field) {
const selected = this.store.reviewers.map(u => u.id);
// If there are no ids, that means we have to unassign (which is id = 0)
// And it only accepts an array, hence [0]
const reviewers = selected.length === 0 ? [0] : selected;
const data = { reviewer_ids: reviewers };
return this.service.update(field, data);
}
setMoveToProjectId(projectId) {
this.store.setMoveToProjectId(projectId);
}
@ -55,6 +66,7 @@ export default class SidebarMediator {
processFetchedData(data) {
this.store.setAssigneeData(data);
this.store.setReviewerData(data);
this.store.setTimeTrackingData(data);
this.store.setParticipantsData(data);
this.store.setSubscriptionsData(data);

View File

@ -18,8 +18,10 @@ export default class SidebarStore {
this.humanTimeSpent = '';
this.timeTrackingLimitToHours = timeTrackingLimitToHours;
this.assignees = [];
this.reviewers = [];
this.isFetching = {
assignees: true,
reviewers: true,
participants: true,
subscriptions: true,
};
@ -42,6 +44,13 @@ export default class SidebarStore {
}
}
setReviewerData(data) {
this.isFetching.reviewers = false;
if (data.reviewers) {
this.reviewers = data.reviewers;
}
}
setTimeTrackingData(data) {
this.timeEstimate = data.time_estimate;
this.totalTimeSpent = data.total_time_spent;
@ -75,20 +84,40 @@ export default class SidebarStore {
}
}
addReviewer(reviewer) {
if (!this.findReviewer(reviewer)) {
this.reviewers.push(reviewer);
}
}
findAssignee(findAssignee) {
return this.assignees.find(assignee => assignee.id === findAssignee.id);
}
findReviewer(findReviewer) {
return this.reviewers.find(reviewer => reviewer.id === findReviewer.id);
}
removeAssignee(removeAssignee) {
if (removeAssignee) {
this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id);
}
}
removeReviewer(removeReviewer) {
if (removeReviewer) {
this.reviewers = this.reviewers.filter(reviewer => reviewer.id !== removeReviewer.id);
}
}
removeAllAssignees() {
this.assignees = [];
}
removeAllReviewers() {
this.reviewers = [];
}
setAssigneesFromRealtime(data) {
this.assignees = data;
}

View File

@ -19,6 +19,7 @@ import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
function UsersSelect(currentUser, els, options = {}) {
const elsClassName = els?.toString().match('.(.+$)')[1];
const $els = $(els || '.js-user-search');
this.users = this.users.bind(this);
this.user = this.user.bind(this);
@ -127,9 +128,16 @@ function UsersSelect(currentUser, els, options = {}) {
.find(`input[name='${$dropdown.data('fieldName')}'][value=${firstSelectedId}]`);
firstSelected.remove();
emitSidebarEvent('sidebar.removeAssignee', {
id: firstSelectedId,
});
if ($dropdown.hasClass(elsClassName)) {
emitSidebarEvent('sidebar.removeReviewer', {
id: firstSelectedId,
});
} else {
emitSidebarEvent('sidebar.removeAssignee', {
id: firstSelectedId,
});
}
}
}
};
@ -392,7 +400,11 @@ function UsersSelect(currentUser, els, options = {}) {
defaultLabel,
hidden() {
if ($dropdown.hasClass('js-multiselect')) {
emitSidebarEvent('sidebar.saveAssignees');
if ($dropdown.hasClass(elsClassName)) {
emitSidebarEvent('sidebar.saveReviewers');
} else {
emitSidebarEvent('sidebar.saveAssignees');
}
}
if (!$dropdown.data('alwaysShowSelectbox')) {
@ -428,10 +440,18 @@ function UsersSelect(currentUser, els, options = {}) {
previouslySelected.each((index, element) => {
element.remove();
});
emitSidebarEvent('sidebar.removeAllAssignees');
if ($dropdown.hasClass(elsClassName)) {
emitSidebarEvent('sidebar.removeAllReviewers');
} else {
emitSidebarEvent('sidebar.removeAllAssignees');
}
} else if (isActive) {
// user selected
emitSidebarEvent('sidebar.addAssignee', user);
if ($dropdown.hasClass(elsClassName)) {
emitSidebarEvent('sidebar.addReviewer', user);
} else {
emitSidebarEvent('sidebar.addAssignee', user);
}
// Remove unassigned selection (if it was previously selected)
const unassignedSelected = $dropdown
@ -448,7 +468,11 @@ function UsersSelect(currentUser, els, options = {}) {
}
// User unselected
emitSidebarEvent('sidebar.removeAssignee', user);
if ($dropdown.hasClass(elsClassName)) {
emitSidebarEvent('sidebar.removeReviewer', user);
} else {
emitSidebarEvent('sidebar.removeAssignee', user);
}
}
if (getSelected().find(u => u === gon.current_user_id)) {

View File

@ -7,6 +7,7 @@ import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
import { sprintf, s__, __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import { deprecatedCreateFlash as createFlash } from '../flash';
import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables';
import Loading from './components/loading.vue';
@ -202,7 +203,10 @@ export default {
},
mounted() {
MRWidgetService.fetchInitialData()
.then(({ data }) => this.initWidget(data))
.then(({ data, headers }) => {
this.startingPollInterval = Number(headers['POLL-INTERVAL']);
this.initWidget(data);
})
.catch(() =>
createFlash(__('Unable to load the merge request widget. Try reloading the page.')),
);
@ -292,9 +296,10 @@ export default {
initPolling() {
this.pollingInterval = new SmartInterval({
callback: this.checkStatus,
startingInterval: 10 * 1000,
maxInterval: 240 * 1000,
hiddenInterval: window.gon?.features?.widgetVisibilityPolling && 360 * 1000,
startingInterval: this.startingPollInterval,
maxInterval: this.startingPollInterval + secondsToMilliseconds(4 * 60),
hiddenInterval:
window.gon?.features?.widgetVisibilityPolling && secondsToMilliseconds(6 * 60),
incrementByFactorOf: 2,
});
},

View File

@ -1,3 +1,4 @@
import { normalizeHeaders } from '~/lib/utils/common_utils';
import axios from '../../lib/utils/axios_utils';
export default class MRWidgetService {
@ -82,6 +83,11 @@ export default class MRWidgetService {
return Promise.all([
axios.get(window.gl.mrWidgetData.merge_request_cached_widget_path),
axios.get(window.gl.mrWidgetData.merge_request_widget_path),
]).then(axios.spread((res, cachedRes) => ({ data: Object.assign(res.data, cachedRes.data) })));
]).then(
axios.spread((res, cachedRes) => ({
data: Object.assign(res.data, cachedRes.data),
headers: normalizeHeaders(res.headers),
})),
);
}
}

View File

@ -386,6 +386,12 @@ module IssuablesHelper
end
end
def reviewer_sidebar_data(reviewer, merge_request: nil)
{ avatar_url: reviewer.avatar_url, name: reviewer.name, username: reviewer.username }.tap do |data|
data[:can_merge] = merge_request.can_be_merged_by?(reviewer) if merge_request
end
end
def issuable_squash_option?(issuable, project)
if issuable.persisted?
issuable.squash

View File

@ -35,6 +35,10 @@ module Ci
end
end
event :pending do
transition all => :pending
end
event :manual do
transition all => :manual
end

View File

@ -237,7 +237,6 @@ module Ci
end
after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline|
next unless pipeline.bridge_triggered?
next unless pipeline.bridge_waiting?
pipeline.run_after_commit do
@ -1072,6 +1071,18 @@ module Ci
.base_and_ancestors
end
# We need `base_and_ancestors` in a specific order to "break" when needed.
# If we use `find_each`, then the order is broken.
# rubocop:disable Rails/FindEach
def reset_ancestor_bridges!
base_and_ancestors.includes(:source_bridge).each do |pipeline|
break unless pipeline.bridge_waiting?
pipeline.source_bridge.pending!
end
end
# rubocop:enable Rails/FindEach
private
def add_message(severity, content)

View File

@ -19,3 +19,5 @@ class ResourceStateEvent < ResourceEvent
issue || merge_request
end
end
ResourceStateEvent.prepend_if_ee('EE::ResourceStateEvent')

View File

@ -15,6 +15,7 @@ module Ci
reprocess!(build).tap do |new_build|
mark_subsequent_stages_as_processable(build)
build.pipeline.reset_ancestor_bridges!
Gitlab::OptimisticLocking.retry_lock(new_build, &:enqueue)

View File

@ -26,6 +26,8 @@ module Ci
retry_optimistic_lock(skipped) { |build| build.process }
end
pipeline.reset_ancestor_bridges!
MergeRequests::AddTodoWhenBuildFailsService
.new(project, current_user)
.close_all(pipeline)

View File

@ -13,14 +13,14 @@ module ResourceEvents
ResourceStateEvent.create(
user: user,
issue: issue,
merge_request: merge_request,
resource.class.underscore => resource,
source_commit: commit_id_of(mentionable_source),
source_merge_request_id: merge_request_id_of(mentionable_source),
state: ResourceStateEvent.states[state],
close_after_error_tracking_resolve: close_after_error_tracking_resolve,
close_auto_resolve_prometheus_alert: close_auto_resolve_prometheus_alert,
created_at: Time.zone.now)
created_at: Time.zone.now
)
resource.expire_note_etag_cache
end
@ -56,17 +56,5 @@ module ResourceEvents
mentionable_source.id
end
def issue
return unless resource.is_a?(Issue)
resource
end
def merge_request
return unless resource.is_a?(MergeRequest)
resource
end
end
end

View File

@ -92,7 +92,7 @@
.loading.hide
.spinner.spinner-md
= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, source_branch: @merge_request.source_branch
= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, reviewers: @merge_request.reviewers, source_branch: @merge_request.source_branch
- if @merge_request.can_be_reverted?(current_user)
= render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title

View File

@ -5,6 +5,7 @@
- signed_in = !!issuable_sidebar.dig(:current_user, :id)
- can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit)
- add_page_startup_api_call "#{issuable_sidebar[:issuable_json_path]}?serializer=sidebar_extras"
- reviewers = local_assigns.fetch(:reviewers, nil)
- if Feature.enabled?(:vue_issuable_sidebar, @project.group)
%aside#js-vue-issuable-sidebar{ data: { signed_in: signed_in,
@ -28,6 +29,10 @@
.block.assignee.qa-assignee-block
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees
- if reviewers
.block.reviewer.qa-reviewer-block
= render "shared/issuable/sidebar_reviewers", issuable_sidebar: issuable_sidebar, reviewers: reviewers
= render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar
- if issuable_sidebar[:supports_milestone]

View File

@ -0,0 +1,56 @@
- issuable_type = issuable_sidebar[:type]
- signed_in = !!issuable_sidebar.dig(:current_user, :id)
#js-vue-sidebar-reviewers{ data: { field: issuable_type, signed_in: signed_in } }
.title.hide-collapsed
= _('Reviewer')
= loading_icon(css_class: 'gl-vertical-align-text-bottom')
.selectbox.hide-collapsed
- if reviewers.none?
= hidden_field_tag "#{issuable_type}[reviewer_ids][]", 0, id: nil
- else
- reviewers.each do |reviewer|
= hidden_field_tag "#{issuable_type}[reviewer_ids][]", reviewer.id, id: nil, data: reviewer_sidebar_data(reviewer, merge_request: @merge_request)
- options = { toggle_class: 'js-reviewer-search js-author-search',
title: _('Request review from'),
filter: true,
dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author',
placeholder: _('Search users'),
data: { first_user: issuable_sidebar.dig(:current_user, :username),
current_user: true,
iid: issuable_sidebar[:iid],
issuable_type: issuable_type,
project_id: issuable_sidebar[:project_id],
author_id: issuable_sidebar[:author_id],
field_name: "#{issuable_type}[reviewer_ids][]",
issue_update: issuable_sidebar[:issuable_json_path],
ability_name: issuable_type,
null_user: true,
display: 'static' } }
- dropdown_options = reviewers_dropdown_options(issuable_type)
- title = dropdown_options[:title]
- options[:toggle_class] += ' js-multiselect js-save-user-data'
- data = { field_name: "#{issuable_type}[reviewer_ids][]" }
- data[:multi_select] = true
- data['dropdown-title'] = title
- data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
- data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select']
- options[:data].merge!(data)
- if experiment_enabled?(:invite_members_version_a) && can_import_members?
- options[:dropdown_class] += ' dropdown-extended-height'
- options[:footer_content] = true
- options[:wrapper_class] = 'js-sidebar-reviewer-dropdown'
= dropdown_tag(title, options: options) do
%ul.dropdown-footer-list
%li
= link_to _('Invite Members'),
project_project_members_path(@project),
title: _('Invite Members'),
data: { 'is-link': true, 'track-event': 'click_invite_members', 'track-label': 'edit_reviewer' }
- else
= dropdown_tag(title, options: options)

View File

@ -1,4 +1,4 @@
- if Feature.enabled?(:snippets_edit_vue)
- if Feature.enabled?(:snippets_edit_vue, default_enabled: true)
- available_visibility_levels = available_visibility_levels(@snippet)
#js-snippet-edit.snippet-form{ data: {'project_path': @snippet.project&.full_path, 'snippet-gid': @snippet.new_record? ? '' : @snippet.to_global_id, 'markdown-preview-path': preview_markdown_path(parent), 'markdown-docs-path': help_page_path('user/markdown'), 'visibility-help-link': help_page_path("public_access/public_access"), 'visibility_levels': available_visibility_levels, 'selected_level': snippets_selected_visibility_level(available_visibility_levels, @snippet.visibility_level), 'multiple_levels_restricted': multiple_visibility_levels_restricted? } }
- else

View File

@ -0,0 +1,5 @@
---
title: Refactored snippets edit form to Vue
merge_request: 42412
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fix upstream pipeline status when strategy dependent
merge_request: 41930
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix GitLab file icon in Firefox
merge_request: 40786
author:
type: fixed

View File

@ -1,5 +0,0 @@
---
title: Display Contributor and Author badges on notes
merge_request: 40198
author: Mycroft Kang @TaehyeokKang
type: added

View File

@ -0,0 +1,5 @@
---
title: Add a warning when any diff files are collapsed
merge_request: 40752
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: DAST Site validation - Model Layer
merge_request: 41639
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Increase widget polling for closed and merged merge requests
merge_request: 42458
author:
type: changed

View File

@ -1,7 +1,7 @@
---
name: snippets_edit_vue
introduced_by_url:
rollout_issue_url:
group:
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25667
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/207239
group: group::editor
type: development
default_enabled: false
default_enabled: true

View File

@ -0,0 +1,41 @@
---
- title: Coverage-guided fuzz testing for Go and C/C++ applications
body: You can now run coverage-guided fuzz tests against your Go and C/C++ apps. This is a great way to start finding security issues and bugs that other security scanners and traditional QA may miss. Coverage-guided fuzz testing uses contextual information about your app to randomly generate inputs and find crashes or other faults that you can then fix before they affect users in production.
stage: Secure
self-managed: true
gitlab-com: true
packages: [Ultimate]
url: https://www.youtube.com/watch?v=3wdWMDRLdp4
image_url: http://i3.ytimg.com/vi/3wdWMDRLdp4/hqdefault.jpg
published_at: 2020-08-22
release: 13.3
- title: Create a matrix of jobs using a simple syntax
body: GitLabs child/parent pipelines let you write your own code to generate an entire pipeline YAML. This is a powerful way to generate custom behaviors, including generating jobs at runtime. This might not be needed for simpler scenarios where you just want to create multiple similar jobs for a defined set of cases. In this release you can find a new matrix keyword that works along with parallel to handle the creation of multiple jobs for you, each with different variables.
stage: Verify
self-managed: true
gitlab-com: true
packages: [All]
url: https://docs.gitlab.com/ee/ci/yaml/#parallel-matrix-jobs
image_url: https://about.gitlab.com/images/13_3/cartesian-matrix.png
published_at: 2020-08-22
release: 13.3
- title: On-demand DAST scans
body: Dynamic Application Security Testing at GitLab has always been focused on integrating DAST into the DevOps pipeline and enabling developers to scan their review app, running website, or API for vulnerabilities as early as possible. However, there are times when it is necessary to run a DAST scan against an already deployed application when no code changes have been made and no Merge Request has been created. These scans could be needed for audit or compliance reasons, to debug and reproduce an issue that has been found, or to support teams who do not commit code, such as security analysts. Because of the need for DAST scans that are not triggered by a code change or MR, on-demand DAST testing is now available. You dont need configuration files or code to start running on-demand scans. Configuration options for on-demand DAST scans are available within the GitLab UI.
stage: Secure
self-managed: true
gitlab-com: true
packages: [Ultimate]
url: https://docs.gitlab.com/ee/user/application_security/dast/#on-demand-scans
image_url: https://about.gitlab.com/images/13_3/dast_on_demand_v13_3.png
published_at: 2020-08-22
release: 13.3
- title: SAST security analyzers available for all
body: We want to help developers write better code and worry less about common security mistakes. Static Application Security Testing (SAST) helps prevent security vulnerabilities by allowing developers to easily identify common security issues as code is being committed and mitigate proactively. As part of our community stewardship commitment we have made all 15 of our open source based SAST analyzers available in every GitLab tier. This allows ALL GitLab users developing in any of our 18 supported languages and frameworks to leverage GitLab SAST in their projects.
stage: Secure
self-managed: true
gitlab-com: true
packages: [All]
url: https://docs.gitlab.com/ee/user/application_security/sast/#making-sast-analyzers-available-to-all-gitlab-tiers
image_url: https://about.gitlab.com/images/13_3/sast-gitlab-languages.png
published_at: 2020-08-22
release: 13.3

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
class CreateDastSiteTokens < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
unless table_exists?(:dast_site_tokens)
with_lock_retries do
create_table :dast_site_tokens do |t|
t.references :project, foreign_key: { on_delete: :cascade }, null: false, index: true
t.timestamps_with_timezone null: false
t.datetime_with_timezone :expired_at
t.text :token, null: false, unique: true
t.text :url, null: false
end
end
end
add_text_limit :dast_site_tokens, :token, 255
add_text_limit :dast_site_tokens, :url, 255
end
def down
with_lock_retries do
drop_table :dast_site_tokens
end
end
end

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
class CreateDastSiteValidations < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
unless table_exists?(:dast_site_validations)
with_lock_retries do
create_table :dast_site_validations do |t|
t.references :dast_site_token, foreign_key: { on_delete: :cascade }, null: false, index: true
t.timestamps_with_timezone null: false
t.datetime_with_timezone :validation_started_at
t.datetime_with_timezone :validation_passed_at
t.datetime_with_timezone :validation_failed_at
t.datetime_with_timezone :validation_last_retried_at
t.integer :validation_strategy, null: false, limit: 2
t.text :url_base, null: false
t.text :url_path, null: false
end
end
end
add_concurrent_index :dast_site_validations, :url_base
add_text_limit :dast_site_validations, :url_base, 255
add_text_limit :dast_site_validations, :url_path, 255
end
def down
with_lock_retries do
drop_table :dast_site_validations
end
end
end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
class AddDastSiteValidationIdToDastSite < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
TABLE_NAME = :dast_sites
RELATION_NAME = :dast_site_validations
FK_NAME = :dast_site_validation_id
INDEX_NAME = "index_dast_sites_on_#{FK_NAME}"
disable_ddl_transaction!
def up
unless column_exists?(TABLE_NAME, FK_NAME)
with_lock_retries do
add_column TABLE_NAME, FK_NAME, :bigint
end
end
add_concurrent_index TABLE_NAME, FK_NAME, name: INDEX_NAME
add_concurrent_foreign_key TABLE_NAME, RELATION_NAME, column: FK_NAME, on_delete: :nullify
end
def down
remove_foreign_key_if_exists TABLE_NAME, RELATION_NAME
remove_concurrent_index_by_name TABLE_NAME, INDEX_NAME
with_lock_retries do
remove_column TABLE_NAME, FK_NAME
end
end
end

View File

@ -0,0 +1 @@
5fba5213226186a1506f672eb3eab2d07f58b019c4ba13760663cb119f62d4e2

View File

@ -0,0 +1 @@
002c92f830762d97dcbdbcf8a0287ebbb576edc27f4f76f4bb18d043e956ba7a

View File

@ -0,0 +1 @@
5f932b8a3503fc275ba6d09436115999b32f6438700e3b719f53730c5527a354

View File

@ -11202,12 +11202,59 @@ CREATE SEQUENCE public.dast_site_profiles_id_seq
ALTER SEQUENCE public.dast_site_profiles_id_seq OWNED BY public.dast_site_profiles.id;
CREATE TABLE public.dast_site_tokens (
id bigint NOT NULL,
project_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
expired_at timestamp with time zone,
token text NOT NULL,
url text NOT NULL,
CONSTRAINT check_02a6bf20a7 CHECK ((char_length(token) <= 255)),
CONSTRAINT check_69ab8622a6 CHECK ((char_length(url) <= 255))
);
CREATE SEQUENCE public.dast_site_tokens_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.dast_site_tokens_id_seq OWNED BY public.dast_site_tokens.id;
CREATE TABLE public.dast_site_validations (
id bigint NOT NULL,
dast_site_token_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
validation_started_at timestamp with time zone,
validation_passed_at timestamp with time zone,
validation_failed_at timestamp with time zone,
validation_last_retried_at timestamp with time zone,
validation_strategy smallint NOT NULL,
url_base text NOT NULL,
url_path text NOT NULL,
CONSTRAINT check_13b34efe4b CHECK ((char_length(url_path) <= 255)),
CONSTRAINT check_cd3b538210 CHECK ((char_length(url_base) <= 255))
);
CREATE SEQUENCE public.dast_site_validations_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.dast_site_validations_id_seq OWNED BY public.dast_site_validations.id;
CREATE TABLE public.dast_sites (
id bigint NOT NULL,
project_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
url text NOT NULL,
dast_site_validation_id bigint,
CONSTRAINT check_46df8b449c CHECK ((char_length(url) <= 255))
);
@ -17166,6 +17213,10 @@ ALTER TABLE ONLY public.dast_scanner_profiles ALTER COLUMN id SET DEFAULT nextva
ALTER TABLE ONLY public.dast_site_profiles ALTER COLUMN id SET DEFAULT nextval('public.dast_site_profiles_id_seq'::regclass);
ALTER TABLE ONLY public.dast_site_tokens ALTER COLUMN id SET DEFAULT nextval('public.dast_site_tokens_id_seq'::regclass);
ALTER TABLE ONLY public.dast_site_validations ALTER COLUMN id SET DEFAULT nextval('public.dast_site_validations_id_seq'::regclass);
ALTER TABLE ONLY public.dast_sites ALTER COLUMN id SET DEFAULT nextval('public.dast_sites_id_seq'::regclass);
ALTER TABLE ONLY public.dependency_proxy_blobs ALTER COLUMN id SET DEFAULT nextval('public.dependency_proxy_blobs_id_seq'::regclass);
@ -18195,6 +18246,12 @@ ALTER TABLE ONLY public.dast_scanner_profiles
ALTER TABLE ONLY public.dast_site_profiles
ADD CONSTRAINT dast_site_profiles_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.dast_site_tokens
ADD CONSTRAINT dast_site_tokens_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.dast_site_validations
ADD CONSTRAINT dast_site_validations_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.dast_sites
ADD CONSTRAINT dast_sites_pkey PRIMARY KEY (id);
@ -19785,6 +19842,14 @@ CREATE INDEX index_dast_site_profiles_on_dast_site_id ON public.dast_site_profil
CREATE UNIQUE INDEX index_dast_site_profiles_on_project_id_and_name ON public.dast_site_profiles USING btree (project_id, name);
CREATE INDEX index_dast_site_tokens_on_project_id ON public.dast_site_tokens USING btree (project_id);
CREATE INDEX index_dast_site_validations_on_dast_site_token_id ON public.dast_site_validations USING btree (dast_site_token_id);
CREATE INDEX index_dast_site_validations_on_url_base ON public.dast_site_validations USING btree (url_base);
CREATE INDEX index_dast_sites_on_dast_site_validation_id ON public.dast_sites USING btree (dast_site_validation_id);
CREATE UNIQUE INDEX index_dast_sites_on_project_id_and_url ON public.dast_sites USING btree (project_id, url);
CREATE INDEX index_dependency_proxy_blobs_on_group_id_and_file_name ON public.dependency_proxy_blobs USING btree (group_id, file_name);
@ -21754,6 +21819,9 @@ ALTER TABLE ONLY public.merge_requests
ALTER TABLE ONLY public.user_interacted_projects
ADD CONSTRAINT fk_0894651f08 FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.dast_sites
ADD CONSTRAINT fk_0a57f2271b FOREIGN KEY (dast_site_validation_id) REFERENCES public.dast_site_validations(id) ON DELETE SET NULL;
ALTER TABLE ONLY public.web_hooks
ADD CONSTRAINT fk_0c8ca6d9d1 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
@ -22537,6 +22605,9 @@ ALTER TABLE ONLY public.lfs_file_locks
ALTER TABLE ONLY public.project_alerting_settings
ADD CONSTRAINT fk_rails_27a84b407d FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.dast_site_validations
ADD CONSTRAINT fk_rails_285c617324 FOREIGN KEY (dast_site_token_id) REFERENCES public.dast_site_tokens(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.resource_state_events
ADD CONSTRAINT fk_rails_29af06892a FOREIGN KEY (issue_id) REFERENCES public.issues(id) ON DELETE CASCADE;
@ -23464,6 +23535,9 @@ ALTER TABLE ONLY public.merge_request_metrics
ALTER TABLE ONLY public.draft_notes
ADD CONSTRAINT fk_rails_e753681674 FOREIGN KEY (merge_request_id) REFERENCES public.merge_requests(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.dast_site_tokens
ADD CONSTRAINT fk_rails_e84f721a8e FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.group_deploy_keys_groups
ADD CONSTRAINT fk_rails_e87145115d FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;

View File

@ -218,6 +218,17 @@ namespace = Namespace.find_by_full_path("")
::Projects::TransferService.new(p, current_user).execute(namespace)
```
### For Removing webhooks that is getting timeout due to large webhook logs
```ruby
# ID will be the webhook_id
WebHookLog.where(web_hook_id: ID).each_slice(ID) do |slice|
slice.each(&:destroy)
end
WebHook.find(ID).destroy
```
### Bulk update service integration password for _all_ projects
For example, change the Jira user's password for all projects that have the Jira

View File

@ -44259,59 +44259,6 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "variables",
"description": "List of supported variables",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "SastCiConfigurationEntityConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@ -44379,6 +44326,59 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "variables",
"description": "List of supported variables",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "SastCiConfigurationEntityConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,

View File

@ -44,7 +44,7 @@ The Advanced Search can be useful in various scenarios.
### Faster searches
If you are dealing with huge amount of data and want to keep GitLab's search
fast, the Advanced Search will help you achieve that.
fast, Advanced Search will help you achieve that.
NOTE: **Note:**
Between versions 12.10 and 13.4, Advanced Search response times have improved by 80%.

View File

@ -46,7 +46,7 @@ module Gitlab
push_frontend_feature_flag(:snippets_vue, default_enabled: true)
push_frontend_feature_flag(:monaco_blobs, default_enabled: true)
push_frontend_feature_flag(:monaco_ci, default_enabled: false)
push_frontend_feature_flag(:snippets_edit_vue, default_enabled: false)
push_frontend_feature_flag(:snippets_edit_vue, default_enabled: true)
push_frontend_feature_flag(:webperf_experiment, default_enabled: false)
push_frontend_feature_flag(:snippets_binary_blob, default_enabled: false)

View File

@ -10374,6 +10374,9 @@ msgstr ""
msgid "Expand all"
msgstr ""
msgid "Expand all files"
msgstr ""
msgid "Expand approvers"
msgstr ""
@ -11216,6 +11219,9 @@ msgstr ""
msgid "Footer message"
msgstr ""
msgid "For a faster browsing experience, some files are collapsed by default."
msgstr ""
msgid "For help setting up the Service Desk for your instance, please contact an administrator."
msgstr ""
@ -15385,6 +15391,9 @@ msgstr ""
msgid "Max seats used"
msgstr ""
msgid "Max size 15 MB"
msgstr ""
msgid "Maximum Users:"
msgstr ""
@ -21456,6 +21465,9 @@ msgstr ""
msgid "Request parameter %{param} is missing."
msgstr ""
msgid "Request review from"
msgstr ""
msgid "Request to link SAML account must be authorized"
msgstr ""
@ -21726,6 +21738,9 @@ msgstr ""
msgid "ReviewApp|Enable Review App"
msgstr ""
msgid "Reviewer"
msgstr ""
msgid "Reviewing"
msgstr ""
@ -23559,6 +23574,9 @@ msgstr ""
msgid "Solution"
msgstr ""
msgid "Some changes are not shown"
msgstr ""
msgid "Some child epics may be hidden due to applied filters"
msgstr ""

View File

@ -58,5 +58,9 @@ FactoryBot.define do
started
status { 'skipped' }
end
trait :strategy_depend do
options { { trigger: { strategy: 'depend' } } }
end
end
end

View File

@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Projects > Snippets > Create Snippet', :js do
include DropzoneHelper
include Spec::Support::Helpers::Features::SnippetSpecHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) do
@ -16,96 +17,115 @@ RSpec.describe 'Projects > Snippets > Create Snippet', :js do
let(:file_content) { 'Hello World!' }
let(:md_description) { 'My Snippet **Description**' }
let(:description) { 'My Snippet Description' }
let(:snippet_title_field) { 'project_snippet_title' }
before do
stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
sign_in(user)
visit new_project_snippet_path(project)
end
def description_field
find('.js-description-input').find('input,textarea')
end
def fill_form
fill_in 'project_snippet_title', with: title
# Click placeholder first to expand full description field
description_field.click
fill_in 'project_snippet_description', with: md_description
page.within('.file-editor') do
el = find('.inputarea')
el.send_keys file_content
shared_examples 'snippet creation' do
def fill_form
snippet_fill_in_form(title: title, content: file_content, description: md_description)
end
end
it 'shows collapsible description input' do
collapsed = description_field
it 'shows collapsible description input' do
collapsed = description_field
expect(page).not_to have_field('project_snippet_description')
expect(collapsed).to be_visible
expect(page).not_to have_field(snippet_description_field)
expect(collapsed).to be_visible
collapsed.click
collapsed.click
expect(page).to have_field('project_snippet_description')
expect(collapsed).not_to be_visible
end
it 'creates a new snippet' do
fill_form
click_button('Create snippet')
wait_for_requests
expect(page).to have_content(title)
expect(page).to have_content(file_content)
page.within('.snippet-header .description') do
expect(page).to have_content(description)
expect(page).to have_selector('strong')
expect(page).to have_field(snippet_description_field)
expect(collapsed).not_to be_visible
end
end
it 'uploads a file when dragging into textarea' do
fill_form
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
expect(page.find_field('project_snippet_description').value).to have_content('banana_sample')
click_button('Create snippet')
wait_for_requests
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
expect(link).to match(%r{/#{Regexp.escape(project.full_path)}/uploads/\h{32}/banana_sample\.gif\z})
end
it 'displays validation errors' do
fill_in 'project_snippet_title', with: title
click_button('Create snippet')
wait_for_requests
expect(page).to have_selector('#error_explanation')
end
context 'when the git operation fails' do
let(:error) { 'Error creating the snippet' }
before do
allow_next_instance_of(Snippets::CreateService) do |instance|
allow(instance).to receive(:create_commit).and_raise(StandardError, error)
end
it 'creates a new snippet' do
fill_form
click_button('Create snippet')
wait_for_requests
expect(page).to have_content(title)
expect(page).to have_content(file_content)
page.within(snippet_description_view_selector) do
expect(page).to have_content(description)
expect(page).to have_selector('strong')
end
end
it 'uploads a file when dragging into textarea' do
fill_form
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
expect(snippet_description_value).to have_content('banana_sample')
click_button('Create snippet')
wait_for_requests
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
expect(link).to match(%r{/#{Regexp.escape(project.full_path)}/uploads/\h{32}/banana_sample\.gif\z})
end
it 'renders the new page and displays the error' do
expect(page).to have_content(error)
expect(page).to have_content('New Snippet')
context 'when the git operation fails' do
let(:error) { 'Error creating the snippet' }
before do
allow_next_instance_of(Snippets::CreateService) do |instance|
allow(instance).to receive(:create_commit).and_raise(StandardError, error)
end
fill_form
click_button('Create snippet')
wait_for_requests
end
it 'renders the new page and displays the error' do
expect(page).to have_content(error)
expect(page).to have_content('New Snippet')
end
end
end
context 'Vue application' do
let(:snippet_description_field) { 'snippet-description' }
let(:snippet_description_view_selector) { '.snippet-header .snippet-description' }
before do
sign_in(user)
visit new_project_snippet_path(project)
end
it_behaves_like 'snippet creation'
it 'does not allow submitting the form without title and content' do
fill_in snippet_title_field, with: title
expect(page).not_to have_button('Create snippet')
snippet_fill_in_form(title: title, content: file_content)
expect(page).to have_button('Create snippet')
end
end
context 'non-Vue application' do
let(:snippet_description_field) { 'project_snippet_description' }
let(:snippet_description_view_selector) { '.snippet-header .description' }
before do
stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
sign_in(user)
visit new_project_snippet_path(project)
end
it_behaves_like 'snippet creation'
it 'displays validation errors' do
fill_in snippet_title_field, with: title
click_button('Create snippet')
wait_for_requests
expect(page).to have_selector('#error_explanation')
end
end
end

View File

@ -3,57 +3,81 @@
require 'spec_helper'
RSpec.describe 'Projects > Snippets > User updates a snippet', :js do
include Spec::Support::Helpers::Features::SnippetSpecHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let_it_be(:snippet, reload: true) { create(:project_snippet, :repository, project: project, author: user) }
before do
stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
let(:snippet_title_field) { 'project_snippet_title' }
def bootstrap_snippet
project.add_maintainer(user)
sign_in(user)
visit(project_snippet_path(project, snippet))
page.visit(edit_project_snippet_path(project, snippet))
page.within('.detail-page-header') do
first(:link, 'Edit').click
end
wait_for_all_requests
end
it 'displays the snippet blob path and content' do
blob = snippet.blobs.first
shared_examples 'snippet update' do
it 'displays the snippet blob path and content' do
blob = snippet.blobs.first
aggregate_failures do
expect(page.find_field('project_snippet_file_name').value).to eq blob.path
expect(page.find('.file-content')).to have_content(blob.data.strip)
expect(page.find('.snippet-file-content', visible: false).value).to eq blob.data
aggregate_failures do
expect(snippet_get_first_blob_path).to eq blob.path
expect(snippet_get_first_blob_value).to have_content(blob.data.strip)
end
end
end
it 'updates a snippet' do
fill_in('project_snippet_title', with: 'Snippet new title')
click_button('Save')
it 'updates a snippet' do
fill_in('project_snippet_title', with: 'Snippet new title')
click_button('Save')
expect(page).to have_content('Snippet new title')
end
expect(page).to have_content('Snippet new title')
end
context 'when the git operation fails' do
before do
allow_next_instance_of(Snippets::UpdateService) do |instance|
allow(instance).to receive(:create_commit).and_raise(StandardError, 'Error Message')
context 'when the git operation fails' do
before do
allow_next_instance_of(Snippets::UpdateService) do |instance|
allow(instance).to receive(:create_commit).and_raise(StandardError, 'Error Message')
end
fill_in(snippet_title_field, with: 'Snippet new title')
fill_in(snippet_blob_path_field, match: :first, with: 'new_file_name')
click_button('Save')
end
fill_in('project_snippet_title', with: 'Snippet new title')
fill_in('project_snippet_file_name', with: 'new_file_name')
it 'renders edit page and displays the error' do
expect(page.find('.flash-container')).to have_content('Error updating the snippet - Error Message')
expect(page).to have_content('Edit Snippet')
end
end
end
click_button('Save')
context 'Vue application' do
before do
bootstrap_snippet
end
it 'renders edit page and displays the error' do
expect(page.find('.flash-container span').text).to eq('Error updating the snippet - Error Message')
expect(page).to have_content('Edit Snippet')
it_behaves_like 'snippet update' do
let(:snippet_blob_path_field) { 'snippet_file_name' }
let(:snippet_blob_content_selector) { '.file-content' }
end
end
context 'non-Vue application' do
before do
stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
bootstrap_snippet
end
it_behaves_like 'snippet update' do
let(:snippet_blob_path_field) { 'project_snippet_file_name' }
let(:snippet_blob_content_selector) { '.file-content' }
end
end
end

View File

@ -11,8 +11,6 @@ RSpec.shared_examples_for 'snippet editor' do
before do
stub_feature_flags(allow_possible_spam: false)
stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
Gitlab::CurrentSettings.update!(
@ -125,5 +123,20 @@ end
RSpec.describe 'User creates snippet', :js do
let_it_be(:user) { create(:user) }
it_behaves_like "snippet editor"
context 'Vue application' do
before do
stub_feature_flags(snippets_edit_vue: false)
end
it_behaves_like "snippet editor"
end
context 'non-Vue application' do
before do
stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
end
it_behaves_like "snippet editor"
end
end

View File

@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'User creates snippet', :js do
include DropzoneHelper
include Spec::Support::Helpers::Features::SnippetSpecHelpers
let_it_be(:user) { create(:user) }
@ -12,149 +13,163 @@ RSpec.describe 'User creates snippet', :js do
let(:md_description) { 'My Snippet **Description**' }
let(:description) { 'My Snippet Description' }
let(:created_snippet) { Snippet.last }
before do
stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
sign_in(user)
end
let(:snippet_title_field) { 'personal_snippet_title' }
def description_field
find('.js-description-input').find('input,textarea')
end
def fill_form
fill_in 'personal_snippet_title', with: title
# Click placeholder first to expand full description field
description_field.click
fill_in 'personal_snippet_description', with: md_description
page.within('.file-editor') do
el = find('.inputarea')
el.send_keys file_content
shared_examples 'snippet creation' do
def fill_form
snippet_fill_in_form(title: title, content: file_content, description: md_description)
end
end
it 'Authenticated user creates a snippet' do
visit new_snippet_path
it 'Authenticated user creates a snippet' do
fill_form
fill_form
click_button('Create snippet')
wait_for_requests
click_button('Create snippet')
wait_for_requests
expect(page).to have_content(title)
page.within('.snippet-header .description') do
expect(page).to have_content(description)
expect(page).to have_selector('strong')
expect(page).to have_content(title)
page.within(snippet_description_view_selector) do
expect(page).to have_content(description)
expect(page).to have_selector('strong')
end
expect(page).to have_content(file_content)
end
expect(page).to have_content(file_content)
end
it 'previews a snippet with file' do
visit new_snippet_path
it 'uploads a file when dragging into textarea' do
fill_form
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
# Click placeholder first to expand full description field
description_field.click
fill_in 'personal_snippet_description', with: 'My Snippet'
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
find('.js-md-preview-button').click
expect(snippet_description_value).to have_content('banana_sample')
page.within('#new_personal_snippet .md-preview-holder') do
expect(page).to have_content('My Snippet')
click_button('Create snippet')
wait_for_requests
link = find('a.no-attachment-icon img.js-lazy-loaded[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/user/#{user.id}/\h{32}/banana_sample\.gif\z})
expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z})
# Adds a cache buster for checking if the image exists as Selenium is now handling the cached requests
# not anymore as requests when they come straight from memory cache.
reqs = inspect_requests { visit("#{link}?ran=#{SecureRandom.base64(20)}") }
expect(reqs.first.status_code).to eq(200)
end
end
it 'uploads a file when dragging into textarea' do
visit new_snippet_path
context 'when the git operation fails' do
let(:error) { 'Error creating the snippet' }
fill_form
before do
allow_next_instance_of(Snippets::CreateService) do |instance|
allow(instance).to receive(:create_commit).and_raise(StandardError, error)
end
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
expect(page.find_field("personal_snippet_description").value).to have_content('banana_sample')
click_button('Create snippet')
wait_for_requests
link = find('a.no-attachment-icon img.js-lazy-loaded[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z})
reqs = inspect_requests { visit("#{link}?ran=#{SecureRandom.base64(20)}") }
expect(reqs.first.status_code).to eq(200)
end
context 'when the git operation fails' do
let(:error) { 'Error creating the snippet' }
before do
allow_next_instance_of(Snippets::CreateService) do |instance|
allow(instance).to receive(:create_commit).and_raise(StandardError, error)
fill_form
click_button('Create snippet')
wait_for_requests
end
visit new_snippet_path
it 'renders the new page and displays the error' do
expect(page).to have_content(error)
expect(page).to have_content('New Snippet')
fill_form
click_button('Create snippet')
wait_for_requests
action = find('form.snippet-form')['action']
expect(action).to include("/snippets")
end
end
it 'renders the new page and displays the error' do
expect(page).to have_content(error)
expect(page).to have_content('New Snippet')
context 'when snippets default visibility level is restricted' do
before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE],
default_snippet_visibility: Gitlab::VisibilityLevel::PRIVATE)
end
action = find('form.snippet-form')['action']
expect(action).to match(%r{/snippets\z})
it 'creates a snippet using the lowest available visibility level as default' do
visit new_snippet_path
fill_form
click_button('Create snippet')
wait_for_requests
expect(find('.blob-content')).to have_content(file_content)
expect(Snippet.last.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
end
it_behaves_like 'personal snippet with references' do
let(:container) { snippet_description_view_selector }
let(:md_description) { references }
subject do
fill_form
click_button('Create snippet')
wait_for_requests
end
end
end
it 'validation fails for the first time' do
visit new_snippet_path
context 'Vue application' do
let(:snippet_description_field) { 'snippet-description' }
let(:snippet_description_view_selector) { '.snippet-header .snippet-description' }
fill_in 'personal_snippet_title', with: title
click_button('Create snippet')
expect(page).to have_selector('#error_explanation')
end
context 'when snippets default visibility level is restricted' do
before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE],
default_snippet_visibility: Gitlab::VisibilityLevel::PRIVATE)
sign_in(user)
visit new_snippet_path
end
it 'creates a snippet using the lowest available visibility level as default' do
visit new_snippet_path
it_behaves_like 'snippet creation'
fill_form
it 'validation fails for the first time' do
fill_in snippet_title_field, with: title
click_button('Create snippet')
wait_for_requests
expect(page).not_to have_button('Create snippet')
expect(created_snippet.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
snippet_fill_in_form(title: title, content: file_content)
expect(page).to have_button('Create snippet')
end
end
it_behaves_like 'personal snippet with references' do
let(:container) { '.snippet-header .description' }
let(:md_description) { references }
context 'non-Vue application' do
let(:snippet_description_field) { 'personal_snippet_description' }
let(:snippet_description_view_selector) { '.snippet-header .description' }
before do
stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
sign_in(user)
subject do
visit new_snippet_path
fill_form
end
it_behaves_like 'snippet creation'
it 'validation fails for the first time' do
fill_in snippet_title_field, with: title
click_button('Create snippet')
wait_for_requests
expect(page).to have_selector('#error_explanation')
end
it 'previews a snippet with file' do
# Click placeholder first to expand full description field
description_field.click
fill_in snippet_description_field, with: 'My Snippet'
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
find('.js-md-preview-button').click
page.within('.md-preview-holder') do
expect(page).to have_content('My Snippet')
link = find('a.no-attachment-icon img.js-lazy-loaded[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/user/#{user.id}/\h{32}/banana_sample\.gif\z})
# Adds a cache buster for checking if the image exists as Selenium is now handling the cached requests
# not anymore as requests when they come straight from memory cache.
reqs = inspect_requests { visit("#{link}?ran=#{SecureRandom.base64(20)}") }
expect(reqs.first.status_code).to eq(200)
end
end
end
end

View File

@ -4,87 +4,114 @@ require 'spec_helper'
RSpec.describe 'User edits snippet', :js do
include DropzoneHelper
include Spec::Support::Helpers::Features::SnippetSpecHelpers
let_it_be(:file_name) { 'test.rb' }
let_it_be(:content) { 'puts "test"' }
let_it_be(:user) { create(:user) }
let_it_be(:snippet, reload: true) { create(:personal_snippet, :repository, :public, file_name: file_name, content: content, author: user) }
before do
stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
let(:snippet_title_field) { 'personal_snippet_title' }
sign_in(user)
shared_examples 'snippet editing' do
it 'displays the snippet blob path and content' do
blob = snippet.blobs.first
visit edit_snippet_path(snippet)
wait_for_all_requests
end
it 'displays the snippet blob path and content' do
blob = snippet.blobs.first
aggregate_failures do
expect(page.find_field('personal_snippet_file_name').value).to eq blob.path
expect(page.find('.file-content')).to have_content(blob.data.strip)
expect(page.find('.snippet-file-content', visible: false).value).to eq blob.data
end
end
it 'updates the snippet' do
fill_in 'personal_snippet_title', with: 'New Snippet Title'
click_button('Save changes')
wait_for_requests
expect(page).to have_content('New Snippet Title')
end
it 'updates the snippet with files attached' do
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
expect(page.find_field('personal_snippet_description').value).to have_content('banana_sample')
click_button('Save changes')
wait_for_requests
link = find('a.no-attachment-icon img:not(.lazy)[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/personal_snippet/#{snippet.id}/\h{32}/banana_sample\.gif\z})
end
it 'updates the snippet to make it internal' do
choose 'Internal'
click_button 'Save changes'
wait_for_requests
expect(page).to have_no_selector('[data-testid="lock-icon"]')
expect(page).to have_selector('[data-testid="shield-icon"]')
end
it 'updates the snippet to make it public' do
choose 'Public'
click_button 'Save changes'
wait_for_requests
expect(page).to have_no_selector('[data-testid="lock-icon"]')
expect(page).to have_selector('[data-testid="earth-icon"]')
end
context 'when the git operation fails' do
before do
allow_next_instance_of(Snippets::UpdateService) do |instance|
allow(instance).to receive(:create_commit).and_raise(StandardError, 'Error Message')
aggregate_failures do
expect(snippet_get_first_blob_path).to eq blob.path
expect(snippet_get_first_blob_value).to have_content(blob.data.strip)
end
end
fill_in 'personal_snippet_title', with: 'New Snippet Title'
fill_in 'personal_snippet_file_name', with: 'new_file_name'
it 'updates the snippet' do
fill_in snippet_title_field, with: 'New Snippet Title'
click_button('Save changes')
wait_for_requests
expect(page).to have_content('New Snippet Title')
end
it 'renders edit page and displays the error' do
expect(page.find('.flash-container span').text).to eq('Error updating the snippet - Error Message')
expect(page).to have_content('Edit Snippet')
it 'updates the snippet with files attached' do
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
expect(snippet_description_value).to have_content('banana_sample')
click_button('Save changes')
wait_for_requests
link = find('a.no-attachment-icon img:not(.lazy)[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/-/system/personal_snippet/#{snippet.id}/\h{32}/banana_sample\.gif\z})
end
it 'updates the snippet to make it internal' do
choose 'Internal'
click_button 'Save changes'
wait_for_requests
expect(page).to have_no_selector('[data-testid="lock-icon"]')
expect(page).to have_selector('[data-testid="shield-icon"]')
end
it 'updates the snippet to make it public' do
choose 'Public'
click_button 'Save changes'
wait_for_requests
expect(page).to have_no_selector('[data-testid="lock-icon"]')
expect(page).to have_selector('[data-testid="earth-icon"]')
end
context 'when the git operation fails' do
before do
allow_next_instance_of(Snippets::UpdateService) do |instance|
allow(instance).to receive(:create_commit).and_raise(StandardError, 'Error Message')
end
fill_in snippet_title_field, with: 'New Snippet Title'
fill_in snippet_blob_path_field, with: 'new_file_name', match: :first
click_button('Save changes')
end
it 'renders edit page and displays the error' do
expect(page.find('.flash-container')).to have_content('Error updating the snippet - Error Message')
expect(page).to have_content('Edit Snippet')
end
end
end
context 'Vue application' do
it_behaves_like 'snippet editing' do
let(:snippet_blob_path_field) { 'snippet_file_name' }
let(:snippet_blob_content_selector) { '.file-content' }
let(:snippet_description_field) { 'snippet-description' }
before do
sign_in(user)
visit edit_snippet_path(snippet)
wait_for_all_requests
end
end
end
context 'non-Vue application' do
it_behaves_like 'snippet editing' do
let(:snippet_blob_path_field) { 'personal_snippet_file_name' }
let(:snippet_blob_content_selector) { '.file-content' }
let(:snippet_description_field) { 'personal_snippet_description' }
before do
stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
sign_in(user)
visit edit_snippet_path(snippet)
wait_for_all_requests
end
end
end
end

View File

@ -9,6 +9,7 @@ import NoChanges from '~/diffs/components/no_changes.vue';
import DiffFile from '~/diffs/components/diff_file.vue';
import CompareVersions from '~/diffs/components/compare_versions.vue';
import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
import CommitWidget from '~/diffs/components/commit_widget.vue';
import TreeList from '~/diffs/components/tree_list.vue';
import { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '~/diffs/constants';
@ -22,6 +23,10 @@ const TEST_ENDPOINT = `${TEST_HOST}/diff/endpoint`;
const COMMIT_URL = '[BASE URL]/OLD';
const UPDATED_COMMIT_URL = '[BASE URL]/NEW';
function getCollapsedFilesWarning(wrapper) {
return wrapper.find(CollapsedFilesWarning);
}
describe('diffs/components/app', () => {
const oldMrTabs = window.mrTabs;
let store;
@ -668,24 +673,51 @@ describe('diffs/components/app', () => {
);
});
it('should render hidden files warning if render overflow warning is present', () => {
createComponent({}, ({ state }) => {
state.diffs.renderOverflowWarning = true;
state.diffs.realSize = '5';
state.diffs.plainDiffPath = 'plain diff path';
state.diffs.emailPatchPath = 'email patch path';
state.diffs.size = 1;
describe('warnings', () => {
describe('hidden files', () => {
it('should render hidden files warning if render overflow warning is present', () => {
createComponent({}, ({ state }) => {
state.diffs.renderOverflowWarning = true;
state.diffs.realSize = '5';
state.diffs.plainDiffPath = 'plain diff path';
state.diffs.emailPatchPath = 'email patch path';
state.diffs.size = 1;
});
expect(wrapper.find(HiddenFilesWarning).exists()).toBe(true);
expect(wrapper.find(HiddenFilesWarning).props()).toEqual(
expect.objectContaining({
total: '5',
plainDiffPath: 'plain diff path',
emailPatchPath: 'email patch path',
visible: 1,
}),
);
});
});
expect(wrapper.find(HiddenFilesWarning).exists()).toBe(true);
expect(wrapper.find(HiddenFilesWarning).props()).toEqual(
expect.objectContaining({
total: '5',
plainDiffPath: 'plain diff path',
emailPatchPath: 'email patch path',
visible: 1,
}),
);
describe('collapsed files', () => {
it('should render the collapsed files warning if there are any collapsed files', () => {
createComponent({}, ({ state }) => {
state.diffs.diffFiles = [{ viewer: { collapsed: true } }];
});
expect(getCollapsedFilesWarning(wrapper).exists()).toBe(true);
});
it('should not render the collapsed files warning if the user has dismissed the alert already', async () => {
createComponent({}, ({ state }) => {
state.diffs.diffFiles = [{ viewer: { collapsed: true } }];
});
expect(getCollapsedFilesWarning(wrapper).exists()).toBe(true);
wrapper.vm.collapsedWarningDismissed = true;
await wrapper.vm.$nextTick();
expect(getCollapsedFilesWarning(wrapper).exists()).toBe(false);
});
});
});
it('should display commit widget if store has a commit', () => {

View File

@ -0,0 +1,88 @@
import Vuex from 'vuex';
import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
import createStore from '~/diffs/store/modules';
import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
import { CENTERED_LIMITED_CONTAINER_CLASSES } from '~/diffs/constants';
const propsData = {
limited: true,
mergeable: true,
resolutionPath: 'a-path',
};
const limitedClasses = CENTERED_LIMITED_CONTAINER_CLASSES.split(' ');
describe('CollapsedFilesWarning', () => {
const localVue = createLocalVue();
let store;
let wrapper;
localVue.use(Vuex);
const getAlertActionButton = () =>
wrapper.find(CollapsedFilesWarning).find('button.gl-alert-action:first-child');
const getAlertCloseButton = () => wrapper.find(CollapsedFilesWarning).find('button');
const createComponent = (props = {}, { full } = { full: false }) => {
const mounter = full ? mount : shallowMount;
store = new Vuex.Store({
modules: {
diffs: createStore(),
},
});
wrapper = mounter(CollapsedFilesWarning, {
propsData: { ...propsData, ...props },
localVue,
store,
});
};
afterEach(() => {
wrapper.destroy();
});
it.each`
limited | containerClasses
${true} | ${limitedClasses}
${false} | ${[]}
`(
'has the correct container classes when limited is $limited',
({ limited, containerClasses }) => {
createComponent({ limited });
expect(wrapper.classes()).toEqual(containerClasses);
},
);
it.each`
present | dismissed
${false} | ${true}
${true} | ${false}
`('toggles the alert when dismissed is $dismissed', ({ present, dismissed }) => {
createComponent({ dismissed });
expect(wrapper.find('[data-testid="root"]').exists()).toBe(present);
});
it('dismisses the component when the alert "x" is clicked', async () => {
createComponent({}, { full: true });
expect(wrapper.find('[data-testid="root"]').exists()).toBe(true);
getAlertCloseButton().element.click();
await wrapper.vm.$nextTick();
expect(wrapper.find('[data-testid="root"]').exists()).toBe(false);
});
it('triggers the expandAllFiles action when the alert action button is clicked', () => {
createComponent({}, { full: true });
jest.spyOn(wrapper.vm.$store, 'dispatch').mockReturnValue(undefined);
getAlertActionButton().vm.$emit('click');
expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('diffs/expandAllFiles', undefined);
});
});

View File

@ -215,7 +215,7 @@ describe('DiffFile', () => {
it('calls handleLoadCollapsedDiff if collapsed changed & file has no lines', done => {
jest.spyOn(vm, 'handleLoadCollapsedDiff').mockImplementation(() => {});
vm.file.highlighted_diff_lines = undefined;
vm.file.highlighted_diff_lines = [];
vm.file.parallel_diff_lines = [];
vm.isCollapsed = true;
@ -242,8 +242,8 @@ describe('DiffFile', () => {
jest.spyOn(vm, 'handleLoadCollapsedDiff').mockImplementation(() => {});
vm.file.highlighted_diff_lines = undefined;
vm.file.parallel_diff_lines = [];
vm.file.highlighted_diff_lines = [];
vm.file.parallel_diff_lines = undefined;
vm.isCollapsed = true;
vm.$nextTick()

View File

@ -306,6 +306,38 @@ RSpec.describe IssuablesHelper do
end
end
describe '#reviewer_sidebar_data' do
let(:user) { create(:user) }
subject { helper.reviewer_sidebar_data(user, merge_request: merge_request) }
context 'without merge_request' do
let(:merge_request) { nil }
it 'returns hash of reviewer data' do
is_expected.to eql({
avatar_url: user.avatar_url,
name: user.name,
username: user.username
})
end
end
context 'with merge_request' do
let(:merge_request) { build(:merge_request) }
where(can_merge: [true, false])
with_them do
before do
allow(merge_request).to receive(:can_be_merged_by?).and_return(can_merge)
end
it { is_expected.to include({ can_merge: can_merge })}
end
end
end
describe '#issuable_squash_option?' do
using RSpec::Parameterized::TableSyntax

View File

@ -482,6 +482,8 @@ project:
- dast_site_profiles
- dast_scanner_profiles
- dast_sites
- dast_site_tokens
- dast_site_validations
- operations_feature_flags
- operations_feature_flags_client
- operations_feature_flags_user_lists
@ -689,6 +691,7 @@ epic:
- due_date_sourcing_epic
- events
- resource_label_events
- resource_state_events
- user_mentions
- note_authors
- boards_epic_user_preferences

View File

@ -1360,34 +1360,126 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
context 'when pipeline is bridge triggered' do
before do
pipeline.source_bridge = create(:ci_bridge)
end
def auto_devops_pipelines_completed_total(status)
Gitlab::Metrics.counter(:auto_devops_pipelines_completed_total, 'Number of completed auto devops pipelines').get(status: status)
end
end
describe 'bridge triggered pipeline' do
shared_examples 'upstream downstream pipeline' do
let!(:source_pipeline) { create(:ci_sources_pipeline, pipeline: downstream_pipeline, source_job: bridge) }
let!(:job) { downstream_pipeline.builds.first }
context 'when source bridge is dependent on pipeline status' do
before do
allow(pipeline.source_bridge).to receive(:dependent?).and_return(true)
end
let!(:bridge) { create(:ci_bridge, :strategy_depend, pipeline: upstream_pipeline) }
it 'schedules the pipeline bridge worker' do
expect(::Ci::PipelineBridgeStatusWorker).to receive(:perform_async)
expect(::Ci::PipelineBridgeStatusWorker).to receive(:perform_async).with(downstream_pipeline.id)
pipeline.succeed!
downstream_pipeline.succeed!
end
context 'when the downstream pipeline first fails then retries and succeeds' do
it 'makes the upstream pipeline successful' do
Sidekiq::Testing.inline! { job.drop! }
expect(downstream_pipeline.reload).to be_failed
expect(upstream_pipeline.reload).to be_failed
Sidekiq::Testing.inline! do
new_job = Ci::Build.retry(job, project.users.first)
expect(downstream_pipeline.reload).to be_running
expect(upstream_pipeline.reload).to be_running
new_job.success!
end
expect(downstream_pipeline.reload).to be_success
expect(upstream_pipeline.reload).to be_success
end
end
context 'when the downstream pipeline first succeeds then retries and fails' do
it 'makes the upstream pipeline failed' do
Sidekiq::Testing.inline! { job.success! }
expect(downstream_pipeline.reload).to be_success
expect(upstream_pipeline.reload).to be_success
Sidekiq::Testing.inline! do
new_job = Ci::Build.retry(job, project.users.first)
expect(downstream_pipeline.reload).to be_running
expect(upstream_pipeline.reload).to be_running
new_job.drop!
end
expect(downstream_pipeline.reload).to be_failed
expect(upstream_pipeline.reload).to be_failed
end
end
context 'when the upstream pipeline has another dependent upstream pipeline' do
let!(:upstream_of_upstream_pipeline) { create(:ci_pipeline) }
before do
upstream_bridge = create(:ci_bridge, :strategy_depend, pipeline: upstream_of_upstream_pipeline)
create(:ci_sources_pipeline, pipeline: upstream_pipeline,
source_job: upstream_bridge)
end
context 'when the downstream pipeline first fails then retries and succeeds' do
it 'makes upstream pipelines successful' do
Sidekiq::Testing.inline! { job.drop! }
expect(downstream_pipeline.reload).to be_failed
expect(upstream_pipeline.reload).to be_failed
expect(upstream_of_upstream_pipeline.reload).to be_failed
Sidekiq::Testing.inline! do
new_job = Ci::Build.retry(job, project.users.first)
expect(downstream_pipeline.reload).to be_running
expect(upstream_pipeline.reload).to be_running
expect(upstream_of_upstream_pipeline.reload).to be_running
new_job.success!
end
expect(downstream_pipeline.reload).to be_success
expect(upstream_pipeline.reload).to be_success
expect(upstream_of_upstream_pipeline.reload).to be_success
end
end
end
end
context 'when source bridge is not dependent on pipeline status' do
let!(:bridge) { create(:ci_bridge, pipeline: upstream_pipeline) }
it 'does not schedule the pipeline bridge worker' do
expect(::Ci::PipelineBridgeStatusWorker).not_to receive(:perform_async)
pipeline.succeed!
downstream_pipeline.succeed!
end
end
end
def auto_devops_pipelines_completed_total(status)
Gitlab::Metrics.counter(:auto_devops_pipelines_completed_total, 'Number of completed auto devops pipelines').get(status: status)
context 'multi-project pipelines' do
let!(:downstream_project) { create(:project, :repository) }
let!(:upstream_pipeline) { create(:ci_pipeline, project: project) }
let!(:downstream_pipeline) { create(:ci_pipeline, :with_job, project: downstream_project) }
it_behaves_like 'upstream downstream pipeline'
end
context 'parent-child pipelines' do
let!(:upstream_pipeline) { create(:ci_pipeline, project: project) }
let!(:downstream_pipeline) { create(:ci_pipeline, :with_job, project: project) }
it_behaves_like 'upstream downstream pipeline'
end
end
@ -3611,4 +3703,65 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
end
describe 'reset_ancestor_bridges!' do
context 'when the pipeline is a child pipeline and the bridge is depended' do
let!(:parent_pipeline) { create(:ci_pipeline, project: project) }
let!(:bridge) { create_bridge(parent_pipeline, pipeline, true) }
it 'marks source bridge as pending' do
pipeline.reset_ancestor_bridges!
expect(bridge.reload).to be_pending
end
context 'when the parent pipeline has a dependent upstream pipeline' do
let!(:upstream_bridge) do
create_bridge(create(:ci_pipeline, project: create(:project)), parent_pipeline, true)
end
it 'marks all source bridges as pending' do
pipeline.reset_ancestor_bridges!
expect(bridge.reload).to be_pending
expect(upstream_bridge.reload).to be_pending
end
end
end
context 'when the pipeline is a child pipeline and the bridge is not depended' do
let!(:parent_pipeline) { create(:ci_pipeline, project: project) }
let!(:bridge) { create_bridge(parent_pipeline, pipeline, false) }
it 'does not touch source bridge' do
pipeline.reset_ancestor_bridges!
expect(bridge.reload).to be_success
end
context 'when the parent pipeline has a dependent upstream pipeline' do
let!(:upstream_bridge) do
create_bridge(create(:ci_pipeline, project: create(:project)), parent_pipeline, true)
end
it 'does not touch any source bridge' do
pipeline.reset_ancestor_bridges!
expect(bridge.reload).to be_success
expect(upstream_bridge.reload).to be_success
end
end
end
private
def create_bridge(upstream, downstream, depend = false)
options = depend ? { trigger: { strategy: 'depend' } } : {}
bridge = create(:ci_bridge, pipeline: upstream, status: 'success', options: options)
create(:ci_sources_pipeline, pipeline: downstream, source_job: bridge)
bridge
end
end
end

View File

@ -11,4 +11,32 @@ RSpec.describe ResourceStateEvent, type: :model do
it_behaves_like 'a resource event'
it_behaves_like 'a resource event for issues'
it_behaves_like 'a resource event for merge requests'
describe 'validations' do
describe 'Issuable validation' do
it 'is valid if an issue is set' do
subject.attributes = { issue: build_stubbed(:issue), merge_request: nil }
expect(subject).to be_valid
end
it 'is valid if a merge request is set' do
subject.attributes = { issue: nil, merge_request: build_stubbed(:merge_request) }
expect(subject).to be_valid
end
it 'is invalid if both issue and merge request are set' do
subject.attributes = { issue: build_stubbed(:issue), merge_request: build_stubbed(:merge_request) }
expect(subject).not_to be_valid
end
it 'is invalid if there is no issuable set' do
subject.attributes = { issue: nil, merge_request: nil }
expect(subject).not_to be_valid
end
end
end
end

View File

@ -232,6 +232,19 @@ RSpec.describe Ci::RetryBuildService do
end
end
end
context 'when the pipeline is a child pipeline and the bridge is depended' do
let!(:parent_pipeline) { create(:ci_pipeline, project: project) }
let!(:pipeline) { create(:ci_pipeline, project: project) }
let!(:bridge) { create(:ci_bridge, :strategy_depend, pipeline: parent_pipeline, status: 'success') }
let!(:source_pipeline) { create(:ci_sources_pipeline, pipeline: pipeline, source_job: bridge) }
it 'marks source bridge as pending' do
service.execute(build)
expect(bridge.reload).to be_pending
end
end
end
context 'when user does not have ability to execute build' do

View File

@ -280,6 +280,20 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
expect(build3.reload.scheduling_type).to eq('dag')
end
end
context 'when the pipeline is a downstream pipeline and the bridge is depended' do
let!(:bridge) { create(:ci_bridge, :strategy_depend, status: 'success') }
before do
create(:ci_sources_pipeline, pipeline: pipeline, source_job: bridge)
end
it 'marks source bridge as pending' do
service.execute(pipeline)
expect(bridge.reload).to be_pending
end
end
end
context 'when user is not allowed to retry pipeline' do

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
# These helpers help you interact within the Editor Lite (single-file editor, snippets, etc.).
#
module Spec
module Support
module Helpers
module Features
module SnippetSpecHelpers
include ActionView::Helpers::JavaScriptHelper
include Spec::Support::Helpers::Features::EditorLiteSpecHelpers
def snippet_get_first_blob_path
page.find_field(snippet_blob_path_field, match: :first).value
end
def snippet_get_first_blob_value
page.find(snippet_blob_content_selector, match: :first)
end
def snippet_description_value
page.find_field(snippet_description_field).value
end
def snippet_fill_in_form(title:, content:, description: '')
# fill_in snippet_title_field, with: title
# editor_set_value(content)
fill_in snippet_title_field, with: title
if description
# Click placeholder first to expand full description field
description_field.click
fill_in snippet_description_field, with: description
end
page.within('.file-editor') do
el = find('.inputarea')
el.send_keys content
end
end
private
def description_field
find('.js-description-input').find('input,textarea')
end
end
end
end
end
end