Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-26 15:08:40 +00:00
parent 6e320396b2
commit a1908447b7
79 changed files with 1492 additions and 356 deletions

View File

@ -1 +1 @@
2c750a230dae024d4f59a85c7dba66bac5546fe6
6689311d652362fc41e5b5cb53aeffede352c8b7

View File

@ -127,7 +127,7 @@ export default {
'board-type-assignee': list.type === 'assignee',
}"
:data-id="list.id"
class="board gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal"
class="board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal"
data-qa-selector="board_list"
>
<div

View File

@ -43,7 +43,7 @@ export default {
<template>
<div class="append-bottom-20">
<label class="form-section-title label-bold" for="board-new-name">
<label class="label-bold gl-font-lg" for="board-new-name">
{{ __('List options') }}
</label>
<p class="text-secondary gl-mb-3">

View File

@ -196,9 +196,7 @@ export default {
<p v-if="isDeleteForm">{{ __('Are you sure you want to delete this board?') }}</p>
<form v-else class="js-board-config-modal" @submit.prevent>
<div v-if="!readonly" class="append-bottom-20">
<label class="form-section-title label-bold" for="board-new-name">{{
__('Title')
}}</label>
<label class="label-bold gl-font-lg" for="board-new-name">{{ __('Title') }}</label>
<input
id="board-new-name"
ref="name"

View File

@ -188,8 +188,9 @@ export default {
'gl-py-3 gl-h-full': !list.isExpanded && !isSwimlanesHeader,
'gl-border-b-0': !list.isExpanded || isSwimlanesHeader,
'gl-py-2': !list.isExpanded && isSwimlanesHeader,
'gl-flex-direction-column': !list.isExpanded,
}"
class="board-title gl-m-0 gl-display-flex js-board-handle"
class="board-title gl-m-0 gl-display-flex gl-align-items-center gl-font-base gl-px-3 js-board-handle"
>
<gl-button
v-if="list.isExpandable"
@ -202,7 +203,15 @@ export default {
@click="toggleExpanded"
/>
<!-- The following is only true in EE and if it is a milestone -->
<span v-if="showMilestoneListDetails" aria-hidden="true" class="gl-mr-2 milestone-icon">
<span
v-if="showMilestoneListDetails"
aria-hidden="true"
class="milestone-icon"
:class="{
'gl-mt-3 gl-rotate-90': !list.isExpanded,
'gl-mr-2': list.isExpanded,
}"
>
<gl-icon name="timer" />
</span>
@ -210,6 +219,9 @@ export default {
v-if="showAssigneeListDetails"
:href="list.assignee.path"
class="user-avatar-link js-no-trigger"
:class="{
'gl-mt-3 gl-rotate-90': !list.isExpanded,
}"
>
<img
v-gl-tooltip.hover.bottom
@ -223,20 +235,28 @@ export default {
</a>
<div
class="board-title-text"
:class="{ 'gl-display-none': !list.isExpanded && isSwimlanesHeader }"
:class="{
'gl-display-none': !list.isExpanded && isSwimlanesHeader,
'gl-flex-grow-0 gl-my-3 gl-mx-0': !list.isExpanded,
'gl-flex-grow-1': list.isExpanded,
}"
>
<span
v-if="list.type !== 'label'"
v-gl-tooltip.hover
:class="{
'gl-display-inline-block': list.type === 'milestone',
'gl-display-block': !list.isExpanded || list.type === 'milestone',
}"
:title="listTitle"
class="board-title-main-text block-truncated"
class="board-title-main-text gl-text-truncate"
>
{{ list.title }}
</span>
<span v-if="list.type === 'assignee'" class="board-title-sub-text gl-ml-2">
<span
v-if="list.type === 'assignee'"
class="board-title-sub-text gl-ml-2 gl-font-weight-normal"
:class="{ 'gl-display-none': !list.isExpanded }"
>
@{{ listAssignee }}
</span>
<gl-label
@ -279,7 +299,10 @@ export default {
<div
v-if="showBoardListAndBoardInfo"
class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag text-secondary"
:class="{ 'gl-display-none!': !list.isExpanded && isSwimlanesHeader }"
:class="{
'gl-display-none!': !list.isExpanded && isSwimlanesHeader,
'gl-p-0': !list.isExpanded,
}"
>
<span class="gl-display-inline-flex">
<gl-tooltip :target="() => $refs.issueCount" :title="issuesTooltipLabel" />

View File

@ -133,7 +133,7 @@ export default {
</script>
<template>
<div>
<div class="d-flex board-card-header" dir="auto">
<div class="gl-display-flex" dir="auto">
<h4 class="board-card-title gl-mb-0 gl-mt-0">
<gl-icon
v-if="issue.blocked"
@ -156,7 +156,7 @@ export default {
}}</a>
</h4>
</div>
<div v-if="showLabelFooter" class="board-card-labels gl-mt-2 d-flex flex-wrap">
<div v-if="showLabelFooter" class="board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap">
<template v-for="label in orderedLabels">
<gl-label
:key="label.id"
@ -169,24 +169,26 @@ export default {
/>
</template>
</div>
<div class="board-card-footer d-flex justify-content-between align-items-end">
<div
class="board-card-footer gl-display-flex gl-justify-content-space-between gl-align-items-flex-end"
>
<div
class="d-flex align-items-start flex-wrap-reverse board-card-number-container overflow-hidden js-board-card-number-container"
class="gl-display-flex align-items-start flex-wrap-reverse board-card-number-container gl-overflow-hidden js-board-card-number-container"
>
<span
v-if="issue.referencePath"
class="board-card-number overflow-hidden d-flex gl-mr-3 gl-mt-3"
class="board-card-number gl-overflow-hidden gl-display-flex gl-mr-3 gl-mt-3"
>
<tooltip-on-truncate
v-if="issueReferencePath"
:title="issueReferencePath"
placement="bottom"
class="board-issue-path block-truncated bold"
class="board-issue-path gl-text-truncate gl-font-weight-bold"
>{{ issueReferencePath }}</tooltip-on-truncate
>
#{{ issue.iid }}
</span>
<span class="board-info-items gl-mt-3 d-inline-block">
<span class="board-info-items gl-mt-3 gl-display-inline-block">
<issue-due-date v-if="issue.dueDate" :date="issue.dueDate" :closed="issue.closed" />
<issue-time-estimate v-if="issue.timeEstimate" :estimate="issue.timeEstimate" />
<issue-card-weight
@ -196,7 +198,7 @@ export default {
/>
</span>
</div>
<div class="board-card-assignee d-flex">
<div class="board-card-assignee gl-display-flex">
<user-avatar-link
v-for="(assignee, index) in issue.assignees"
v-if="shouldRenderAssignee(index)"
@ -209,7 +211,7 @@ export default {
tooltip-placement="bottom"
>
<span class="js-assignee-tooltip">
<span class="bold d-block">{{ __('Assignee') }}</span>
<span class="gl-font-weight-bold gl-display-block">{{ __('Assignee') }}</span>
{{ assignee.name }}
<span class="text-white-50">@{{ assignee.username }}</span>
</span>

View File

@ -26,7 +26,7 @@ function getErrorMessage(res) {
export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
const divHover = '<div class="div-dropzone-hover"></div>';
const iconPaperclip = spriteIcon('paperclip', 'div-dropzone-icon');
const iconPaperclip = spriteIcon('paperclip', 'div-dropzone-icon s24');
const $attachButton = form.find('.button-attach-file');
const $attachingFileMessage = form.find('.attaching-file-message');
const $cancelButton = form.find('.button-cancel-uploading-files');

View File

@ -1,9 +1,9 @@
<script>
import GetBlobContent from 'shared_queries/snippet/snippet_blob_content.query.graphql';
import BlobHeader from '~/blob/components/blob_header.vue';
import BlobContent from '~/blob/components/blob_content.vue';
import GetBlobContent from '../queries/snippet.blob.content.query.graphql';
import {
SIMPLE_BLOB_VIEWER,
RICH_BLOB_VIEWER,
@ -21,7 +21,7 @@ export default {
query: GetBlobContent,
variables() {
return {
ids: this.snippet.id,
ids: [this.snippet.id],
rich: this.activeViewerType === RICH_BLOB_VIEWER,
paths: [this.blob.path],
};

View File

@ -11,12 +11,12 @@ import {
GlButton,
GlTooltipDirective,
} from '@gitlab/ui';
import CanCreatePersonalSnippet from 'shared_queries/snippet/user_permissions.query.graphql';
import CanCreateProjectSnippet from 'shared_queries/snippet/project_permissions.query.graphql';
import { __ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DeleteSnippetMutation from '../mutations/deleteSnippet.mutation.graphql';
import CanCreatePersonalSnippet from '../queries/userPermissions.query.graphql';
import CanCreateProjectSnippet from '../queries/projectPermissions.query.graphql';
import { joinPaths } from '~/lib/utils/url_utility';
import { fetchPolicies } from '~/lib/graphql';

View File

@ -1,4 +1,4 @@
import GetSnippetQuery from '../queries/snippet.query.graphql';
import GetSnippetQuery from 'shared_queries/snippet/snippet.query.graphql';
const blobsDefault = [];
@ -8,7 +8,7 @@ export const getSnippetMixin = {
query: GetSnippetQuery,
variables() {
return {
ids: this.snippetGid,
ids: [this.snippetGid],
};
},
update: data => {

View File

@ -1,15 +0,0 @@
#import '../fragments/snippetBase.fragment.graphql'
#import '../fragments/project.fragment.graphql'
#import "~/graphql_shared/fragments/author.fragment.graphql"
query GetSnippetQuery($ids: [ID!]) {
snippets(ids: $ids) {
nodes {
...SnippetBase
...SnippetProject
author {
...Author
}
}
}
}

View File

@ -61,17 +61,17 @@ export default {
<span v-if="canAttachFile" class="uploading-container">
<span class="uploading-progress-container hide">
<template>
<gl-icon name="media" :size="16" class="gl-vertical-align-text-bottom" />
<gl-icon name="media" />
</template>
<span class="attaching-file-message"></span>
<!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
<span class="uploading-progress">0%</span>
<gl-loading-icon inline class="align-text-bottom" />
<gl-loading-icon inline />
</span>
<span class="uploading-error-container hide">
<span class="uploading-error-icon">
<template>
<gl-icon name="media" :size="16" class="gl-vertical-align-text-bottom" />
<gl-icon name="media" />
</template>
</span>
<span class="uploading-error-message"></span>

View File

@ -25,7 +25,6 @@
@import './pages/notes';
@import './pages/notifications';
@import './pages/pages';
@import './pages/pipeline_schedules';
@import './pages/pipelines';
@import './pages/profile';
@import './pages/profiles/preferences';

View File

@ -69,7 +69,7 @@
line-height: 28px;
white-space: normal;
/* Small devices (phones, tablets, 768px and lower) */
/* Small devices (phones, 768px and lower) */
@include media-breakpoint-down(xs) {
width: 100%;
}
@ -92,7 +92,7 @@
padding: 16px 15px 11px;
}
/* Small devices (phones, tablets, 768px and lower) */
/* Small devices (phones, 768px and lower) */
@include media-breakpoint-down(sm) {
width: 100%;
}
@ -102,15 +102,6 @@
display: inline-block;
text-align: right;
@include media-breakpoint-down(sm) {
margin-top: $gl-padding-8;
}
@include media-breakpoint-up(md) {
display: flex;
align-items: center;
}
> .btn,
> .btn-group,
> .btn-container,
@ -146,6 +137,35 @@
}
}
@include media-breakpoint-up(md) {
display: flex;
align-items: center;
}
@include media-breakpoint-down(md) {
$controls-margin: $btn-margin-5 - 2px;
flex: 0 0 100%;
margin-top: $gl-padding-8;
.controls-item,
.controls-item-full,
.controls-item:last-child {
flex: 1 1 35%;
display: block;
width: 100%;
margin: $controls-margin;
.btn,
.dropdown {
margin: 0;
}
}
.controls-item-full {
flex: 1 1 100%;
}
}
@include media-breakpoint-down(sm) {
padding-bottom: 0;
width: 100%;
@ -239,32 +259,6 @@
pre {
width: 100%;
}
@include media-breakpoint-down(md) {
.nav-controls {
$controls-margin: $btn-margin-5 - 2px;
flex: 0 0 100%;
margin-top: $gl-padding-8;
.controls-item,
.controls-item-full,
.controls-item:last-child {
flex: 1 1 35%;
display: block;
width: 100%;
margin: $controls-margin;
.btn,
.dropdown {
margin: 0;
}
}
.controls-item-full {
flex: 1 1 100%;
}
}
}
}
.scrolling-tabs-container {

View File

@ -83,9 +83,6 @@
}
.board {
// the next line cannot be replaced with .d-inline-block because it breaks display: none of SortableJS
// see https://gitlab.com/gitlab-org/gitlab-foss/issues/64828
display: inline-block;
width: calc(85vw - 15px);
@include media-breakpoint-up(sm) {
@ -116,39 +113,10 @@
&.is-collapsed {
width: 50px;
.board-title {
flex-direction: column;
}
.board-title-caret {
margin-top: 1px;
}
.user-avatar-link,
.milestone-icon {
margin-top: $gl-padding-8;
transform: rotate(90deg);
}
.board-title-text {
flex-grow: 0;
margin: $gl-padding-8 0;
.board-title-main-text {
display: block;
}
.board-title-sub-text {
display: none;
}
}
.issue-count-badge {
border: 0;
white-space: nowrap;
padding: 0;
}
.board-title-text > span,
.issue-count-badge > span {
height: 16px;
@ -197,10 +165,7 @@
}
.board-title {
align-items: center;
font-size: 1em;
border-bottom: 1px solid var(--gray-100, $gray-100);
padding: 0 $gl-spacing-scale-3;
height: 3rem;
.js-max-issue-size::before {
@ -208,21 +173,6 @@
}
}
.board-title-text {
flex-grow: 1;
}
.board-delete.gl-button {
background-color: transparent;
outline: 0;
&:hover {
color: var(--blue-600, $blue-600);
box-shadow: none;
}
}
.board-blank-state,
.board-promotion-state {
background-color: var(--white, $white);
flex: 1;
@ -230,19 +180,6 @@
overflow-x: hidden;
}
.board-blank-state-list {
> li:not(:last-child) {
margin-bottom: 8px;
}
.label-color {
top: 2px;
width: 16px;
height: 16px;
margin-right: 3px;
}
}
.board-list-component {
min-height: 0; // firefox fix
}
@ -311,10 +248,6 @@
}
}
.board-card-header {
text-align: initial;
}
.board-card-assignee {
margin-top: -$gl-padding-4;
margin-bottom: -$gl-padding-4;
@ -586,28 +519,6 @@
}
}
.board-swimlanes {
overflow-x: auto;
}
.board-header-collapsed-info-icon:hover {
color: var(--gray-900, $gray-900);
}
$epic-icons-spacing: 40px;
.board-epic-lane {
max-width: calc(100vw - #{$contextual-sidebar-width} - #{$epic-icons-spacing});
.page-with-icon-sidebar & {
max-width: calc(100vw - #{$contextual-sidebar-collapsed-width} - #{$epic-icons-spacing});
}
.page-with-icon-sidebar .is-compact & {
max-width: calc(100vw - #{$contextual-sidebar-collapsed-width} - #{$gutter-width} - #{$epic-icons-spacing});
}
.is-compact & {
max-width: calc(100vw - #{$contextual-sidebar-width} - #{$gutter-width} - #{$epic-icons-spacing});
}
}

View File

@ -1,3 +1,5 @@
@import 'mixins_and_variables_and_functions';
.pipeline-schedule-form {
.gl-field-error {
margin: 10px 0 0;
@ -32,11 +34,11 @@
}
.next-run-cell {
color: $gl-text-color-secondary;
color: var(--gray-500, $gray-500);
}
a {
color: $text-color;
color: var(--gl-text-color, $gl-text-color);
}
svg {
@ -46,13 +48,13 @@
.pipeline-schedules-user-callout {
.bordered-box.content-block {
border: 1px solid $border-color;
border: 1px solid var(--border-color, $border-color);
background-color: transparent;
padding: 16px;
}
#dismiss-callout-btn {
color: $gl-text-color;
color: var(--gl-text-color, $gl-text-color);
}
}

View File

@ -193,6 +193,8 @@ class GroupsController < Groups::ApplicationController
protected
def render_show_html
record_experiment_user(:invite_members_empty_group_version_a) if ::Gitlab.com?
render 'groups/show', locals: { trial: params[:trial] }
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
module Mutations
module Terraform
module State
class Base < BaseMutation
authorize :admin_terraform_state
argument :id,
Types::GlobalIDType[::Terraform::State],
required: true,
description: 'Global ID of the Terraform state'
private
def find_object(id:)
GitlabSchema.find_by_gid(id)
end
end
end
end
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
module Mutations
module Terraform
module State
class Delete < Base
graphql_name 'TerraformStateDelete'
def resolve(id:)
state = authorized_find!(id: id)
state.destroy
{ errors: errors_on_object(state) }
end
end
end
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Mutations
module Terraform
module State
class Lock < Base
graphql_name 'TerraformStateLock'
def resolve(id:)
state = authorized_find!(id: id)
if state.locked?
state.errors.add(:base, 'state is already locked')
else
state.update(lock_xid: lock_xid, locked_by_user: current_user, locked_at: Time.current)
end
{ errors: errors_on_object(state) }
end
private
def lock_xid
SecureRandom.uuid
end
end
end
end
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
module Mutations
module Terraform
module State
class Unlock < Base
graphql_name 'TerraformStateUnlock'
def resolve(id:)
state = authorized_find!(id: id)
state.update(lock_xid: nil, locked_by_user: nil, locked_at: nil)
{ errors: errors_on_object(state) }
end
end
end
end
end

View File

@ -1,6 +1,8 @@
query CanCreateProjectSnippet($fullPath: ID!) {
project(fullPath: $fullPath) {
__typename
userPermissions {
__typename
createSnippet
}
}

View File

@ -0,0 +1,65 @@
query GetSnippetQuery($ids: [ID!]) {
snippets(ids: $ids) {
__typename
nodes {
__typename
id
title
description
descriptionHtml
createdAt
updatedAt
visibilityLevel
webUrl
httpUrlToRepo
sshUrlToRepo
blobs {
__typename
nodes {
__typename
binary
name
path
rawPath
size
externalStorage
renderedAsText
simpleViewer {
__typename
collapsed
renderError
tooLarge
type
fileType
}
richViewer {
__typename
collapsed
renderError
tooLarge
type
fileType
}
}
}
userPermissions {
__typename
adminSnippet
updateSnippet
}
project {
__typename
fullPath
webUrl
}
author {
__typename
id
avatarUrl
name
username
webUrl
}
}
}
}

View File

@ -1,9 +1,13 @@
query SnippetBlobContent($ids: [ID!], $rich: Boolean!, $paths: [String!]) {
snippets(ids: $ids) {
__typename
nodes {
__typename
id
blobs(paths: $paths) {
__typename
nodes {
__typename
path
richData @include(if: $rich)
plainData @skip(if: $rich)

View File

@ -1,6 +1,8 @@
query CanCreatePersonalSnippet {
currentUser {
__typename
userPermissions {
__typename
createSnippet
}
}

View File

@ -54,6 +54,9 @@ module Types
'If the body of the Note contains only quick actions, the Note will be ' \
'destroyed during the update, and no Note will be returned'
mount_mutation Mutations::Notes::Destroy
mount_mutation Mutations::Terraform::State::Delete
mount_mutation Mutations::Terraform::State::Lock
mount_mutation Mutations::Terraform::State::Unlock
mount_mutation Mutations::Todos::MarkDone
mount_mutation Mutations::Todos::Restore
mount_mutation Mutations::Todos::MarkAllDone

View File

@ -18,4 +18,8 @@ module InviteMembersHelper
experiment_enabled?(:invite_members_version_b) && !can_import_members?
end
end
def invite_group_members?(group)
experiment_enabled?(:invite_members_empty_group_version_a) && Ability.allowed?(current_user, :admin_group_member, group)
end
end

View File

@ -57,7 +57,10 @@ module PageLayoutHelper
subject = @project || @user || @group
image = subject.avatar_url if subject.present?
args = {}
args[:only_path] = false if Feature.enabled?(:avatar_with_host)
image = subject.avatar_url(args) if subject.present?
image || default
end

View File

@ -12,13 +12,21 @@ module Ci
validates :data, json_schema: { filename: "daily_build_group_report_result_data" }
scope :with_included_projects, -> { includes(:project) }
scope :by_projects, -> (ids) { where(project_id: ids) }
scope :with_coverage, -> { where("(data->'coverage') IS NOT NULL") }
def self.upsert_reports(data)
upsert_all(data, unique_by: :index_daily_build_group_report_results_unique_columns) if data.any?
end
store_accessor :data, :coverage
def self.recent_results(attrs, limit: nil)
where(attrs).order(date: :desc, group_name: :asc).limit(limit)
class << self
def upsert_reports(data)
upsert_all(data, unique_by: :index_daily_build_group_report_results_unique_columns) if data.any?
end
def recent_results(attrs, limit: nil)
where(attrs).order(date: :desc, group_name: :asc).limit(limit)
end
end
end
end
Ci::DailyBuildGroupReportResult.prepend_if_ee('EE::Ci::DailyBuildGroupReportResult')

View File

@ -51,11 +51,11 @@ module Issuable
end
end
def create_wip_note(old_title)
def create_draft_note(old_title)
return unless issuable.is_a?(MergeRequest)
if MergeRequest.work_in_progress?(old_title) != issuable.work_in_progress?
SystemNoteService.handle_merge_request_wip(issuable, issuable.project, current_user)
SystemNoteService.handle_merge_request_draft(issuable, issuable.project, current_user)
end
end
@ -69,7 +69,7 @@ module Issuable
end
def create_title_change_note(old_title)
create_wip_note(old_title)
create_draft_note(old_title)
if issuable.wipless_title_changed(old_title)
SystemNoteService.change_title(issuable, issuable.project, current_user, old_title)

View File

@ -42,7 +42,7 @@ module MergeRequests
end
notify_about_push(mr)
mark_mr_as_wip_from_commits(mr)
mark_mr_as_draft_from_commits(mr)
execute_mr_web_hooks(mr)
end
@ -246,7 +246,7 @@ module MergeRequests
notification_service.push_to_merge_request(merge_request, @current_user, new_commits: new_commits, existing_commits: existing_commits)
end
def mark_mr_as_wip_from_commits(merge_request)
def mark_mr_as_draft_from_commits(merge_request)
return unless @commits.present?
commit_shas = merge_request.commit_shas
@ -257,7 +257,7 @@ module MergeRequests
if wip_commit && !merge_request.work_in_progress?
merge_request.update(title: merge_request.wip_title)
SystemNoteService.add_merge_request_wip_from_commit(
SystemNoteService.add_merge_request_draft_from_commit(
merge_request,
merge_request.project,
@current_user,

View File

@ -130,12 +130,12 @@ module SystemNoteService
::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author).abort_merge_when_pipeline_succeeds(reason)
end
def handle_merge_request_wip(noteable, project, author)
::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author).handle_merge_request_wip
def handle_merge_request_draft(noteable, project, author)
::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author).handle_merge_request_draft
end
def add_merge_request_wip_from_commit(noteable, project, author, commit)
::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author).add_merge_request_wip_from_commit(commit)
def add_merge_request_draft_from_commit(noteable, project, author, commit)
::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author).add_merge_request_draft_from_commit(commit)
end
def resolve_all_discussions(merge_request, project, author)

View File

@ -26,16 +26,16 @@ module SystemNotes
create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
end
def handle_merge_request_wip
prefix = noteable.work_in_progress? ? "marked" : "unmarked"
def handle_merge_request_draft
action = noteable.work_in_progress? ? "draft" : "ready"
body = "#{prefix} as a **Work In Progress**"
body = "marked this merge request as **#{action}**"
create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
end
def add_merge_request_wip_from_commit(commit)
body = "marked as a **Work In Progress** from #{commit.to_reference(project)}"
def add_merge_request_draft_from_commit(commit)
body = "marked this merge request as **draft** from #{commit.to_reference(project)}"
create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
end

View File

@ -1,20 +1,30 @@
.devops
.devops-header
%h2.devops-header-title{ class: "devops-#{score_level(@metric.average_percentage_score)}-score" }
= number_to_percentage(@metric.average_percentage_score, precision: 1)
.devops-header-subtitle
= _('DevOps')
%br
= _('Score')
= link_to sprite_icon('question-o', css_class: 'devops-header-icon'), help_page_path('user/admin_area/analytics/dev_ops_report')
- usage_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled
.devops-cards.board-card-container
- @metric.cards.each do |card|
= render 'card', card: card
- if usage_ping_enabled && show_callout?('dev_ops_report_intro_callout_dismissed')
= render 'callout'
.devops-steps.d-none.d-lg-block
- @metric.idea_to_production_steps.each_with_index do |step, index|
.devops-step{ class: "devops-#{score_level(step.percentage_score)}-score" }
= custom_icon("i2p_step_#{index + 1}")
%h4.devops-step-title
= step.title
- if !usage_ping_enabled
#js-devops-empty-state{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_usage_ping_link: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/product_analytics/usage_ping') } }
- elsif @metric.blank?
= render 'no_data'
- else
.devops
.devops-header
%h2.devops-header-title{ class: "devops-#{score_level(@metric.average_percentage_score)}-score" }
= number_to_percentage(@metric.average_percentage_score, precision: 1)
.devops-header-subtitle
= _('DevOps')
%br
= _('Score')
= link_to sprite_icon('question-o', css_class: 'devops-header-icon'), help_page_path('user/admin_area/analytics/dev_ops_report')
.devops-cards.board-card-container
- @metric.cards.each do |card|
= render 'card', card: card
.devops-steps.d-none.d-lg-block
- @metric.idea_to_production_steps.each_with_index do |step, index|
.devops-step{ class: "devops-#{score_level(step.percentage_score)}-score" }
= custom_icon("i2p_step_#{index + 1}")
%h4.devops-step-title
= step.title

View File

@ -1,15 +1,6 @@
- page_title _('DevOps Report')
- usage_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled
- add_page_specific_style 'page_bundles/dev_ops_report'
.container
- if usage_ping_enabled && show_callout?('dev_ops_report_intro_callout_dismissed')
= render 'callout'
.gl-mt-3
- if !usage_ping_enabled
#js-devops-empty-state{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_usage_ping_link: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/product_analytics/usage_ping') } }
- elsif @metric.blank?
= render 'no_data'
- else
= render 'report'

View File

@ -6,6 +6,7 @@
= render 'dashboard/groups_head'
- if params[:filter].blank? && @groups.empty?
= render 'shared/groups/empty_state'
.empty-state
= render 'shared/groups/empty_state'
- else
= render 'groups'

View File

@ -1,6 +1,7 @@
- add_to_breadcrumbs _("Schedules"), pipeline_schedules_path(@project)
- breadcrumb_title "##{@schedule.id}"
- page_title _("Edit"), @schedule.description, _("Pipeline Schedule")
- add_page_specific_style 'page_bundles/pipeline_schedules'
%h3.page-title
= _("Edit Pipeline Schedule %{id}") % { id: @schedule.id }

View File

@ -1,6 +1,6 @@
- breadcrumb_title _("Schedules")
- page_title _("Pipeline Schedules")
- add_page_specific_style 'page_bundles/pipeline_schedules'
#pipeline-schedules-callout{ data: { docs_url: help_page_path('ci/pipelines/schedules'), image_url: image_path('illustrations/pipeline_schedule_callout.svg') } }
.top-area

View File

@ -1,6 +1,7 @@
- breadcrumb_title "Schedules"
- @breadcrumb_link = namespace_project_pipeline_schedules_path(@project.namespace, @project)
- page_title _("New Pipeline Schedule")
- add_page_specific_style 'page_bundles/pipeline_schedules'
- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))

View File

@ -1,8 +1,13 @@
.group-empty-state.row.align-items-center.justify-content-center
.icon.text-center.order-md-2
.row.gl-align-items-center.gl-justify-content-center
.order-md-2
= custom_icon("icon_empty_groups")
.text-content.m-0.order-md-1
.text-content.order-md-1{ class: 'gl-m-0!' }
%h4= s_("GroupsEmptyState|A group is a collection of several projects.")
%p= s_("GroupsEmptyState|If you organize your projects under a group, it works like a folder.")
%p= s_("GroupsEmptyState|You can manage your group members permissions and access to each project in the group.")
- if invite_group_members?(@group)
= link_to _('Invite your team'),
group_group_members_path(@group),
class: 'gl-button btn btn-success-secondary',
data: { track_event: 'click_invite_team_group_empty_state', track_label: 'invite_team_group_empty_state' }

View File

@ -1,3 +1,9 @@
- add_page_startup_graphql_call('snippet/snippet', { ids: [@snippet.to_global_id.uri] })
- add_page_startup_graphql_call('snippet/snippet_blob_content', { ids: [@snippet.to_global_id.uri], rich: false, paths: [@snippet.file_name] })
- if @snippet.project_id?
- add_page_startup_graphql_call('snippet/project_permissions', { fullPath: @snippet.project_id })
- else
- add_page_startup_graphql_call('snippet/user_permissions')
- @hide_top_links = true
- @content_class = "limit-container-width limited-inner-width-container" unless fluid_layout
- add_to_breadcrumbs _("Snippets"), dashboard_snippets_path

View File

@ -0,0 +1,5 @@
---
title: Add GraphQL endpoints to lock, unlock and delete Terraform states
merge_request: 43955
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Update system note when marking merge request as draft or ready
merge_request: 45644
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Pre-fetched GraphQL queries for snippet view
merge_request: 46130
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Boards - Fix Milestone icon alignment in header
merge_request: 45965
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix dropzone paperclip and loading icons
merge_request: 46093
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Refactor secondary_navigation_elements.scss
merge_request: 45763
author: Takuya Noguchi
type: other

View File

@ -0,0 +1,5 @@
---
title: Remove Cycle Analytics message from en i18n message
merge_request: 45178
author: Takuya Noguchi
type: other

View File

@ -0,0 +1,5 @@
---
title: Create snippet_repository_storage_moves database table
merge_request: 45990
author:
type: added

View File

@ -174,6 +174,7 @@ module Gitlab
config.assets.precompile << "notify.css"
config.assets.precompile << "mailers/*.css"
config.assets.precompile << "page_bundles/_mixins_and_variables_and_functions.css"
config.assets.precompile << "page_bundles/alert_management_details.css"
config.assets.precompile << "page_bundles/boards.css"
config.assets.precompile << "page_bundles/ci_status.css"
config.assets.precompile << "page_bundles/cycle_analytics.css"
@ -191,13 +192,13 @@ module Gitlab
config.assets.precompile << "page_bundles/milestone.css"
config.assets.precompile << "page_bundles/pipeline.css"
config.assets.precompile << "page_bundles/pipelines.css"
config.assets.precompile << "page_bundles/pipeline_schedules.css"
config.assets.precompile << "page_bundles/productivity_analytics.css"
config.assets.precompile << "page_bundles/terminal.css"
config.assets.precompile << "page_bundles/todos.css"
config.assets.precompile << "page_bundles/reports.css"
config.assets.precompile << "page_bundles/xterm.css"
config.assets.precompile << "page_bundles/wiki.css"
config.assets.precompile << "page_bundles/alert_management_details.css"
config.assets.precompile << "page_bundles/xterm.css"
config.assets.precompile << "lazy_bundles/cropper.css"
config.assets.precompile << "performance_bar.css"
config.assets.precompile << "lib/ace.js"

View File

@ -0,0 +1,7 @@
---
name: avatar_with_host
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45776
rollout_issue_url:
type: development
group: group::editor
default_enabled: false

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
class CreateSnippetRepositoryStorageMove < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
unless table_exists?(:snippet_repository_storage_moves)
with_lock_retries do
create_table :snippet_repository_storage_moves do |t|
t.timestamps_with_timezone
t.references :snippet, index: true, null: false, foreign_key: { on_delete: :cascade }
t.integer :state, limit: 2, default: 1, null: false
t.text :source_storage_name, null: false
t.text :destination_storage_name, null: false
end
end
end
add_text_limit(:snippet_repository_storage_moves, :source_storage_name, 255, constraint_name: 'snippet_repository_storage_moves_source_storage_name')
add_text_limit(:snippet_repository_storage_moves, :destination_storage_name, 255, constraint_name: 'snippet_repository_storage_moves_destination_storage_name')
end
def down
with_lock_retries do
drop_table :snippet_repository_storage_moves
end
end
end

View File

@ -0,0 +1 @@
f9a573d50f8b4aeb3d8d2cc2f0223ab9970776d663e49e0f022e96158593d929

View File

@ -16146,6 +16146,27 @@ CREATE TABLE snippet_repositories (
CONSTRAINT snippet_repositories_verification_failure_text_limit CHECK ((char_length(verification_failure) <= 255))
);
CREATE TABLE snippet_repository_storage_moves (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
snippet_id bigint NOT NULL,
state smallint DEFAULT 1 NOT NULL,
source_storage_name text NOT NULL,
destination_storage_name text NOT NULL,
CONSTRAINT snippet_repository_storage_moves_destination_storage_name CHECK ((char_length(destination_storage_name) <= 255)),
CONSTRAINT snippet_repository_storage_moves_source_storage_name CHECK ((char_length(source_storage_name) <= 255))
);
CREATE SEQUENCE snippet_repository_storage_moves_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE snippet_repository_storage_moves_id_seq OWNED BY snippet_repository_storage_moves.id;
CREATE TABLE snippet_statistics (
snippet_id bigint NOT NULL,
repository_size bigint DEFAULT 0 NOT NULL,
@ -18048,6 +18069,8 @@ ALTER TABLE ONLY slack_integrations ALTER COLUMN id SET DEFAULT nextval('slack_i
ALTER TABLE ONLY smartcard_identities ALTER COLUMN id SET DEFAULT nextval('smartcard_identities_id_seq'::regclass);
ALTER TABLE ONLY snippet_repository_storage_moves ALTER COLUMN id SET DEFAULT nextval('snippet_repository_storage_moves_id_seq'::regclass);
ALTER TABLE ONLY snippet_user_mentions ALTER COLUMN id SET DEFAULT nextval('snippet_user_mentions_id_seq'::regclass);
ALTER TABLE ONLY snippets ALTER COLUMN id SET DEFAULT nextval('snippets_id_seq'::regclass);
@ -19434,6 +19457,9 @@ ALTER TABLE ONLY smartcard_identities
ALTER TABLE ONLY snippet_repositories
ADD CONSTRAINT snippet_repositories_pkey PRIMARY KEY (snippet_id);
ALTER TABLE ONLY snippet_repository_storage_moves
ADD CONSTRAINT snippet_repository_storage_moves_pkey PRIMARY KEY (id);
ALTER TABLE ONLY snippet_statistics
ADD CONSTRAINT snippet_statistics_pkey PRIMARY KEY (snippet_id);
@ -21700,6 +21726,8 @@ CREATE UNIQUE INDEX index_snippet_repositories_on_disk_path ON snippet_repositor
CREATE INDEX index_snippet_repositories_on_shard_id ON snippet_repositories USING btree (shard_id);
CREATE INDEX index_snippet_repository_storage_moves_on_snippet_id ON snippet_repository_storage_moves USING btree (snippet_id);
CREATE UNIQUE INDEX index_snippet_user_mentions_on_note_id ON snippet_user_mentions USING btree (note_id) WHERE (note_id IS NOT NULL);
CREATE INDEX index_snippets_on_author_id ON snippets USING btree (author_id);
@ -23408,6 +23436,9 @@ ALTER TABLE ONLY ci_pipeline_artifacts
ALTER TABLE ONLY group_deletion_schedules
ADD CONSTRAINT fk_rails_4b8c694a6c FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY snippet_repository_storage_moves
ADD CONSTRAINT fk_rails_4b950f5b94 FOREIGN KEY (snippet_id) REFERENCES snippets(id) ON DELETE CASCADE;
ALTER TABLE ONLY design_management_designs
ADD CONSTRAINT fk_rails_4bb1073360 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;

View File

@ -2515,6 +2515,26 @@ Identifier of Clusters::Cluster
"""
scalar ClustersClusterID
"""
Represents the code coverage summary for a project
"""
type CodeCoverageSummary {
"""
Average percentage of the different code coverage results available for the project.
"""
averageCoverage: Float
"""
Number of different code coverage results available.
"""
coverageCount: Int
"""
Latest date when the code coverage was created for the project.
"""
lastUpdatedAt: Time
}
type Commit {
"""
Author of the commit
@ -12531,6 +12551,9 @@ type Mutation {
removeProjectFromSecurityDashboard(input: RemoveProjectFromSecurityDashboardInput!): RemoveProjectFromSecurityDashboardPayload
revertVulnerabilityToDetected(input: RevertVulnerabilityToDetectedInput!): RevertVulnerabilityToDetectedPayload @deprecated(reason: "Use vulnerabilityRevertToDetected. Deprecated in 13.5")
runDastScan(input: RunDASTScanInput!): RunDASTScanPayload @deprecated(reason: "Use DastOnDemandScanCreate. Deprecated in 13.4")
terraformStateDelete(input: TerraformStateDeleteInput!): TerraformStateDeletePayload
terraformStateLock(input: TerraformStateLockInput!): TerraformStateLockPayload
terraformStateUnlock(input: TerraformStateUnlockInput!): TerraformStateUnlockPayload
todoMarkDone(input: TodoMarkDoneInput!): TodoMarkDonePayload
todoRestore(input: TodoRestoreInput!): TodoRestorePayload
todoRestoreMany(input: TodoRestoreManyInput!): TodoRestoreManyPayload
@ -13791,6 +13814,12 @@ type Project {
last: Int
): ClusterAgentConnection
"""
Code coverages summary associated with the project. Available only when
feature flag `group_coverage_data_report` is enabled
"""
codeCoverageSummary: CodeCoverageSummary
"""
Compliance frameworks associated with the project
"""
@ -19010,6 +19039,36 @@ type TerraformStateConnection {
pageInfo: PageInfo!
}
"""
Autogenerated input type of TerraformStateDelete
"""
input TerraformStateDeleteInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Global ID of the Terraform state
"""
id: TerraformStateID!
}
"""
Autogenerated return type of TerraformStateDelete
"""
type TerraformStateDeletePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
"""
An edge in a connection.
"""
@ -19025,6 +19084,71 @@ type TerraformStateEdge {
node: TerraformState
}
"""
Identifier of Terraform::State
"""
scalar TerraformStateID
"""
Autogenerated input type of TerraformStateLock
"""
input TerraformStateLockInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Global ID of the Terraform state
"""
id: TerraformStateID!
}
"""
Autogenerated return type of TerraformStateLock
"""
type TerraformStateLockPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
"""
Autogenerated input type of TerraformStateUnlock
"""
input TerraformStateUnlockInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Global ID of the Terraform state
"""
id: TerraformStateID!
}
"""
Autogenerated return type of TerraformStateUnlock
"""
type TerraformStateUnlockPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
"""
Represents the Geo sync and verification state of a terraform state version
"""

View File

@ -6819,6 +6819,61 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "CodeCoverageSummary",
"description": "Represents the code coverage summary for a project",
"fields": [
{
"name": "averageCoverage",
"description": "Average percentage of the different code coverage results available for the project.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Float",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "coverageCount",
"description": "Number of different code coverage results available.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "lastUpdatedAt",
"description": "Latest date when the code coverage was created for the project.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Commit",
@ -36352,6 +36407,87 @@
"isDeprecated": true,
"deprecationReason": "Use DastOnDemandScanCreate. Deprecated in 13.4"
},
{
"name": "terraformStateDelete",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "TerraformStateDeleteInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "TerraformStateDeletePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "terraformStateLock",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "TerraformStateLockInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "TerraformStateLockPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "terraformStateUnlock",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "TerraformStateUnlockInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "TerraformStateUnlockPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "todoMarkDone",
"description": null,
@ -40474,6 +40610,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "codeCoverageSummary",
"description": "Code coverages summary associated with the project. Available only when feature flag `group_coverage_data_report` is enabled",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "CodeCoverageSummary",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "complianceFrameworks",
"description": "Compliance frameworks associated with the project",
@ -55103,6 +55253,94 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "TerraformStateDeleteInput",
"description": "Autogenerated input type of TerraformStateDelete",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "Global ID of the Terraform state",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "TerraformStateID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "TerraformStateDeletePayload",
"description": "Autogenerated return type of TerraformStateDelete",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "TerraformStateEdge",
@ -55148,6 +55386,192 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "TerraformStateID",
"description": "Identifier of Terraform::State",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "TerraformStateLockInput",
"description": "Autogenerated input type of TerraformStateLock",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "Global ID of the Terraform state",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "TerraformStateID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "TerraformStateLockPayload",
"description": "Autogenerated return type of TerraformStateLock",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "TerraformStateUnlockInput",
"description": "Autogenerated input type of TerraformStateUnlock",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "Global ID of the Terraform state",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "TerraformStateID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "TerraformStateUnlockPayload",
"description": "Autogenerated return type of TerraformStateUnlock",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "TerraformStateVersionRegistry",

View File

@ -390,6 +390,16 @@ Autogenerated return type of ClusterAgentTokenDelete.
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### CodeCoverageSummary
Represents the code coverage summary for a project.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `averageCoverage` | Float | Average percentage of the different code coverage results available for the project. |
| `coverageCount` | Int | Number of different code coverage results available. |
| `lastUpdatedAt` | Time | Latest date when the code coverage was created for the project. |
### Commit
| Field | Type | Description |
@ -1984,6 +1994,7 @@ Autogenerated return type of PipelineRetry.
| `avatarUrl` | String | URL to avatar image file of the project |
| `board` | Board | A single board of the project |
| `clusterAgent` | ClusterAgent | Find a single cluster agent by name |
| `codeCoverageSummary` | CodeCoverageSummary | Code coverages summary associated with the project. Available only when feature flag `group_coverage_data_report` is enabled |
| `containerExpirationPolicy` | ContainerExpirationPolicy | The container expiration policy of the project |
| `containerRegistryEnabled` | Boolean | Indicates if the project stores Docker container images in a container registry |
| `createdAt` | Time | Timestamp of the project creation |
@ -2629,6 +2640,33 @@ Completion status of tasks.
| `name` | String! | Name of the Terraform state |
| `updatedAt` | Time! | Timestamp the Terraform state was updated |
### TerraformStateDeletePayload
Autogenerated return type of TerraformStateDelete.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### TerraformStateLockPayload
Autogenerated return type of TerraformStateLock.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### TerraformStateUnlockPayload
Autogenerated return type of TerraformStateUnlock.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### TerraformStateVersionRegistry
Represents the Geo sync and verification state of a terraform state version.

View File

@ -577,3 +577,7 @@ Should an error occur during a push, GitLab will display an "Error" highlight fo
### 13:Received RST_STREAM with error code 2 with GitHub
If you receive an "13:Received RST_STREAM with error code 2" while mirroring to a GitHub repository, your GitHub settings might be set to block pushes that expose your email address used in commits. Either set your email address on GitHub to be public, or disable the [Block command line pushes that expose my email](https://github.com/settings/emails) setting.
### 4:Deadline Exceeded
When upgrading to GitLab 11.11.8 or newer, a change in how usernames are represented means that you may need to update your mirroring username and password to ensure that `%40` characters are replaced with `@`.

View File

@ -51,6 +51,9 @@ module Gitlab
invite_members_version_b: {
tracking_category: 'Growth::Expansion::Experiment::InviteMembersVersionB'
},
invite_members_empty_group_version_a: {
tracking_category: 'Growth::Expansion::Experiment::InviteMembersEmptyGroupVersionA'
},
new_create_project_ui: {
tracking_category: 'Manage::Import::Experiment::NewCreateProjectUi'
},

View File

@ -305,12 +305,6 @@ msgstr ""
msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
msgstr ""
msgid "Cycle Analytics"
msgstr ""
msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
msgstr ""
msgid "CycleAnalyticsStage|Code"
msgstr ""
@ -483,9 +477,6 @@ msgstr ""
msgid "Interval Pattern"
msgstr ""
msgid "Introducing Cycle Analytics"
msgstr ""
msgid "Jobs for last month"
msgstr ""

View File

@ -14425,6 +14425,9 @@ msgstr ""
msgid "Invite teammates (optional)"
msgstr ""
msgid "Invite your team"
msgstr ""
msgid "InviteEmail|%{inviter} invited you to join the %{project_or_group_name} %{project_or_group} as a %{role}"
msgstr ""

View File

@ -22,7 +22,7 @@ module QA
it 'allows 2FA code recovery via ssh' do
recovery_code = Support::SSH.perform do |ssh|
ssh.key = ssh_key
ssh.uri = address.gsub(uri.port.to_s, ssh_port)
ssh.uri = address.gsub(/(?<=:)(#{uri.port})/, ssh_port)
ssh.setup
output = ssh.reset_2fa_codes
output.scan(/([A-Za-z0-9]{16})\n/).flatten.first

View File

@ -3,7 +3,7 @@
FactoryBot.define do
factory :ci_daily_build_group_report_result, class: 'Ci::DailyBuildGroupReportResult' do
ref_path { Gitlab::Git::BRANCH_REF_PREFIX + 'master' }
date { Time.zone.now.to_date }
date { Date.current }
project
last_pipeline factory: :ci_pipeline
group_name { 'rspec' }

View File

@ -87,26 +87,22 @@ RSpec.describe "User browses files" do
end
it "shows correct files and links" do
# rubocop:disable Lint/Void
# Test the full URLs of links instead of relative paths by `have_link(text: "...", href: "...")`.
find("a", text: /^empty$/)["href"] == project_tree_url(project, "markdown")
find("a", text: /^#id$/)["href"] == project_tree_url(project, "markdown", anchor: "#id")
find("a", text: %r{^/#id$})["href"] == project_tree_url(project, "markdown", anchor: "#id")
find("a", text: /^README.md#id$/)["href"] == project_blob_url(project, "markdown/README.md", anchor: "#id")
find("a", text: %r{^d/README.md#id$})["href"] == project_blob_url(project, "d/markdown/README.md", anchor: "#id")
# rubocop:enable Lint/Void
expect(current_path).to eq(project_tree_path(project, "markdown"))
expect(page).to have_content("README.md")
.and have_content("CHANGELOG")
.and have_content("Welcome to GitLab GitLab is a free project and repository management application")
.and have_link("GitLab API doc")
.and have_link("GitLab API website")
.and have_link("Rake tasks")
.and have_link("backup and restore procedure")
.and have_link("GitLab API doc directory")
.and have_link("Maintenance")
.and have_header_with_correct_id_and_link(2, "Application details", "application-details")
.and have_content("CHANGELOG")
.and have_content("Welcome to GitLab GitLab is a free project and repository management application")
.and have_link("GitLab API doc")
.and have_link("GitLab API website")
.and have_link("Rake tasks")
.and have_link("backup and restore procedure")
.and have_link("GitLab API doc directory")
.and have_link("Maintenance")
.and have_header_with_correct_id_and_link(2, "Application details", "application-details")
.and have_link("empty", href: "")
.and have_link("#id", href: "#id")
.and have_link("/#id", href: project_blob_path(project, "markdown/README.md", anchor: "id"))
.and have_link("README.md#id", href: project_blob_path(project, "markdown/README.md", anchor: "id"))
.and have_link("d/README.md#id", href: project_blob_path(project, "markdown/db/README.md", anchor: "id"))
end
it "shows correct content of file" do
@ -114,10 +110,10 @@ RSpec.describe "User browses files" do
expect(current_path).to eq(project_blob_path(project, "markdown/doc/api/README.md"))
expect(page).to have_content("All API requests require authentication")
.and have_content("Contents")
.and have_link("Users")
.and have_link("Rake tasks")
.and have_header_with_correct_id_and_link(1, "GitLab API", "gitlab-api")
.and have_content("Contents")
.and have_link("Users")
.and have_link("Rake tasks")
.and have_header_with_correct_id_and_link(1, "GitLab API", "gitlab-api")
click_link("Users")
@ -148,16 +144,13 @@ RSpec.describe "User browses files" do
click_link("d")
end
# rubocop:disable Lint/Void
# Test the full URLs of links instead of relative paths by `have_link(text: "...", href: "...")`.
find("a", text: "..")["href"] == project_tree_url(project, "markdown/d")
# rubocop:enable Lint/Void
expect(page).to have_link("..", href: project_tree_path(project, "markdown/"))
page.within(".tree-table") do
click_link("README.md")
end
# Test the full URLs of links instead of relative paths by `have_link(text: "...", href: "...")`.
find("a", text: /^empty$/)["href"] == project_blob_url(project, "markdown/d/README.md")
expect(page).to have_link("empty", href: "")
end
it "shows correct content of directory" do

View File

@ -53,7 +53,7 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
class="div-dropzone-hover"
>
<svg
class="div-dropzone-icon"
class="div-dropzone-icon s24"
>
<use
xlink:href="undefined#paperclip"

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Terraform::State::Delete do
let_it_be(:user) { create(:user) }
let_it_be(:state) { create(:terraform_state) }
let(:mutation) do
described_class.new(
object: double,
context: { current_user: user },
field: double
)
end
it { expect(described_class.graphql_name).to eq('TerraformStateDelete') }
it { expect(described_class).to require_graphql_authorizations(:admin_terraform_state) }
describe '#resolve' do
let(:global_id) { state.to_global_id }
subject { mutation.resolve(id: global_id) }
context 'user does not have permission' do
it 'raises an error', :aggregate_failures do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
expect { state.reload }.not_to raise_error
end
end
context 'user has permission' do
before do
state.project.add_maintainer(user)
end
it 'deletes the state', :aggregate_failures do
expect do
expect(subject).to eq(errors: [])
end.to change { ::Terraform::State.count }.by(-1)
expect { state.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context 'with invalid params' do
let(:global_id) { user.to_global_id }
it 'raises an error', :aggregate_failures do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
expect { state.reload }.not_to raise_error
end
end
end
end

View File

@ -0,0 +1,68 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Terraform::State::Lock do
let_it_be(:user) { create(:user) }
let_it_be(:state) { create(:terraform_state) }
let(:mutation) do
described_class.new(
object: double,
context: { current_user: user },
field: double
)
end
it { expect(described_class.graphql_name).to eq('TerraformStateLock') }
it { expect(described_class).to require_graphql_authorizations(:admin_terraform_state) }
describe '#resolve' do
let(:global_id) { state.to_global_id }
subject { mutation.resolve(id: global_id) }
context 'user does not have permission' do
it 'raises an error', :aggregate_failures do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
expect(state.reload).not_to be_locked
end
end
context 'user has permission' do
before do
state.project.add_maintainer(user)
end
it 'locks the state', :aggregate_failures do
expect(subject).to eq(errors: [])
expect(state.reload).to be_locked
expect(state.locked_by_user).to eq(user)
expect(state.lock_xid).to be_present
expect(state.locked_at).to be_present
end
context 'state is already locked' do
let(:locked_by_user) { create(:user) }
let(:state) { create(:terraform_state, :locked, locked_by_user: locked_by_user) }
it 'does not modify the existing lock', :aggregate_failures do
expect(subject).to eq(errors: ['state is already locked'])
expect(state.reload).to be_locked
expect(state.locked_by_user).to eq(locked_by_user)
end
end
end
context 'with invalid params' do
let(:global_id) { user.to_global_id }
it 'raises an error', :aggregate_failures do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
expect(state.reload).not_to be_locked
end
end
end
end

View File

@ -0,0 +1,61 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Terraform::State::Unlock do
let_it_be(:user) { create(:user) }
let_it_be(:state) { create(:terraform_state, :locked) }
let(:mutation) do
described_class.new(
object: double,
context: { current_user: user },
field: double
)
end
it { expect(described_class.graphql_name).to eq('TerraformStateUnlock') }
it { expect(described_class).to require_graphql_authorizations(:admin_terraform_state) }
describe '#resolve' do
let(:global_id) { state.to_global_id }
subject { mutation.resolve(id: global_id) }
context 'user does not have permission' do
it 'raises an error', :aggregate_failures do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
expect(state.reload).to be_locked
end
end
context 'user has permission' do
before do
state.project.add_maintainer(user)
end
it 'unlocks the state', :aggregate_failures do
expect(subject).to eq(errors: [])
expect(state.reload).not_to be_locked
end
context 'state is already unlocked' do
let(:state) { create(:terraform_state) }
it 'does not modify the state' do
expect(subject).to eq(errors: [])
expect(state.reload).not_to be_locked
end
end
end
context 'with invalid params' do
let(:global_id) { user.to_global_id }
it 'raises an error', :aggregate_failures do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
expect(state.reload).to be_locked
end
end
end
end

View File

@ -7,70 +7,110 @@ RSpec.describe InviteMembersHelper do
let_it_be(:developer) { create(:user, developer_projects: [project]) }
let(:owner) { project.owner }
before do
assign(:project, project)
end
context 'with project' do
before do
assign(:project, project)
end
describe "#directly_invite_members?" do
context 'when the user is an owner' do
before do
allow(helper).to receive(:current_user) { owner }
describe "#directly_invite_members?" do
context 'when the user is an owner' do
before do
allow(helper).to receive(:current_user) { owner }
end
it 'returns false' do
allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_a) { false }
expect(helper.directly_invite_members?).to eq false
end
it 'returns true' do
allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_a) { true }
expect(helper.directly_invite_members?).to eq true
end
end
it 'returns false' do
allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_a) { false }
context 'when the user is a developer' do
before do
allow(helper).to receive(:current_user) { developer }
end
expect(helper.directly_invite_members?).to eq false
end
it 'returns false' do
allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_a) { true }
it 'returns true' do
allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_a) { true }
expect(helper.directly_invite_members?).to eq true
expect(helper.directly_invite_members?).to eq false
end
end
end
context 'when the user is a developer' do
before do
allow(helper).to receive(:current_user) { developer }
describe "#indirectly_invite_members?" do
context 'when a user is a developer' do
before do
allow(helper).to receive(:current_user) { developer }
end
it 'returns false' do
allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { false }
expect(helper.indirectly_invite_members?).to eq false
end
it 'returns true' do
allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { true }
expect(helper.indirectly_invite_members?).to eq true
end
end
it 'returns false' do
allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_a) { true }
context 'when a user is an owner' do
before do
allow(helper).to receive(:current_user) { owner }
end
expect(helper.directly_invite_members?).to eq false
it 'returns false' do
allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { true }
expect(helper.indirectly_invite_members?).to eq false
end
end
end
end
describe "#indirectly_invite_members?" do
context 'when a user is a developer' do
before do
allow(helper).to receive(:current_user) { developer }
context 'with group' do
let_it_be(:group) { create(:group) }
describe "#invite_group_members?" do
context 'when the user is an owner' do
before do
group.add_owner(owner)
allow(helper).to receive(:current_user) { owner }
end
it 'returns false' do
allow(helper).to receive(:experiment_enabled?).with(:invite_members_empty_group_version_a) { false }
expect(helper.invite_group_members?(group)).to eq false
end
it 'returns true' do
allow(helper).to receive(:experiment_enabled?).with(:invite_members_empty_group_version_a) { true }
expect(helper.invite_group_members?(group)).to eq true
end
end
it 'returns false' do
allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { false }
context 'when the user is a developer' do
before do
group.add_developer(developer)
allow(helper).to receive(:current_user) { developer }
end
expect(helper.indirectly_invite_members?).to eq false
end
it 'returns false' do
allow(helper).to receive(:experiment_enabled?).with(:invite_members_empty_group_version_a) { true }
it 'returns true' do
allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { true }
expect(helper.indirectly_invite_members?).to eq true
end
end
context 'when a user is an owner' do
before do
allow(helper).to receive(:current_user) { owner }
end
it 'returns false' do
allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { true }
expect(helper.indirectly_invite_members?).to eq false
expect(helper.invite_group_members?(group)).to eq false
end
end
end
end

View File

@ -56,19 +56,24 @@ RSpec.describe PageLayoutHelper do
end
%w(project user group).each do |type|
context "with @#{type} assigned" do
it "uses #{type.titlecase} avatar if available" do
object = double(avatar_url: 'http://example.com/uploads/-/system/avatar.png')
assign(type, object)
let(:object) { build(type, trait) }
let(:trait) { :with_avatar }
expect(helper.page_image).to eq object.avatar_url
context "with @#{type} assigned" do
before do
assign(type, object)
end
it 'falls back to the default when avatar_url is nil' do
object = double(avatar_url: nil)
assign(type, object)
it "uses #{type.titlecase} avatar full url" do
expect(helper.page_image).to eq object.avatar_url(only_path: false)
end
expect(helper.page_image).to match_asset_path 'assets/gitlab_logo.png'
context 'when avatar_url is nil' do
let(:trait) { nil }
it 'falls back to the default when avatar_url is nil' do
expect(helper.page_image).to match_asset_path 'assets/gitlab_logo.png'
end
end
end
@ -77,6 +82,16 @@ RSpec.describe PageLayoutHelper do
expect(helper.page_image).to match_asset_path 'assets/gitlab_logo.png'
end
end
context 'if avatar_with_host is disabled' do
it "#{type.titlecase} does not generate avatar full url" do
stub_feature_flags(avatar_with_host: false)
assign(type, object)
expect(helper.page_image).to eq object.avatar_url(only_path: true)
end
end
end
end

View File

@ -81,4 +81,28 @@ RSpec.describe Ci::DailyBuildGroupReportResult do
end
end
end
describe 'scopes' do
let_it_be(:project) { create(:project) }
let(:recent_build_group_report_result) { create(:ci_daily_build_group_report_result, project: project) }
let(:old_build_group_report_result) do
create(:ci_daily_build_group_report_result, date: 1.week.ago, project: project)
end
describe '.by_projects' do
subject { described_class.by_projects([project.id]) }
it 'returns records by projects' do
expect(subject).to contain_exactly(recent_build_group_report_result, old_build_group_report_result)
end
end
describe '.with_coverage' do
subject { described_class.with_coverage }
it 'returns data with coverage' do
expect(subject).to contain_exactly(recent_build_group_report_result, old_build_group_report_result)
end
end
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'delete a terraform state' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user, maintainer_projects: [project]) }
let(:state) { create(:terraform_state, project: project) }
let(:mutation) { graphql_mutation(:terraform_state_delete, id: state.to_global_id.to_s) }
before do
post_graphql_mutation(mutation, current_user: user)
end
include_examples 'a working graphql query'
it 'deletes the state' do
expect { state.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'lock a terraform state' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user, maintainer_projects: [project]) }
let(:state) { create(:terraform_state, project: project) }
let(:mutation) { graphql_mutation(:terraform_state_lock, id: state.to_global_id.to_s) }
before do
expect(state).not_to be_locked
post_graphql_mutation(mutation, current_user: user)
end
include_examples 'a working graphql query'
it 'locks the state' do
expect(state.reload).to be_locked
expect(state.locked_by_user).to eq(user)
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'unlock a terraform state' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user, maintainer_projects: [project]) }
let(:state) { create(:terraform_state, :locked, project: project) }
let(:mutation) { graphql_mutation(:terraform_state_unlock, id: state.to_global_id.to_s) }
before do
expect(state).to be_locked
post_graphql_mutation(mutation, current_user: user)
end
include_examples 'a working graphql query'
it 'unlocks the state' do
expect(state.reload).not_to be_locked
end
end

View File

@ -36,28 +36,28 @@ RSpec.describe Issuable::CommonSystemNotesService do
context 'adding Draft note' do
let(:issuable) { create(:merge_request, title: "merge request") }
it_behaves_like 'system note creation', { title: "Draft: merge request" }, 'marked as a **Work In Progress**'
it_behaves_like 'system note creation', { title: "Draft: merge request" }, 'marked this merge request as **draft**'
context 'and changing title' do
before do
issuable.update_attribute(:title, "Draft: changed title")
end
it_behaves_like 'draft notes creation', 'marked'
it_behaves_like 'draft notes creation', 'draft'
end
end
context 'removing Draft note' do
let(:issuable) { create(:merge_request, title: "Draft: merge request") }
it_behaves_like 'system note creation', { title: "merge request" }, 'unmarked as a **Work In Progress**'
it_behaves_like 'system note creation', { title: "merge request" }, 'marked this merge request as **ready**'
context 'and changing title' do
before do
issuable.update_attribute(:title, "changed title")
end
it_behaves_like 'draft notes creation', 'unmarked'
it_behaves_like 'draft notes creation', 'ready'
end
end
end

View File

@ -683,14 +683,14 @@ RSpec.describe MergeRequests::RefreshService do
end
end
context 'marking the merge request as work in progress' do
context 'marking the merge request as draft' do
let(:refresh_service) { service.new(@project, @user) }
before do
allow(refresh_service).to receive(:execute_hooks)
end
it 'marks the merge request as work in progress from fixup commits' do
it 'marks the merge request as draft from fixup commits' do
fixup_merge_request = create(:merge_request,
source_project: @project,
source_branch: 'wip',
@ -705,11 +705,11 @@ RSpec.describe MergeRequests::RefreshService do
expect(fixup_merge_request.work_in_progress?).to eq(true)
expect(fixup_merge_request.notes.last.note).to match(
/marked as a \*\*Work In Progress\*\* from #{Commit.reference_pattern}/
/marked this merge request as \*\*draft\*\* from #{Commit.reference_pattern}/
)
end
it 'references the commit that caused the Work in Progress status' do
it 'references the commit that caused the draft status' do
wip_merge_request = create(:merge_request,
source_project: @project,
source_branch: 'wip',
@ -724,11 +724,11 @@ RSpec.describe MergeRequests::RefreshService do
refresh_service.execute(oldrev, newrev, 'refs/heads/wip')
expect(wip_merge_request.reload.notes.last.note).to eq(
"marked as a **Work In Progress** from #{wip_commit.id}"
"marked this merge request as **draft** from #{wip_commit.id}"
)
end
it 'does not mark as WIP based on commits that do not belong to an MR' do
it 'does not mark as draft based on commits that do not belong to an MR' do
allow(refresh_service).to receive(:find_new_commits)
refresh_service.instance_variable_set("@commits", [
double(

View File

@ -566,25 +566,25 @@ RSpec.describe SystemNoteService do
end
end
describe '.handle_merge_request_wip' do
describe '.handle_merge_request_draft' do
it 'calls MergeRequestsService' do
expect_next_instance_of(::SystemNotes::MergeRequestsService) do |service|
expect(service).to receive(:handle_merge_request_wip)
expect(service).to receive(:handle_merge_request_draft)
end
described_class.handle_merge_request_wip(noteable, project, author)
described_class.handle_merge_request_draft(noteable, project, author)
end
end
describe '.add_merge_request_wip_from_commit' do
describe '.add_merge_request_draft_from_commit' do
it 'calls MergeRequestsService' do
commit = double
expect_next_instance_of(::SystemNotes::MergeRequestsService) do |service|
expect(service).to receive(:add_merge_request_wip_from_commit).with(commit)
expect(service).to receive(:add_merge_request_draft_from_commit).with(commit)
end
described_class.add_merge_request_wip_from_commit(noteable, project, author, commit)
described_class.add_merge_request_draft_from_commit(noteable, project, author, commit)
end
end

View File

@ -51,44 +51,44 @@ RSpec.describe ::SystemNotes::MergeRequestsService do
end
end
describe '.handle_merge_request_wip' do
describe '.handle_merge_request_draft' do
context 'adding draft note' do
let(:noteable) { create(:merge_request, source_project: project, title: 'Draft: Lorem ipsum') }
subject { service.handle_merge_request_wip }
subject { service.handle_merge_request_draft }
it_behaves_like 'a system note' do
let(:action) { 'title' }
end
it 'sets the note text' do
expect(subject.note).to eq 'marked as a **Work In Progress**'
expect(subject.note).to eq 'marked this merge request as **draft**'
end
end
context 'removing wip note' do
subject { service.handle_merge_request_wip }
context 'removing draft note' do
subject { service.handle_merge_request_draft }
it_behaves_like 'a system note' do
let(:action) { 'title' }
end
it 'sets the note text' do
expect(subject.note).to eq 'unmarked as a **Work In Progress**'
expect(subject.note).to eq 'marked this merge request as **ready**'
end
end
end
describe '.add_merge_request_wip_from_commit' do
subject { service.add_merge_request_wip_from_commit(noteable.diff_head_commit) }
describe '.add_merge_request_draft_from_commit' do
subject { service.add_merge_request_draft_from_commit(noteable.diff_head_commit) }
it_behaves_like 'a system note' do
let(:action) { 'title' }
end
it "posts the 'marked as a Work In Progress from commit' system note" do
it "posts the 'marked this merge request as draft from commit' system note" do
expect(subject.note).to match(
/marked as a \*\*Work In Progress\*\* from #{Commit.reference_pattern}/
/marked this merge request as \*\*draft\*\* from #{Commit.reference_pattern}/
)
end
end

View File

@ -17,13 +17,13 @@ RSpec.shared_examples 'system note creation' do |update_params, note_text|
end
end
RSpec.shared_examples 'draft notes creation' do |wip_action|
RSpec.shared_examples 'draft notes creation' do |action|
subject { described_class.new(project, user).execute(issuable, old_labels: []) }
it 'creates Draft toggle and title change notes' do
expect { subject }.to change { Note.count }.from(0).to(2)
expect(Note.first.note).to match("#{wip_action} as a **Work In Progress**")
expect(Note.first.note).to match("marked this merge request as **#{action}**")
expect(Note.second.note).to match('changed title')
end
end