Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-02 12:09:26 +00:00
parent 3256c55b0f
commit c658e2d292
56 changed files with 518 additions and 176 deletions

View File

@ -164,21 +164,6 @@ Layout/HashAlignment:
- 'app/models/user_status.rb'
- 'app/models/wiki.rb'
- 'app/models/work_items/type.rb'
- 'db/migrate/20210601080039_group_protected_environments_add_index_and_constraint.rb'
- 'db/migrate/20210804150320_create_base_work_item_types.rb'
- 'db/migrate/20210831203408_upsert_base_work_item_types.rb'
- 'db/migrate/20210901065504_add_index_on_name_and_id_to_public_groups.rb'
- 'db/post_migrate/20210311120156_backfill_push_event_payload_event_id_for_bigint_conversion.rb'
- 'db/post_migrate/20210622045705_finalize_events_bigint_conversion.rb'
- 'db/post_migrate/20210701141346_finalize_ci_builds_stage_id_bigint_conversion.rb'
- 'db/post_migrate/20210707210916_finalize_ci_stages_bigint_conversion.rb'
- 'db/post_migrate/20210708011426_finalize_ci_builds_metadata_bigint_conversion.rb'
- 'db/post_migrate/20210802043253_finalize_push_event_payloads_bigint_conversion_3.rb'
- 'db/post_migrate/20210804151444_prepare_indexes_for_ci_job_artifact_bigint_conversion.rb'
- 'db/post_migrate/20210804153307_prepare_indexes_for_tagging_bigint_conversion.rb'
- 'db/post_migrate/20210804154407_prepare_indexes_for_ci_stage_bigint_conversion.rb'
- 'db/post_migrate/20210817024335_prepare_indexes_for_events_bigint_conversion.rb'
- 'db/post_migrate/20210824174615_prepare_ci_builds_metadata_and_ci_build_async_indexes.rb'
- 'ee/app/controllers/ee/search_controller.rb'
- 'ee/app/controllers/projects/integrations/zentao/issues_controller.rb'
- 'ee/app/graphql/mutations/iterations/cadences/create.rb'

View File

@ -1 +1 @@
157e6b6ad8fd7aa0ebdd43727f00b81f34b100a1
0a03f045e065b9e7a157322fbe486e1f02fd8617

View File

