Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a3ac132686
commit
01ef10900a
|
@ -189,7 +189,7 @@
|
|||
- "config.ru"
|
||||
# List explicitly all the app/ dirs that are backend (i.e. all except app/assets).
|
||||
- "{,ee/,jh/}{app/channels,app/controllers,app/finders,app/graphql,app/helpers,app/mailers,app/models,app/policies,app/presenters,app/serializers,app/services,app/uploaders,app/validators,app/views,app/workers}/**/*"
|
||||
- "{,ee/,jh/}{bin,cable,config,db,lib}/**/*"
|
||||
- "{,ee/,jh/}{bin,cable,config,db,generator_templates,lib}/**/*"
|
||||
- "{,ee/,jh/}spec/**/*.rb"
|
||||
# CI changes
|
||||
- ".gitlab-ci.yml"
|
||||
|
@ -239,7 +239,7 @@
|
|||
- "Rakefile"
|
||||
- "tests.yml"
|
||||
- "config.ru"
|
||||
- "{,ee/,jh/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
|
||||
- "{,ee/,jh/}{app,bin,config,db,generator_templates,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
|
||||
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
|
||||
- "data/whats_new/*.yml"
|
||||
|
||||
|
@ -264,7 +264,7 @@
|
|||
- "Rakefile"
|
||||
- "tests.yml"
|
||||
- "config.ru"
|
||||
- "{,ee/,jh/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
|
||||
- "{,ee/,jh/}{app,bin,config,db,generator_templates,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
|
||||
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
|
||||
- "data/whats_new/*.yml"
|
||||
# Backstage changes
|
||||
|
@ -292,7 +292,7 @@
|
|||
- "Rakefile"
|
||||
- "tests.yml"
|
||||
- "config.ru"
|
||||
- "{,ee/,jh/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
|
||||
- "{,ee/,jh/}{app,bin,config,db,generator_templates,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
|
||||
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
|
||||
- "data/whats_new/*.yml"
|
||||
# QA changes
|
||||
|
@ -316,7 +316,7 @@
|
|||
- "Rakefile"
|
||||
- "tests.yml"
|
||||
- "config.ru"
|
||||
- "{,ee/,jh/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
|
||||
- "{,ee/,jh/}{app,bin,config,db,generator_templates,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
|
||||
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
|
||||
- "data/whats_new/*.yml"
|
||||
# Backstage changes
|
||||
|
|
|
@ -1 +1 @@
|
|||
9cde7b7d1ecc68f5ca3b68df5ad32e6b0bc9d661
|
||||
ebabe4399c781ea1f4a1a43774b14489446f1f68
|
||||
|
|
|
@ -11,6 +11,7 @@ import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assig
|
|||
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
|
||||
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
|
||||
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
|
||||
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
||||
export default {
|
||||
|
@ -24,6 +25,7 @@ export default {
|
|||
BoardSidebarLabelsSelect,
|
||||
SidebarSubscriptionsWidget,
|
||||
SidebarDropdownWidget,
|
||||
SidebarTodoWidget,
|
||||
MountingPortal,
|
||||
SidebarWeightWidget: () =>
|
||||
import('ee_component/sidebar/components/weight/sidebar_weight_widget.vue'),
|
||||
|
@ -90,6 +92,15 @@ export default {
|
|||
<template #title>
|
||||
<h2 class="gl-my-0 gl-font-size-h2 gl-line-height-24">{{ __('Issue details') }}</h2>
|
||||
</template>
|
||||
<template #header>
|
||||
<sidebar-todo-widget
|
||||
class="gl-mt-3"
|
||||
:issuable-id="activeBoardItem.fullId"
|
||||
:issuable-iid="activeBoardItem.iid"
|
||||
:full-path="fullPath"
|
||||
:issuable-type="issuableType"
|
||||
/>
|
||||
</template>
|
||||
<template #default>
|
||||
<board-sidebar-title />
|
||||
<sidebar-assignees-widget
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
|
||||
import TodoButton from '~/vue_shared/components/sidebar/todo_button.vue';
|
||||
import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
|
||||
import createDesignTodoMutation from '../graphql/mutations/create_design_todo.mutation.graphql';
|
||||
import getDesignQuery from '../graphql/queries/get_design.query.graphql';
|
||||
import allVersionsMixin from '../mixins/all_versions';
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import $ from 'jquery';
|
||||
import Cookies from 'js-cookie';
|
||||
import { fixTitle, hide } from '~/tooltips';
|
||||
import { hide } from '~/tooltips';
|
||||
import createFlash from './flash';
|
||||
import axios from './lib/utils/axios_utils';
|
||||
import { sprintf, s__, __ } from './locale';
|
||||
|
@ -107,36 +107,6 @@ Sidebar.prototype.toggleTodo = function (e) {
|
|||
);
|
||||
};
|
||||
|
||||
Sidebar.prototype.todoUpdateDone = function (data) {
|
||||
const deletePath = data.delete_path ? data.delete_path : null;
|
||||
const attrPrefix = deletePath ? 'mark' : 'todo';
|
||||
const $todoBtns = $('.js-issuable-todo');
|
||||
|
||||
$(document).trigger('todo:toggle', data.count);
|
||||
|
||||
$todoBtns.each((i, el) => {
|
||||
const $el = $(el);
|
||||
const $elText = $el.find('.js-issuable-todo-inner');
|
||||
|
||||
$el
|
||||
.removeClass('is-loading')
|
||||
.enable()
|
||||
.attr('aria-label', $el.data(`${attrPrefix}Text`))
|
||||
.attr('title', $el.data(`${attrPrefix}Text`))
|
||||
.data('deletePath', deletePath);
|
||||
|
||||
if ($el.hasClass('has-tooltip')) {
|
||||
fixTitle(el);
|
||||
}
|
||||
|
||||
if (typeof $el.data('isCollapsed') !== 'undefined') {
|
||||
$elText.html($el.data(`${attrPrefix}Icon`));
|
||||
} else {
|
||||
$elText.text($el.data(`${attrPrefix}Text`));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Sidebar.prototype.sidebarCollapseClicked = function (e) {
|
||||
if ($(e.currentTarget).hasClass('dont-change-state')) {
|
||||
return;
|
||||
|
|
|
@ -1,18 +1,26 @@
|
|||
<script>
|
||||
import { GlTooltipDirective } from '@gitlab/ui';
|
||||
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { produce } from 'immer';
|
||||
import createFlash from '~/flash';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { todoQueries, TodoMutationTypes, todoMutations } from '~/sidebar/constants';
|
||||
import TodoButton from '~/vue_shared/components/sidebar/todo_button.vue';
|
||||
import { todoLabel } from '~/vue_shared/components/sidebar/todo_toggle//utils';
|
||||
import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlIcon,
|
||||
TodoButton,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
inject: {
|
||||
isClassicSidebar: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
issuableId: {
|
||||
type: String,
|
||||
|
@ -86,6 +94,12 @@ export default {
|
|||
}
|
||||
return TodoMutationTypes.Create;
|
||||
},
|
||||
collapsedButtonIcon() {
|
||||
return this.hasTodo ? 'todo-done' : 'todo-add';
|
||||
},
|
||||
tootltipTitle() {
|
||||
return todoLabel(this.hasTodo);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleTodo() {
|
||||
|
@ -158,7 +172,24 @@ export default {
|
|||
:is-todo="hasTodo"
|
||||
:loading="isLoading"
|
||||
size="small"
|
||||
class="hide-collapsed"
|
||||
@click.stop.prevent="toggleTodo"
|
||||
/>
|
||||
<gl-button
|
||||
v-if="isClassicSidebar"
|
||||
category="tertiary"
|
||||
type="reset"
|
||||
class="sidebar-collapsed-icon sidebar-collapsed-container gl-rounded-0! gl-shadow-none!"
|
||||
@click.stop.prevent="toggleTodo"
|
||||
>
|
||||
<gl-icon
|
||||
v-gl-tooltip.left.viewport
|
||||
:title="tootltipTitle"
|
||||
:size="16"
|
||||
:class="{ 'todo-undone': hasTodo }"
|
||||
:name="collapsedButtonIcon"
|
||||
:aria-label="collapsedButtonIcon"
|
||||
/>
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -13,10 +13,12 @@ import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
|
|||
import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
|
||||
import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql';
|
||||
import issueTimeTrackingQuery from '~/sidebar/queries/issue_time_tracking.query.graphql';
|
||||
import issueTodoQuery from '~/sidebar/queries/issue_todo.query.graphql';
|
||||
import mergeRequestMilestone from '~/sidebar/queries/merge_request_milestone.query.graphql';
|
||||
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
|
||||
import mergeRequestSubscribed from '~/sidebar/queries/merge_request_subscribed.query.graphql';
|
||||
import mergeRequestTimeTrackingQuery from '~/sidebar/queries/merge_request_time_tracking.query.graphql';
|
||||
import mergeRequestTodoQuery from '~/sidebar/queries/merge_request_todo.query.graphql';
|
||||
import todoCreateMutation from '~/sidebar/queries/todo_create.mutation.graphql';
|
||||
import todoMarkDoneMutation from '~/sidebar/queries/todo_mark_done.mutation.graphql';
|
||||
import updateEpicConfidentialMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
|
||||
|
@ -216,6 +218,12 @@ export const todoQueries = {
|
|||
[IssuableType.Epic]: {
|
||||
query: epicTodoQuery,
|
||||
},
|
||||
[IssuableType.Issue]: {
|
||||
query: issueTodoQuery,
|
||||
},
|
||||
[IssuableType.MergeRequest]: {
|
||||
query: mergeRequestTodoQuery,
|
||||
},
|
||||
};
|
||||
|
||||
export const TodoMutationTypes = {
|
||||
|
|
|
@ -2,6 +2,8 @@ import $ from 'jquery';
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createFlash from '~/flash';
|
||||
import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
|
||||
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
|
||||
import { IssuableType } from '~/issue_show/constants';
|
||||
|
@ -19,6 +21,7 @@ import SidebarDueDateWidget from '~/sidebar/components/date/sidebar_date_widget.
|
|||
import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue';
|
||||
import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue';
|
||||
import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue';
|
||||
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
|
||||
import { apolloProvider } from '~/sidebar/graphql';
|
||||
import trackShowInviteMemberLink from '~/sidebar/track_invite_members';
|
||||
import Translate from '../vue_shared/translate';
|
||||
|
@ -40,6 +43,40 @@ function getSidebarOptions(sidebarOptEl = document.querySelector('.js-sidebar-op
|
|||
return JSON.parse(sidebarOptEl.innerHTML);
|
||||
}
|
||||
|
||||
function mountSidebarToDoWidget() {
|
||||
const el = document.querySelector('.js-issuable-todo');
|
||||
|
||||
if (!el) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { projectPath, iid, id } = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
apolloProvider,
|
||||
components: {
|
||||
SidebarTodoWidget,
|
||||
},
|
||||
provide: {
|
||||
isClassicSidebar: true,
|
||||
},
|
||||
render: (createElement) =>
|
||||
createElement('sidebar-todo-widget', {
|
||||
props: {
|
||||
fullPath: projectPath,
|
||||
issuableId:
|
||||
isInIssuePage() || isInDesignPage()
|
||||
? convertToGraphQLId(TYPE_ISSUE, id)
|
||||
: convertToGraphQLId(TYPE_MERGE_REQUEST, id),
|
||||
issuableIid: iid,
|
||||
issuableType:
|
||||
isInIssuePage() || isInDesignPage() ? IssuableType.Issue : IssuableType.MergeRequest,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function getSidebarAssigneeAvailabilityData() {
|
||||
const sidebarAssigneeEl = document.querySelectorAll('.js-sidebar-assignee-data input');
|
||||
return Array.from(sidebarAssigneeEl)
|
||||
|
@ -497,6 +534,7 @@ export function mountSidebar(mediator) {
|
|||
initInviteMembersModal();
|
||||
initInviteMembersTrigger();
|
||||
|
||||
mountSidebarToDoWidget();
|
||||
if (isAssigneesWidgetShown) {
|
||||
mountAssigneesComponent();
|
||||
} else {
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
query issueTodos($fullPath: ID!, $iid: String!) {
|
||||
workspace: project(fullPath: $fullPath) {
|
||||
__typename
|
||||
issuable: issue(iid: $iid) {
|
||||
__typename
|
||||
id
|
||||
currentUserTodos(state: pending) {
|
||||
nodes {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
query mergeRequestTodos($fullPath: ID!, $iid: String!) {
|
||||
workspace: project(fullPath: $fullPath) {
|
||||
__typename
|
||||
issuable: mergeRequest(iid: $iid) {
|
||||
__typename
|
||||
id
|
||||
currentUserTodos(state: pending) {
|
||||
nodes {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import TodoButton from './todo_button.vue';
|
|||
|
||||
export default {
|
||||
component: TodoButton,
|
||||
title: 'vue_shared/components/todo_button',
|
||||
title: 'vue_shared/components/todo_toggle/todo_button',
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import { todoLabel } from './utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -15,7 +15,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
buttonLabel() {
|
||||
return this.isTodo ? __('Mark as done') : __('Add a to do');
|
||||
return todoLabel(this.isTodo);
|
||||
},
|
||||
},
|
||||
methods: {
|
|
@ -0,0 +1,5 @@
|
|||
import { __ } from '~/locale';
|
||||
|
||||
export const todoLabel = (hasTodo) => {
|
||||
return hasTodo ? __('Mark as done') : __('Add a to do');
|
||||
};
|
|
@ -175,7 +175,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.block {
|
||||
.block,
|
||||
.issuable-sidebar-header {
|
||||
@include clearfix;
|
||||
padding: $gl-padding 0;
|
||||
border-bottom: 1px solid $border-gray-normal;
|
||||
|
@ -184,11 +185,6 @@
|
|||
width: $gutter-inner-width;
|
||||
// --
|
||||
|
||||
&.issuable-sidebar-header {
|
||||
padding-top: 0;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border: 0;
|
||||
}
|
||||
|
@ -273,10 +269,6 @@
|
|||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.issuable-sidebar-header {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
&:not(.boards-sidebar):not([data-signed-in]):not([data-always-show-toggle]) {
|
||||
.issuable-sidebar-header {
|
||||
display: none;
|
||||
|
@ -302,7 +294,6 @@
|
|||
}
|
||||
|
||||
.gutter-toggle {
|
||||
margin-top: 7px;
|
||||
border-left: 1px solid $border-gray-normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -331,20 +322,21 @@
|
|||
width: $gutter-collapsed-width;
|
||||
padding: 0;
|
||||
|
||||
.block {
|
||||
.block,
|
||||
.issuable-sidebar-header {
|
||||
width: $gutter-collapsed-width - 2px;
|
||||
padding: 0;
|
||||
border-bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.block,
|
||||
.gutter-toggle,
|
||||
.sidebar-collapsed-container {
|
||||
&.with-sub-blocks .sub-block:hover,
|
||||
&:not(.with-sub-blocks):hover {
|
||||
background-color: $gray-100;
|
||||
}
|
||||
|
||||
&.issuable-sidebar-header {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.participants {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class Projects::MergeRequests::DiffsController < Projects::MergeRequests::ApplicationController
|
||||
include DiffHelper
|
||||
include RendersNotes
|
||||
include Gitlab::Cache::Helpers
|
||||
|
||||
before_action :commit
|
||||
before_action :define_diff_vars
|
||||
|
@ -40,7 +41,16 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
|
|||
pagination_data: diffs.pagination_data
|
||||
}
|
||||
|
||||
render json: PaginatedDiffSerializer.new(current_user: current_user).represent(diffs, options)
|
||||
if diff_options_hash[:paths].blank? && Feature.enabled?(:diffs_batch_render_cached, project, default_enabled: :yaml)
|
||||
render_cached(
|
||||
diffs,
|
||||
with: PaginatedDiffSerializer.new(current_user: current_user),
|
||||
cache_context: -> (_) { [diff_view, params[:w], params[:expanded], params[:per_page], params[:page]] },
|
||||
**options
|
||||
)
|
||||
else
|
||||
render json: PaginatedDiffSerializer.new(current_user: current_user).represent(diffs, options)
|
||||
end
|
||||
end
|
||||
|
||||
def diffs_metadata
|
||||
|
|
|
@ -285,7 +285,7 @@ module ApplicationHelper
|
|||
def page_class
|
||||
class_names = []
|
||||
class_names << 'issue-boards-page gl-overflow-auto' if current_controller?(:boards)
|
||||
class_names << 'epic-boards-page' if current_controller?(:epic_boards)
|
||||
class_names << 'epic-boards-page gl-overflow-auto' if current_controller?(:epic_boards)
|
||||
class_names << 'environment-logs-page' if current_controller?(:logs)
|
||||
class_names << 'with-performance-bar' if performance_bar_enabled?
|
||||
class_names << system_message_class
|
||||
|
|
|
@ -50,6 +50,10 @@ module Analytics
|
|||
end
|
||||
end
|
||||
|
||||
def events_hash_code
|
||||
Digest::SHA256.hexdigest("#{start_event.hash_code}-#{end_event.hash_code}")
|
||||
end
|
||||
|
||||
def start_event_label_based?
|
||||
start_event_identifier && start_event.label_based?
|
||||
end
|
||||
|
|
|
@ -42,6 +42,13 @@ class DiffDiscussion < Discussion
|
|||
)
|
||||
end
|
||||
|
||||
def cache_key
|
||||
[
|
||||
super,
|
||||
Digest::SHA1.hexdigest(position.to_json)
|
||||
].join(':')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_params
|
||||
|
|
|
@ -18,12 +18,6 @@ module MergeRequests
|
|||
end
|
||||
|
||||
def rebase
|
||||
# Ensure Gitaly isn't already running a rebase
|
||||
if source_project.repository.rebase_in_progress?(merge_request.id)
|
||||
log_error(exception: nil, message: 'Rebase task canceled: Another rebase is already in progress', save_message_on_model: true)
|
||||
return false
|
||||
end
|
||||
|
||||
repository.rebase(current_user, merge_request, skip_ci: @skip_ci)
|
||||
|
||||
true
|
||||
|
|
|
@ -10,19 +10,13 @@
|
|||
|
||||
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in }, issuable_type: issuable_type }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite', 'aria-label': issuable_type }
|
||||
.issuable-sidebar
|
||||
.block.issuable-sidebar-header
|
||||
- if signed_in
|
||||
%span.issuable-header-text.hide-collapsed.float-left
|
||||
= _('To Do')
|
||||
.issuable-sidebar-header.gl-py-3
|
||||
%a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => _('Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
|
||||
= sidebar_gutter_toggle_icon
|
||||
- if signed_in
|
||||
= render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar
|
||||
.js-issuable-todo{ data: { project_path: issuable_sidebar[:project_full_path], iid: issuable_sidebar[:iid], id: issuable_sidebar[:id] } }
|
||||
|
||||
= form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
|
||||
- if signed_in
|
||||
.block.todo.hide-expanded
|
||||
= render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar, is_collapsed: true
|
||||
.block.assignee.qa-assignee-block
|
||||
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees, signed_in: signed_in
|
||||
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
- is_collapsed = local_assigns.fetch(:is_collapsed, false)
|
||||
- has_todo = !!issuable_sidebar.dig(:current_user, :todo, :id)
|
||||
|
||||
- todo_button_data = issuable_todo_button_data(issuable_sidebar, is_collapsed)
|
||||
- button_title = has_todo ? todo_button_data[:mark_text] : todo_button_data[:todo_text]
|
||||
- button_icon = has_todo ? todo_button_data[:mark_icon] : todo_button_data[:todo_icon]
|
||||
|
||||
%button.issuable-todo-btn.js-issuable-todo{ type: 'button',
|
||||
class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'gl-button btn btn-default issuable-header-btn float-right'),
|
||||
title: button_title,
|
||||
'aria-label' => button_title,
|
||||
data: todo_button_data }
|
||||
%span.issuable-todo-inner.js-issuable-todo-inner<
|
||||
= is_collapsed ? button_icon : button_title
|
||||
= loading_icon(css_class: is_collapsed ? '' : 'gl-ml-3')
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: diffs_batch_render_cached
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/1509
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334762
|
||||
milestone: '14.1'
|
||||
type: development
|
||||
group: group::code review
|
||||
default_enabled: false
|
|
@ -114,7 +114,7 @@ The action is irreversible.
|
|||
|
||||
## Triggering a pipeline
|
||||
|
||||
To trigger a job you need to send a `POST` request to the GitLab API endpoint:
|
||||
To trigger a pipeline you need to send a `POST` request to the GitLab API endpoint:
|
||||
|
||||
```plaintext
|
||||
POST /projects/:id/trigger/pipeline
|
||||
|
@ -126,7 +126,7 @@ branches or tags. The `:id` of a project can be found by
|
|||
[querying the API](../../api/projects.md) or by visiting the **CI/CD**
|
||||
settings page which provides self-explanatory examples.
|
||||
|
||||
When a rerun of a pipeline is triggered, jobs are marked as triggered `by API` in
|
||||
When a rerun of a pipeline is triggered, jobs are labeled as `triggered` in
|
||||
**CI/CD > Jobs**.
|
||||
|
||||
You can see which trigger caused a job to run by visiting the single job page.
|
||||
|
|
|
@ -26,39 +26,40 @@ A job is defined as a list of keywords that define the job's behavior.
|
|||
|
||||
The keywords available for jobs are:
|
||||
|
||||
| Keyword | Description |
|
||||
| :-----------------------------------|:------------|
|
||||
| [`after_script`](#after_script) | Override a set of commands that are executed after job. |
|
||||
| [`allow_failure`](#allow_failure) | Allow job to fail. A failed job does not cause the pipeline to fail. |
|
||||
| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. |
|
||||
| [`before_script`](#before_script) | Override a set of commands that are executed before job. |
|
||||
| [`cache`](#cache) | List of files that should be cached between subsequent runs. |
|
||||
| [`coverage`](#coverage) | Code coverage settings for a given job. |
|
||||
| [`dependencies`](#dependencies) | Restrict which artifacts are passed to a specific job by providing a list of jobs to fetch artifacts from. |
|
||||
| [`environment`](#environment) | Name of an environment to which the job deploys. |
|
||||
| [`except`](#only--except) | Control when jobs are not created. |
|
||||
| [`extends`](#extends) | Configuration entries that this job inherits from. |
|
||||
| [`image`](#image) | Use Docker images. |
|
||||
| [`include`](#include) | Include external YAML files. |
|
||||
| [`inherit`](#inherit) | Select which global defaults all jobs inherit. |
|
||||
| [`interruptible`](#interruptible) | Defines if a job can be canceled when made redundant by a newer run. |
|
||||
| [`needs`](#needs) | Execute jobs earlier than the stage ordering. |
|
||||
| [`only`](#only--except) | Control when jobs are created. |
|
||||
| [`pages`](#pages) | Upload the result of a job to use with GitLab Pages. |
|
||||
| [`parallel`](#parallel) | How many instances of a job should be run in parallel. |
|
||||
| [`release`](#release) | Instructs the runner to generate a [release](../../user/project/releases/index.md) object. |
|
||||
| [`resource_group`](#resource_group) | Limit job concurrency. |
|
||||
| [`retry`](#retry) | When and how many times a job can be auto-retried in case of a failure. |
|
||||
| [`rules`](#rules) | List of conditions to evaluate and determine selected attributes of a job, and whether or not it's created. |
|
||||
| [`script`](#script) | Shell script that is executed by a runner. |
|
||||
| [`secrets`](#secrets) | The CI/CD secrets the job needs. |
|
||||
| [`services`](#services) | Use Docker services images. |
|
||||
| [`stage`](#stage) | Defines a job stage. |
|
||||
| [`tags`](#tags) | List of tags that are used to select a runner. |
|
||||
| [`timeout`](#timeout) | Define a custom job-level timeout that takes precedence over the project-wide setting. |
|
||||
| [`trigger`](#trigger) | Defines a downstream pipeline trigger. |
|
||||
| [`variables`](#variables) | Define job variables on a job level. |
|
||||
| [`when`](#when) | When to run job. |
|
||||
| Keyword | Description |
|
||||
| :-------------------------------------------|:------------|
|
||||
| [`after_script`](#after_script) | Override a set of commands that are executed after job. |
|
||||
| [`allow_failure`](#allow_failure) | Allow job to fail. A failed job does not cause the pipeline to fail. |
|
||||
| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. |
|
||||
| [`before_script`](#before_script) | Override a set of commands that are executed before job. |
|
||||
| [`cache`](#cache) | List of files that should be cached between subsequent runs. |
|
||||
| [`coverage`](#coverage) | Code coverage settings for a given job. |
|
||||
| [`dast_configuration`](#dast_configuration) | Use configuration from DAST profiles on a job level. |
|
||||
| [`dependencies`](#dependencies) | Restrict which artifacts are passed to a specific job by providing a list of jobs to fetch artifacts from. |
|
||||
| [`environment`](#environment) | Name of an environment to which the job deploys. |
|
||||
| [`except`](#only--except) | Control when jobs are not created. |
|
||||
| [`extends`](#extends) | Configuration entries that this job inherits from. |
|
||||
| [`image`](#image) | Use Docker images. |
|
||||
| [`include`](#include) | Include external YAML files. |
|
||||
| [`inherit`](#inherit) | Select which global defaults all jobs inherit. |
|
||||
| [`interruptible`](#interruptible) | Defines if a job can be canceled when made redundant by a newer run. |
|
||||
| [`needs`](#needs) | Execute jobs earlier than the stage ordering. |
|
||||
| [`only`](#only--except) | Control when jobs are created. |
|
||||
| [`pages`](#pages) | Upload the result of a job to use with GitLab Pages. |
|
||||
| [`parallel`](#parallel) | How many instances of a job should be run in parallel. |
|
||||
| [`release`](#release) | Instructs the runner to generate a [release](../../user/project/releases/index.md) object. |
|
||||
| [`resource_group`](#resource_group) | Limit job concurrency. |
|
||||
| [`retry`](#retry) | When and how many times a job can be auto-retried in case of a failure. |
|
||||
| [`rules`](#rules) | List of conditions to evaluate and determine selected attributes of a job, and whether or not it's created. |
|
||||
| [`script`](#script) | Shell script that is executed by a runner. |
|
||||
| [`secrets`](#secrets) | The CI/CD secrets the job needs. |
|
||||
| [`services`](#services) | Use Docker services images. |
|
||||
| [`stage`](#stage) | Defines a job stage. |
|
||||
| [`tags`](#tags) | List of tags that are used to select a runner. |
|
||||
| [`timeout`](#timeout) | Define a custom job-level timeout that takes precedence over the project-wide setting. |
|
||||
| [`trigger`](#trigger) | Defines a downstream pipeline trigger. |
|
||||
| [`variables`](#variables) | Define job variables on a job level. |
|
||||
| [`when`](#when) | When to run job. |
|
||||
|
||||
### Unavailable names for jobs
|
||||
|
||||
|
@ -4502,6 +4503,50 @@ You can use [CI/CD variables](../variables/index.md) to configure how the runner
|
|||
You can also use variables to configure how many times a runner
|
||||
[attempts certain stages of job execution](../runners/configure_runners.md#job-stages-attempts).
|
||||
|
||||
## `dast_configuration` **(ULTIMATE)**
|
||||
|
||||
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5981) in GitLab 14.1.
|
||||
|
||||
Use the `dast_configuration` keyword to specify a site profile and scanner profile to be used in a
|
||||
CI/CD configuration. Both profiles must first have been created in the project. The job's stage must
|
||||
be `dast`.
|
||||
|
||||
**Keyword type**: Job keyword. You can use only as part of a job.
|
||||
|
||||
**Possible inputs**: One each of `site_profile` and `scanner_profile`.
|
||||
|
||||
- Use `site_profile` to specify the site profile to be used in the job.
|
||||
- Use `scanner_profile` to specify the scanner profile to be used in the job.
|
||||
|
||||
**Example of `dast_configuration`**:
|
||||
|
||||
```yaml
|
||||
stages:
|
||||
- build
|
||||
- dast
|
||||
|
||||
include:
|
||||
- template: DAST.gitlab-ci.yml
|
||||
|
||||
dast:
|
||||
dast_configuration:
|
||||
site_profile: "Example Co"
|
||||
scanner_profile: "Quick Passive Test"
|
||||
```
|
||||
|
||||
In this example, the `dast` job extends the `dast` configuration added with the `include:` keyword
|
||||
to select a specific site profile and scanner profile.
|
||||
|
||||
**Additional details**:
|
||||
|
||||
- Settings contained in either a site profile or scanner profile take precedence over those
|
||||
contained in the DAST template.
|
||||
|
||||
**Related topics**:
|
||||
|
||||
- [Site profile](../../user/application_security/dast/index.md#site-profile).
|
||||
- [Scanner profile](../../user/application_security/dast/index.md#scanner-profile).
|
||||
|
||||
## YAML-specific features
|
||||
|
||||
In your `.gitlab-ci.yml` file, you can use YAML-specific features like anchors (`&`), aliases (`*`),
|
||||
|
|
|
@ -47,5 +47,5 @@ To add a story:
|
|||
|
||||
Notes:
|
||||
- Specify the `title` field of the story as the component's file path from the `javascripts/` directory,
|
||||
e.g. if the component is located at `app/assets/javascripts/vue_shared/components/sidebar/todo_button.vue`, specify the `title` as
|
||||
e.g. if the component is located at `app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue`, specify the `title` as
|
||||
`vue_shared/components/To-do Button`. This will ensure the Storybook navigation maps closely to our internal directory structure.
|
||||
|
|
|
@ -145,6 +145,7 @@ To enable DAST to run automatically, either:
|
|||
by [Auto DevOps](../../../topics/autodevops/index.md)).
|
||||
- [Include the DAST template](#include-the-dast-template) in your existing
|
||||
`.gitlab-ci.yml` file.
|
||||
- [Configure DAST using the UI](#configure-dast-using-the-ui).
|
||||
|
||||
### DAST job order
|
||||
|
||||
|
@ -265,6 +266,28 @@ image. Using the `DAST_VERSION` variable, you can choose how DAST updates:
|
|||
Find the latest DAST versions on the [Releases](https://gitlab.com/security-products/dast/-/releases)
|
||||
page.
|
||||
|
||||
#### Configure DAST using the UI
|
||||
|
||||
You can enable or configure DAST settings using the UI. The generated settings are formatted so they
|
||||
can be conveniently pasted into the `.gitlab-ci.yml` file.
|
||||
|
||||
1. From the project's home page, go to **Security & Compliance > Configuration**.
|
||||
1. In the **Dynamic Application Security Testing (DAST)** section, select **Enable DAST** or
|
||||
**Configure DAST**.
|
||||
1. Select the desired **Scanner profile**, or select **Create scanner profile** and save a
|
||||
scanner profile. For more details, see [scanner profiles](#scanner-profile).
|
||||
1. Select the desired **Site profile**, or select **Create site profile** and save a site
|
||||
profile. For more details, see [site profiles](#site-profile).
|
||||
1. Select **Generate code snippet**. A modal opens with the YAML snippet corresponding to the
|
||||
options you selected.
|
||||
1. Do one of the following:
|
||||
1. Select **Copy code only** to copy the snippet to your clipboard.
|
||||
1. Select **Copy code and open `.gitlab-ci.yml` file** to copy the snippet to your clipboard. The
|
||||
CI/CD Editor then opens.
|
||||
1. Paste the snippet into the `.gitlab-ci.yml` file.
|
||||
1. Select the **Lint** tab to confirm the edited `.gitlab-ci.yml` file is valid.
|
||||
1. Select **Commit changes**.
|
||||
|
||||
#### Crawling web applications dependent on JavaScript
|
||||
|
||||
GitLab has released a new browser-based crawler, an add-on to DAST that uses a browser to crawl web applications for content. This crawler replaces the standard DAST Spider and Ajax Crawler, and uses the same authentication mechanisms as a normal DAST scan.
|
||||
|
|
|
@ -115,8 +115,16 @@ permission enables an electronic signature for approvals, such as the one define
|
|||
## Security approvals in merge requests **(ULTIMATE)**
|
||||
|
||||
You can require that a member of your security team approves a merge request if a
|
||||
merge request could introduce a vulnerability. To learn more, see
|
||||
[Security approvals in merge requests](../../../application_security/index.md#security-approvals-in-merge-requests).
|
||||
merge request could introduce a vulnerability.
|
||||
|
||||
To learn more, see [Security approvals in merge requests](../../../application_security/index.md#security-approvals-in-merge-requests).
|
||||
|
||||
## Code coverage check approvals **(PREMIUM)**
|
||||
|
||||
You can require specific approvals if a merge request would result in a decline in code test
|
||||
coverage.
|
||||
|
||||
To learn more, see [Coverage check approval rule](../../../../ci/pipelines/settings.md#coverage-check-approval-rule).
|
||||
|
||||
## Related links
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class ProjectIntegration < Entities::ProjectIntegrationBasic
|
||||
# Expose serialized properties
|
||||
expose :properties do |integration, options|
|
||||
# TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
|
||||
|
||||
attributes =
|
||||
if integration.data_fields_present?
|
||||
integration.data_fields.as_json.keys
|
||||
else
|
||||
integration.properties.keys
|
||||
end
|
||||
|
||||
attributes &= integration.api_field_names
|
||||
|
||||
attributes.each_with_object({}) do |attribute, hash|
|
||||
hash[attribute] = integration.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
module API
|
||||
module Entities
|
||||
class ProjectServiceBasic < Grape::Entity
|
||||
class ProjectIntegrationBasic < Grape::Entity
|
||||
expose :id, :title
|
||||
expose :slug do |service|
|
||||
service.to_param.dasherize
|
||||
expose :slug do |integration|
|
||||
integration.to_param.dasherize
|
||||
end
|
||||
expose :created_at, :updated_at, :active
|
||||
expose :commit_events, :push_events, :issues_events, :confidential_issues_events
|
|
@ -1,25 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class ProjectService < Entities::ProjectServiceBasic
|
||||
# Expose serialized properties
|
||||
expose :properties do |service, options|
|
||||
# TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
|
||||
|
||||
attributes =
|
||||
if service.data_fields_present?
|
||||
service.data_fields.as_json.keys
|
||||
else
|
||||
service.properties.keys
|
||||
end
|
||||
|
||||
attributes &= service.api_field_names
|
||||
|
||||
attributes.each_with_object({}) do |attribute, hash|
|
||||
hash[attribute] = service.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -35,7 +35,7 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
TRIGGER_SERVICES = {
|
||||
TRIGGER_INTEGRATIONS = {
|
||||
'mattermost-slash-commands' => [
|
||||
{
|
||||
name: :token,
|
||||
|
@ -60,24 +60,24 @@ module API
|
|||
before { authorize_admin_project }
|
||||
|
||||
helpers do
|
||||
def service_attributes(service)
|
||||
service.fields.inject([]) do |arr, hash|
|
||||
def integration_attributes(integration)
|
||||
integration.fields.inject([]) do |arr, hash|
|
||||
arr << hash[:name].to_sym
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Get all active project services' do
|
||||
success Entities::ProjectServiceBasic
|
||||
desc 'Get all active project integrations' do
|
||||
success Entities::ProjectIntegrationBasic
|
||||
end
|
||||
get ":id/services" do
|
||||
services = user_project.integrations.active
|
||||
integrations = user_project.integrations.active
|
||||
|
||||
present services, with: Entities::ProjectServiceBasic
|
||||
present integrations, with: Entities::ProjectIntegrationBasic
|
||||
end
|
||||
|
||||
INTEGRATIONS.each do |slug, settings|
|
||||
desc "Set #{slug} service for project"
|
||||
desc "Set #{slug} integration for project"
|
||||
params do
|
||||
settings.each do |setting|
|
||||
if setting[:required]
|
||||
|
@ -92,7 +92,7 @@ module API
|
|||
params = declared_params(include_missing: false).merge(active: true)
|
||||
|
||||
if integration.update(params)
|
||||
present integration, with: Entities::ProjectService
|
||||
present integration, with: Entities::ProjectIntegration
|
||||
else
|
||||
render_api_error!('400 Bad Request', 400)
|
||||
end
|
||||
|
@ -107,14 +107,14 @@ module API
|
|||
integration = user_project.find_or_initialize_integration(params[:slug].underscore)
|
||||
|
||||
destroy_conditionally!(integration) do
|
||||
attrs = service_attributes(integration).index_with { nil }.merge(active: false)
|
||||
attrs = integration_attributes(integration).index_with { nil }.merge(active: false)
|
||||
|
||||
render_api_error!('400 Bad Request', 400) unless integration.update(attrs)
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Get the integration settings for a project' do
|
||||
success Entities::ProjectService
|
||||
success Entities::ProjectIntegration
|
||||
end
|
||||
params do
|
||||
requires :slug, type: String, values: INTEGRATIONS.keys, desc: 'The name of the service'
|
||||
|
@ -124,15 +124,15 @@ module API
|
|||
|
||||
not_found!('Service') unless integration&.persisted?
|
||||
|
||||
present integration, with: Entities::ProjectService
|
||||
present integration, with: Entities::ProjectIntegration
|
||||
end
|
||||
end
|
||||
|
||||
TRIGGER_SERVICES.each do |service_slug, settings|
|
||||
TRIGGER_INTEGRATIONS.each do |integration_slug, settings|
|
||||
helpers do
|
||||
def slash_command_service(project, service_slug, params)
|
||||
project.integrations.active.find do |service|
|
||||
service.try(:token) == params[:token] && service.to_param == service_slug.underscore
|
||||
def slash_command_integration(project, integration_slug, params)
|
||||
project.integrations.active.find do |integration|
|
||||
integration.try(:token) == params[:token] && integration.to_param == integration_slug.underscore
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -141,7 +141,7 @@ module API
|
|||
requires :id, type: String, desc: 'The ID of a project'
|
||||
end
|
||||
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
||||
desc "Trigger a slash command for #{service_slug}" do
|
||||
desc "Trigger a slash command for #{integration_slug}" do
|
||||
detail 'Added in GitLab 8.13'
|
||||
end
|
||||
params do
|
||||
|
@ -149,14 +149,14 @@ module API
|
|||
requires setting[:name], type: setting[:type], desc: setting[:desc]
|
||||
end
|
||||
end
|
||||
post ":id/services/#{service_slug.underscore}/trigger" do
|
||||
post ":id/services/#{integration_slug.underscore}/trigger" do
|
||||
project = find_project(params[:id])
|
||||
|
||||
# This is not accurate, but done to prevent leakage of the project names
|
||||
not_found!('Service') unless project
|
||||
|
||||
service = slash_command_service(project, service_slug, params)
|
||||
result = service.try(:trigger, params)
|
||||
integration = slash_command_integration(project, integration_slug, params)
|
||||
result = integration.try(:trigger, params)
|
||||
|
||||
if result
|
||||
status result[:status] || 200
|
||||
|
|
|
@ -31,6 +31,10 @@ module Gitlab
|
|||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def hash_code
|
||||
Digest::SHA256.hexdigest(self.class.identifier.to_s)
|
||||
end
|
||||
|
||||
# Each StageEvent must expose a timestamp or a timestamp like expression in order to build a range query.
|
||||
# Example: get me all the Issue records between start event end end event
|
||||
def timestamp_projection
|
||||
|
|
|
@ -7,9 +7,9 @@ module Gitlab
|
|||
ERROR_MESSAGE = 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'
|
||||
|
||||
def validate!
|
||||
# This feature flag is used for disabling integrify check on some envs
|
||||
# This feature flag is used for disabling integrity check on some envs
|
||||
# because these costy calculations may cause performance issues
|
||||
return unless Feature.enabled?(:lfs_check, default_enabled: true)
|
||||
return unless Feature.enabled?(:lfs_check, project, default_enabled: :yaml)
|
||||
|
||||
return unless project.lfs_enabled?
|
||||
|
||||
|
|
|
@ -10,6 +10,10 @@ module Gitlab
|
|||
diff_options: diff_options,
|
||||
diff_refs: commit.diff_refs)
|
||||
end
|
||||
|
||||
def cache_key
|
||||
['commit', @diffable.id]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,6 +14,10 @@ module Gitlab
|
|||
def unfold_diff_lines(positions)
|
||||
# no-op
|
||||
end
|
||||
|
||||
def cache_key
|
||||
['compare', @diffable.head.id, @diffable.base.id]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,6 +6,8 @@ module Gitlab
|
|||
class MergeRequestDiffBase < Base
|
||||
extend ::Gitlab::Utils::Override
|
||||
|
||||
delegate :real_size, :overflow?, :cache_key, to: :@merge_request_diff
|
||||
|
||||
def initialize(merge_request_diff, diff_options:)
|
||||
@merge_request_diff = merge_request_diff
|
||||
|
||||
|
@ -44,14 +46,6 @@ module Gitlab
|
|||
diff_stats_cache.clear
|
||||
end
|
||||
|
||||
def real_size
|
||||
@merge_request_diff.real_size
|
||||
end
|
||||
|
||||
def overflow?
|
||||
@merge_request_diff.overflow?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def highlight_cache
|
||||
|
@ -62,7 +56,7 @@ module Gitlab
|
|||
|
||||
def diff_stats_cache
|
||||
strong_memoize(:diff_stats_cache) do
|
||||
Gitlab::Diff::StatsCache.new(cachable_key: @merge_request_diff.cache_key)
|
||||
Gitlab::Diff::StatsCache.new(cachable_key: cache_key)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -883,12 +883,6 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def rebase_in_progress?(rebase_id)
|
||||
wrapped_gitaly_errors do
|
||||
gitaly_repository_client.rebase_in_progress?(rebase_id)
|
||||
end
|
||||
end
|
||||
|
||||
def squash(user, squash_id, start_sha:, end_sha:, author:, message:)
|
||||
wrapped_gitaly_errors do
|
||||
gitaly_operation_client.user_squash(user, squash_id, start_sha, end_sha, author, message)
|
||||
|
|
|
@ -152,23 +152,6 @@ module Gitlab
|
|||
)
|
||||
end
|
||||
|
||||
def rebase_in_progress?(rebase_id)
|
||||
request = Gitaly::IsRebaseInProgressRequest.new(
|
||||
repository: @gitaly_repo,
|
||||
rebase_id: rebase_id.to_s
|
||||
)
|
||||
|
||||
response = GitalyClient.call(
|
||||
@storage,
|
||||
:repository_service,
|
||||
:is_rebase_in_progress,
|
||||
request,
|
||||
timeout: GitalyClient.fast_timeout
|
||||
)
|
||||
|
||||
response.in_progress
|
||||
end
|
||||
|
||||
def squash_in_progress?(squash_id)
|
||||
request = Gitaly::IsSquashInProgressRequest.new(
|
||||
repository: @gitaly_repo,
|
||||
|
|
|
@ -10,7 +10,8 @@ import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.v
|
|||
import { ISSUABLE } from '~/boards/constants';
|
||||
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
|
||||
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
|
||||
import { mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
|
||||
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
|
||||
import { mockActiveIssue, mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
|
||||
|
||||
describe('BoardContentSidebar', () => {
|
||||
let wrapper;
|
||||
|
@ -26,7 +27,7 @@ describe('BoardContentSidebar', () => {
|
|||
},
|
||||
getters: {
|
||||
activeBoardItem: () => {
|
||||
return { ...mockIssue, epic: null };
|
||||
return { ...mockActiveIssue, epic: null };
|
||||
},
|
||||
groupPathForActiveIssue: () => mockIssueGroupPath,
|
||||
projectPathForActiveIssue: () => mockIssueProjectPath,
|
||||
|
@ -110,6 +111,10 @@ describe('BoardContentSidebar', () => {
|
|||
expect(wrapper.findComponent(GlDrawer).props('open')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders SidebarTodoWidget', () => {
|
||||
expect(wrapper.findComponent(SidebarTodoWidget).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders BoardSidebarLabelsSelect', () => {
|
||||
expect(wrapper.findComponent(BoardSidebarLabelsSelect).exists()).toBe(true);
|
||||
});
|
||||
|
@ -147,7 +152,7 @@ describe('BoardContentSidebar', () => {
|
|||
|
||||
expect(toggleBoardItem).toHaveBeenCalledTimes(1);
|
||||
expect(toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), {
|
||||
boardItem: { ...mockIssue, epic: null },
|
||||
boardItem: { ...mockActiveIssue, epic: null },
|
||||
sidebarType: ISSUABLE,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { GlAlert } from '@gitlab/ui';
|
||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import Draggable from 'vuedraggable';
|
||||
import Vuex from 'vuex';
|
||||
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
|
||||
|
@ -8,8 +9,7 @@ import BoardColumnDeprecated from '~/boards/components/board_column_deprecated.v
|
|||
import BoardContent from '~/boards/components/board_content.vue';
|
||||
import { mockLists, mockListsWithModel } from '../mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
Vue.use(Vuex);
|
||||
|
||||
const actions = {
|
||||
moveList: jest.fn(),
|
||||
|
@ -44,7 +44,6 @@ describe('BoardContent', () => {
|
|||
...state,
|
||||
});
|
||||
wrapper = shallowMount(BoardContent, {
|
||||
localVue,
|
||||
propsData: {
|
||||
lists: mockListsWithModel,
|
||||
disabled: false,
|
||||
|
|
|
@ -182,6 +182,7 @@ export const mockIssue = {
|
|||
|
||||
export const mockActiveIssue = {
|
||||
...mockIssue,
|
||||
fullId: 'gid://gitlab/Issue/436',
|
||||
id: 436,
|
||||
iid: '27',
|
||||
subscribed: false,
|
||||
|
|
|
@ -1,171 +0,0 @@
|
|||
/* eslint-disable no-new */
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { clone } from 'lodash';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import Sidebar from '~/right_sidebar';
|
||||
import { fixTitle } from '~/tooltips';
|
||||
|
||||
jest.mock('~/tooltips');
|
||||
|
||||
describe('Issuable right sidebar collapsed todo toggle', () => {
|
||||
const fixtureName = 'issues/open-issue.html';
|
||||
const jsonFixtureName = 'todos/todos.json';
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
const todoData = getJSONFixture(jsonFixtureName);
|
||||
new Sidebar();
|
||||
loadFixtures(fixtureName);
|
||||
|
||||
document.querySelector('.js-right-sidebar').classList.toggle('right-sidebar-expanded');
|
||||
document.querySelector('.js-right-sidebar').classList.toggle('right-sidebar-collapsed');
|
||||
|
||||
mock = new MockAdapter(axios);
|
||||
|
||||
mock.onPost(`${TEST_HOST}/frontend-fixtures/issues-project/todos`).reply(() => {
|
||||
const response = clone(todoData);
|
||||
|
||||
return [200, response];
|
||||
});
|
||||
|
||||
mock.onDelete(/(.*)\/dashboard\/todos\/\d+$/).reply(() => {
|
||||
const response = clone(todoData);
|
||||
delete response.delete_path;
|
||||
|
||||
return [200, response];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('shows add todo button', () => {
|
||||
expect(document.querySelector('.js-issuable-todo.sidebar-collapsed-icon')).not.toBeNull();
|
||||
|
||||
expect(
|
||||
document
|
||||
.querySelector('.js-issuable-todo.sidebar-collapsed-icon svg')
|
||||
.getAttribute('data-testid'),
|
||||
).toBe('todo-add-icon');
|
||||
|
||||
expect(
|
||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('sets default tooltip title', () => {
|
||||
expect(
|
||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('title'),
|
||||
).toBe('Add a to do');
|
||||
});
|
||||
|
||||
it('toggle todo state', (done) => {
|
||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
|
||||
|
||||
setImmediate(() => {
|
||||
expect(
|
||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
|
||||
).not.toBeNull();
|
||||
|
||||
expect(
|
||||
document
|
||||
.querySelector('.js-issuable-todo.sidebar-collapsed-icon svg.todo-undone')
|
||||
.getAttribute('data-testid'),
|
||||
).toBe('todo-done-icon');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('toggle todo state of expanded todo toggle', (done) => {
|
||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
|
||||
|
||||
setImmediate(() => {
|
||||
expect(
|
||||
document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
|
||||
).toBe('Mark as done');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('toggles todo button tooltip', (done) => {
|
||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
|
||||
|
||||
setImmediate(() => {
|
||||
const el = document.querySelector('.js-issuable-todo.sidebar-collapsed-icon');
|
||||
|
||||
expect(el.getAttribute('title')).toBe('Mark as done');
|
||||
expect(fixTitle).toHaveBeenCalledWith(el);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('marks todo as done', (done) => {
|
||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
|
||||
|
||||
waitForPromises()
|
||||
.then(() => {
|
||||
expect(
|
||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
|
||||
).not.toBeNull();
|
||||
|
||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
|
||||
})
|
||||
.then(waitForPromises)
|
||||
.then(() => {
|
||||
expect(
|
||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
|
||||
).toBeNull();
|
||||
|
||||
expect(
|
||||
document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
|
||||
).toBe('Add a to do');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('updates aria-label to Mark as done', (done) => {
|
||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
|
||||
|
||||
setImmediate(() => {
|
||||
expect(
|
||||
document
|
||||
.querySelector('.js-issuable-todo.sidebar-collapsed-icon')
|
||||
.getAttribute('aria-label'),
|
||||
).toBe('Mark as done');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('updates aria-label to add todo', (done) => {
|
||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
|
||||
|
||||
waitForPromises()
|
||||
.then(() => {
|
||||
expect(
|
||||
document
|
||||
.querySelector('.js-issuable-todo.sidebar-collapsed-icon')
|
||||
.getAttribute('aria-label'),
|
||||
).toBe('Mark as done');
|
||||
|
||||
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
|
||||
})
|
||||
.then(waitForPromises)
|
||||
.then(() => {
|
||||
expect(
|
||||
document
|
||||
.querySelector('.js-issuable-todo.sidebar-collapsed-icon')
|
||||
.getAttribute('aria-label'),
|
||||
).toBe('Add a to do');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
|
@ -2,7 +2,7 @@ import { shallowMount, mount } from '@vue/test-utils';
|
|||
import DesignTodoButton from '~/design_management/components/design_todo_button.vue';
|
||||
import createDesignTodoMutation from '~/design_management/graphql/mutations/create_design_todo.mutation.graphql';
|
||||
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
|
||||
import TodoButton from '~/vue_shared/components/sidebar/todo_button.vue';
|
||||
import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
|
||||
import mockDesign from '../mock_data/design';
|
||||
|
||||
const mockDesignWithPendingTodos = {
|
||||
|
|
|
@ -66,22 +66,6 @@ describe('RightSidebar', () => {
|
|||
assertSidebarState('collapsed');
|
||||
});
|
||||
|
||||
it('should broadcast todo:toggle event when add todo clicked', (done) => {
|
||||
const todos = getJSONFixture('todos/todos.json');
|
||||
mock.onPost(/(.*)\/todos$/).reply(200, todos);
|
||||
|
||||
const todoToggleSpy = jest.fn();
|
||||
$(document).on('todo:toggle', todoToggleSpy);
|
||||
|
||||
$('.issuable-sidebar-header .js-issuable-todo').click();
|
||||
|
||||
setImmediate(() => {
|
||||
expect(todoToggleSpy.mock.calls.length).toEqual(1);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not hide collapsed icons', () => {
|
||||
[].forEach.call(document.querySelectorAll('.sidebar-collapsed-icon'), (el) => {
|
||||
expect(el.querySelector('.fa, svg').classList.contains('hidden')).toBeFalsy();
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { GlIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
|
@ -6,7 +7,7 @@ import waitForPromises from 'helpers/wait_for_promises';
|
|||
import createFlash from '~/flash';
|
||||
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
|
||||
import epicTodoQuery from '~/sidebar/queries/epic_todo.query.graphql';
|
||||
import TodoButton from '~/vue_shared/components/sidebar/todo_button.vue';
|
||||
import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
|
||||
import { todosResponse, noTodosResponse } from '../../mock_data';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
@ -28,6 +29,7 @@ describe('Sidebar Todo Widget', () => {
|
|||
apolloProvider: fakeApollo,
|
||||
provide: {
|
||||
canUpdate: true,
|
||||
isClassicSidebar: true,
|
||||
},
|
||||
propsData: {
|
||||
fullPath: 'group',
|
||||
|
@ -83,4 +85,42 @@ describe('Sidebar Todo Widget', () => {
|
|||
|
||||
expect(createFlash).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('collapsed', () => {
|
||||
const event = { stopPropagation: jest.fn(), preventDefault: jest.fn() };
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
todosQueryHandler: jest.fn().mockResolvedValue(noTodosResponse),
|
||||
});
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('shows add todo icon', () => {
|
||||
expect(wrapper.find(GlIcon).exists()).toBe(true);
|
||||
|
||||
expect(wrapper.find(GlIcon).props('name')).toBe('todo-add');
|
||||
});
|
||||
|
||||
it('sets default tooltip title', () => {
|
||||
expect(wrapper.find(GlIcon).attributes('title')).toBe('Add a to do');
|
||||
});
|
||||
|
||||
it('when user has a to do', async () => {
|
||||
createComponent({
|
||||
todosQueryHandler: jest.fn().mockResolvedValue(todosResponse),
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
expect(wrapper.find(GlIcon).props('name')).toBe('todo-done');
|
||||
expect(wrapper.find(GlIcon).attributes('title')).toBe('Mark as done');
|
||||
});
|
||||
|
||||
it('emits `todoUpdated` event on click on icon', async () => {
|
||||
wrapper.find(GlIcon).vm.$emit('click', event);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.emitted('todoUpdated')).toEqual([[false]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { GlButton } from '@gitlab/ui';
|
||||
import { shallowMount, mount } from '@vue/test-utils';
|
||||
import TodoButton from '~/vue_shared/components/sidebar/todo_button.vue';
|
||||
import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
|
||||
|
||||
describe('Todo Button', () => {
|
||||
let wrapper;
|
||||
|
|
|
@ -75,4 +75,12 @@ RSpec.describe Gitlab::Diff::FileCollection::Commit do
|
|||
]
|
||||
end
|
||||
end
|
||||
|
||||
describe '#cache_key' do
|
||||
subject(:cache_key) { described_class.new(diffable, diff_options: nil).cache_key }
|
||||
|
||||
it 'returns with the commit id' do
|
||||
expect(cache_key).to eq ['commit', diffable.id]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,29 +15,20 @@ RSpec.describe Gitlab::Diff::FileCollection::Compare do
|
|||
head_commit.id)
|
||||
end
|
||||
|
||||
it_behaves_like 'diff statistics' do
|
||||
let(:collection_default_args) do
|
||||
{
|
||||
project: diffable.project,
|
||||
diff_options: {},
|
||||
diff_refs: diffable.diff_refs
|
||||
}
|
||||
end
|
||||
let(:diffable) { Compare.new(raw_compare, project) }
|
||||
let(:collection_default_args) do
|
||||
{
|
||||
project: diffable.project,
|
||||
diff_options: {},
|
||||
diff_refs: diffable.diff_refs
|
||||
}
|
||||
end
|
||||
|
||||
let(:diffable) { Compare.new(raw_compare, project) }
|
||||
it_behaves_like 'diff statistics' do
|
||||
let(:stub_path) { '.gitignore' }
|
||||
end
|
||||
|
||||
it_behaves_like 'sortable diff files' do
|
||||
let(:diffable) { Compare.new(raw_compare, project) }
|
||||
let(:collection_default_args) do
|
||||
{
|
||||
project: diffable.project,
|
||||
diff_options: {},
|
||||
diff_refs: diffable.diff_refs
|
||||
}
|
||||
end
|
||||
|
||||
let(:unsorted_diff_files_paths) do
|
||||
[
|
||||
'.DS_Store',
|
||||
|
@ -66,4 +57,12 @@ RSpec.describe Gitlab::Diff::FileCollection::Compare do
|
|||
]
|
||||
end
|
||||
end
|
||||
|
||||
describe '#cache_key' do
|
||||
subject(:cache_key) { described_class.new(diffable, **collection_default_args).cache_key }
|
||||
|
||||
it 'returns with head and base' do
|
||||
expect(cache_key).to eq ['compare', head_commit.id, start_commit.id]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,4 +25,12 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBase do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#cache_key' do
|
||||
subject(:cache_key) { described_class.new(diffable, diff_options: nil).cache_key }
|
||||
|
||||
it 'returns cache_key from merge_request_diff' do
|
||||
expect(cache_key).to eq diffable.cache_key
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -209,19 +209,6 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#rebase_in_progress?' do
|
||||
let(:rebase_id) { 1 }
|
||||
|
||||
it 'sends a repository_rebase_in_progress message' do
|
||||
expect_any_instance_of(Gitaly::RepositoryService::Stub)
|
||||
.to receive(:is_rebase_in_progress)
|
||||
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
|
||||
.and_return(double(in_progress: true))
|
||||
|
||||
client.rebase_in_progress?(rebase_id)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#squash_in_progress?' do
|
||||
let(:squash_id) { 1 }
|
||||
|
||||
|
|
|
@ -126,4 +126,13 @@ RSpec.describe DiffDiscussion do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#cache_key' do
|
||||
it 'returns the cache key with the position sha' do
|
||||
notes_sha = Digest::SHA1.hexdigest("#{diff_note.id}")
|
||||
position_sha = Digest::SHA1.hexdigest(diff_note.position.to_json)
|
||||
|
||||
expect(subject.cache_key).to eq("#{described_class::CACHE_VERSION}:#{diff_note.latest_cached_markdown_version}:#{subject.id}:#{notes_sha}:#{diff_note.updated_at}::#{position_sha}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,7 +31,7 @@ RSpec.describe API::Services do
|
|||
it "returns a list of all active integrations" do
|
||||
get api("/projects/#{project.id}/services", user)
|
||||
|
||||
aggregate_failures 'expect successful response with all active services' do
|
||||
aggregate_failures 'expect successful response with all active integrations' do
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response).to be_an Array
|
||||
expect(json_response.count).to eq(1)
|
||||
|
@ -42,40 +42,38 @@ RSpec.describe API::Services do
|
|||
end
|
||||
end
|
||||
|
||||
Integration.available_integration_names.each do |service|
|
||||
describe "PUT /projects/:id/services/#{service.dasherize}" do
|
||||
include_context service
|
||||
Integration.available_integration_names.each do |integration|
|
||||
describe "PUT /projects/:id/services/#{integration.dasherize}" do
|
||||
include_context integration
|
||||
|
||||
it "updates #{service} settings" do
|
||||
put api("/projects/#{project.id}/services/#{dashed_service}", user), params: service_attrs
|
||||
it "updates #{integration} settings" do
|
||||
put api("/projects/#{project.id}/services/#{dashed_integration}", user), params: integration_attrs
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
|
||||
current_service = project.integrations.first
|
||||
events = current_service.event_names.empty? ? ["foo"].freeze : current_service.event_names
|
||||
current_integration = project.integrations.first
|
||||
events = current_integration.event_names.empty? ? ["foo"].freeze : current_integration.event_names
|
||||
query_strings = []
|
||||
events.each do |event|
|
||||
query_strings << "#{event}=#{!current_service[event]}"
|
||||
query_strings << "#{event}=#{!current_integration[event]}"
|
||||
end
|
||||
query_strings = query_strings.join('&')
|
||||
|
||||
put api("/projects/#{project.id}/services/#{dashed_service}?#{query_strings}", user), params: service_attrs
|
||||
put api("/projects/#{project.id}/services/#{dashed_integration}?#{query_strings}", user), params: integration_attrs
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response['slug']).to eq(dashed_service)
|
||||
expect(json_response['slug']).to eq(dashed_integration)
|
||||
events.each do |event|
|
||||
next if event == "foo"
|
||||
|
||||
expect(project.integrations.first[event]).not_to eq(current_service[event]),
|
||||
"expected #{!current_service[event]} for event #{event} for service #{current_service.title}, got #{current_service[event]}"
|
||||
expect(project.integrations.first[event]).not_to eq(current_integration[event]),
|
||||
"expected #{!current_integration[event]} for event #{event} for service #{current_integration.title}, got #{current_integration[event]}"
|
||||
end
|
||||
end
|
||||
|
||||
it "returns if required fields missing" do
|
||||
attrs = service_attrs
|
||||
|
||||
required_attributes = service_attrs_list.select do |attr|
|
||||
service_klass.validators_on(attr).any? do |v|
|
||||
required_attributes = integration_attrs_list.select do |attr|
|
||||
integration_klass.validators_on(attr).any? do |v|
|
||||
v.instance_of?(ActiveRecord::Validations::PresenceValidator) &&
|
||||
# exclude presence validators with conditional since those are not really required
|
||||
![:if, :unless].any? { |cond| v.options.include?(cond) }
|
||||
|
@ -85,74 +83,74 @@ RSpec.describe API::Services do
|
|||
if required_attributes.empty?
|
||||
expected_code = :ok
|
||||
else
|
||||
attrs.delete(required_attributes.sample)
|
||||
integration_attrs.delete(required_attributes.sample)
|
||||
expected_code = :bad_request
|
||||
end
|
||||
|
||||
put api("/projects/#{project.id}/services/#{dashed_service}", user), params: attrs
|
||||
put api("/projects/#{project.id}/services/#{dashed_integration}", user), params: integration_attrs
|
||||
|
||||
expect(response).to have_gitlab_http_status(expected_code)
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE /projects/:id/services/#{service.dasherize}" do
|
||||
include_context service
|
||||
describe "DELETE /projects/:id/services/#{integration.dasherize}" do
|
||||
include_context integration
|
||||
|
||||
before do
|
||||
initialize_integration(service)
|
||||
initialize_integration(integration)
|
||||
end
|
||||
|
||||
it "deletes #{service}" do
|
||||
delete api("/projects/#{project.id}/services/#{dashed_service}", user)
|
||||
it "deletes #{integration}" do
|
||||
delete api("/projects/#{project.id}/services/#{dashed_integration}", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:no_content)
|
||||
project.send(service_method).reload
|
||||
expect(project.send(service_method).activated?).to be_falsey
|
||||
project.send(integration_method).reload
|
||||
expect(project.send(integration_method).activated?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /projects/:id/services/#{service.dasherize}" do
|
||||
include_context service
|
||||
describe "GET /projects/:id/services/#{integration.dasherize}" do
|
||||
include_context integration
|
||||
|
||||
let!(:initialized_service) { initialize_integration(service, active: true) }
|
||||
let!(:initialized_integration) { initialize_integration(integration, active: true) }
|
||||
|
||||
let_it_be(:project2) do
|
||||
create(:project, creator_id: user.id, namespace: user.namespace)
|
||||
end
|
||||
|
||||
def deactive_service!
|
||||
return initialized_service.update!(active: false) unless initialized_service.is_a?(::Integrations::Prometheus)
|
||||
def deactive_integration!
|
||||
return initialized_integration.update!(active: false) unless initialized_integration.is_a?(::Integrations::Prometheus)
|
||||
|
||||
# Integrations::Prometheus sets `#active` itself within a `before_save`:
|
||||
initialized_service.manual_configuration = false
|
||||
initialized_service.save!
|
||||
initialized_integration.manual_configuration = false
|
||||
initialized_integration.save!
|
||||
end
|
||||
|
||||
it 'returns authentication error when unauthenticated' do
|
||||
get api("/projects/#{project.id}/services/#{dashed_service}")
|
||||
get api("/projects/#{project.id}/services/#{dashed_integration}")
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it "returns all properties of active service #{service}" do
|
||||
get api("/projects/#{project.id}/services/#{dashed_service}", user)
|
||||
it "returns all properties of active service #{integration}" do
|
||||
get api("/projects/#{project.id}/services/#{dashed_integration}", user)
|
||||
|
||||
expect(initialized_service).to be_active
|
||||
expect(initialized_integration).to be_active
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response['properties'].keys).to match_array(service_instance.api_field_names)
|
||||
expect(json_response['properties'].keys).to match_array(integration_instance.api_field_names)
|
||||
end
|
||||
|
||||
it "returns all properties of inactive integration #{service}" do
|
||||
deactive_service!
|
||||
it "returns all properties of inactive integration #{integration}" do
|
||||
deactive_integration!
|
||||
|
||||
get api("/projects/#{project.id}/services/#{dashed_service}", user)
|
||||
get api("/projects/#{project.id}/services/#{dashed_integration}", user)
|
||||
|
||||
expect(initialized_service).not_to be_active
|
||||
expect(initialized_integration).not_to be_active
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response['properties'].keys).to match_array(service_instance.api_field_names)
|
||||
expect(json_response['properties'].keys).to match_array(integration_instance.api_field_names)
|
||||
end
|
||||
|
||||
it "returns not found if integration does not exist" do
|
||||
get api("/projects/#{project2.id}/services/#{dashed_service}", user)
|
||||
get api("/projects/#{project2.id}/services/#{dashed_integration}", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(json_response['message']).to eq('404 Service Not Found')
|
||||
|
@ -160,10 +158,10 @@ RSpec.describe API::Services do
|
|||
|
||||
it "returns not found if service exists but is in `Project#disabled_integrations`" do
|
||||
expect_next_found_instance_of(Project) do |project|
|
||||
expect(project).to receive(:disabled_integrations).at_least(:once).and_return([service])
|
||||
expect(project).to receive(:disabled_integrations).at_least(:once).and_return([integration])
|
||||
end
|
||||
|
||||
get api("/projects/#{project.id}/services/#{dashed_service}", user)
|
||||
get api("/projects/#{project.id}/services/#{dashed_integration}", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(json_response['message']).to eq('404 Service Not Found')
|
||||
|
@ -171,7 +169,7 @@ RSpec.describe API::Services do
|
|||
|
||||
it "returns error when authenticated but not a project owner" do
|
||||
project.add_developer(user2)
|
||||
get api("/projects/#{project.id}/services/#{dashed_service}", user2)
|
||||
get api("/projects/#{project.id}/services/#{dashed_integration}", user2)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
|
@ -235,8 +233,8 @@ RSpec.describe API::Services do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'Slack Service' do
|
||||
let(:service_name) { 'slack_slash_commands' }
|
||||
describe 'Slack Integration' do
|
||||
let(:integration_name) { 'slack_slash_commands' }
|
||||
|
||||
before do
|
||||
project.create_slack_slash_commands_integration(
|
||||
|
@ -246,7 +244,7 @@ RSpec.describe API::Services do
|
|||
end
|
||||
|
||||
it 'returns status 200' do
|
||||
post api("/projects/#{project.id}/services/#{service_name}/trigger"), params: { token: 'token', text: 'help' }
|
||||
post api("/projects/#{project.id}/services/#{integration_name}/trigger"), params: { token: 'token', text: 'help' }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response['response_type']).to eq("ephemeral")
|
||||
|
@ -309,8 +307,8 @@ RSpec.describe API::Services do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'Hangouts Chat service' do
|
||||
let(:service_name) { 'hangouts-chat' }
|
||||
describe 'Hangouts Chat integration' do
|
||||
let(:integration_name) { 'hangouts-chat' }
|
||||
let(:params) do
|
||||
{
|
||||
webhook: 'https://hook.example.com',
|
||||
|
@ -326,21 +324,21 @@ RSpec.describe API::Services do
|
|||
end
|
||||
|
||||
it 'accepts branches_to_be_notified for update', :aggregate_failures do
|
||||
put api("/projects/#{project.id}/services/#{service_name}", user), params: params.merge(branches_to_be_notified: 'all')
|
||||
put api("/projects/#{project.id}/services/#{integration_name}", user), params: params.merge(branches_to_be_notified: 'all')
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response['properties']['branches_to_be_notified']).to eq('all')
|
||||
end
|
||||
|
||||
it 'only requires the webhook param' do
|
||||
put api("/projects/#{project.id}/services/#{service_name}", user), params: { webhook: 'https://hook.example.com' }
|
||||
put api("/projects/#{project.id}/services/#{integration_name}", user), params: { webhook: 'https://hook.example.com' }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Pipelines Email Integration' do
|
||||
let(:service_name) { 'pipelines-email' }
|
||||
let(:integration_name) { 'pipelines-email' }
|
||||
|
||||
context 'notify_only_broken_pipelines property was saved as a string' do
|
||||
before do
|
||||
|
@ -354,7 +352,7 @@ RSpec.describe API::Services do
|
|||
end
|
||||
|
||||
it 'returns boolean values for notify_only_broken_pipelines' do
|
||||
get api("/projects/#{project.id}/services/#{service_name}", user)
|
||||
get api("/projects/#{project.id}/services/#{integration_name}", user)
|
||||
|
||||
expect(json_response['properties']['notify_only_broken_pipelines']).to eq(true)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Merge Requests Diffs' do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
describe 'GET diffs_batch' do
|
||||
let(:headers) { {} }
|
||||
|
||||
shared_examples_for 'serializes diffs with expected arguments' do
|
||||
it 'serializes paginated merge request diff collection' do
|
||||
expect_next_instance_of(PaginatedDiffSerializer) do |instance|
|
||||
expect(instance).to receive(:represent)
|
||||
.with(an_instance_of(collection), expected_options)
|
||||
.and_call_original
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
def collection_arguments(pagination_data = {})
|
||||
{
|
||||
environment: nil,
|
||||
merge_request: merge_request,
|
||||
diff_view: :inline,
|
||||
merge_ref_head_diff: nil,
|
||||
pagination_data: {
|
||||
total_pages: nil
|
||||
}.merge(pagination_data)
|
||||
}
|
||||
end
|
||||
|
||||
def go(extra_params = {})
|
||||
params = {
|
||||
namespace_id: project.namespace.to_param,
|
||||
project_id: project,
|
||||
id: merge_request.iid,
|
||||
page: 0,
|
||||
per_page: 20,
|
||||
format: 'json'
|
||||
}
|
||||
|
||||
get diffs_batch_namespace_project_json_merge_request_path(params.merge(extra_params)), headers: headers
|
||||
end
|
||||
|
||||
context 'with caching', :use_clean_rails_memory_store_caching do
|
||||
subject { go(page: 0, per_page: 5) }
|
||||
|
||||
context 'when the request has not been cached' do
|
||||
it_behaves_like 'serializes diffs with expected arguments' do
|
||||
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
|
||||
let(:expected_options) { collection_arguments(total_pages: 20) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the request has already been cached' do
|
||||
before do
|
||||
go(page: 0, per_page: 5)
|
||||
end
|
||||
|
||||
it 'does not serialize diffs' do
|
||||
expect_next_instance_of(PaginatedDiffSerializer) do |instance|
|
||||
expect(instance).not_to receive(:represent)
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
context 'with the different pagination option' do
|
||||
subject { go(page: 5, per_page: 5) }
|
||||
|
||||
it_behaves_like 'serializes diffs with expected arguments' do
|
||||
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
|
||||
let(:expected_options) { collection_arguments(total_pages: 20) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with the different diff_view' do
|
||||
subject { go(page: 0, per_page: 5, view: :parallel) }
|
||||
|
||||
it_behaves_like 'serializes diffs with expected arguments' do
|
||||
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
|
||||
let(:expected_options) { collection_arguments(total_pages: 20).merge(diff_view: :parallel) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with the different expanded option' do
|
||||
subject { go(page: 0, per_page: 5, expanded: true ) }
|
||||
|
||||
it_behaves_like 'serializes diffs with expected arguments' do
|
||||
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
|
||||
let(:expected_options) { collection_arguments(total_pages: 20) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with the different ignore_whitespace_change option' do
|
||||
subject { go(page: 0, per_page: 5, w: 1) }
|
||||
|
||||
it_behaves_like 'serializes diffs with expected arguments' do
|
||||
let(:collection) { Gitlab::Diff::FileCollection::Compare }
|
||||
let(:expected_options) { collection_arguments(total_pages: 20) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the paths is given' do
|
||||
subject { go(page: 0, per_page: 5, paths: %w[README CHANGELOG]) }
|
||||
|
||||
it 'does not use cache' do
|
||||
expect(Rails.cache).not_to receive(:fetch).with(/cache:gitlab:PaginatedDiffSerializer/).and_call_original
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -158,6 +158,29 @@ RSpec.describe 'merge requests discussions' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the diff note position changes' do
|
||||
before do
|
||||
# This replicates a position change wherein timestamps aren't updated
|
||||
# which is why `Gitlab::Timeless.timeless` is utilized. This is the
|
||||
# same approach being used in Discussions::UpdateDiffPositionService
|
||||
# which is responsible for updating the positions of diff discussions
|
||||
# when MR updates.
|
||||
first_note.position = Gitlab::Diff::Position.new(
|
||||
old_path: first_note.position.old_path,
|
||||
new_path: first_note.position.new_path,
|
||||
old_line: first_note.position.old_line,
|
||||
new_line: first_note.position.new_line + 1,
|
||||
diff_refs: first_note.position.diff_refs
|
||||
)
|
||||
|
||||
Gitlab::Timeless.timeless(first_note, &:save)
|
||||
end
|
||||
|
||||
it_behaves_like 'cache miss' do
|
||||
let(:changed_notes) { [first_note, second_note] }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when merge_request_discussion_cache is disabled' do
|
||||
before do
|
||||
stub_feature_flags(merge_request_discussion_cache: false)
|
||||
|
|
|
@ -25,30 +25,6 @@ RSpec.describe MergeRequests::RebaseService do
|
|||
end
|
||||
|
||||
describe '#execute' do
|
||||
context 'when another rebase is already in progress' do
|
||||
before do
|
||||
allow(repository).to receive(:rebase_in_progress?).with(merge_request.id).and_return(true)
|
||||
end
|
||||
|
||||
it 'saves the error message' do
|
||||
service.execute(merge_request)
|
||||
|
||||
expect(merge_request.reload.merge_error).to eq 'Rebase task canceled: Another rebase is already in progress'
|
||||
end
|
||||
|
||||
it 'returns an error' do
|
||||
expect(service.execute(merge_request)).to match(status: :error,
|
||||
message: described_class::REBASE_ERROR)
|
||||
end
|
||||
|
||||
it 'clears rebase_jid' do
|
||||
expect { service.execute(merge_request) }
|
||||
.to change { merge_request.rebase_jid }
|
||||
.from(rebase_jid)
|
||||
.to(nil)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'sequence of failure and success' do
|
||||
it 'properly clears the error message' do
|
||||
allow(repository).to receive(:gitaly_operation_client).and_raise('Something went wrong')
|
||||
|
@ -150,6 +126,13 @@ RSpec.describe MergeRequests::RebaseService do
|
|||
|
||||
it_behaves_like 'a service that can execute a successful rebase'
|
||||
|
||||
it 'clears rebase_jid' do
|
||||
expect { service.execute(merge_request) }
|
||||
.to change(merge_request, :rebase_jid)
|
||||
.from(rebase_jid)
|
||||
.to(nil)
|
||||
end
|
||||
|
||||
context 'when skip_ci flag is set' do
|
||||
let(:skip_ci) { true }
|
||||
|
||||
|
|
|
@ -1,38 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Integration.available_integration_names.each do |service|
|
||||
RSpec.shared_context service do
|
||||
include JiraServiceHelper if service == 'jira'
|
||||
Integration.available_integration_names.each do |integration|
|
||||
RSpec.shared_context integration do
|
||||
include JiraServiceHelper if integration == 'jira'
|
||||
|
||||
let(:dashed_service) { service.dasherize }
|
||||
let(:service_method) { Project.integration_association_name(service) }
|
||||
let(:service_klass) { Integration.integration_name_to_model(service) }
|
||||
let(:service_instance) { service_klass.new }
|
||||
let(:service_fields) { service_instance.fields }
|
||||
let(:service_attrs_list) { service_fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } }
|
||||
let(:service_attrs) do
|
||||
service_attrs_list.inject({}) do |hash, k|
|
||||
let(:dashed_integration) { integration.dasherize }
|
||||
let(:integration_method) { Project.integration_association_name(integration) }
|
||||
let(:integration_klass) { Integration.integration_name_to_model(integration) }
|
||||
let(:integration_instance) { integration_klass.new }
|
||||
let(:integration_fields) { integration_instance.fields }
|
||||
let(:integration_attrs_list) { integration_fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } }
|
||||
let(:integration_attrs) do
|
||||
integration_attrs_list.inject({}) do |hash, k|
|
||||
if k =~ /^(token*|.*_token|.*_key)/
|
||||
hash.merge!(k => 'secrettoken')
|
||||
elsif service == 'confluence' && k == :confluence_url
|
||||
elsif integration == 'confluence' && k == :confluence_url
|
||||
hash.merge!(k => 'https://example.atlassian.net/wiki')
|
||||
elsif service == 'datadog' && k == :datadog_site
|
||||
elsif integration == 'datadog' && k == :datadog_site
|
||||
hash.merge!(k => 'datadoghq.com')
|
||||
elsif service == 'packagist' && k == :server
|
||||
elsif integration == 'packagist' && k == :server
|
||||
hash.merge!(k => 'https://packagist.example.com')
|
||||
elsif k =~ /^(.*_url|url|webhook)/
|
||||
hash.merge!(k => "http://example.com")
|
||||
elsif service_klass.method_defined?("#{k}?")
|
||||
elsif integration_klass.method_defined?("#{k}?")
|
||||
hash.merge!(k => true)
|
||||
elsif service == 'irker' && k == :recipients
|
||||
elsif integration == 'irker' && k == :recipients
|
||||
hash.merge!(k => 'irc://irc.network.net:666/#channel')
|
||||
elsif service == 'irker' && k == :server_port
|
||||
elsif integration == 'irker' && k == :server_port
|
||||
hash.merge!(k => 1234)
|
||||
elsif service == 'jira' && k == :jira_issue_transition_id
|
||||
elsif integration == 'jira' && k == :jira_issue_transition_id
|
||||
hash.merge!(k => '1,2,3')
|
||||
elsif service == 'emails_on_push' && k == :recipients
|
||||
elsif integration == 'emails_on_push' && k == :recipients
|
||||
hash.merge!(k => 'foo@bar.com')
|
||||
elsif service == 'slack' || service == 'mattermost' && k == :labels_to_be_notified_behavior
|
||||
elsif integration == 'slack' || integration == 'mattermost' && k == :labels_to_be_notified_behavior
|
||||
hash.merge!(k => "match_any")
|
||||
else
|
||||
hash.merge!(k => "someword")
|
||||
|
@ -47,24 +47,24 @@ Integration.available_integration_names.each do |service|
|
|||
end
|
||||
|
||||
before do
|
||||
enable_license_for_service(service)
|
||||
stub_jira_integration_test if service == 'jira'
|
||||
enable_license_for_integration(integration)
|
||||
stub_jira_integration_test if integration == 'jira'
|
||||
end
|
||||
|
||||
def initialize_integration(integration, attrs = {})
|
||||
record = project.find_or_initialize_integration(integration)
|
||||
record.attributes = attrs
|
||||
record.properties = service_attrs
|
||||
record.properties = integration_attrs
|
||||
record.save!
|
||||
record
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def enable_license_for_service(service)
|
||||
def enable_license_for_integration(integration)
|
||||
return unless respond_to?(:stub_licensed_features)
|
||||
|
||||
licensed_feature = licensed_features[service]
|
||||
licensed_feature = licensed_features[integration]
|
||||
return unless licensed_feature
|
||||
|
||||
stub_licensed_features(licensed_feature => true)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
RSpec.shared_examples_for 'value stream analytics event' do
|
||||
let(:params) { {} }
|
||||
let(:instance) { described_class.new(params) }
|
||||
let(:expected_hash_code) { Digest::SHA256.hexdigest(instance.class.identifier.to_s) }
|
||||
|
||||
it { expect(described_class.name).to be_a_kind_of(String) }
|
||||
it { expect(described_class.identifier).to be_a_kind_of(Symbol) }
|
||||
|
@ -19,4 +20,16 @@ RSpec.shared_examples_for 'value stream analytics event' do
|
|||
expect(output_query).to be_a_kind_of(ActiveRecord::Relation)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#hash_code' do
|
||||
it 'returns a hash that uniquely identifies an event' do
|
||||
expect(instance.hash_code).to eq(expected_hash_code)
|
||||
end
|
||||
|
||||
it 'does not differ when the same object is built with the same params' do
|
||||
another_instance_with_same_params = described_class.new(params)
|
||||
|
||||
expect(another_instance_with_same_params.hash_code).to eq(instance.hash_code)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -122,6 +122,22 @@ RSpec.shared_examples 'value stream analytics stage' do
|
|||
expect(stage.parent_id).to eq(parent.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#hash_code' do
|
||||
it 'does not differ when the same object is built with the same params' do
|
||||
stage_1 = build(factory)
|
||||
stage_2 = build(factory)
|
||||
|
||||
expect(stage_1.events_hash_code).to eq(stage_2.events_hash_code)
|
||||
end
|
||||
|
||||
it 'differs when the stage events are different' do
|
||||
stage_1 = build(factory, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged)
|
||||
stage_2 = build(factory, start_event_identifier: :issue_created, end_event_identifier: :issue_first_mentioned_in_commit)
|
||||
|
||||
expect(stage_1.events_hash_code).not_to eq(stage_2.events_hash_code)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'value stream analytics label based stage' do
|
||||
|
|
Loading…
Reference in New Issue