Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-21 12:09:34 +00:00
parent d05604c95a
commit 7985071975
109 changed files with 1278 additions and 332 deletions

View File

@ -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'

View File

@ -1 +1 @@
2f16d97afa2e8accb4144f04e2e1e90bf4d1e9fb
40de6ac3d3e6db6a5fd85b63ff3ae8f60aece271

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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: {

View File

@ -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);

View File

@ -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]

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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' },

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -227,7 +227,7 @@ class Todo < ApplicationRecord
end
def self_assigned?
assigned? && self_added?
self_added? && (assigned? || review_requested?)
end
private

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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!

View File

@ -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

View File

@ -31,7 +31,7 @@
- if todo.self_assigned?
%span.title-item.action-name
to yourself
= todo_self_addressing(todo)
%span.title-item
&middot;

View File

@ -0,0 +1,2 @@
%p
= change_reviewer_notification_text(@merge_request.reviewers, @previous_reviewers, :strong)

View File

@ -0,0 +1 @@
<%= change_reviewer_notification_text(@merge_request.reviewers, @previous_reviewers) %>

View File

@ -1,3 +1,3 @@
- page_title _('Incidents')
#js-incidents{ data: incidents_data(@project) }
#js-incidents{ data: incidents_data(@project, params) }

View File

@ -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 projects 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

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Resolve Add filter capabilities to Incident list
merge_request: 42377
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Replace bootstrap alerts in app/views/admin/projects/show.html.haml
merge_request: 41389
author: Gilang Gumilar
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add notification setting for merge request reviewers
merge_request: 41851
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Cleanup request http method/code metrics
merge_request: 42618
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Color/position tweaks for collapsed diff files
merge_request: 42465
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Fix Rails/SaveBang offenses for spec files in spec/services/issuable/*
merge_request: 42780
author: Rajendra Kadam
type: other

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,7 @@
---
name: approval_rule
introduced_by_url:
rollout_issue_url:
group:
type: development
default_enabled: true

View File

@ -0,0 +1,7 @@
---
name: code_navigation
introduced_by_url:
rollout_issue_url:
group:
type: development
default_enabled: true

View File

@ -0,0 +1,7 @@
---
name: coverage_report_view
introduced_by_url:
rollout_issue_url:
group:
type: development
default_enabled: true

View File

@ -0,0 +1,7 @@
---
name: dag_pipeline_tab
introduced_by_url:
rollout_issue_url:
group:
type: development
default_enabled: true

View File

@ -0,0 +1,7 @@
---
name: default_merge_ref_for_diffs
introduced_by_url:
rollout_issue_url:
type: development
group:
default_enabled: false

View File

@ -0,0 +1,7 @@
---
name: graphql_releases_page
introduced_by_url:
rollout_issue_url:
group:
type: development
default_enabled: true

View File

@ -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

View File

@ -0,0 +1,7 @@
---
name: merge_ref_head_comments
introduced_by_url:
rollout_issue_url:
group:
type: development
default_enabled: true

View File

@ -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

View File

@ -0,0 +1,7 @@
---
name: usage_data_a_compliance_audit_events_api
introduced_by_url:
rollout_issue_url:
group:
type: development
default_enabled: false

View File

@ -0,0 +1,7 @@
---
name: usage_data_i_source_code_code_intelligence
introduced_by_url:
rollout_issue_url:
group:
type: development
default_enabled: true

View File

@ -0,0 +1,7 @@
---
name: vue_sidebar_labels
introduced_by_url:
rollout_issue_url:
group:
type: development
default_enabled: false

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
8b2090e953e6205b65555408a88d3da7f6bce28b0baa52d1a43a3a3e8001b7e1

View File

@ -0,0 +1 @@
8d9e75f7c6344b03cb740fa691fcbb5bea1751802741229158701bc1af975897

View File

@ -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

View File

@ -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,

View File

@ -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.

View File

@ -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",

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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')

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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 '

View File

@ -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

View File

@ -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('');
});
});

View File

@ -0,0 +1,14 @@
[
{
"type": "assignee_username",
"value": { "data": "root2" }
},
{
"type": "author_username",
"value": { "data": "root" }
},
{
"type": "filtered-search-term",
"value": { "data": "bar" }
}
]

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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>&lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;</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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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')

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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