@ -285,7 +285,7 @@ export default {
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
/>
<div>
<div data-testid="project-name">{{ project.name }}</div>
<div data-testid="project-name" data-qa-selector="project_name">{{ project.name }}</div>
<div class="gl-text-gray-500" data-testid="project-full-path">
{{ project.fullPath }}
</div>

View File

@ -7,6 +7,7 @@ import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
import { s__, __, sprintf } from '~/locale';
import { isUserBusy } from '~/set_status_modal/utils';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import { state } from '~/sidebar/components/reviewers/sidebar_reviewers.vue';
import AjaxCache from './lib/utils/ajax_cache';
import { spriteIcon } from './lib/utils/common_utils';
import { parsePikadayDate } from './lib/utils/datetime_utility';
@ -352,8 +353,7 @@ class GfmAutoComplete {
// Cache assignees & reviewers list for easier filtering later
assignees =
SidebarMediator.singleton?.store?.assignees?.map(createMemberSearchString) || [];
reviewers =
SidebarMediator.singleton?.store?.reviewers?.map(createMemberSearchString) || [];
reviewers = state.issuable?.reviewers?.nodes?.map(createMemberSearchString) || [];
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
return match && match.length ? match[1] : null;

View File

@ -133,7 +133,7 @@ export default {
},
computed: {
workItemsEnabled() {
return this.glFeatures.workItems;
return this.glFeatures.workItemsCreateFromMarkdown;
},
taskWorkItemType() {
return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id;

View File

@ -256,6 +256,7 @@ export default {
:error-message="i18n.branchesErrorMessage"
:show-header="showSectionHeaders"
data-testid="branches-section"
data-qa-selector="branches_section"
@selected="selectRef($event)"
/>

View File

@ -33,7 +33,7 @@ export default {
return this.users.length > 2;
},
allReviewersCanMerge() {
return this.users.every((user) => user.can_merge);
return this.users.every((user) => user.mergeRequestInteraction?.canMerge);
},
sidebarAvatarCounter() {
if (this.users.length > DEFAULT_MAX_COUNTER) {
@ -48,7 +48,7 @@ export default {
return this.users.slice(0, collapsedLength);
},
tooltipTitleMergeStatus() {
const mergeLength = this.users.filter((u) => u.can_merge).length;
const mergeLength = this.users.filter((u) => u.mergeRequestInteraction?.canMerge).length;
if (mergeLength === this.users.length) {
return '';

View File

@ -23,10 +23,10 @@ export default {
return sprintf(__("%{userName}'s avatar"), { userName: this.user.name });
},
avatarUrl() {
return this.user.avatar || this.user.avatar_url || gon.default_avatar_url;
return this.user.avatarUrl || this.user.avatar_url || gon.default_avatar_url;
},
hasMergeIcon() {
return !this.user.can_merge;
return !this.user.mergeRequestInteraction?.canMerge;
},
},
};

View File

@ -40,7 +40,7 @@ export default {
},
computed: {
cannotMerge() {
return this.issuableType === 'merge_request' && !this.user.can_merge;
return this.issuableType === 'merge_request' && !this.user.mergeRequestInteraction?.canMerge;
},
tooltipTitle() {
if (this.cannotMerge && this.tooltipHasName) {
@ -59,7 +59,7 @@ export default {
};
},
reviewerUrl() {
return this.user.web_url;
return this.user.webUrl;
},
},
};

View File

@ -36,8 +36,8 @@ export default {
return !this.users.length;
},
sortedReviewers() {
const canMergeUsers = this.users.filter((user) => user.can_merge);
const canNotMergeUsers = this.users.filter((user) => !user.can_merge);
const canMergeUsers = this.users.filter((user) => user.mergeRequestInteraction?.canMerge);
const canNotMergeUsers = this.users.filter((user) => !user.mergeRequestInteraction?.canMerge);
return [...canMergeUsers, ...canNotMergeUsers];
},

View File

@ -1,15 +1,23 @@
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import Vue from 'vue';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import createFlash from '~/flash';
import { __ } from '~/locale';
import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getMergeRequestReviewersQuery from '~/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql';
import ReviewerTitle from './reviewer_title.vue';
import Reviewers from './reviewers.vue';
export const state = Vue.observable({
issuable: {},
loading: false,
initialLoading: true,
});
export default {
name: 'SidebarReviewers',
components: {
@ -40,18 +48,49 @@ export default {
required: true,
},
},
apollo: {
issuable: {
query: getMergeRequestReviewersQuery,
variables() {
return {
iid: this.issuableIid,
fullPath: this.projectPath,
};
},
update(data) {
return data.workspace?.issuable;
},
result() {
this.initialLoading = false;
},
error() {
createFlash({ message: __('An error occurred while fetching reviewers.') });
},
},
},
data() {
return {
store: new Store(),
loading: false,
};
return state;
},
computed: {
relativeUrlRoot() {
return gon.relative_url_root ?? '';
},
reviewers() {
return this.issuable.reviewers?.nodes || [];
},
graphqlFetching() {
return this.$apollo.queries.issuable.loading;
},
isLoading() {
return this.loading || this.$apollo.queries.issuable.loading;
},
canUpdate() {
return this.issuable.userPermissions?.updateMergeRequest || false;
},
},
created() {
this.store = new Store();
this.removeReviewer = this.store.removeReviewer.bind(this.store);
this.addReviewer = this.store.addReviewer.bind(this.store);
this.removeAllReviewers = this.store.removeAllReviewers.bind(this.store);
@ -77,6 +116,7 @@ export default {
.then(() => {
this.loading = false;
refreshUserMergeRequestCounts();
this.$apollo.queries.issuable.refetch();
})
.catch(() => {
this.loading = false;
@ -95,15 +135,15 @@ export default {
<template>
<div>
<reviewer-title
:number-of-reviewers="store.reviewers.length"
:loading="loading || store.isFetching.reviewers"
:editable="store.editable"
:number-of-reviewers="reviewers.length"
:loading="isLoading"
:editable="canUpdate"
/>
<reviewers
v-if="!store.isFetching.reviewers"
v-if="!initialLoading"
:root-path="relativeUrlRoot"
:users="store.reviewers"
:editable="store.editable"
:users="reviewers"
:editable="canUpdate"
:issuable-type="issuableType"
@request-review="requestReview"
/>

View File

@ -105,7 +105,7 @@ export default {
</div>
</reviewer-avatar-link>
<gl-icon
v-if="user.approved"
v-if="user.mergeRequestInteraction.approved"
v-gl-tooltip.left
:size="16"
:title="approvedByTooltipTitle(user)"
@ -121,7 +121,7 @@ export default {
data-testid="re-request-success"
/>
<gl-button
v-else-if="user.can_update_merge_request && user.reviewed"
v-else-if="user.mergeRequestInteraction.canUpdate && user.mergeRequestInteraction.reviewed"
v-gl-tooltip.left
:title="$options.i18n.reRequestReview"
:aria-label="$options.i18n.reRequestReview"

View File

@ -0,0 +1,28 @@
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
query mergeRequestReviewers($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
__typename
id
issuable: mergeRequest(iid: $iid) {
__typename
id
reviewers {
nodes {
...User
...UserAvailability
mergeRequestInteraction {
canMerge
canUpdate
approved
reviewed
}
}
}
userPermissions {
updateMergeRequest
}
}
}
}

View File

@ -95,8 +95,14 @@ export default {
toggle() {
this.isOpen = !this.isOpen;
},
toggleAddForm() {
this.isShownAddForm = !this.isShownAddForm;
showAddForm() {
this.isShownAddForm = true;
this.$nextTick(() => {
this.$refs.wiLinksForm.$refs.wiTitleInput?.$el.focus();
});
},
hideAddForm() {
this.isShownAddForm = false;
},
addChild(child) {
this.children = [child, ...this.children];
@ -122,10 +128,10 @@ export default {
>
<h5 class="gl-m-0 gl-line-height-32 gl-flex-grow-1">{{ $options.i18n.title }}</h5>
<gl-button
v-if="!isShownAddForm && canUpdate"
v-if="canUpdate"
category="secondary"
data-testid="toggle-add-form"
@click="toggleAddForm"
@click="showAddForm"
>
{{ $options.i18n.addChildButtonLabel }}
</gl-button>
@ -154,10 +160,11 @@ export default {
</div>
<work-item-links-form
v-if="isShownAddForm"
ref="wiLinksForm"
data-testid="add-links-form"
:issuable-gid="issuableGid"
:children-ids="childrenIds"
@cancel="toggleAddForm"
@cancel="hideAddForm"
@addWorkItemChild="addChild"
/>
<div

View File

@ -1,9 +1,11 @@
<script>
import { GlAlert, GlForm, GlFormCombobox, GlButton } from '@gitlab/ui';
import { GlAlert, GlFormGroup, GlForm, GlFormCombobox, GlButton, GlFormInput } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __, s__ } from '~/locale';
import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
import createWorkItemMutation from '../../graphql/create_work_item.mutation.graphql';
import { WORK_ITEM_TYPE_IDS } from '../../constants';
export default {
components: {
@ -11,6 +13,8 @@ export default {
GlForm,
GlFormCombobox,
GlButton,
GlFormGroup,
GlFormInput,
},
inject: ['projectPath'],
props: {
@ -28,6 +32,7 @@ export default {
apollo: {
availableWorkItems: {
query: projectWorkItemsQuery,
debounce: 200,
variables() {
return {
projectPath: this.projectPath,
@ -50,8 +55,29 @@ export default {
availableWorkItems: [],
search: '',
error: null,
childToCreateTitle: null,
};
},
computed: {
actionsList() {
return [
{
label: this.$options.i18n.createChildOptionLabel,
fn: () => {
this.childToCreateTitle = this.search?.title || this.search;
},
},
];
},
addOrCreateButtonLabel() {
return this.childToCreateTitle
? this.$options.i18n.createChildOptionLabel
: this.$options.i18n.addTaskButtonLabel;
},
addOrCreateMethod() {
return this.childToCreateTitle ? this.createChild : this.addChild;
},
},
methods: {
getIdFromGraphQLId,
unsetError() {
@ -79,35 +105,77 @@ export default {
}
})
.catch(() => {
this.error = this.$options.i18n.errorMessage;
this.error = this.$options.i18n.addChildErrorMessage;
})
.finally(() => {
this.search = '';
});
},
createChild() {
this.$apollo
.mutate({
mutation: createWorkItemMutation,
variables: {
input: {
title: this.search?.title || this.search,
projectPath: this.projectPath,
workItemTypeId: WORK_ITEM_TYPE_IDS.TASK,
hierarchyWidget: {
parentId: this.issuableGid,
},
},
},
})
.then(({ data }) => {
if (data.workItemCreate?.errors?.length) {
[this.error] = data.workItemCreate.errors;
} else {
this.unsetError();
this.$emit('addWorkItemChild', data.workItemCreate.workItem);
}
})
.catch(() => {
this.error = this.$options.i18n.createChildErrorMessage;
})
.finally(() => {
this.search = '';
this.childToCreateTitle = null;
});
},
},
i18n: {
inputLabel: __('Children'),
errorMessage: s__(
inputLabel: __('Title'),
addTaskButtonLabel: s__('WorkItem|Add task'),
addChildErrorMessage: s__(
'WorkItem|Something went wrong when trying to add a child. Please try again.',
),
createChildOptionLabel: s__('WorkItem|Create task'),
createChildErrorMessage: s__(
'WorkItem|Something went wrong when trying to create a child. Please try again.',
),
placeholder: s__('WorkItem|Add a title'),
fieldValidationMessage: __('Maximum of 255 characters'),
},
};
</script>
<template>
<gl-form
class="gl-mb-3 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base"
class="gl-bg-white gl-mb-3 gl-p-4 gl-border gl-border-gray-100 gl-rounded-base"
@submit.prevent="createChild"
>
<gl-alert v-if="error" variant="danger" class="gl-mb-3" @dismiss="unsetError">
{{ error }}
</gl-alert>
<!-- Follow up issue to turn this functionality back on https://gitlab.com/gitlab-org/gitlab/-/issues/368757 -->
<gl-form-combobox
v-if="false"
v-model="search"
:token-list="availableWorkItems"
match-value-to-attr="title"
class="gl-mb-4"
:label-text="$options.i18n.inputLabel"
:action-list="actionsList"
label-sr-only
autofocus
>
@ -117,11 +185,34 @@ export default {
<div>{{ item.title }}</div>
</div>
</template>
<template #action="{ item }">
<span class="gl-text-blue-500">{{ item.label }}</span>
</template>
</gl-form-combobox>
<gl-button category="secondary" data-testid="add-child-button" @click="addChild">
{{ s__('WorkItem|Add task') }}
<gl-form-group
:label="$options.i18n.inputLabel"
:description="$options.i18n.fieldValidationMessage"
>
<gl-form-input
ref="wiTitleInput"
v-model="search"
:placeholder="$options.i18n.placeholder"
maxlength="255"
class="gl-mb-3"
autofocus
/>
</gl-form-group>
<gl-button
category="primary"
variant="confirm"
size="small"
type="submit"
:disabled="search.length === 0"
data-testid="add-child-button"
>
{{ $options.i18n.createChildOptionLabel }}
</gl-button>
<gl-button category="tertiary" @click="$emit('cancel')">
<gl-button category="secondary" size="small" @click="$emit('cancel')">
{{ s__('WorkItem|Cancel') }}
</gl-button>
</gl-form>

View File

@ -35,3 +35,7 @@ export const WORK_ITEM_STATUS_TEXT = {
CLOSED: s__('WorkItem|Closed'),
OPEN: s__('WorkItem|Open'),
};
export const WORK_ITEM_TYPE_IDS = {
TASK: 'gid://gitlab/WorkItems::Type/5',
};

View File

@ -5,5 +5,6 @@ mutation createWorkItem($input: WorkItemCreateInput!) {
workItem {
...WorkItem
}
errors
}
}

View File

@ -53,6 +53,7 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:realtime_labels, project)
push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?)
push_frontend_feature_flag(:work_items_hierarchy, project)
push_force_frontend_feature_flag(:work_items_create_from_markdown, project&.work_items_create_from_markdown_feature_flag_enabled?)
end
around_action :allow_gitaly_ref_name_caching, only: [:discussions]

View File

@ -859,6 +859,10 @@ class Group < Namespace
feature_flag_enabled_for_self_or_ancestor?(:work_items_mvc_2)
end
def work_items_create_from_markdown_feature_flag_enabled?
feature_flag_enabled_for_self_or_ancestor?(:work_items_create_from_markdown)
end
# Check for enabled features, similar to `Project#feature_available?`
# NOTE: We still want to keep this after removing `Namespace#feature_available?`.
override :feature_available?

View File

@ -2995,6 +2995,10 @@ class Project < ApplicationRecord
group&.work_items_mvc_2_feature_flag_enabled? || Feature.enabled?(:work_items_mvc_2)
end
def work_items_create_from_markdown_feature_flag_enabled?
work_items_feature_flag_enabled? && (group&.work_items_create_from_markdown_feature_flag_enabled? || Feature.enabled?(:work_items_create_from_markdown))
end
def enqueue_record_project_target_platforms
return unless Gitlab.com?
return unless Feature.enabled?(:record_projects_target_platforms, self)

View File

@ -34,8 +34,8 @@ class EnvironmentSerializer < BaseSerializer
# rubocop: disable CodeReuse/ActiveRecord
def itemize(resource)
items = resource.order('folder ASC')
.group('COALESCE(environment_type, name)')
.select('COALESCE(environment_type, name) AS folder',
.group('COALESCE(environment_type, id::text)', 'COALESCE(environment_type, name)')
.select('COALESCE(environment_type, id::text), COALESCE(environment_type, name) AS folder',
'COUNT(*) AS size', 'MAX(id) AS last_id')
# It makes a difference when you call `paginate` method, because

View File

@ -0,0 +1,8 @@
---
name: work_items_create_from_markdown
introduced_by_url:
rollout_issue_url:
milestone: '15.3'
type: development
group: group::project management
default_enabled: false

View File

@ -9,7 +9,7 @@ class GroupProtectedEnvironmentsAddIndexAndConstraint < ActiveRecord::Migration[
def up
add_concurrent_index :protected_environments, [:group_id, :name], unique: true,
name: INDEX_NAME, where: 'group_id IS NOT NULL'
name: INDEX_NAME, where: 'group_id IS NOT NULL'
add_concurrent_foreign_key :protected_environments, :namespaces, column: :group_id, on_delete: :cascade
add_check_constraint :protected_environments,

View File

@ -8,9 +8,9 @@ class CreateBaseWorkItemTypes < ActiveRecord::Migration[6.1]
self.table_name = 'work_item_types'
enum base_type: {
issue: 0,
incident: 1,
test_case: 2,
issue: 0,
incident: 1,
test_case: 2,
requirement: 3
}

View File

@ -6,9 +6,9 @@ class UpsertBaseWorkItemTypes < ActiveRecord::Migration[6.1]
self.table_name = 'work_item_types'
enum base_type: {
issue: 0,
incident: 1,
test_case: 2,
issue: 0,
incident: 1,
test_case: 2,
requirement: 3
}
end

View File

@ -7,8 +7,9 @@ class AddIndexOnNameAndIdToPublicGroups < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
add_concurrent_index :namespaces, [:name, :id], name: INDEX_NAME,
where: "type = 'Group' AND visibility_level = #{PUBLIC_VISIBILITY_LEVEL}"
add_concurrent_index :namespaces, [:name, :id],
name: INDEX_NAME,
where: "type = 'Group' AND visibility_level = #{PUBLIC_VISIBILITY_LEVEL}"
end
def down

View File

@ -11,7 +11,7 @@ class BackfillPushEventPayloadEventIdForBigintConversion < ActiveRecord::Migrati
return unless should_run?
backfill_conversion_of_integer_to_bigint :push_event_payloads, :event_id, primary_key: :event_id,
batch_size: 15000, sub_batch_size: 100
batch_size: 15000, sub_batch_size: 100
end
def down

View File

@ -31,7 +31,7 @@ class FinalizeEventsBigintConversion < ActiveRecord::Migration[6.1]
add_concurrent_index TABLE_NAME, [:project_id, :id_convert_to_bigint], name: 'index_events_on_project_id_and_id_convert_to_bigint'
# This is to replace the existing "index_events_on_project_id_and_id_desc_on_merged_action" btree (project_id, id DESC) WHERE action = 7
add_concurrent_index TABLE_NAME, [:project_id, :id_convert_to_bigint], order: { id_convert_to_bigint: :desc },
where: "action = 7", name: 'index_events_on_project_id_and_id_bigint_desc_on_merged_action'
where: "action = 7", name: 'index_events_on_project_id_and_id_bigint_desc_on_merged_action'
# Add a FK on `push_event_payloads(event_id)` to `id_convert_to_bigint`, the old FK (fk_36c74129da)
# will be removed when events_pkey constraint is droppped.

View File

@ -30,7 +30,7 @@ class FinalizeCiBuildsStageIdBigintConversion < ActiveRecord::Migration[6.1]
# Create a copy of the original column's FK on the new column
add_concurrent_foreign_key TABLE_NAME, :ci_stages, column: :stage_id_convert_to_bigint, on_delete: :cascade,
reverse_lock_order: true
reverse_lock_order: true
with_lock_retries(raise_on_exhaustion: true) do
quoted_table_name = quote_table_name(TABLE_NAME)

View File

@ -37,10 +37,10 @@ class FinalizeCiStagesBigintConversion < ActiveRecord::Migration[6.1]
fk_stage_id = concurrent_foreign_key_name(:ci_builds, :stage_id)
fk_stage_id_tmp = "#{fk_stage_id}_tmp"
add_concurrent_foreign_key :ci_builds, :ci_stages, column: :stage_id,
target_column: :id_convert_to_bigint,
name: fk_stage_id_tmp,
on_delete: :cascade,
reverse_lock_order: true
target_column: :id_convert_to_bigint,
name: fk_stage_id_tmp,
on_delete: :cascade,
reverse_lock_order: true
# Now it's time to do things in a transaction
with_lock_retries(raise_on_exhaustion: true) do

View File

@ -40,7 +40,7 @@ class FinalizeCiBuildsMetadataBigintConversion < Gitlab::Database::Migration[1.0
# rubocop:enable Migration/PreventIndexCreation
add_concurrent_foreign_key TABLE_NAME, :ci_builds, column: :build_id_convert_to_bigint, on_delete: :cascade,
reverse_lock_order: true
reverse_lock_order: true
with_lock_retries(raise_on_exhaustion: true) do
execute "LOCK TABLE ci_builds, #{TABLE_NAME} IN ACCESS EXCLUSIVE MODE"

View File

@ -40,7 +40,7 @@ class FinalizePushEventPayloadsBigintConversion3 < ActiveRecord::Migration[6.1]
# Add a foreign key on `event_id_convert_to_bigint` before we swap the columns and drop the old FK (fk_36c74129da)
add_concurrent_foreign_key TABLE_NAME, :events, column: :event_id_convert_to_bigint,
on_delete: :cascade, reverse_lock_order: true
on_delete: :cascade, reverse_lock_order: true
with_lock_retries(raise_on_exhaustion: true) do
# We'll need ACCESS EXCLUSIVE lock on the related tables,

View File

@ -5,19 +5,23 @@ class PrepareIndexesForCiJobArtifactBigintConversion < ActiveRecord::Migration[6
def up
prepare_async_index :ci_job_artifacts, :id_convert_to_bigint, unique: true,
name: :index_ci_job_artifact_on_id_convert_to_bigint
name: :index_ci_job_artifact_on_id_convert_to_bigint
prepare_async_index :ci_job_artifacts, [:project_id, :id_convert_to_bigint], where: 'file_type = 18',
name: :index_ci_job_artifacts_for_terraform_reports_bigint
prepare_async_index :ci_job_artifacts,
[:project_id, :id_convert_to_bigint],
where: 'file_type = 18', name: :index_ci_job_artifacts_for_terraform_reports_bigint
prepare_async_index :ci_job_artifacts, :id_convert_to_bigint, where: 'file_type = 18',
name: :index_ci_job_artifacts_id_for_terraform_reports_bigint
prepare_async_index :ci_job_artifacts, :id_convert_to_bigint,
where: 'file_type = 18',
name: :index_ci_job_artifacts_id_for_terraform_reports_bigint
prepare_async_index :ci_job_artifacts, [:expire_at, :job_id_convert_to_bigint],
name: :index_ci_job_artifacts_on_expire_at_and_job_id_bigint
prepare_async_index :ci_job_artifacts,
[:expire_at, :job_id_convert_to_bigint],
name: :index_ci_job_artifacts_on_expire_at_and_job_id_bigint
prepare_async_index :ci_job_artifacts, [:job_id_convert_to_bigint, :file_type], unique: true,
name: :index_ci_job_artifacts_on_job_id_and_file_type_bigint
prepare_async_index :ci_job_artifacts,
[:job_id_convert_to_bigint, :file_type],
unique: true, name: :index_ci_job_artifacts_on_job_id_and_file_type_bigint
end
def down

View File

@ -5,7 +5,7 @@ class PrepareIndexesForTaggingBigintConversion < ActiveRecord::Migration[6.1]
def up
prepare_async_index :taggings, :id_convert_to_bigint, unique: true,
name: :index_taggings_on_id_convert_to_bigint
name: :index_taggings_on_id_convert_to_bigint
prepare_async_index :taggings, [:taggable_id_convert_to_bigint, :taggable_type],
name: :i_taggings_on_taggable_id_convert_to_bigint_and_taggable_type

View File

@ -5,14 +5,16 @@ class PrepareIndexesForCiStageBigintConversion < ActiveRecord::Migration[6.1]
def up
prepare_async_index :ci_stages, :id_convert_to_bigint, unique: true,
name: :index_ci_stages_on_id_convert_to_bigint
name: :index_ci_stages_on_id_convert_to_bigint
prepare_async_index :ci_stages, [:pipeline_id, :id_convert_to_bigint], where: 'status in (0, 1, 2, 8, 9, 10)',
name: :index_ci_stages_on_pipeline_id_and_id_convert_to_bigint
prepare_async_index :ci_stages, [:pipeline_id, :id_convert_to_bigint],
where: 'status in (0, 1, 2, 8, 9, 10)',
name: :index_ci_stages_on_pipeline_id_and_id_convert_to_bigint
end
def down
unprepare_async_index_by_name :ci_stages, :index_ci_stages_on_pipeline_id_and_id_convert_to_bigint
unprepare_async_index_by_name :ci_stages,
:index_ci_stages_on_pipeline_id_and_id_convert_to_bigint
unprepare_async_index_by_name :ci_stages, :index_ci_stages_on_id_convert_to_bigint
end

View File

@ -7,13 +7,14 @@ class PrepareIndexesForEventsBigintConversion < ActiveRecord::Migration[6.1]
def up
prepare_async_index TABLE_NAME, :id_convert_to_bigint, unique: true,
name: :index_events_on_id_convert_to_bigint
name: :index_events_on_id_convert_to_bigint
prepare_async_index TABLE_NAME, [:project_id, :id_convert_to_bigint],
name: :index_events_on_project_id_and_id_convert_to_bigint
prepare_async_index TABLE_NAME, [:project_id, :id_convert_to_bigint], order: { id_convert_to_bigint: :desc },
where: 'action = 7', name: :index_events_on_project_id_and_id_bigint_desc_on_merged_action
prepare_async_index TABLE_NAME, [:project_id, :id_convert_to_bigint],
order: { id_convert_to_bigint: :desc },
where: 'action = 7', name: :index_events_on_project_id_and_id_bigint_desc_on_merged_action
end
def down

View File

@ -5,13 +5,13 @@ class PrepareCiBuildsMetadataAndCiBuildAsyncIndexes < ActiveRecord::Migration[6.
def up
prepare_async_index :ci_builds_metadata, :id_convert_to_bigint, unique: true,
name: :index_ci_builds_metadata_on_id_convert_to_bigint
name: :index_ci_builds_metadata_on_id_convert_to_bigint
prepare_async_index :ci_builds_metadata, :build_id_convert_to_bigint, unique: true,
name: :index_ci_builds_metadata_on_build_id_convert_to_bigint
name: :index_ci_builds_metadata_on_build_id_convert_to_bigint
prepare_async_index :ci_builds_metadata, :build_id_convert_to_bigint, where: 'has_exposed_artifacts IS TRUE',
name: :index_ci_builds_metadata_on_build_id_int8_and_exposed_artifacts
name: :index_ci_builds_metadata_on_build_id_int8_and_exposed_artifacts
prepare_async_index_from_sql(:ci_builds_metadata, :index_ci_builds_metadata_on_build_id_int8_where_interruptible, <<~SQL.squish)
CREATE INDEX CONCURRENTLY "index_ci_builds_metadata_on_build_id_int8_where_interruptible"
@ -20,7 +20,7 @@ class PrepareCiBuildsMetadataAndCiBuildAsyncIndexes < ActiveRecord::Migration[6.
SQL
prepare_async_index :ci_builds, :id_convert_to_bigint, unique: true,
name: :index_ci_builds_on_converted_id
name: :index_ci_builds_on_converted_id
end
def down

View File

@ -205,10 +205,10 @@ field populated.
### Timeline events
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/344059) in GitLab 15.2 [with a flag](../../administration/feature_flags.md) named `incident_timeline`. Enabled on GitLab.com. Disabled on self-managed.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/344059) in GitLab 15.2 [with a flag](../../administration/feature_flags.md) named `incident_timeline`. Enabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `incident_timeline`.
On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `incident_timeline`.
On GitLab.com, this feature is available.
Incident timelines are an important part of record keeping for incidents.

View File

@ -28,25 +28,26 @@ module Gitlab
private
def validate
pgrp = nil
pgrps = nil
valid_archive = true
validate_archive_path
Timeout.timeout(TIMEOUT_LIMIT) do
stdin, stdout, stderr, wait_thr = Open3.popen3(command, pgroup: true)
stdin.close
stderr_r, stderr_w = IO.pipe
stdout, wait_threads = Open3.pipeline_r(*command, pgroup: true, err: stderr_w )
# When validation is performed on a small archive (e.g. 100 bytes)
# `wait_thr` finishes before we can get process group id. Do not
# raise exception in this scenario.
pgrp = begin
pgrps = wait_threads.map do |wait_thr|
Process.getpgid(wait_thr[:pid])
rescue Errno::ESRCH
nil
end
pgrps.compact!
status = wait_thr.value
status = wait_threads.last.value
if status.success?
result = stdout.readline
@ -64,20 +65,21 @@ module Gitlab
ensure
stdout.close
stderr.close
stderr_w.close
stderr_r.close
end
valid_archive
rescue Timeout::Error
log_error('Timeout reached during archive decompression')
Process.kill(-1, pgrp) if pgrp
pgrps.each { |pgrp| Process.kill(-1, pgrp) } if pgrps
false
rescue StandardError => e
log_error(e.message)
Process.kill(-1, pgrp) if pgrp
pgrps.each { |pgrp| Process.kill(-1, pgrp) } if pgrps
false
end
@ -91,7 +93,7 @@ module Gitlab
end
def command
"gzip -dc #{@archive_path} | wc -c"
[['gzip', '-dc', @archive_path], ['wc', '-c']]
end
def log_error(error)

View File

@ -4101,6 +4101,9 @@ msgstr ""
msgid "An error occurred while fetching reference"
msgstr ""
msgid "An error occurred while fetching reviewers."
msgstr ""
msgid "An error occurred while fetching tags. Retry the search."
msgstr ""
@ -7972,9 +7975,6 @@ msgstr ""
msgid "Child issues and epics"
msgstr ""
msgid "Children"
msgstr ""
msgid "Chinese language support using"
msgstr ""
@ -27438,6 +27438,9 @@ msgstr ""
msgid "OperationsDashboard|Operations Dashboard"
msgstr ""
msgid "OperationsDashboard|The Operations and Environments dashboards share the same list of projects. When you add or remove a project from one, GitLab adds or removes the project from the other. %{linkStart}More information%{linkEnd}"
msgstr ""
msgid "OperationsDashboard|The operations dashboard provides a summary of each project's operational health, including pipeline and alert statuses."
msgstr ""
@ -44165,6 +44168,9 @@ msgstr ""
msgid "WorkItem|Add a task"
msgstr ""
msgid "WorkItem|Add a title"
msgstr ""
msgid "WorkItem|Add assignee"
msgstr ""
@ -44251,6 +44257,9 @@ msgstr ""
msgid "WorkItem|Something went wrong when trying to add a child. Please try again."
msgstr ""
msgid "WorkItem|Something went wrong when trying to create a child. Please try again."
msgstr ""
msgid "WorkItem|Something went wrong while updating the work item. Please try again."
msgstr ""

View File

@ -6,6 +6,16 @@ module QA
extend self
def perform_before_hooks
if QA::Runtime::Env.admin_personal_access_token.present?
QA::Resource::PersonalAccessTokenCache.set_token_for_username(QA::Runtime::User.admin_username,
QA::Runtime::Env.admin_personal_access_token)
end
if QA::Runtime::Env.personal_access_token.present? && QA::Runtime::Env.user_username.present?
QA::Resource::PersonalAccessTokenCache.set_token_for_username(QA::Runtime::Env.user_username,
QA::Runtime::Env.personal_access_token)
end
# The login page could take some time to load the first time it is visited.
# We visit the login page and wait for it to properly load only once before the tests.
QA::Runtime::Logger.info("Performing sanity check for environment!")

View File

@ -17,20 +17,17 @@ module QA
# If Runtime::Env.admin_personal_access_token is provided, fabricate via the API,
# else, fabricate via the browser.
def fabricate_via_api!
QA::Resource::PersonalAccessTokenCache.get_token_for_username(user.username).tap do |cached_token|
@token = cached_token if cached_token
end
return if @token
return if find_and_set_value
resource = if Runtime::Env.admin_personal_access_token && !@user.nil?
self.api_client = Runtime::API::Client.as_admin
super
else
fabricate!
end
QA::Resource::PersonalAccessTokenCache.set_token_for_username(user.username, token) if @user
self.token = api_response[:token] unless api_response.nil?
cache_token
resource
end
@ -60,7 +57,17 @@ module QA
# this particular resource does not expose a web_url property
end
def find_and_set_value
@token ||= QA::Resource::PersonalAccessTokenCache.get_token_for_username(user.username)
end
def cache_token
QA::Resource::PersonalAccessTokenCache.set_token_for_username(user.username, self.token) if @user && self.token
end
def fabricate!
return if find_and_set_value
Flow::Login.sign_in_unless_signed_in(user: user)
Page::Main::Menu.perform(&:click_edit_profile_link)
@ -74,6 +81,10 @@ module QA
token_page.click_create_token_button
self.token = Page::Profile::PersonalAccessTokens.perform(&:created_access_token)
cache_token
self.token
end
end
end

View File

@ -6,7 +6,17 @@ module QA
@personal_access_tokens = {}
def self.get_token_for_username(username)
@personal_access_tokens[username]
token = @personal_access_tokens[username]
log_message = if token
%Q[Retrieved cached token for username: #{username}, last six chars of token:#{token[-6..]}]
else
%Q[No cached token found for username: #{username}]
end
QA::Runtime::Logger.info(log_message)
token
end
def self.set_token_for_username(username, token)

View File

@ -11,7 +11,9 @@ class StaticAnalysis
# https://github.com/browserslist/browserslist/blob/d0ec62eb48c41c218478cd3ac28684df051cc865/node.js#L329
# warns if caniuse-lite package is older than 6 months. Ignore this
# warning message so that GitLab backports don't fail.
"Browserslist: caniuse-lite is outdated. Please run next command `yarn upgrade`"
"Browserslist: caniuse-lite is outdated. Please run next command `yarn upgrade`",
# https://github.com/mime-types/mime-types-data/pull/50#issuecomment-1060908930
"Type application/netcdf is already registered as a variant of application/netcdf"
].freeze
Task = Struct.new(:command, :duration) do

View File

@ -504,6 +504,20 @@ FactoryBot.define do
artifacts_expire_at { 1.minute.ago }
end
trait :with_artifacts_paths do
options do
{
artifacts: {
name: 'artifacts_file',
untracked: false,
paths: ['out/'],
when: 'always',
expire_in: '7d'
}
}
end
end
trait :with_commit do
after(:build) do |build|
commit = build(:commit, :without_author)

View File

@ -266,7 +266,7 @@ describe('Description component', () => {
});
});
describe('with work items feature flag is enabled', () => {
describe('with work_items_create_from_markdown feature flag enabled', () => {
describe('empty description', () => {
beforeEach(() => {
createComponent({
@ -275,7 +275,7 @@ describe('Description component', () => {
},
provide: {
glFeatures: {
workItems: true,
workItemsCreateFromMarkdown: true,
},
},
});
@ -295,7 +295,7 @@ describe('Description component', () => {
},
provide: {
glFeatures: {
workItems: true,
workItemsCreateFromMarkdown: true,
},
},
});
@ -344,7 +344,7 @@ describe('Description component', () => {
descriptionHtml: descriptionHtmlWithTask,
},
provide: {
glFeatures: { workItems: true },
glFeatures: { workItemsCreateFromMarkdown: true },
},
});
return nextTick();
@ -406,7 +406,7 @@ describe('Description component', () => {
createComponent({
props: { descriptionHtml: descriptionHtmlWithTask },
provide: { glFeatures: { workItems: true } },
provide: { glFeatures: { workItemsCreateFromMarkdown: true } },
});
expect(showDetailsModal).toHaveBeenCalledTimes(modalOpened);
@ -422,7 +422,7 @@ describe('Description component', () => {
descriptionHtml: descriptionHtmlWithTask,
},
provide: {
glFeatures: { workItems: true },
glFeatures: { workItemsCreateFromMarkdown: true },
},
});
return nextTick();

View File

@ -2,7 +2,21 @@ import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue';
import UncollapsedReviewerList from '~/sidebar/components/reviewers/uncollapsed_reviewer_list.vue';
import userDataMock from '../../user_data_mock';
const userDataMock = () => ({
id: 1,
name: 'Root',
state: 'active',
username: 'root',
webUrl: `${TEST_HOST}/root`,
avatarUrl: `${TEST_HOST}/avatar/root.png`,
mergeRequestInteraction: {
canMerge: true,
canUpdate: true,
reviewed: true,
approved: false,
},
});
describe('UncollapsedReviewerList component', () => {
let wrapper;
@ -69,7 +83,10 @@ describe('UncollapsedReviewerList component', () => {
id: 2,
name: 'nonrooty-nonrootersen',
username: 'hello-world',
approved: true,
mergeRequestInteraction: {
...user.mergeRequestInteraction,
approved: true,
},
};
beforeEach(() => {

View File

@ -1,9 +1,23 @@
import { GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
import UsersMockHelper from 'helpers/user_mock_data_helper';
import { TEST_HOST } from 'helpers/test_constants';
import Reviewer from '~/sidebar/components/reviewers/reviewers.vue';
import UsersMock from './mock_data';
const usersMock = (id = 1) => ({
id,
name: 'Root',
state: 'active',
username: 'root',
webUrl: `${TEST_HOST}/root`,
avatarUrl: `${TEST_HOST}/avatar/root.png`,
mergeRequestInteraction: {
canMerge: true,
canUpdate: true,
reviewed: true,
approved: false,
},
});
describe('Reviewer component', () => {
const getDefaultProps = () => ({
@ -42,23 +56,23 @@ describe('Reviewer component', () => {
it('displays one reviewer icon when collapsed', () => {
createWrapper({
...getDefaultProps(),
users: [UsersMock.user],
users: [usersMock()],
});
const collapsedChildren = findCollapsedChildren();
const reviewer = collapsedChildren.at(0);
expect(collapsedChildren.length).toBe(1);
expect(reviewer.find('.avatar').attributes('src')).toBe(UsersMock.user.avatar);
expect(reviewer.find('.avatar').attributes('alt')).toBe(`${UsersMock.user.name}'s avatar`);
expect(reviewer.find('.avatar').attributes('src')).toContain('avatar/root.png');
expect(reviewer.find('.avatar').attributes('alt')).toBe(`Root's avatar`);
expect(trimText(reviewer.find('.author').text())).toBe(UsersMock.user.name);
expect(trimText(reviewer.find('.author').text())).toBe('Root');
});
});
describe('Two or more reviewers/users', () => {
it('displays two reviewer icons when collapsed', () => {
const users = UsersMockHelper.createNumberRandomUsers(2);
const users = [usersMock(), usersMock(2)];
createWrapper({
...getDefaultProps(),
users,
@ -70,21 +84,21 @@ describe('Reviewer component', () => {
const first = collapsedChildren.at(0);
expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar_url);
expect(first.find('.avatar').attributes('src')).toBe(users[0].avatarUrl);
expect(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`);
expect(trimText(first.find('.author').text())).toBe(users[0].name);
const second = collapsedChildren.at(1);
expect(second.find('.avatar').attributes('src')).toBe(users[1].avatar_url);
expect(second.find('.avatar').attributes('src')).toBe(users[1].avatarUrl);
expect(second.find('.avatar').attributes('alt')).toBe(`${users[1].name}'s avatar`);
expect(trimText(second.find('.author').text())).toBe(users[1].name);
});
it('displays one reviewer icon and counter when collapsed', () => {
const users = UsersMockHelper.createNumberRandomUsers(3);
const users = [usersMock(), usersMock(2), usersMock(3)];
createWrapper({
...getDefaultProps(),
users,
@ -96,7 +110,7 @@ describe('Reviewer component', () => {
const first = collapsedChildren.at(0);
expect(first.find('.avatar').attributes('src')).toBe(users[0].avatar_url);
expect(first.find('.avatar').attributes('src')).toBe(users[0].avatarUrl);
expect(first.find('.avatar').attributes('alt')).toBe(`${users[0].name}'s avatar`);
expect(trimText(first.find('.author').text())).toBe(users[0].name);
@ -107,7 +121,7 @@ describe('Reviewer component', () => {
});
it('Shows two reviewers', () => {
const users = UsersMockHelper.createNumberRandomUsers(2);
const users = [usersMock(), usersMock(2)];
createWrapper({
...getDefaultProps(),
users,
@ -118,10 +132,10 @@ describe('Reviewer component', () => {
});
it('shows sorted reviewer where "can merge" users are sorted first', () => {
const users = UsersMockHelper.createNumberRandomUsers(3);
users[0].can_merge = false;
users[1].can_merge = false;
users[2].can_merge = true;
const users = [usersMock(), usersMock(2), usersMock(3)];
users[0].mergeRequestInteraction.canMerge = false;
users[1].mergeRequestInteraction.canMerge = false;
users[2].mergeRequestInteraction.canMerge = true;
createWrapper({
...getDefaultProps(),
@ -129,14 +143,14 @@ describe('Reviewer component', () => {
editable: true,
});
expect(wrapper.vm.sortedReviewers[0].can_merge).toBe(true);
expect(wrapper.vm.sortedReviewers[0].mergeRequestInteraction.canMerge).toBe(true);
});
it('passes the sorted reviewers to the uncollapsed-reviewer-list', () => {
const users = UsersMockHelper.createNumberRandomUsers(3);
users[0].can_merge = false;
users[1].can_merge = false;
users[2].can_merge = true;
const users = [usersMock(), usersMock(2), usersMock(3)];
users[0].mergeRequestInteraction.canMerge = false;
users[1].mergeRequestInteraction.canMerge = false;
users[2].mergeRequestInteraction.canMerge = true;
createWrapper({
...getDefaultProps(),
@ -149,10 +163,10 @@ describe('Reviewer component', () => {
});
it('passes the sorted reviewers to the collapsed-reviewer-list', () => {
const users = UsersMockHelper.createNumberRandomUsers(3);
users[0].can_merge = false;
users[1].can_merge = false;
users[2].can_merge = true;
const users = [usersMock(), usersMock(2), usersMock(3)];
users[0].mergeRequestInteraction.canMerge = false;
users[1].mergeRequestInteraction.canMerge = false;
users[2].mergeRequestInteraction.canMerge = true;
createWrapper({
...getDefaultProps(),

View File

@ -1,13 +1,18 @@
import Vue from 'vue';
import { GlForm, GlFormCombobox } from '@gitlab/ui';
import { GlForm, GlFormInput, GlFormCombobox } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue';
import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import { availableWorkItemsResponse, updateWorkItemMutationResponse } from '../../mock_data';
import {
availableWorkItemsResponse,
createWorkItemMutationResponse,
updateWorkItemMutationResponse,
} from '../../mock_data';
Vue.use(VueApollo);
@ -15,12 +20,14 @@ describe('WorkItemLinksForm', () => {
let wrapper;
const updateMutationResolver = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
const createMutationResolver = jest.fn().mockResolvedValue(createWorkItemMutationResponse);
const createComponent = async ({ listResponse = availableWorkItemsResponse } = {}) => {
wrapper = shallowMountExtended(WorkItemLinksForm, {
apolloProvider: createMockApollo([
[projectWorkItemsQuery, jest.fn().mockResolvedValue(listResponse)],
[updateWorkItemMutation, updateMutationResolver],
[createWorkItemMutation, createMutationResolver],
]),
propsData: { issuableGid: 'gid://gitlab/WorkItem/1' },
provide: {
@ -33,6 +40,7 @@ describe('WorkItemLinksForm', () => {
const findForm = () => wrapper.findComponent(GlForm);
const findCombobox = () => wrapper.findComponent(GlFormCombobox);
const findInput = () => wrapper.findComponent(GlFormInput);
const findAddChildButton = () => wrapper.findByTestId('add-child-button');
beforeEach(async () => {
@ -47,19 +55,41 @@ describe('WorkItemLinksForm', () => {
expect(findForm().exists()).toBe(true);
});
it('passes available work items as prop when typing in combobox', async () => {
findCombobox().vm.$emit('input', 'Task');
await waitForPromises();
it('creates child task', async () => {
findInput().vm.$emit('input', 'Create task test');
expect(findCombobox().exists()).toBe(true);
expect(findCombobox().props('tokenList').length).toBe(2);
findForm().vm.$emit('submit', {
preventDefault: jest.fn(),
});
await waitForPromises();
expect(createMutationResolver).toHaveBeenCalled();
});
it('selects and add child', async () => {
// Follow up issue to turn this functionality back on https://gitlab.com/gitlab-org/gitlab/-/issues/368757
// eslint-disable-next-line jest/no-disabled-tests
it.skip('selects and add child', async () => {
findCombobox().vm.$emit('input', availableWorkItemsResponse.data.workspace.workItems.edges[0]);
findAddChildButton().vm.$emit('click');
await waitForPromises();
expect(updateMutationResolver).toHaveBeenCalled();
});
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('when typing in combobox', () => {
beforeEach(async () => {
findCombobox().vm.$emit('input', 'Task');
await waitForPromises();
await jest.runOnlyPendingTimers();
});
it('passes available work items as prop', () => {
expect(findCombobox().exists()).toBe(true);
expect(findCombobox().props('tokenList').length).toBe(2);
});
it('passes action to create task', () => {
expect(findCombobox().props('actionList').length).toBe(1);
});
});
});

View File

@ -219,6 +219,7 @@ export const createWorkItemMutationResponse = {
},
widgets: [],
},
errors: [],
},
},
};

View File

@ -2,8 +2,9 @@
require "spec_helper"
RSpec.describe Gitlab::Git::Branch, :seed_helper do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
RSpec.describe Gitlab::Git::Branch do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw }
subject { repository.branches }
@ -54,14 +55,14 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do
describe '#size' do
subject { super().size }
it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) }
it { is_expected.to eq(TestEnv::BRANCH_SHA.size) }
end
describe 'first branch' do
let(:branch) { repository.branches.first }
it { expect(branch.name).to eq(SeedRepo::Repo::BRANCHES.first) }
it { expect(branch.dereferenced_target.sha).to eq("0b4bc9a49b562e85de7cc9e834518ea6828729b9") }
it { expect(branch.name).to eq(TestEnv::BRANCH_SHA.keys.min) }
it { expect(branch.dereferenced_target.sha).to start_with(TestEnv::BRANCH_SHA[TestEnv::BRANCH_SHA.keys.min]) }
end
describe 'master branch' do
@ -69,14 +70,10 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do
repository.branches.find { |branch| branch.name == 'master' }
end
it { expect(branch.dereferenced_target.sha).to eq(SeedRepo::LastCommit::ID) }
it { expect(branch.dereferenced_target.sha).to start_with(TestEnv::BRANCH_SHA['master']) }
end
context 'with active, stale and future branches' do
let(:repository) do
Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '', 'group/project')
end
let(:user) { create(:user) }
let(:stale_sha) { travel_to(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago - 5.days) { create_commit } }
let(:active_sha) { travel_to(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago + 5.days) { create_commit } }
@ -88,10 +85,6 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do
repository.create_branch('future-1', future_sha)
end
after do
ensure_seeds
end
describe 'examine if the branch is active or stale' do
let(:stale_branch) { repository.find_branch('stale-1') }
let(:active_branch) { repository.find_branch('active-1') }
@ -117,8 +110,6 @@ RSpec.describe Gitlab::Git::Branch, :seed_helper do
end
end
it { expect(repository.branches.size).to eq(SeedRepo::Repo::BRANCHES.size) }
def create_commit
repository.multi_action(
user,

View File

@ -51,10 +51,11 @@ RSpec.describe Gitlab::ImportExport::DecompressedArchiveSizeValidator do
shared_examples 'logs raised exception and terminates validator process group' do
let(:std) { double(:std, close: nil, value: nil) }
let(:wait_thr) { double }
let(:wait_threads) { [wait_thr, wait_thr] }
before do
allow(Process).to receive(:getpgid).and_return(2)
allow(Open3).to receive(:popen3).and_return([std, std, std, wait_thr])
allow(Open3).to receive(:pipeline_r).and_return([std, wait_threads])
allow(wait_thr).to receive(:[]).with(:pid).and_return(1)
allow(wait_thr).to receive(:value).and_raise(exception)
end
@ -67,7 +68,7 @@ RSpec.describe Gitlab::ImportExport::DecompressedArchiveSizeValidator do
import_upload_archive_size: File.size(filepath),
message: error_message
)
expect(Process).to receive(:kill).with(-1, 2)
expect(Process).to receive(:kill).with(-1, 2).twice
expect(subject.valid?).to eq(false)
end
end

View File

@ -3390,6 +3390,13 @@ RSpec.describe Group do
end
end
describe '#work_items_create_from_markdown_feature_flag_enabled?' do
it_behaves_like 'checks self and root ancestor feature flag' do
let(:feature_flag) { :work_items_create_from_markdown }
let(:feature_flag_method) { :work_items_create_from_markdown_feature_flag_enabled? }
end
end
describe 'group shares' do
let!(:sub_group) { create(:group, parent: group) }
let!(:sub_sub_group) { create(:group, parent: sub_group) }

View File

@ -8281,6 +8281,16 @@ RSpec.describe Project, factory_default: :keep do
end
end
describe '#work_items_create_from_markdown_feature_flag_enabled?' do
let_it_be(:group_project) { create(:project, :in_subgroup) }
it_behaves_like 'checks parent group feature flag' do
let(:feature_flag_method) { :work_items_create_from_markdown_feature_flag_enabled? }
let(:feature_flag) { :work_items_create_from_markdown }
let(:subject_project) { group_project }
end
end
describe 'serialization' do
let(:object) { build(:project) }

View File

@ -101,6 +101,37 @@ RSpec.describe EnvironmentSerializer do
expect(subject.third[:latest][:environment_type]).to be_nil
end
end
context 'when folders and standalone environments share the same name' do
before do
create(:environment, project: project, name: 'staging/my-review-1')
create(:environment, project: project, name: 'staging/my-review-2')
create(:environment, project: project, name: 'production/my-review-3')
create(:environment, project: project, name: 'staging')
create(:environment, project: project, name: 'testing')
end
it 'does not group standalone environments with folders that have the same name' do
expect(subject.count).to eq 4
expect(subject.first[:name]).to eq 'production'
expect(subject.first[:size]).to eq 1
expect(subject.first[:latest][:name]).to eq 'production/my-review-3'
expect(subject.first[:latest][:environment_type]).to eq 'production'
expect(subject.second[:name]).to eq 'staging'
expect(subject.second[:size]).to eq 1
expect(subject.second[:latest][:name]).to eq 'staging'
expect(subject.second[:latest][:environment_type]).to be_nil
expect(subject.third[:name]).to eq 'staging'
expect(subject.third[:size]).to eq 2
expect(subject.third[:latest][:name]).to eq 'staging/my-review-2'
expect(subject.third[:latest][:environment_type]).to eq 'staging'
expect(subject.fourth[:name]).to eq 'testing'
expect(subject.fourth[:size]).to eq 1
expect(subject.fourth[:latest][:name]).to eq 'testing'
expect(subject.fourth[:latest][:environment_type]).to be_nil
end
end
end
context 'when used with pagination' do

View File

@ -66,7 +66,7 @@ Integration.available_integration_names.each do |integration|
hash.merge!(k => 'foo@bar.com')
elsif (integration == 'slack' || integration == 'mattermost') && k == :labels_to_be_notified_behavior
hash.merge!(k => "match_any")
elsif integration == 'campfire' && k = :room
elsif integration == 'campfire' && k == :room
hash.merge!(k => '1234')
else
hash.merge!(k => "someword")