Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d05604c95a
commit
7985071975
|
@ -1139,9 +1139,6 @@ Rails/SaveBang:
|
|||
- 'spec/services/emails/confirm_service_spec.rb'
|
||||
- 'spec/services/groups/destroy_service_spec.rb'
|
||||
- 'spec/services/groups/import_export/import_service_spec.rb'
|
||||
- 'spec/services/issuable/bulk_update_service_spec.rb'
|
||||
- 'spec/services/issuable/clone/attributes_rewriter_spec.rb'
|
||||
- 'spec/services/issuable/common_system_notes_service_spec.rb'
|
||||
- 'spec/services/labels/promote_service_spec.rb'
|
||||
- 'spec/services/notes/create_service_spec.rb'
|
||||
- 'spec/services/notification_recipients/build_service_spec.rb'
|
||||
|
@ -1160,7 +1157,6 @@ Rails/SaveBang:
|
|||
- 'spec/services/projects/unlink_fork_service_spec.rb'
|
||||
- 'spec/services/projects/update_pages_service_spec.rb'
|
||||
- 'spec/services/projects/update_service_spec.rb'
|
||||
- 'spec/services/quick_actions/interpret_service_spec.rb'
|
||||
- 'spec/services/reset_project_cache_service_spec.rb'
|
||||
- 'spec/services/resource_events/change_milestone_service_spec.rb'
|
||||
- 'spec/services/system_hooks_service_spec.rb'
|
||||
|
|
|
@ -1 +1 @@
|
|||
2f16d97afa2e8accb4144f04e2e1e90bf4d1e9fb
|
||||
40de6ac3d3e6db6a5fd85b63ff3ae8f60aece271
|
||||
|
|
|
@ -209,15 +209,14 @@ export default {
|
|||
<div v-safe-html="errorMessage" class="nothing-here-block"></div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-show="isCollapsed" class="gl-p-7 gl-text-center collapsed-file-warning">
|
||||
<div
|
||||
v-show="isCollapsed"
|
||||
class="gl-p-7 gl-bg-gray-10 gl-text-center collapsed-file-warning"
|
||||
>
|
||||
<p class="gl-mb-8 gl-mt-5">
|
||||
{{ $options.i18n.collapsed }}
|
||||
</p>
|
||||
<gl-button
|
||||
class="gl-alert-action gl-mb-5"
|
||||
data-testid="expandButton"
|
||||
@click="handleToggle"
|
||||
>
|
||||
<gl-button class="gl-mb-5" data-testid="expandButton" @click="handleToggle">
|
||||
{{ $options.i18n.expand }}
|
||||
</gl-button>
|
||||
</div>
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
GlAvatar,
|
||||
GlTooltipDirective,
|
||||
GlButton,
|
||||
GlSearchBoxByType,
|
||||
GlIcon,
|
||||
GlPagination,
|
||||
GlTabs,
|
||||
|
@ -16,16 +15,25 @@ import {
|
|||
GlBadge,
|
||||
GlEmptyState,
|
||||
} from '@gitlab/ui';
|
||||
import { debounce } from 'lodash';
|
||||
import Api from '~/api';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
|
||||
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
|
||||
import { convertToSnakeCase } from '~/lib/utils/text_utility';
|
||||
import { s__ } from '~/locale';
|
||||
import { mergeUrlParams, joinPaths, visitUrl } from '~/lib/utils/url_utility';
|
||||
import { s__, __ } from '~/locale';
|
||||
import { urlParamsToObject } from '~/lib/utils/common_utils';
|
||||
import {
|
||||
visitUrl,
|
||||
mergeUrlParams,
|
||||
joinPaths,
|
||||
updateHistory,
|
||||
setUrlParams,
|
||||
} from '~/lib/utils/url_utility';
|
||||
import getIncidents from '../graphql/queries/get_incidents.query.graphql';
|
||||
import getIncidentsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
|
||||
import SeverityToken from '~/sidebar/components/severity/severity.vue';
|
||||
import { INCIDENT_SEVERITY } from '~/sidebar/components/severity/constants';
|
||||
import { I18N, DEFAULT_PAGE_SIZE, INCIDENT_SEARCH_DELAY, INCIDENT_STATUS_TABS } from '../constants';
|
||||
import { I18N, DEFAULT_PAGE_SIZE, INCIDENT_STATUS_TABS } from '../constants';
|
||||
|
||||
const TH_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' };
|
||||
const tdClass =
|
||||
|
@ -82,7 +90,6 @@ export default {
|
|||
GlAvatar,
|
||||
GlButton,
|
||||
TimeAgoTooltip,
|
||||
GlSearchBoxByType,
|
||||
GlIcon,
|
||||
GlPagination,
|
||||
GlTabs,
|
||||
|
@ -91,6 +98,7 @@ export default {
|
|||
GlBadge,
|
||||
GlEmptyState,
|
||||
SeverityToken,
|
||||
FilteredSearchBar,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
@ -103,6 +111,9 @@ export default {
|
|||
'issuePath',
|
||||
'publishedAvailable',
|
||||
'emptyListSvgPath',
|
||||
'textQuery',
|
||||
'authorUsernamesQuery',
|
||||
'assigneeUsernamesQuery',
|
||||
],
|
||||
apollo: {
|
||||
incidents: {
|
||||
|
@ -118,6 +129,8 @@ export default {
|
|||
lastPageSize: this.pagination.lastPageSize,
|
||||
prevPageCursor: this.pagination.prevPageCursor,
|
||||
nextPageCursor: this.pagination.nextPageCursor,
|
||||
authorUsername: this.authorUsername,
|
||||
assigneeUsernames: this.assigneeUsernames,
|
||||
};
|
||||
},
|
||||
update({ project: { issues: { nodes = [], pageInfo = {} } = {} } = {} }) {
|
||||
|
@ -135,6 +148,8 @@ export default {
|
|||
variables() {
|
||||
return {
|
||||
searchTerm: this.searchTerm,
|
||||
authorUsername: this.authorUsername,
|
||||
assigneeUsernames: this.assigneeUsernames,
|
||||
projectPath: this.projectPath,
|
||||
issueTypes: ['INCIDENT'],
|
||||
};
|
||||
|
@ -149,7 +164,7 @@ export default {
|
|||
errored: false,
|
||||
isErrorAlertDismissed: false,
|
||||
redirecting: false,
|
||||
searchTerm: '',
|
||||
searchTerm: this.textQuery,
|
||||
pagination: initialPaginationState,
|
||||
incidents: {},
|
||||
sort: 'created_desc',
|
||||
|
@ -157,6 +172,9 @@ export default {
|
|||
sortDesc: true,
|
||||
statusFilter: '',
|
||||
filteredByStatus: '',
|
||||
authorUsername: this.authorUsernamesQuery,
|
||||
assigneeUsernames: this.assigneeUsernamesQuery,
|
||||
filterParams: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -242,14 +260,57 @@ export default {
|
|||
btnText: createIncidentBtnLabel,
|
||||
};
|
||||
},
|
||||
filteredSearchTokens() {
|
||||
return [
|
||||
{
|
||||
type: 'author_username',
|
||||
icon: 'user',
|
||||
title: __('Author'),
|
||||
unique: true,
|
||||
symbol: '@',
|
||||
token: AuthorToken,
|
||||
operators: [{ value: '=', description: __('is'), default: 'true' }],
|
||||
fetchPath: this.projectPath,
|
||||
fetchAuthors: Api.projectUsers.bind(Api),
|
||||
},
|
||||
{
|
||||
type: 'assignee_username',
|
||||
icon: 'user',
|
||||
title: __('Assignees'),
|
||||
unique: true,
|
||||
symbol: '@',
|
||||
token: AuthorToken,
|
||||
operators: [{ value: '=', description: __('is'), default: 'true' }],
|
||||
fetchPath: this.projectPath,
|
||||
fetchAuthors: Api.projectUsers.bind(Api),
|
||||
},
|
||||
];
|
||||
},
|
||||
filteredSearchValue() {
|
||||
const value = [];
|
||||
|
||||
if (this.authorUsername) {
|
||||
value.push({
|
||||
type: 'author_username',
|
||||
value: { data: this.authorUsername },
|
||||
});
|
||||
}
|
||||
|
||||
if (this.assigneeUsernames) {
|
||||
value.push({
|
||||
type: 'assignee_username',
|
||||
value: { data: this.assigneeUsernames },
|
||||
});
|
||||
}
|
||||
|
||||
if (this.searchTerm) {
|
||||
value.push(this.searchTerm);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onInputChange: debounce(function debounceSearch(input) {
|
||||
const trimmedInput = input.trim();
|
||||
if (trimmedInput !== this.searchTerm) {
|
||||
this.searchTerm = trimmedInput;
|
||||
}
|
||||
}, INCIDENT_SEARCH_DELAY),
|
||||
filterIncidentsByStatus(tabIndex) {
|
||||
const { filters, status } = this.$options.statusTabs[tabIndex];
|
||||
this.statusFilter = filters;
|
||||
|
@ -292,6 +353,61 @@ export default {
|
|||
getSeverity(severity) {
|
||||
return INCIDENT_SEVERITY[severity];
|
||||
},
|
||||
handleFilterIncidents(filters) {
|
||||
const filterParams = { authorUsername: '', assigneeUsername: [], search: '' };
|
||||
|
||||
filters.forEach(filter => {
|
||||
if (typeof filter === 'object') {
|
||||
switch (filter.type) {
|
||||
case 'author_username':
|
||||
filterParams.authorUsername = filter.value.data;
|
||||
break;
|
||||
case 'assignee_username':
|
||||
filterParams.assigneeUsername.push(filter.value.data);
|
||||
break;
|
||||
case 'filtered-search-term':
|
||||
if (filter.value.data !== '') filterParams.search = filter.value.data;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.filterParams = filterParams;
|
||||
this.updateUrl();
|
||||
this.searchTerm = filterParams?.search;
|
||||
this.authorUsername = filterParams?.authorUsername;
|
||||
this.assigneeUsernames = filterParams?.assigneeUsername;
|
||||
},
|
||||
updateUrl() {
|
||||
const queryParams = urlParamsToObject(window.location.search);
|
||||
const { authorUsername, assigneeUsername, search } = this.filterParams || {};
|
||||
|
||||
if (authorUsername) {
|
||||
queryParams.author_username = authorUsername;
|
||||
} else {
|
||||
delete queryParams.author_username;
|
||||
}
|
||||
|
||||
if (assigneeUsername) {
|
||||
queryParams.assignee_username = assigneeUsername;
|
||||
} else {
|
||||
delete queryParams.assignee_username;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
queryParams.search = search;
|
||||
} else {
|
||||
delete queryParams.search;
|
||||
}
|
||||
|
||||
updateHistory({
|
||||
url: setUrlParams(queryParams, window.location.href, true),
|
||||
title: document.title,
|
||||
replace: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -331,12 +447,16 @@ export default {
|
|||
</gl-button>
|
||||
</div>
|
||||
|
||||
<div class="gl-bg-gray-10 gl-p-5 gl-border-b-solid gl-border-b-1 gl-border-gray-100">
|
||||
<gl-search-box-by-type
|
||||
:value="searchTerm"
|
||||
class="gl-bg-white"
|
||||
:placeholder="$options.i18n.searchPlaceholder"
|
||||
@input="onInputChange"
|
||||
<div class="filtered-search-wrapper">
|
||||
<filtered-search-bar
|
||||
:namespace="projectPath"
|
||||
:search-input-placeholder="$options.i18n.searchPlaceholder"
|
||||
:tokens="filteredSearchTokens"
|
||||
:initial-filter-value="filteredSearchValue"
|
||||
initial-sortby="created_desc"
|
||||
recent-searches-storage-key="incidents"
|
||||
class="row-content-block"
|
||||
@onFilter="handleFilterIncidents"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ export const I18N = {
|
|||
unassigned: s__('IncidentManagement|Unassigned'),
|
||||
createIncidentBtnLabel: s__('IncidentManagement|Create incident'),
|
||||
unPublished: s__('IncidentManagement|Unpublished'),
|
||||
searchPlaceholder: __('Search results…'),
|
||||
searchPlaceholder: __('Search or filter results…'),
|
||||
emptyState: {
|
||||
title: s__('IncidentManagement|Display your incidents in a dedicated view'),
|
||||
emptyClosedTabTitle: s__('IncidentManagement|There are no closed incidents'),
|
||||
|
@ -34,5 +34,4 @@ export const INCIDENT_STATUS_TABS = [
|
|||
},
|
||||
];
|
||||
|
||||
export const INCIDENT_SEARCH_DELAY = 300;
|
||||
export const DEFAULT_PAGE_SIZE = 20;
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
query getIncidentsCountByStatus($searchTerm: String, $projectPath: ID!, $issueTypes: [IssueType!]) {
|
||||
query getIncidentsCountByStatus(
|
||||
$searchTerm: String
|
||||
$projectPath: ID!
|
||||
$issueTypes: [IssueType!]
|
||||
$authorUsername: String = ""
|
||||
$assigneeUsernames: [String!] = []
|
||||
) {
|
||||
project(fullPath: $projectPath) {
|
||||
issueStatusCounts(search: $searchTerm, types: $issueTypes) {
|
||||
issueStatusCounts(
|
||||
search: $searchTerm
|
||||
types: $issueTypes
|
||||
authorUsername: $authorUsername
|
||||
assigneeUsername: $assigneeUsernames
|
||||
) {
|
||||
all
|
||||
opened
|
||||
closed
|
||||
|
|
|
@ -9,7 +9,9 @@ query getIncidents(
|
|||
$lastPageSize: Int
|
||||
$prevPageCursor: String = ""
|
||||
$nextPageCursor: String = ""
|
||||
$searchTerm: String
|
||||
$searchTerm: String = ""
|
||||
$authorUsername: String = ""
|
||||
$assigneeUsernames: [String!] = []
|
||||
) {
|
||||
project(fullPath: $projectPath) {
|
||||
issues(
|
||||
|
@ -17,6 +19,8 @@ query getIncidents(
|
|||
types: $issueTypes
|
||||
sort: $sort
|
||||
state: $status
|
||||
authorUsername: $authorUsername
|
||||
assigneeUsername: $assigneeUsernames
|
||||
first: $firstPageSize
|
||||
last: $lastPageSize
|
||||
after: $nextPageCursor
|
||||
|
|
|
@ -16,6 +16,9 @@ export default () => {
|
|||
issuePath,
|
||||
publishedAvailable,
|
||||
emptyListSvgPath,
|
||||
textQuery,
|
||||
authorUsernamesQuery,
|
||||
assigneeUsernamesQuery,
|
||||
} = domEl.dataset;
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
|
@ -32,6 +35,9 @@ export default () => {
|
|||
issuePath,
|
||||
publishedAvailable,
|
||||
emptyListSvgPath,
|
||||
textQuery,
|
||||
authorUsernamesQuery,
|
||||
assigneeUsernamesQuery,
|
||||
},
|
||||
apolloProvider,
|
||||
components: {
|
||||
|
|
|
@ -13,9 +13,9 @@ import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptio
|
|||
import SidebarSeverity from './components/severity/sidebar_severity.vue';
|
||||
import Translate from '../vue_shared/translate';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import { store } from '~/notes/stores';
|
||||
import { isInIssuePage, parseBoolean } from '~/lib/utils/common_utils';
|
||||
import mergeRequestStore from '~/mr_notes/stores';
|
||||
import createFlash from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
@ -89,47 +89,72 @@ function mountConfidentialComponent(mediator) {
|
|||
const dataNode = document.getElementById('js-confidential-issue-data');
|
||||
const initialData = JSON.parse(dataNode.innerHTML);
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
store,
|
||||
components: {
|
||||
ConfidentialIssueSidebar,
|
||||
},
|
||||
render: createElement =>
|
||||
createElement('confidential-issue-sidebar', {
|
||||
props: {
|
||||
iid: String(iid),
|
||||
fullPath,
|
||||
isEditable: initialData.is_editable,
|
||||
service: mediator.service,
|
||||
},
|
||||
}),
|
||||
});
|
||||
import(/* webpackChunkName: 'notesStore' */ '~/notes/stores')
|
||||
.then(
|
||||
({ store }) =>
|
||||
new Vue({
|
||||
el,
|
||||
store,
|
||||
components: {
|
||||
ConfidentialIssueSidebar,
|
||||
},
|
||||
render: createElement =>
|
||||
createElement('confidential-issue-sidebar', {
|
||||
props: {
|
||||
iid: String(iid),
|
||||
fullPath,
|
||||
isEditable: initialData.is_editable,
|
||||
service: mediator.service,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.catch(() => {
|
||||
createFlash({ message: __('Failed to load sidebar confidential toggle') });
|
||||
});
|
||||
}
|
||||
|
||||
function mountLockComponent() {
|
||||
const el = document.getElementById('js-lock-entry-point');
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { fullPath } = getSidebarOptions();
|
||||
|
||||
const dataNode = document.getElementById('js-lock-issue-data');
|
||||
const initialData = JSON.parse(dataNode.innerHTML);
|
||||
|
||||
return el
|
||||
? new Vue({
|
||||
el,
|
||||
store: isInIssuePage() ? store : mergeRequestStore,
|
||||
provide: {
|
||||
fullPath,
|
||||
},
|
||||
render: createElement =>
|
||||
createElement(IssuableLockForm, {
|
||||
props: {
|
||||
isEditable: initialData.is_editable,
|
||||
},
|
||||
}),
|
||||
})
|
||||
: undefined;
|
||||
let importStore;
|
||||
if (isInIssuePage()) {
|
||||
importStore = import(/* webpackChunkName: 'notesStore' */ '~/notes/stores').then(
|
||||
({ store }) => store,
|
||||
);
|
||||
} else {
|
||||
importStore = import(/* webpackChunkName: 'mrNotesStore' */ '~/mr_notes/stores');
|
||||
}
|
||||
|
||||
importStore
|
||||
.then(
|
||||
store =>
|
||||
new Vue({
|
||||
el,
|
||||
store,
|
||||
provide: {
|
||||
fullPath,
|
||||
},
|
||||
render: createElement =>
|
||||
createElement(IssuableLockForm, {
|
||||
props: {
|
||||
isEditable: initialData.is_editable,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.catch(() => {
|
||||
createFlash({ message: __('Failed to load sidebar lock status') });
|
||||
});
|
||||
}
|
||||
|
||||
function mountParticipantsComponent(mediator) {
|
||||
|
@ -219,7 +244,7 @@ function mountSeverityComponent() {
|
|||
export function mountSidebar(mediator) {
|
||||
mountAssigneesComponent(mediator);
|
||||
mountConfidentialComponent(mediator);
|
||||
mountLockComponent(mediator);
|
||||
mountLockComponent();
|
||||
mountParticipantsComponent(mediator);
|
||||
mountSubscriptionsComponent(mediator);
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
before_action only: :index do
|
||||
push_frontend_feature_flag(:scoped_labels, @project)
|
||||
push_frontend_feature_flag(:scoped_labels, @project, type: :licensed)
|
||||
end
|
||||
|
||||
around_action :allow_gitaly_ref_name_caching, only: [:discussions]
|
||||
|
|
|
@ -12,7 +12,7 @@ class Projects::ServicesController < Projects::ApplicationController
|
|||
before_action :set_deprecation_notice_for_prometheus_service, only: [:edit, :update]
|
||||
before_action :redirect_deprecated_prometheus_service, only: [:update]
|
||||
before_action only: :edit do
|
||||
push_frontend_feature_flag(:jira_issues_integration, @project, { default_enabled: true })
|
||||
push_frontend_feature_flag(:jira_issues_integration, @project, type: :licensed, default_enabled: true)
|
||||
end
|
||||
|
||||
respond_to :html
|
||||
|
|
|
@ -18,7 +18,10 @@ module IssueResolverArguments
|
|||
argument :milestone_title, GraphQL::STRING_TYPE.to_list_type,
|
||||
required: false,
|
||||
description: 'Milestone applied to this issue'
|
||||
argument :assignee_username, GraphQL::STRING_TYPE,
|
||||
argument :author_username, GraphQL::STRING_TYPE,
|
||||
required: false,
|
||||
description: 'Username of the author of the issue'
|
||||
argument :assignee_username, [GraphQL::STRING_TYPE],
|
||||
required: false,
|
||||
description: 'Username of a user assigned to the issue'
|
||||
argument :assignee_id, GraphQL::STRING_TYPE,
|
||||
|
|
|
@ -218,8 +218,28 @@ module EmailsHelper
|
|||
_('Please contact your administrator with any questions.')
|
||||
end
|
||||
|
||||
def change_reviewer_notification_text(new_reviewers, previous_reviewers, html_tag = nil)
|
||||
new = new_reviewers.any? ? users_to_sentence(new_reviewers) : s_('ChangeReviewer|Unassigned')
|
||||
old = previous_reviewers.any? ? users_to_sentence(previous_reviewers) : nil
|
||||
|
||||
if html_tag.present?
|
||||
new = content_tag(html_tag, new)
|
||||
old = content_tag(html_tag, old) if old.present?
|
||||
end
|
||||
|
||||
if old.present?
|
||||
s_('ChangeReviewer|Reviewer changed from %{old} to %{new}').html_safe % { old: old, new: new }
|
||||
else
|
||||
s_('ChangeReviewer|Reviewer changed to %{new}').html_safe % { new: new }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def users_to_sentence(users)
|
||||
sanitize_name(users.map(&:name).to_sentence)
|
||||
end
|
||||
|
||||
def generate_link(text, url)
|
||||
link_to(text, url, target: :_blank, rel: 'noopener noreferrer')
|
||||
end
|
||||
|
|
|
@ -109,10 +109,6 @@ module MergeRequestsHelper
|
|||
@merge_request_diffs.size - @merge_request_diffs.index(merge_request_diff)
|
||||
end
|
||||
|
||||
def different_base?(version1, version2)
|
||||
version1 && version2 && version1.base_commit_sha != version2.base_commit_sha
|
||||
end
|
||||
|
||||
def merge_params(merge_request)
|
||||
{
|
||||
auto_merge_strategy: AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS,
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Projects::IncidentsHelper
|
||||
def incidents_data(project)
|
||||
def incidents_data(project, params)
|
||||
{
|
||||
'project-path' => project.full_path,
|
||||
'new-issue-path' => new_project_issue_path(project),
|
||||
'incident-template-name' => 'incident',
|
||||
'incident-type' => 'incident',
|
||||
'issue-path' => project_issues_path(project),
|
||||
'empty-list-svg-path' => image_path('illustrations/incident-empty-state.svg')
|
||||
'empty-list-svg-path' => image_path('illustrations/incident-empty-state.svg'),
|
||||
'text-query': params[:search],
|
||||
'author-usernames-query': params[:author_username],
|
||||
'assignee-usernames-query': params[:assignee_username]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,6 +16,7 @@ module TodosHelper
|
|||
def todo_action_name(todo)
|
||||
case todo.action
|
||||
when Todo::ASSIGNED then todo.self_added? ? 'assigned' : 'assigned you'
|
||||
when Todo::REVIEW_REQUESTED then 'requested a review of'
|
||||
when Todo::MENTIONED then "mentioned #{todo_action_subject(todo)} on"
|
||||
when Todo::BUILD_FAILED then 'The build failed for'
|
||||
when Todo::MARKED then 'added a todo for'
|
||||
|
@ -26,6 +27,13 @@ module TodosHelper
|
|||
end
|
||||
end
|
||||
|
||||
def todo_self_addressing(todo)
|
||||
case todo.action
|
||||
when Todo::ASSIGNED then 'to yourself'
|
||||
when Todo::REVIEW_REQUESTED then 'from yourself'
|
||||
end
|
||||
end
|
||||
|
||||
def todo_target_link(todo)
|
||||
text = raw(todo_target_type_name(todo) + ' ') +
|
||||
if todo.for_commit?
|
||||
|
@ -141,6 +149,7 @@ module TodosHelper
|
|||
[
|
||||
{ id: '', text: 'Any Action' },
|
||||
{ id: Todo::ASSIGNED, text: 'Assigned' },
|
||||
{ id: Todo::REVIEW_REQUESTED, text: 'Review requested' },
|
||||
{ id: Todo::MENTIONED, text: 'Mentioned' },
|
||||
{ id: Todo::MARKED, text: 'Added' },
|
||||
{ id: Todo::BUILD_FAILED, text: 'Pipelines' },
|
||||
|
|
|
@ -34,6 +34,17 @@ module Emails
|
|||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def changed_reviewer_of_merge_request_email(recipient_id, merge_request_id, previous_reviewer_ids, updated_by_user_id, reason = nil)
|
||||
setup_merge_request_mail(merge_request_id, recipient_id)
|
||||
|
||||
@previous_reviewers = []
|
||||
@previous_reviewers = User.where(id: previous_reviewer_ids) if previous_reviewer_ids.any?
|
||||
|
||||
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def relabeled_merge_request_email(recipient_id, merge_request_id, label_names, updated_by_user_id, reason = nil)
|
||||
setup_merge_request_mail(merge_request_id, recipient_id)
|
||||
|
||||
|
|
|
@ -121,6 +121,8 @@ class MergeRequest < ApplicationRecord
|
|||
# when creating new merge request
|
||||
attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
|
||||
|
||||
participant :reviewers
|
||||
|
||||
# Keep states definition to be evaluated before the state_machine block to avoid spec failures.
|
||||
# If this gets evaluated after, the `merged` and `locked` states which are overrided can be nil.
|
||||
def self.available_state_names
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
class NotificationReason
|
||||
OWN_ACTIVITY = 'own_activity'
|
||||
ASSIGNED = 'assigned'
|
||||
REVIEW_REQUESTED = 'review_requested'
|
||||
MENTIONED = 'mentioned'
|
||||
SUBSCRIBED = 'subscribed'
|
||||
|
||||
|
@ -12,6 +13,7 @@ class NotificationReason
|
|||
REASON_PRIORITY = [
|
||||
OWN_ACTIVITY,
|
||||
ASSIGNED,
|
||||
REVIEW_REQUESTED,
|
||||
MENTIONED,
|
||||
SUBSCRIBED
|
||||
].freeze
|
||||
|
|
|
@ -43,6 +43,7 @@ class NotificationSetting < ApplicationRecord
|
|||
:reopen_merge_request,
|
||||
:close_merge_request,
|
||||
:reassign_merge_request,
|
||||
:change_reviewer_merge_request,
|
||||
:merge_merge_request,
|
||||
:failed_pipeline,
|
||||
:fixed_pipeline,
|
||||
|
|
|
@ -227,7 +227,7 @@ class Todo < ApplicationRecord
|
|||
end
|
||||
|
||||
def self_assigned?
|
||||
assigned? && self_added?
|
||||
self_added? && (assigned? || review_requested?)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -365,6 +365,7 @@ class IssuableBaseService < BaseService
|
|||
}
|
||||
associations[:total_time_spent] = issuable.total_time_spent if issuable.respond_to?(:total_time_spent)
|
||||
associations[:description] = issuable.description
|
||||
associations[:reviewers] = issuable.reviewers.to_a if issuable.allows_reviewers?
|
||||
|
||||
associations
|
||||
end
|
||||
|
|
|
@ -112,6 +112,7 @@ module MergeRequests
|
|||
end
|
||||
|
||||
def handle_reviewers_change(merge_request, old_reviewers)
|
||||
notification_service.async.changed_reviewer_of_merge_request(merge_request, current_user, old_reviewers)
|
||||
todo_service.reassigned_reviewable(merge_request, current_user, old_reviewers)
|
||||
end
|
||||
|
||||
|
|
|
@ -34,6 +34,9 @@ module NotificationRecipients
|
|||
when :reassign_merge_request, :reassign_issue
|
||||
add_recipients(previous_assignees, :mention, nil)
|
||||
add_recipients(target.assignees, :mention, NotificationReason::ASSIGNED)
|
||||
when :change_reviewer_merge_request
|
||||
add_recipients(previous_assignees, :mention, nil)
|
||||
add_recipients(target.reviewers, :mention, NotificationReason::REVIEW_REQUESTED)
|
||||
end
|
||||
|
||||
add_subscribed_users
|
||||
|
|
|
@ -238,6 +238,33 @@ class NotificationService
|
|||
end
|
||||
end
|
||||
|
||||
# When we change reviewer in a merge_request we should send an email to:
|
||||
#
|
||||
# * merge_request old reviewers if their notification level is not Disabled
|
||||
# * merge_request new reviewers if their notification level is not Disabled
|
||||
# * users with custom level checked with "change reviewer merge request"
|
||||
#
|
||||
def changed_reviewer_of_merge_request(merge_request, current_user, previous_reviewers = [])
|
||||
recipients = NotificationRecipients::BuildService.build_recipients(
|
||||
merge_request,
|
||||
current_user,
|
||||
action: "change_reviewer",
|
||||
previous_assignees: previous_reviewers
|
||||
)
|
||||
|
||||
previous_reviewer_ids = previous_reviewers.map(&:id)
|
||||
|
||||
recipients.each do |recipient|
|
||||
mailer.changed_reviewer_of_merge_request_email(
|
||||
recipient.user.id,
|
||||
merge_request.id,
|
||||
previous_reviewer_ids,
|
||||
current_user.id,
|
||||
recipient.reason
|
||||
).deliver_later
|
||||
end
|
||||
end
|
||||
|
||||
# When we add labels to a merge request we should send an email to:
|
||||
#
|
||||
# * watchers of the mr's labels
|
||||
|
|
|
@ -2,12 +2,14 @@
|
|||
|
||||
module Projects
|
||||
class UpdateRemoteMirrorService < BaseService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
MAX_TRIES = 3
|
||||
|
||||
def execute(remote_mirror, tries)
|
||||
return success unless remote_mirror.enabled?
|
||||
|
||||
if Gitlab::UrlBlocker.blocked_url?(CGI.unescape(Gitlab::UrlSanitizer.sanitize(remote_mirror.url)))
|
||||
if Gitlab::UrlBlocker.blocked_url?(normalized_url(remote_mirror.url))
|
||||
return error("The remote mirror URL is invalid.")
|
||||
end
|
||||
|
||||
|
@ -27,6 +29,12 @@ module Projects
|
|||
|
||||
private
|
||||
|
||||
def normalized_url(url)
|
||||
strong_memoize(:normalized_url) do
|
||||
CGI.unescape(Gitlab::UrlSanitizer.sanitize(url))
|
||||
end
|
||||
end
|
||||
|
||||
def update_mirror(remote_mirror)
|
||||
remote_mirror.update_start!
|
||||
remote_mirror.ensure_remote!
|
||||
|
|
|
@ -13,8 +13,9 @@
|
|||
- if @project.last_repository_check_failed?
|
||||
.row
|
||||
.col-md-12
|
||||
.card
|
||||
.card-header.alert.alert-danger
|
||||
.gl-alert.gl-alert-danger.gl-mb-5
|
||||
= sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
|
||||
.gl-alert-body
|
||||
- last_check_message = _("Last repository check (%{last_check_timestamp}) failed. See the 'repocheck.log' file for error messages.")
|
||||
- last_check_message = last_check_message % { last_check_timestamp: time_ago_with_tooltip(@project.last_repository_check_at) }
|
||||
= last_check_message.html_safe
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
|
||||
- if todo.self_assigned?
|
||||
%span.title-item.action-name
|
||||
to yourself
|
||||
= todo_self_addressing(todo)
|
||||
|
||||
%span.title-item
|
||||
·
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
%p
|
||||
= change_reviewer_notification_text(@merge_request.reviewers, @previous_reviewers, :strong)
|
|
@ -0,0 +1 @@
|
|||
<%= change_reviewer_notification_text(@merge_request.reviewers, @previous_reviewers) %>
|
|
@ -1,3 +1,3 @@
|
|||
- page_title _('Incidents')
|
||||
|
||||
#js-incidents{ data: incidents_data(@project) }
|
||||
#js-incidents{ data: incidents_data(@project, params) }
|
||||
|
|
|
@ -14,13 +14,13 @@
|
|||
.col-sm-10
|
||||
%p.text-success.gl-mt-3
|
||||
= s_('PrometheusService|Prometheus is being automatically managed on your clusters')
|
||||
= link_to s_('PrometheusService|Manage clusters'), project_clusters_path(project), class: 'btn'
|
||||
= link_to s_('PrometheusService|Manage clusters'), project_clusters_path(project), class: 'btn gl-button'
|
||||
- else
|
||||
.col-sm-2
|
||||
= image_tag 'illustrations/monitoring/loading.svg'
|
||||
.col-sm-10
|
||||
%p.gl-mt-3
|
||||
= s_('PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments')
|
||||
= link_to s_('PrometheusService|Install Prometheus on clusters'), project_clusters_path(project), class: 'btn btn-success'
|
||||
= link_to s_('PrometheusService|Install Prometheus on clusters'), project_clusters_path(project), class: 'btn gl-button btn-success'
|
||||
|
||||
%hr
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
-# haml-lint:disable NoPlainNodes
|
||||
%span.badge.badge-pill.js-custom-monitored-count 0
|
||||
-# haml-lint:enable NoPlainNodes
|
||||
= link_to s_('PrometheusService|New metric'), new_project_prometheus_metric_path(project), class: 'btn btn-success js-new-metric-button hidden', data: { qa_selector: 'new_metric_button' }
|
||||
= link_to s_('PrometheusService|New metric'), new_project_prometheus_metric_path(project), class: 'btn gl-button btn-success js-new-metric-button hidden', data: { qa_selector: 'new_metric_button' }
|
||||
.card-body
|
||||
.flash-container.hidden
|
||||
.flash-warning
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Resolve Add filter capabilities to Incident list
|
||||
merge_request: 42377
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Replace bootstrap alerts in app/views/admin/projects/show.html.haml
|
||||
merge_request: 41389
|
||||
author: Gilang Gumilar
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add notification setting for merge request reviewers
|
||||
merge_request: 41851
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Cleanup request http method/code metrics
|
||||
merge_request: 42618
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Color/position tweaks for collapsed diff files
|
||||
merge_request: 42465
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix Rails/SaveBang offenses for spec files in spec/services/issuable/*
|
||||
merge_request: 42780
|
||||
author: Rajendra Kadam
|
||||
type: other
|
|
@ -28,6 +28,8 @@ development:
|
|||
username: postgres
|
||||
password: "secure password"
|
||||
host: localhost
|
||||
variables:
|
||||
statement_timeout: 15s
|
||||
|
||||
#
|
||||
# Staging specific
|
||||
|
@ -51,3 +53,5 @@ test: &test
|
|||
password:
|
||||
host: localhost
|
||||
prepared_statements: false
|
||||
variables:
|
||||
statement_timeout: 15s
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: additional_snowplow_tracking
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/12088
|
||||
rollout_issue_url:
|
||||
group: group::telemetry
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: approval_rule
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
type: development
|
||||
default_enabled: true
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: code_navigation
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
type: development
|
||||
default_enabled: true
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: coverage_report_view
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
type: development
|
||||
default_enabled: true
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: dag_pipeline_tab
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
type: development
|
||||
default_enabled: true
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: default_merge_ref_for_diffs
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
type: development
|
||||
group:
|
||||
default_enabled: false
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: graphql_releases_page
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
type: development
|
||||
default_enabled: true
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: invite_email_experiment
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39628
|
||||
rollout_issue_url:
|
||||
group: group::acquisition
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: merge_ref_head_comments
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
type: development
|
||||
default_enabled: true
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: suggest_pipeline
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25547
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/212896
|
||||
group: group::expansion
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: usage_data_a_compliance_audit_events_api
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
type: development
|
||||
default_enabled: false
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: usage_data_i_source_code_code_intelligence
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
type: development
|
||||
default_enabled: true
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: vue_sidebar_labels
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
type: development
|
||||
default_enabled: false
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: whats_new_drawer
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38975
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/254186
|
||||
group: group::retention
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
|
@ -17,6 +17,13 @@ unless Gitlab::Runtime.sidekiq?
|
|||
data[:duration_s] = Gitlab::Utils.ms_to_round_sec(data.delete(:duration)) if data[:duration]
|
||||
data.merge!(::Gitlab::Metrics::Subscribers::ActiveRecord.db_counter_payload)
|
||||
|
||||
# Remove empty hashes to prevent type mismatches
|
||||
# These are set to empty hashes in Lograge's ActionCable subscriber
|
||||
# https://github.com/roidrage/lograge/blob/v0.11.2/lib/lograge/log_subscribers/action_cable.rb#L14-L16
|
||||
%i(method path format).each do |key|
|
||||
data[key] = nil if data[key] == {}
|
||||
end
|
||||
|
||||
data
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddChangeReviewerMergeRequestToNotificationSettings < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :notification_settings, :change_reviewer_merge_request, :boolean
|
||||
end
|
||||
end
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveDuplicatedCsFindingsWithoutVulnerabilityId < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
BATCH_SIZE = 1_000
|
||||
|
||||
INTERVAL = 2.minutes
|
||||
|
||||
# 1_500 records will be deleted
|
||||
def up
|
||||
return unless Gitlab.com?
|
||||
|
||||
migration = Gitlab::BackgroundMigration::RemoveDuplicatedCsFindingsWithoutVulnerabilityId
|
||||
migration_name = migration.to_s.demodulize
|
||||
relation = migration::Finding.container_scanning.with_broken_fingerprint.where(vulnerability_id: nil)
|
||||
queue_background_migration_jobs_by_range_at_intervals(relation,
|
||||
migration_name,
|
||||
INTERVAL,
|
||||
batch_size: BATCH_SIZE)
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
8b2090e953e6205b65555408a88d3da7f6bce28b0baa52d1a43a3a3e8001b7e1
|
|
@ -0,0 +1 @@
|
|||
8d9e75f7c6344b03cb740fa691fcbb5bea1751802741229158701bc1af975897
|
|
@ -13727,7 +13727,8 @@ CREATE TABLE notification_settings (
|
|||
notification_email character varying,
|
||||
fixed_pipeline boolean,
|
||||
new_release boolean,
|
||||
moved_project boolean DEFAULT true NOT NULL
|
||||
moved_project boolean DEFAULT true NOT NULL,
|
||||
change_reviewer_merge_request boolean
|
||||
);
|
||||
|
||||
CREATE SEQUENCE notification_settings_id_seq
|
||||
|
|
|
@ -100,9 +100,9 @@ The ActionCable connection or channel class is used as the `controller`.
|
|||
|
||||
```json
|
||||
{
|
||||
"method":{},
|
||||
"path":{},
|
||||
"format":{},
|
||||
"method":null,
|
||||
"path":null,
|
||||
"format":null,
|
||||
"controller":"IssuesChannel",
|
||||
"action":"subscribe",
|
||||
"status":200,
|
||||
|
|
|
@ -6778,7 +6778,12 @@ type Group {
|
|||
"""
|
||||
Username of a user assigned to the issue
|
||||
"""
|
||||
assigneeUsername: String
|
||||
assigneeUsername: [String!]
|
||||
|
||||
"""
|
||||
Username of the author of the issue
|
||||
"""
|
||||
authorUsername: String
|
||||
|
||||
"""
|
||||
Returns the elements in the list that come before the specified cursor.
|
||||
|
@ -12248,7 +12253,12 @@ type Project {
|
|||
"""
|
||||
Username of a user assigned to the issue
|
||||
"""
|
||||
assigneeUsername: String
|
||||
assigneeUsername: [String!]
|
||||
|
||||
"""
|
||||
Username of the author of the issue
|
||||
"""
|
||||
authorUsername: String
|
||||
|
||||
"""
|
||||
Issues closed after this date
|
||||
|
@ -12338,7 +12348,12 @@ type Project {
|
|||
"""
|
||||
Username of a user assigned to the issue
|
||||
"""
|
||||
assigneeUsername: String
|
||||
assigneeUsername: [String!]
|
||||
|
||||
"""
|
||||
Username of the author of the issue
|
||||
"""
|
||||
authorUsername: String
|
||||
|
||||
"""
|
||||
Issues closed after this date
|
||||
|
@ -12418,7 +12433,12 @@ type Project {
|
|||
"""
|
||||
Username of a user assigned to the issue
|
||||
"""
|
||||
assigneeUsername: String
|
||||
assigneeUsername: [String!]
|
||||
|
||||
"""
|
||||
Username of the author of the issue
|
||||
"""
|
||||
authorUsername: String
|
||||
|
||||
"""
|
||||
Returns the elements in the list that come before the specified cursor.
|
||||
|
|
|
@ -18849,8 +18849,8 @@
|
|||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeUsername",
|
||||
"description": "Username of a user assigned to the issue",
|
||||
"name": "authorUsername",
|
||||
"description": "Username of the author of the issue",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
|
@ -18858,6 +18858,24 @@
|
|||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeUsername",
|
||||
"description": "Username of a user assigned to the issue",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeId",
|
||||
"description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported",
|
||||
|
@ -36420,8 +36438,8 @@
|
|||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeUsername",
|
||||
"description": "Username of a user assigned to the issue",
|
||||
"name": "authorUsername",
|
||||
"description": "Username of the author of the issue",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
|
@ -36429,6 +36447,24 @@
|
|||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeUsername",
|
||||
"description": "Username of a user assigned to the issue",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeId",
|
||||
"description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported",
|
||||
|
@ -36631,8 +36667,8 @@
|
|||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeUsername",
|
||||
"description": "Username of a user assigned to the issue",
|
||||
"name": "authorUsername",
|
||||
"description": "Username of the author of the issue",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
|
@ -36640,6 +36676,24 @@
|
|||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeUsername",
|
||||
"description": "Username of a user assigned to the issue",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeId",
|
||||
"description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported",
|
||||
|
@ -36808,8 +36862,8 @@
|
|||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeUsername",
|
||||
"description": "Username of a user assigned to the issue",
|
||||
"name": "authorUsername",
|
||||
"description": "Username of the author of the issue",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
|
@ -36817,6 +36871,24 @@
|
|||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeUsername",
|
||||
"description": "Username of a user assigned to the issue",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeId",
|
||||
"description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported",
|
||||
|
|
|
@ -13,7 +13,7 @@ class Feature
|
|||
TYPES = {
|
||||
development: {
|
||||
description: 'Short lived, used to enable unfinished code to be deployed',
|
||||
optional: true,
|
||||
optional: false,
|
||||
rollout_issue: true,
|
||||
default_enabled: false,
|
||||
example: <<-EOS
|
||||
|
@ -46,11 +46,11 @@ class Feature
|
|||
|
||||
PARAMS = %i[
|
||||
name
|
||||
default_enabled
|
||||
type
|
||||
introduced_by_url
|
||||
rollout_issue_url
|
||||
type
|
||||
group
|
||||
default_enabled
|
||||
].freeze
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
# rubocop:disable Style/Documentation
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
class RemoveDuplicatedCsFindingsWithoutVulnerabilityId
|
||||
def perform(start_id, stop_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Gitlab::BackgroundMigration::RemoveDuplicatedCsFindingsWithoutVulnerabilityId.prepend_if_ee('EE::Gitlab::BackgroundMigration::RemoveDuplicatedCsFindingsWithoutVulnerabilityId')
|
|
@ -19,7 +19,7 @@ module Gitlab
|
|||
@logger = logger
|
||||
end
|
||||
|
||||
def execute
|
||||
def perform
|
||||
raise ReindexError, "index #{index_name} does not exist" unless index_exists?
|
||||
|
||||
raise ReindexError, 'UNIQUE indexes are currently not supported' if index_unique?
|
||||
|
@ -51,6 +51,7 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
delegate :execute, to: :connection
|
||||
def connection
|
||||
@connection ||= ActiveRecord::Base.connection
|
||||
end
|
||||
|
|
|
@ -4,15 +4,13 @@ module Gitlab
|
|||
module Metrics
|
||||
class RequestsRackMiddleware
|
||||
HTTP_METHODS = {
|
||||
"delete" => %w(200 202 204 303 400 401 403 404 410 422 500 503),
|
||||
"get" => %w(200 204 301 302 303 304 307 400 401 403 404 410 412 422 429 500 503),
|
||||
"head" => %w(200 204 301 302 303 304 400 401 403 404 410 429 500 503),
|
||||
"delete" => %w(200 202 204 303 400 401 403 404 500 503),
|
||||
"get" => %w(200 204 301 302 303 304 307 400 401 403 404 410 422 429 500 503),
|
||||
"head" => %w(200 204 301 302 303 401 403 404 410 500),
|
||||
"options" => %w(200 404),
|
||||
"patch" => %w(200 202 204 400 403 404 409 416 422 500),
|
||||
"post" => %w(200 201 202 204 301 302 303 304 400 401 403 404 406 409 410 412 413 415 422 429 500 503),
|
||||
"propfind" => %w(404),
|
||||
"put" => %w(200 202 204 400 401 403 404 405 406 409 410 415 422 500),
|
||||
"report" => %w(404)
|
||||
"patch" => %w(200 202 204 400 403 404 409 416 500),
|
||||
"post" => %w(200 201 202 204 301 302 303 304 400 401 403 404 406 409 410 412 422 429 500 503),
|
||||
"put" => %w(200 202 204 400 401 403 404 405 406 409 410 422 500)
|
||||
}.freeze
|
||||
|
||||
HEALTH_ENDPOINT = /^\/-\/(liveness|readiness|health|metrics)\/?$/.freeze
|
||||
|
@ -48,6 +46,7 @@ module Gitlab
|
|||
|
||||
def call(env)
|
||||
method = env['REQUEST_METHOD'].downcase
|
||||
method = 'INVALID' unless HTTP_METHODS.key?(method)
|
||||
started = Time.now.to_f
|
||||
|
||||
begin
|
||||
|
|
|
@ -176,7 +176,7 @@ namespace :gitlab do
|
|||
|
||||
raise ArgumentError, 'must give the index name to reindex' unless args[:index_name]
|
||||
|
||||
Gitlab::Database::ConcurrentReindex.new(args[:index_name], logger: Logger.new(STDOUT)).execute
|
||||
Gitlab::Database::ConcurrentReindex.new(args[:index_name], logger: Logger.new(STDOUT)).perform
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3964,7 +3964,7 @@ msgstr ""
|
|||
msgid "BillingPlans|@%{user_name} you are currently using the %{plan_name} plan."
|
||||
msgstr ""
|
||||
|
||||
msgid "BillingPlans|Congratulations, your new trial is activated"
|
||||
msgid "BillingPlans|Congratulations, your free trial is activated."
|
||||
msgstr ""
|
||||
|
||||
msgid "BillingPlans|If you would like to downgrade your plan please contact %{support_link_start}Customer Support%{support_link_end}."
|
||||
|
@ -4621,6 +4621,15 @@ msgstr ""
|
|||
msgid "Change your password or recover your current one"
|
||||
msgstr ""
|
||||
|
||||
msgid "ChangeReviewer|Reviewer changed from %{old} to %{new}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ChangeReviewer|Reviewer changed to %{new}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ChangeReviewer|Unassigned"
|
||||
msgstr ""
|
||||
|
||||
msgid "ChangeTypeActionLabel|Pick into branch"
|
||||
msgstr ""
|
||||
|
||||
|
@ -10659,6 +10668,12 @@ msgstr ""
|
|||
msgid "Failed to load related branches"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load sidebar confidential toggle"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load sidebar lock status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load stacktrace."
|
||||
msgstr ""
|
||||
|
||||
|
@ -17410,6 +17425,9 @@ msgstr ""
|
|||
msgid "Notification settings saved"
|
||||
msgstr ""
|
||||
|
||||
msgid "NotificationEvent|Change reviewer merge request"
|
||||
msgstr ""
|
||||
|
||||
msgid "NotificationEvent|Close issue"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22236,9 +22254,6 @@ msgstr ""
|
|||
msgid "Search requirements"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search results…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search test cases"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -16,3 +16,4 @@ N_('NotificationEvent|Merge merge request')
|
|||
N_('NotificationEvent|Failed pipeline')
|
||||
N_('NotificationEvent|Fixed pipeline')
|
||||
N_('NotificationEvent|New release')
|
||||
N_('NotificationEvent|Change reviewer merge request')
|
||||
|
|
|
@ -8,6 +8,10 @@ load File.expand_path('../../bin/feature-flag', __dir__)
|
|||
RSpec.describe 'bin/feature-flag' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
before do
|
||||
skip_feature_flags_yaml_validation
|
||||
end
|
||||
|
||||
describe FeatureFlagCreator do
|
||||
let(:argv) { %w[feature-flag-name -t development -g group::memory -i https://url -m http://url] }
|
||||
let(:options) { FeatureFlagOptionParser.parse(argv) }
|
||||
|
|
|
@ -12,6 +12,10 @@ FactoryBot.define do
|
|||
action { Todo::ASSIGNED }
|
||||
end
|
||||
|
||||
trait :review_requested do
|
||||
action { Todo::REVIEW_REQUESTED }
|
||||
end
|
||||
|
||||
trait :mentioned do
|
||||
action { Todo::MENTIONED }
|
||||
end
|
||||
|
|
|
@ -46,7 +46,7 @@ RSpec.describe 'Admin uses repository checks', :request_store, :clean_gitlab_red
|
|||
)
|
||||
visit_admin_project_page(project)
|
||||
|
||||
page.within('.alert') do
|
||||
page.within('.gl-alert') do
|
||||
expect(page.text).to match(/Last repository check \(just now\) failed/)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -130,6 +130,7 @@ RSpec.describe 'Dashboard > User filters todos', :js do
|
|||
before do
|
||||
create(:todo, :build_failed, user: user_1, author: user_2, project: project_1)
|
||||
create(:todo, :marked, user: user_1, author: user_2, project: project_1, target: issue1)
|
||||
create(:todo, :review_requested, user: user_1, author: user_2, project: project_1, target: issue1)
|
||||
end
|
||||
|
||||
it 'filters by Assigned' do
|
||||
|
@ -138,6 +139,12 @@ RSpec.describe 'Dashboard > User filters todos', :js do
|
|||
expect_to_see_action(:assigned)
|
||||
end
|
||||
|
||||
it 'filters by Review Requested' do
|
||||
filter_action('Review requested')
|
||||
|
||||
expect_to_see_action(:review_requested)
|
||||
end
|
||||
|
||||
it 'filters by Mentioned' do
|
||||
filter_action('Mentioned')
|
||||
|
||||
|
@ -168,6 +175,7 @@ RSpec.describe 'Dashboard > User filters todos', :js do
|
|||
def expect_to_see_action(action_name)
|
||||
action_names = {
|
||||
assigned: ' assigned you ',
|
||||
review_requested: ' requested a review of ',
|
||||
mentioned: ' mentioned ',
|
||||
marked: ' added a todo for ',
|
||||
build_failed: ' build failed for '
|
||||
|
|
|
@ -197,6 +197,21 @@ RSpec.describe 'Dashboard Todos' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'review request todo' do
|
||||
let(:merge_request) { create(:merge_request, title: "Fixes issue") }
|
||||
|
||||
before do
|
||||
create(:todo, :review_requested, user: user, project: project, target: merge_request, author: user)
|
||||
visit dashboard_todos_path
|
||||
end
|
||||
|
||||
it 'shows you set yourself as an reviewer message' do
|
||||
page.within('.js-todos-all') do
|
||||
expect(page).to have_content("You requested a review of merge request #{merge_request.to_reference} \"Fixes issue\" at #{project.namespace.owner_name} / #{project.name} from yourself")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'User has done todos', :js do
|
||||
|
|
|
@ -5,7 +5,6 @@ import {
|
|||
GlTable,
|
||||
GlAvatar,
|
||||
GlPagination,
|
||||
GlSearchBoxByType,
|
||||
GlTab,
|
||||
GlTabs,
|
||||
GlBadge,
|
||||
|
@ -15,13 +14,18 @@ import { visitUrl, joinPaths, mergeUrlParams } from '~/lib/utils/url_utility';
|
|||
import IncidentsList from '~/incidents/components/incidents_list.vue';
|
||||
import SeverityToken from '~/sidebar/components/severity/severity.vue';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
|
||||
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
|
||||
import { I18N, INCIDENT_STATUS_TABS } from '~/incidents/constants';
|
||||
import mockIncidents from '../mocks/incidents.json';
|
||||
import mockFilters from '../mocks/incidents_filter.json';
|
||||
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
visitUrl: jest.fn().mockName('visitUrlMock'),
|
||||
joinPaths: jest.fn().mockName('joinPaths'),
|
||||
mergeUrlParams: jest.fn().mockName('mergeUrlParams'),
|
||||
setUrlParams: jest.fn().mockName('setUrlParams'),
|
||||
updateHistory: jest.fn().mockName('updateHistory'),
|
||||
}));
|
||||
|
||||
describe('Incidents List', () => {
|
||||
|
@ -43,7 +47,7 @@ describe('Incidents List', () => {
|
|||
const findTimeAgo = () => wrapper.findAll(TimeAgoTooltip);
|
||||
const findDateColumnHeader = () =>
|
||||
wrapper.find('[data-testid="incident-management-created-at-sort"]');
|
||||
const findSearch = () => wrapper.find(GlSearchBoxByType);
|
||||
const findSearch = () => wrapper.find(FilteredSearchBar);
|
||||
const findAssingees = () => wrapper.findAll('[data-testid="incident-assignees"]');
|
||||
const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]');
|
||||
const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']");
|
||||
|
@ -76,6 +80,9 @@ describe('Incidents List', () => {
|
|||
issuePath: '/project/isssues',
|
||||
publishedAvailable: true,
|
||||
emptyListSvgPath,
|
||||
textQuery: '',
|
||||
authorUsernamesQuery: '',
|
||||
assigneeUsernamesQuery: '',
|
||||
},
|
||||
stubs: {
|
||||
GlButton: true,
|
||||
|
@ -315,7 +322,7 @@ describe('Incidents List', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Search', () => {
|
||||
describe('Filtered search component', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent({
|
||||
data: {
|
||||
|
@ -331,15 +338,62 @@ describe('Incidents List', () => {
|
|||
});
|
||||
|
||||
it('renders the search component for incidents', () => {
|
||||
expect(findSearch().exists()).toBe(true);
|
||||
expect(findSearch().props('searchInputPlaceholder')).toBe('Search or filter results…');
|
||||
expect(findSearch().props('tokens')).toEqual([
|
||||
{
|
||||
type: 'author_username',
|
||||
icon: 'user',
|
||||
title: 'Author',
|
||||
unique: true,
|
||||
symbol: '@',
|
||||
token: AuthorToken,
|
||||
operators: [{ value: '=', description: 'is', default: 'true' }],
|
||||
fetchPath: '/project/path',
|
||||
fetchAuthors: expect.any(Function),
|
||||
},
|
||||
{
|
||||
type: 'assignee_username',
|
||||
icon: 'user',
|
||||
title: 'Assignees',
|
||||
unique: true,
|
||||
symbol: '@',
|
||||
token: AuthorToken,
|
||||
operators: [{ value: '=', description: 'is', default: 'true' }],
|
||||
fetchPath: '/project/path',
|
||||
fetchAuthors: expect.any(Function),
|
||||
},
|
||||
]);
|
||||
expect(findSearch().props('recentSearchesStorageKey')).toBe('incidents');
|
||||
});
|
||||
|
||||
it('sets the `searchTerm` graphql variable', () => {
|
||||
const SEARCH_TERM = 'Simple Incident';
|
||||
it('returns correctly applied filter search values', async () => {
|
||||
const searchTerm = 'foo';
|
||||
wrapper.setData({
|
||||
searchTerm,
|
||||
});
|
||||
|
||||
findSearch().vm.$emit('input', SEARCH_TERM);
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.vm.filteredSearchValue).toEqual([searchTerm]);
|
||||
});
|
||||
|
||||
expect(wrapper.vm.$data.searchTerm).toBe(SEARCH_TERM);
|
||||
it('updates props tied to getIncidents GraphQL query', () => {
|
||||
wrapper.vm.handleFilterIncidents(mockFilters);
|
||||
|
||||
expect(wrapper.vm.authorUsername).toBe('root');
|
||||
expect(wrapper.vm.assigneeUsernames).toEqual(['root2']);
|
||||
expect(wrapper.vm.searchTerm).toBe(mockFilters[2].value.data);
|
||||
});
|
||||
|
||||
it('updates props `searchTerm` and `authorUsername` with empty values when passed filters param is empty', () => {
|
||||
wrapper.setData({
|
||||
authorUsername: 'foo',
|
||||
searchTerm: 'bar',
|
||||
});
|
||||
|
||||
wrapper.vm.handleFilterIncidents([]);
|
||||
|
||||
expect(wrapper.vm.authorUsername).toBe('');
|
||||
expect(wrapper.vm.searchTerm).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
[
|
||||
{
|
||||
"type": "assignee_username",
|
||||
"value": { "data": "root2" }
|
||||
},
|
||||
{
|
||||
"type": "author_username",
|
||||
"value": { "data": "root" }
|
||||
},
|
||||
{
|
||||
"type": "filtered-search-term",
|
||||
"value": { "data": "bar" }
|
||||
}
|
||||
]
|
|
@ -12,6 +12,10 @@ RSpec.describe 'Graphql Field feature flags' do
|
|||
let(:query_string) { '{ item { name } }' }
|
||||
let(:result) { execute_query(query_type)['data'] }
|
||||
|
||||
before do
|
||||
skip_feature_flags_yaml_validation
|
||||
end
|
||||
|
||||
subject { result }
|
||||
|
||||
describe 'Feature flagged field' do
|
||||
|
|
|
@ -54,10 +54,21 @@ RSpec.describe Resolvers::IssuesResolver do
|
|||
expect(resolve_issues(assignee_id: IssuableFinder::Params::FILTER_ANY)).to contain_exactly(issue2)
|
||||
end
|
||||
|
||||
it 'filters by two assignees' do
|
||||
user_2 = create(:user)
|
||||
issue2.update!(assignees: [assignee, user_2])
|
||||
|
||||
expect(resolve_issues(assignee_id: [assignee.id, user_2.id])).to contain_exactly(issue2)
|
||||
end
|
||||
|
||||
it 'filters by no assignee' do
|
||||
expect(resolve_issues(assignee_id: IssuableFinder::Params::FILTER_NONE)).to contain_exactly(issue1)
|
||||
end
|
||||
|
||||
it 'filters by author' do
|
||||
expect(resolve_issues(author_username: issue1.author.username)).to contain_exactly(issue1, issue2)
|
||||
end
|
||||
|
||||
it 'filters by labels' do
|
||||
expect(resolve_issues(label_name: [label1.title])).to contain_exactly(issue1, issue2)
|
||||
expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2)
|
||||
|
|
|
@ -126,6 +126,10 @@ RSpec.describe Types::BaseField do
|
|||
let(:field) { described_class.new(name: 'test', type: GraphQL::STRING_TYPE, feature_flag: flag, null: false) }
|
||||
let(:context) { {} }
|
||||
|
||||
before do
|
||||
skip_feature_flags_yaml_validation
|
||||
end
|
||||
|
||||
it 'returns false if the feature is not enabled' do
|
||||
stub_feature_flags(flag => false)
|
||||
|
||||
|
|
|
@ -361,4 +361,116 @@ RSpec.describe EmailsHelper do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#change_reviewer_notification_text' do
|
||||
let(:mary) { build(:user, name: 'Mary') }
|
||||
let(:john) { build(:user, name: 'John') }
|
||||
let(:ted) { build(:user, name: 'Ted') }
|
||||
|
||||
context 'to new reviewers only' do
|
||||
let(:previous_reviewers) { [] }
|
||||
let(:new_reviewers) { [john] }
|
||||
|
||||
context 'with no html tag' do
|
||||
let(:expected_output) do
|
||||
'Reviewer changed to John'
|
||||
end
|
||||
|
||||
it 'returns the expected output' do
|
||||
expect(change_reviewer_notification_text(new_reviewers, previous_reviewers)).to eq(expected_output)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with <strong> tag' do
|
||||
let(:expected_output) do
|
||||
'Reviewer changed to <strong>John</strong>'
|
||||
end
|
||||
|
||||
it 'returns the expected output' do
|
||||
expect(change_reviewer_notification_text(new_reviewers, previous_reviewers, :strong)).to eq(expected_output)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'from previous reviewers to new reviewers' do
|
||||
let(:previous_reviewers) { [john, mary] }
|
||||
let(:new_reviewers) { [ted] }
|
||||
|
||||
context 'with no html tag' do
|
||||
let(:expected_output) do
|
||||
'Reviewer changed from John and Mary to Ted'
|
||||
end
|
||||
|
||||
it 'returns the expected output' do
|
||||
expect(change_reviewer_notification_text(new_reviewers, previous_reviewers)).to eq(expected_output)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with <strong> tag' do
|
||||
let(:expected_output) do
|
||||
'Reviewer changed from <strong>John and Mary</strong> to <strong>Ted</strong>'
|
||||
end
|
||||
|
||||
it 'returns the expected output' do
|
||||
expect(change_reviewer_notification_text(new_reviewers, previous_reviewers, :strong)).to eq(expected_output)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'from previous reviewers to no reviewers' do
|
||||
let(:previous_reviewers) { [john, mary] }
|
||||
let(:new_reviewers) { [] }
|
||||
|
||||
context 'with no html tag' do
|
||||
let(:expected_output) do
|
||||
'Reviewer changed from John and Mary to Unassigned'
|
||||
end
|
||||
|
||||
it 'returns the expected output' do
|
||||
expect(change_reviewer_notification_text(new_reviewers, previous_reviewers)).to eq(expected_output)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with <strong> tag' do
|
||||
let(:expected_output) do
|
||||
'Reviewer changed from <strong>John and Mary</strong> to <strong>Unassigned</strong>'
|
||||
end
|
||||
|
||||
it 'returns the expected output' do
|
||||
expect(change_reviewer_notification_text(new_reviewers, previous_reviewers, :strong)).to eq(expected_output)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with a <script> tag in user's name" do
|
||||
let(:previous_reviewers) { [] }
|
||||
let(:new_reviewers) { [fishy_user] }
|
||||
let(:fishy_user) { build(:user, name: "<script>alert('hi')</script>") }
|
||||
|
||||
let(:expected_output) do
|
||||
'Reviewer changed to <strong><script>alert('hi')</script></strong>'
|
||||
end
|
||||
|
||||
it 'escapes the html tag' do
|
||||
expect(change_reviewer_notification_text(new_reviewers, previous_reviewers, :strong)).to eq(expected_output)
|
||||
end
|
||||
end
|
||||
|
||||
context "with url in user's name" do
|
||||
subject(:email_helper) { Object.new.extend(described_class) }
|
||||
|
||||
let(:previous_reviewers) { [] }
|
||||
let(:new_reviewers) { [fishy_user] }
|
||||
let(:fishy_user) { build(:user, name: "example.com") }
|
||||
|
||||
let(:expected_output) do
|
||||
'Reviewer changed to example_com'
|
||||
end
|
||||
|
||||
it "sanitizes user's name" do
|
||||
expect(email_helper).to receive(:sanitize_name).and_call_original
|
||||
expect(email_helper.change_reviewer_notification_text(new_reviewers, previous_reviewers)).to eq(expected_output)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,9 +9,16 @@ RSpec.describe Projects::IncidentsHelper do
|
|||
let(:project_path) { project.full_path }
|
||||
let(:new_issue_path) { new_project_issue_path(project) }
|
||||
let(:issue_path) { project_issues_path(project) }
|
||||
let(:params) do
|
||||
{
|
||||
search: 'search text',
|
||||
author_username: 'root',
|
||||
assignee_username: 'max.power'
|
||||
}
|
||||
end
|
||||
|
||||
describe '#incidents_data' do
|
||||
subject(:data) { helper.incidents_data(project) }
|
||||
subject(:data) { helper.incidents_data(project, params) }
|
||||
|
||||
it 'returns frontend configuration' do
|
||||
expect(data).to match(
|
||||
|
@ -20,7 +27,10 @@ RSpec.describe Projects::IncidentsHelper do
|
|||
'incident-template-name' => 'incident',
|
||||
'incident-type' => 'incident',
|
||||
'issue-path' => issue_path,
|
||||
'empty-list-svg-path' => match_asset_path('/assets/illustrations/incident-empty-state.svg')
|
||||
'empty-list-svg-path' => match_asset_path('/assets/illustrations/incident-empty-state.svg'),
|
||||
'text-query': 'search text',
|
||||
'author-usernames-query': 'root',
|
||||
'assignee-usernames-query': 'max.power'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -195,6 +195,10 @@ RSpec.describe API::Helpers do
|
|||
let(:unknown_event) { 'unknown' }
|
||||
let(:feature) { "usage_data_#{event_name}" }
|
||||
|
||||
before do
|
||||
skip_feature_flags_yaml_validation
|
||||
end
|
||||
|
||||
context 'with feature enabled' do
|
||||
before do
|
||||
stub_feature_flags(feature => true)
|
||||
|
|
|
@ -105,6 +105,7 @@ RSpec.describe Feature::Definition do
|
|||
describe '.load_all!' do
|
||||
let(:store1) { Dir.mktmpdir('path1') }
|
||||
let(:store2) { Dir.mktmpdir('path2') }
|
||||
let(:definitions) { {} }
|
||||
|
||||
before do
|
||||
allow(described_class).to receive(:paths).and_return(
|
||||
|
@ -113,6 +114,10 @@ RSpec.describe Feature::Definition do
|
|||
File.join(store2, '**', '*.yml')
|
||||
]
|
||||
)
|
||||
|
||||
# We stub `definitions` to ensure that they
|
||||
# are not overwritten by `.load_all!`
|
||||
allow(described_class).to receive(:definitions).and_return(definitions)
|
||||
end
|
||||
|
||||
it "when there's no feature flags a list of definitions is empty" do
|
||||
|
|
|
@ -6,6 +6,7 @@ RSpec.describe Feature, stub_feature_flags: false do
|
|||
before do
|
||||
# reset Flipper AR-engine
|
||||
Feature.reset
|
||||
skip_feature_flags_yaml_validation
|
||||
end
|
||||
|
||||
describe '.get' do
|
||||
|
@ -253,6 +254,9 @@ RSpec.describe Feature, stub_feature_flags: false do
|
|||
end
|
||||
|
||||
before do
|
||||
stub_env('LAZILY_CREATE_FEATURE_FLAG', '0')
|
||||
|
||||
allow(Feature::Definition).to receive(:valid_usage!).and_call_original
|
||||
allow(Feature::Definition).to receive(:definitions) do
|
||||
{ definition.key => definition }
|
||||
end
|
||||
|
|
|
@ -73,7 +73,7 @@ RSpec.describe Gitlab::BackgroundMigration::MergeRequestAssigneesMigrationProgre
|
|||
|
||||
described_class.new.perform
|
||||
|
||||
expect(Feature.enabled?(:multiple_merge_request_assignees)).to eq(true)
|
||||
expect(Feature.enabled?(:multiple_merge_request_assignees, type: :licensed)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Database::ConcurrentReindex, '#execute' do
|
||||
RSpec.describe Gitlab::Database::ConcurrentReindex, '#perform' do
|
||||
subject { described_class.new(index_name, logger: logger) }
|
||||
|
||||
let(:table_name) { '_test_reindex_table' }
|
||||
|
@ -29,7 +29,7 @@ RSpec.describe Gitlab::Database::ConcurrentReindex, '#execute' do
|
|||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject.execute }.to raise_error(described_class::ReindexError, /does not exist/)
|
||||
expect { subject.perform }.to raise_error(described_class::ReindexError, /does not exist/)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -43,7 +43,7 @@ RSpec.describe Gitlab::Database::ConcurrentReindex, '#execute' do
|
|||
|
||||
it 'raises an error' do
|
||||
expect do
|
||||
subject.execute
|
||||
subject.perform
|
||||
end.to raise_error(described_class::ReindexError, /UNIQUE indexes are currently not supported/)
|
||||
end
|
||||
end
|
||||
|
@ -57,35 +57,20 @@ RSpec.describe Gitlab::Database::ConcurrentReindex, '#execute' do
|
|||
|
||||
let!(:original_index) { find_index_create_statement }
|
||||
|
||||
before do
|
||||
allow(subject).to receive(:connection).and_return(connection)
|
||||
allow(subject).to receive(:disable_statement_timeout).and_yield
|
||||
end
|
||||
|
||||
it 'replaces the existing index with an identical index' do
|
||||
expect(subject).to receive(:disable_statement_timeout).exactly(3).times.and_yield
|
||||
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
expect_to_execute_concurrently_in_order(create_index)
|
||||
|
||||
expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
|
||||
expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield
|
||||
it 'integration test: executing full index replacement without mocks' do
|
||||
allow(connection).to receive(:execute).and_wrap_original do |method, sql|
|
||||
method.call(sql.sub(/CONCURRENTLY/, ''))
|
||||
end
|
||||
|
||||
expect_to_execute_in_order("ALTER INDEX #{index_name} RENAME TO #{replaced_name}")
|
||||
expect_to_execute_in_order("ALTER INDEX #{replacement_name} RENAME TO #{index_name}")
|
||||
expect_to_execute_in_order("ALTER INDEX #{replaced_name} RENAME TO #{replacement_name}")
|
||||
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
|
||||
subject.execute
|
||||
subject.perform
|
||||
|
||||
check_index_exists
|
||||
end
|
||||
|
||||
context 'when a dangling index is left from a previous run' do
|
||||
context 'mocked specs' do
|
||||
before do
|
||||
connection.execute("CREATE INDEX #{replacement_name} ON #{table_name} (#{column_name})")
|
||||
allow(subject).to receive(:connection).and_return(connection)
|
||||
allow(subject).to receive(:disable_statement_timeout).and_yield
|
||||
end
|
||||
|
||||
it 'replaces the existing index with an identical index' do
|
||||
|
@ -104,78 +89,105 @@ RSpec.describe Gitlab::Database::ConcurrentReindex, '#execute' do
|
|||
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
|
||||
subject.execute
|
||||
subject.perform
|
||||
|
||||
check_index_exists
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it fails to create the replacement index' do
|
||||
it 'safely cleans up and signals the error' do
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
|
||||
expect(connection).to receive(:execute).with(create_index).ordered
|
||||
.and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout')
|
||||
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
|
||||
expect { subject.execute }.to raise_error(described_class::ReindexError, /connect timeout/)
|
||||
|
||||
check_index_exists
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the replacement index is not valid' do
|
||||
it 'safely cleans up and signals the error' do
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
expect_to_execute_concurrently_in_order(create_index)
|
||||
|
||||
expect(subject).to receive(:replacement_index_valid?).and_return(false)
|
||||
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
|
||||
expect { subject.execute }.to raise_error(described_class::ReindexError, /replacement index was created as INVALID/)
|
||||
|
||||
check_index_exists
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a database error occurs while swapping the indexes' do
|
||||
it 'safely cleans up and signals the error' do
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
expect_to_execute_concurrently_in_order(create_index)
|
||||
|
||||
expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
|
||||
expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield
|
||||
context 'when a dangling index is left from a previous run' do
|
||||
before do
|
||||
connection.execute("CREATE INDEX #{replacement_name} ON #{table_name} (#{column_name})")
|
||||
end
|
||||
|
||||
expect(connection).to receive(:execute).ordered
|
||||
.with("ALTER INDEX #{index_name} RENAME TO #{replaced_name}")
|
||||
.and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout')
|
||||
it 'replaces the existing index with an identical index' do
|
||||
expect(subject).to receive(:disable_statement_timeout).exactly(3).times.and_yield
|
||||
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
expect_to_execute_concurrently_in_order(create_index)
|
||||
|
||||
expect { subject.execute }.to raise_error(described_class::ReindexError, /connect timeout/)
|
||||
expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
|
||||
expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield
|
||||
end
|
||||
|
||||
check_index_exists
|
||||
end
|
||||
end
|
||||
expect_to_execute_in_order("ALTER INDEX #{index_name} RENAME TO #{replaced_name}")
|
||||
expect_to_execute_in_order("ALTER INDEX #{replacement_name} RENAME TO #{index_name}")
|
||||
expect_to_execute_in_order("ALTER INDEX #{replaced_name} RENAME TO #{replacement_name}")
|
||||
|
||||
context 'when with_lock_retries fails to acquire the lock' do
|
||||
it 'safely cleans up and signals the error' do
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
expect_to_execute_concurrently_in_order(create_index)
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
|
||||
expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
|
||||
expect(instance).to receive(:run).with(raise_on_exhaustion: true)
|
||||
.and_raise(::Gitlab::Database::WithLockRetries::AttemptsExhaustedError, 'exhausted')
|
||||
subject.perform
|
||||
|
||||
check_index_exists
|
||||
end
|
||||
end
|
||||
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
context 'when it fails to create the replacement index' do
|
||||
it 'safely cleans up and signals the error' do
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
|
||||
expect { subject.execute }.to raise_error(described_class::ReindexError, /exhausted/)
|
||||
expect(connection).to receive(:execute).with(create_index).ordered
|
||||
.and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout')
|
||||
|
||||
check_index_exists
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
|
||||
expect { subject.perform }.to raise_error(described_class::ReindexError, /connect timeout/)
|
||||
|
||||
check_index_exists
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the replacement index is not valid' do
|
||||
it 'safely cleans up and signals the error' do
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
expect_to_execute_concurrently_in_order(create_index)
|
||||
|
||||
expect(subject).to receive(:replacement_index_valid?).and_return(false)
|
||||
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
|
||||
expect { subject.perform }.to raise_error(described_class::ReindexError, /replacement index was created as INVALID/)
|
||||
|
||||
check_index_exists
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a database error occurs while swapping the indexes' do
|
||||
it 'safely cleans up and signals the error' do
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
expect_to_execute_concurrently_in_order(create_index)
|
||||
|
||||
expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
|
||||
expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield
|
||||
end
|
||||
|
||||
expect(connection).to receive(:execute).ordered
|
||||
.with("ALTER INDEX #{index_name} RENAME TO #{replaced_name}")
|
||||
.and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout')
|
||||
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
|
||||
expect { subject.perform }.to raise_error(described_class::ReindexError, /connect timeout/)
|
||||
|
||||
check_index_exists
|
||||
end
|
||||
end
|
||||
|
||||
context 'when with_lock_retries fails to acquire the lock' do
|
||||
it 'safely cleans up and signals the error' do
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
expect_to_execute_concurrently_in_order(create_index)
|
||||
|
||||
expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
|
||||
expect(instance).to receive(:run).with(raise_on_exhaustion: true)
|
||||
.and_raise(::Gitlab::Database::WithLockRetries::AttemptsExhaustedError, 'exhausted')
|
||||
end
|
||||
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
|
||||
expect { subject.perform }.to raise_error(described_class::ReindexError, /exhausted/)
|
||||
|
||||
check_index_exists
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -514,6 +514,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
|
|||
allow(migration).to receive(:table_exists?).with(partitioned_table).and_return(true)
|
||||
allow(migration).to receive(:copy_missed_records)
|
||||
allow(migration).to receive(:execute).with(/VACUUM/)
|
||||
allow(migration).to receive(:execute).with(/^(RE)?SET/)
|
||||
end
|
||||
|
||||
it 'finishes remaining jobs for the correct table' do
|
||||
|
@ -567,6 +568,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
|
|||
|
||||
allow(Gitlab::BackgroundMigration).to receive(:steal)
|
||||
allow(migration).to receive(:execute).with(/VACUUM/)
|
||||
allow(migration).to receive(:execute).with(/^(RE)?SET/)
|
||||
end
|
||||
|
||||
it 'idempotently cleans up after failed background migrations' do
|
||||
|
|
|
@ -7,7 +7,7 @@ require 'tempfile'
|
|||
RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, :seed_helper do
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:repository) { project.repository }
|
||||
let(:feature_flag_name) { 'feature-flag-name' }
|
||||
let(:feature_flag_name) { wrapper.rugged_feature_keys.first }
|
||||
let(:temp_gitaly_metadata_file) { create_temporary_gitaly_metadata_file }
|
||||
|
||||
before(:all) do
|
||||
|
@ -47,7 +47,7 @@ RSpec.describe Gitlab::Git::RuggedImpl::UseRugged, :seed_helper do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when feature flag is not persisted' do
|
||||
context 'when feature flag is not persisted', stub_feature_flags: false do
|
||||
context 'when running puma with multiple threads' do
|
||||
before do
|
||||
allow(subject).to receive(:running_puma_with_multiple_threads?).and_return(true)
|
||||
|
|
|
@ -10,6 +10,10 @@ RSpec.describe Gitlab::GonHelper do
|
|||
end
|
||||
|
||||
describe '#push_frontend_feature_flag' do
|
||||
before do
|
||||
skip_feature_flags_yaml_validation
|
||||
end
|
||||
|
||||
it 'pushes a feature flag to the frontend' do
|
||||
gon = instance_double('gon')
|
||||
thing = stub_feature_flag_gate('thing')
|
||||
|
|
|
@ -19,6 +19,10 @@ RSpec.describe Gitlab::JobWaiter do
|
|||
describe '#wait' do
|
||||
let(:waiter) { described_class.new(2) }
|
||||
|
||||
before do
|
||||
allow_any_instance_of(described_class).to receive(:wait).and_call_original
|
||||
end
|
||||
|
||||
it 'returns when all jobs have been completed' do
|
||||
described_class.notify(waiter.key, 'a')
|
||||
described_class.notify(waiter.key, 'b')
|
||||
|
|
|
@ -216,6 +216,10 @@ RSpec.describe Gitlab::Utils::UsageData do
|
|||
let(:unknown_event) { 'unknown' }
|
||||
let(:feature) { "usage_data_#{event_name}" }
|
||||
|
||||
before do
|
||||
skip_feature_flags_yaml_validation
|
||||
end
|
||||
|
||||
context 'with feature enabled' do
|
||||
before do
|
||||
stub_feature_flags(feature => true)
|
||||
|
|
|
@ -4256,24 +4256,6 @@ RSpec.describe MergeRequest, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#allows_reviewers?' do
|
||||
it 'returns false without merge_request_reviewers feature' do
|
||||
stub_feature_flags(merge_request_reviewers: false)
|
||||
|
||||
merge_request = build_stubbed(:merge_request)
|
||||
|
||||
expect(merge_request.allows_reviewers?).to be(false)
|
||||
end
|
||||
|
||||
it 'returns true with merge_request_reviewers feature' do
|
||||
stub_feature_flags(merge_request_reviewers: true)
|
||||
|
||||
merge_request = build_stubbed(:merge_request)
|
||||
|
||||
expect(merge_request.allows_reviewers?).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#merge_ref_head' do
|
||||
let(:merge_request) { create(:merge_request) }
|
||||
|
||||
|
@ -4299,4 +4281,22 @@ RSpec.describe MergeRequest, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#allows_reviewers?' do
|
||||
it 'returns false without merge_request_reviewers feature' do
|
||||
stub_feature_flags(merge_request_reviewers: false)
|
||||
|
||||
merge_request = build_stubbed(:merge_request)
|
||||
|
||||
expect(merge_request.allows_reviewers?).to be(false)
|
||||
end
|
||||
|
||||
it 'returns true with merge_request_reviewers feature' do
|
||||
stub_feature_flags(merge_request_reviewers: true)
|
||||
|
||||
merge_request = build_stubbed(:merge_request)
|
||||
|
||||
expect(merge_request.allows_reviewers?).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -175,6 +175,7 @@ RSpec.describe NotificationSetting do
|
|||
:reopen_merge_request,
|
||||
:close_merge_request,
|
||||
:reassign_merge_request,
|
||||
:change_reviewer_merge_request,
|
||||
:merge_merge_request,
|
||||
:failed_pipeline,
|
||||
:success_pipeline,
|
||||
|
|
|
@ -200,26 +200,42 @@ RSpec.describe Todo do
|
|||
describe '#self_assigned?' do
|
||||
let(:user_1) { build(:user) }
|
||||
|
||||
before do
|
||||
subject.user = user_1
|
||||
subject.author = user_1
|
||||
subject.action = Todo::ASSIGNED
|
||||
context 'when self_added' do
|
||||
before do
|
||||
subject.user = user_1
|
||||
subject.author = user_1
|
||||
end
|
||||
|
||||
it 'returns true for ASSIGNED' do
|
||||
subject.action = Todo::ASSIGNED
|
||||
|
||||
expect(subject).to be_self_assigned
|
||||
end
|
||||
|
||||
it 'returns true for REVIEW_REQUESTED' do
|
||||
subject.action = Todo::REVIEW_REQUESTED
|
||||
|
||||
expect(subject).to be_self_assigned
|
||||
end
|
||||
|
||||
it 'returns false for other action' do
|
||||
subject.action = Todo::MENTIONED
|
||||
|
||||
expect(subject).not_to be_self_assigned
|
||||
end
|
||||
end
|
||||
|
||||
it 'is true when todo is ASSIGNED and self_added' do
|
||||
expect(subject).to be_self_assigned
|
||||
end
|
||||
context 'when todo is not self_added' do
|
||||
before do
|
||||
subject.user = user_1
|
||||
subject.author = build(:user)
|
||||
end
|
||||
|
||||
it 'is false when the todo is not ASSIGNED' do
|
||||
subject.action = Todo::MENTIONED
|
||||
it 'returns false' do
|
||||
subject.action = Todo::ASSIGNED
|
||||
|
||||
expect(subject).not_to be_self_assigned
|
||||
end
|
||||
|
||||
it 'is false when todo is not self_added' do
|
||||
subject.author = build(:user)
|
||||
|
||||
expect(subject).not_to be_self_assigned
|
||||
expect(subject).not_to be_self_assigned
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@ RSpec.describe API::Features, stub_feature_flags: false do
|
|||
Flipper.register(:perf_team) do |actor|
|
||||
actor.respond_to?(:admin) && actor.admin?
|
||||
end
|
||||
|
||||
skip_feature_flags_yaml_validation
|
||||
end
|
||||
|
||||
describe 'GET /features' do
|
||||
|
|
|
@ -66,6 +66,10 @@ RSpec.describe API::UsageData do
|
|||
end
|
||||
|
||||
context 'with unknown event' do
|
||||
before do
|
||||
skip_feature_flags_yaml_validation
|
||||
end
|
||||
|
||||
it 'returns status ok' do
|
||||
expect(Gitlab::Redis::HLL).not_to receive(:add)
|
||||
|
||||
|
|
|
@ -254,7 +254,7 @@ RSpec.describe Issuable::BulkUpdateService do
|
|||
describe 'unsubscribe from issues' do
|
||||
let(:issues) do
|
||||
create_list(:closed_issue, 2, project: project) do |issue|
|
||||
issue.subscriptions.create(user: user, project: project, subscribed: true)
|
||||
issue.subscriptions.create!(user: user, project: project, subscribed: true)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ RSpec.describe Issuable::Clone::AttributesRewriter do
|
|||
group_label = create(:group_label, title: 'group_label', group: group)
|
||||
create(:label, title: 'label3', project: project2)
|
||||
|
||||
original_issue.update(labels: [project1_label_1, project1_label_2, group_label])
|
||||
original_issue.update!(labels: [project1_label_1, project1_label_2, group_label])
|
||||
|
||||
subject.execute
|
||||
|
||||
|
@ -48,7 +48,7 @@ RSpec.describe Issuable::Clone::AttributesRewriter do
|
|||
it 'sets milestone to nil when old issue milestone is not in the new project' do
|
||||
milestone = create(:milestone, title: 'milestone', project: project1)
|
||||
|
||||
original_issue.update(milestone: milestone)
|
||||
original_issue.update!(milestone: milestone)
|
||||
|
||||
subject.execute
|
||||
|
||||
|
@ -59,7 +59,7 @@ RSpec.describe Issuable::Clone::AttributesRewriter do
|
|||
milestone_project1 = create(:milestone, title: 'milestone', project: project1)
|
||||
milestone_project2 = create(:milestone, title: 'milestone', project: project2)
|
||||
|
||||
original_issue.update(milestone: milestone_project1)
|
||||
original_issue.update!(milestone: milestone_project1)
|
||||
|
||||
subject.execute
|
||||
|
||||
|
@ -69,7 +69,7 @@ RSpec.describe Issuable::Clone::AttributesRewriter do
|
|||
it 'copies the milestone when old issue milestone is a group milestone' do
|
||||
milestone = create(:milestone, title: 'milestone', group: group)
|
||||
|
||||
original_issue.update(milestone: milestone)
|
||||
original_issue.update!(milestone: milestone)
|
||||
|
||||
subject.execute
|
||||
|
||||
|
@ -85,7 +85,7 @@ RSpec.describe Issuable::Clone::AttributesRewriter do
|
|||
let!(:milestone2_project2) { create(:milestone, title: 'milestone2', project: project2) }
|
||||
|
||||
before do
|
||||
original_issue.update(milestone: milestone2_project1)
|
||||
original_issue.update!(milestone: milestone2_project1)
|
||||
|
||||
create_event(milestone1_project1)
|
||||
create_event(milestone2_project1)
|
||||
|
|
|
@ -19,7 +19,7 @@ RSpec.describe Issuable::CommonSystemNotesService do
|
|||
|
||||
before do
|
||||
issuable.labels << label
|
||||
issuable.save
|
||||
issuable.save!
|
||||
end
|
||||
|
||||
it 'creates a resource label event' do
|
||||
|
@ -69,7 +69,7 @@ RSpec.describe Issuable::CommonSystemNotesService do
|
|||
subject { described_class.new(project, user).execute(issuable, old_labels: [], is_update: false) }
|
||||
|
||||
it 'does not create system note for title and description' do
|
||||
issuable.save
|
||||
issuable.save!
|
||||
|
||||
expect { subject }.not_to change { issuable.notes.count }
|
||||
end
|
||||
|
@ -78,7 +78,7 @@ RSpec.describe Issuable::CommonSystemNotesService do
|
|||
label = create(:label, project: project)
|
||||
|
||||
issuable.labels << label
|
||||
issuable.save
|
||||
issuable.save!
|
||||
|
||||
expect { subject }.to change { issuable.resource_label_events.count }.from(0).to(1)
|
||||
|
||||
|
@ -104,7 +104,7 @@ RSpec.describe Issuable::CommonSystemNotesService do
|
|||
|
||||
it 'creates a system note for due_date set' do
|
||||
issuable.due_date = Date.today
|
||||
issuable.save
|
||||
issuable.save!
|
||||
|
||||
expect { subject }.to change { issuable.notes.count }.from(0).to(1)
|
||||
expect(issuable.notes.last.note).to match('changed due date')
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue