Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-10-25 12:10:19 +00:00
parent 45760607bc
commit e5d3d8c323
104 changed files with 2003 additions and 397 deletions

View File

@ -15,7 +15,7 @@ stages:
# in cases where jobs require Docker-in-Docker, the job
# definition must be extended with `.use-docker-in-docker`
default:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.patched-golang-1.16-git-2.31-lfs-2.9-chrome-89-node-14.15-yarn-1.22-postgresql-11-graphicsmagick-1.3.36"
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.patched-golang-1.16-git-2.33-lfs-2.9-chrome-89-node-14.15-yarn-1.22-postgresql-11-graphicsmagick-1.3.36"
tags:
- gitlab-org
# All jobs are interruptible by default

View File

@ -11,7 +11,7 @@
- .default-retry
- .default-before_script
- .assets-compile-cache
image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7-git-2.31-lfs-2.9-node-14.15-yarn-1.22-graphicsmagick-1.3.36
image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7-git-2.33-lfs-2.9-node-14.15-yarn-1.22-graphicsmagick-1.3.36
variables:
SETUP_DB: "false"
WEBPACK_VENDOR_DLL: "true"

View File

@ -204,7 +204,7 @@
- *storybook-node-modules-cache-push
.use-pg11:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.patched-golang-1.16-git-2.31-lfs-2.9-chrome-89-node-14.15-yarn-1.22-postgresql-11-graphicsmagick-1.3.36"
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.patched-golang-1.16-git-2.33-lfs-2.9-chrome-89-node-14.15-yarn-1.22-postgresql-11-graphicsmagick-1.3.36"
services:
- name: postgres:11.6
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
@ -213,7 +213,7 @@
POSTGRES_HOST_AUTH_METHOD: trust
.use-pg12:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.patched-golang-1.16-git-2.31-lfs-2.9-chrome-89-node-14.15-yarn-1.22-postgresql-12-graphicsmagick-1.3.36"
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.patched-golang-1.16-git-2.33-lfs-2.9-chrome-89-node-14.15-yarn-1.22-postgresql-12-graphicsmagick-1.3.36"
services:
- name: postgres:12
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
@ -222,7 +222,7 @@
POSTGRES_HOST_AUTH_METHOD: trust
.use-pg13:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.patched-golang-1.16-git-2.31-lfs-2.9-chrome-89-node-14.15-yarn-1.22-postgresql-13-graphicsmagick-1.3.36"
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.patched-golang-1.16-git-2.33-lfs-2.9-chrome-89-node-14.15-yarn-1.22-postgresql-13-graphicsmagick-1.3.36"
services:
- name: postgres:13
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
@ -231,7 +231,7 @@
POSTGRES_HOST_AUTH_METHOD: trust
.use-pg11-ee:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.patched-golang-1.16-git-2.31-lfs-2.9-chrome-89-node-14.15-yarn-1.22-postgresql-11-graphicsmagick-1.3.36"
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.patched-golang-1.16-git-2.33-lfs-2.9-chrome-89-node-14.15-yarn-1.22-postgresql-11-graphicsmagick-1.3.36"
services:
- name: postgres:11.6
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
@ -242,7 +242,7 @@
POSTGRES_HOST_AUTH_METHOD: trust
.use-pg12-ee:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.patched-golang-1.16-git-2.31-lfs-2.9-chrome-89-node-14.15-yarn-1.22-postgresql-12-graphicsmagick-1.3.36"
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.patched-golang-1.16-git-2.33-lfs-2.9-chrome-89-node-14.15-yarn-1.22-postgresql-12-graphicsmagick-1.3.36"
services:
- name: postgres:12
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
@ -253,7 +253,7 @@
POSTGRES_HOST_AUTH_METHOD: trust
.use-pg13-ee:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.patched-golang-1.16-git-2.31-lfs-2.9-chrome-89-node-14.15-yarn-1.22-postgresql-13-graphicsmagick-1.3.36"
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.patched-golang-1.16-git-2.33-lfs-2.9-chrome-89-node-14.15-yarn-1.22-postgresql-13-graphicsmagick-1.3.36"
services:
- name: postgres:13
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]

View File

@ -1165,6 +1165,7 @@ Gitlab/NamespacedClass:
- 'app/models/members/group_member.rb'
- 'app/models/members/last_group_owner_assigner.rb'
- 'app/models/members/project_member.rb'
- 'app/models/members/member_task.rb'
- 'app/models/members_preloader.rb'
- 'app/models/merge_request.rb'
- 'app/models/merge_request_assignee.rb'

View File

@ -1,5 +1,6 @@
<script>
import {
GlAlert,
GlFormGroup,
GlModal,
GlDropdown,
@ -16,12 +17,14 @@ import Api from '~/api';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import { sanitize } from '~/lib/dompurify';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { getParameterValues } from '~/lib/utils/url_utility';
import { s__, sprintf } from '~/locale';
import {
INVITE_MEMBERS_IN_COMMENT,
GROUP_FILTERS,
USERS_FILTER_ALL,
MEMBER_AREAS_OF_FOCUS,
INVITE_MEMBERS_FOR_TASK,
} from '../constants';
import eventHub from '../event_hub';
import {
@ -34,6 +37,7 @@ import MembersTokenSelect from './members_token_select.vue';
export default {
name: 'InviteMembersModal',
components: {
GlAlert,
GlFormGroup,
GlDatepicker,
GlLink,
@ -47,6 +51,7 @@ export default {
MembersTokenSelect,
GroupSelect,
},
inject: ['newProjectPath'],
props: {
id: {
type: String,
@ -100,6 +105,14 @@ export default {
type: Array,
required: true,
},
tasksToBeDoneOptions: {
type: Array,
required: true,
},
projects: {
type: Array,
required: true,
},
},
data() {
return {
@ -110,6 +123,8 @@ export default {
newUsersToInvite: [],
selectedDate: undefined,
selectedAreasOfFocus: [],
selectedTasksToBeDone: [],
selectedTaskProject: this.projects[0],
groupToBeSharedWith: {},
source: 'unknown',
invalidFeedbackMessage: '',
@ -156,7 +171,7 @@ export default {
);
},
areasOfFocusEnabled() {
return this.areasOfFocusOptions.length !== 0;
return !this.tasksToBeDoneEnabled && this.areasOfFocusOptions.length !== 0;
},
areasOfFocusForPost() {
if (this.selectedAreasOfFocus.length === 0 && this.areasOfFocusEnabled) {
@ -172,12 +187,40 @@ export default {
return this.$options.labels[this.inviteeType].placeHolder;
},
tasksToBeDoneEnabled() {
return (
getParameterValues('open_modal')[0] === 'invite_members_for_task' &&
this.tasksToBeDoneOptions.length
);
},
showTasksToBeDone() {
return (
this.tasksToBeDoneEnabled &&
this.selectedAccessLevel >= INVITE_MEMBERS_FOR_TASK.minimum_access_level
);
},
showTaskProjects() {
return !this.isProject && this.selectedTasksToBeDone.length;
},
tasksToBeDoneForPost() {
return this.showTasksToBeDone ? this.selectedTasksToBeDone : [];
},
tasksProjectForPost() {
return this.showTasksToBeDone && this.selectedTasksToBeDone.length
? this.selectedTaskProject.id
: '';
},
},
mounted() {
eventHub.$on('openModal', (options) => {
this.openModal(options);
this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.view);
});
if (this.tasksToBeDoneEnabled) {
this.openModal({ inviteeType: 'members', source: 'in_product_marketing_email' });
this.trackEvent(INVITE_MEMBERS_FOR_TASK.name, INVITE_MEMBERS_FOR_TASK.view);
}
},
methods: {
partitionNewUsersToInvite() {
@ -219,6 +262,12 @@ export default {
this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.submit);
},
trackinviteMembersForTask() {
const label = 'selected_tasks_to_be_done';
const property = this.selectedTasksToBeDone.join(',');
const tracking = new ExperimentTracking(INVITE_MEMBERS_FOR_TASK.name, { label, property });
tracking.event(INVITE_MEMBERS_FOR_TASK.submit);
},
resetFields() {
this.isLoading = false;
this.selectedAccessLevel = this.defaultAccessLevel;
@ -227,10 +276,15 @@ export default {
this.groupToBeSharedWith = {};
this.invalidFeedbackMessage = '';
this.selectedAreasOfFocus = [];
this.selectedTasksToBeDone = [];
[this.selectedTaskProject] = this.projects;
},
changeSelectedItem(item) {
this.selectedAccessLevel = item;
},
changeSelectedTaskProject(project) {
this.selectedTaskProject = project;
},
submitShareWithGroup() {
const apiShareWithGroup = this.isProject
? Api.projectShareWithGroup.bind(Api)
@ -263,6 +317,7 @@ export default {
promises.push(apiAddByUserId(this.id, this.addByUserIdPostData(usersToAddById)));
}
this.trackInvite();
this.trackinviteMembersForTask();
Promise.all(promises)
.then(this.conditionallyShowToastSuccess)
@ -275,6 +330,8 @@ export default {
access_level: this.selectedAccessLevel,
invite_source: this.source,
areas_of_focus: this.areasOfFocusForPost,
tasks_to_be_done: this.tasksToBeDoneForPost,
tasks_project_id: this.tasksProjectForPost,
};
},
addByUserIdPostData(usersToAddById) {
@ -284,6 +341,8 @@ export default {
access_level: this.selectedAccessLevel,
invite_source: this.source,
areas_of_focus: this.areasOfFocusForPost,
tasks_to_be_done: this.tasksToBeDoneForPost,
tasks_project_id: this.tasksProjectForPost,
};
},
shareWithGroupPostData(groupToBeSharedWith) {
@ -337,6 +396,17 @@ export default {
"InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} project.",
),
},
tasksToBeDone: {
title: s__(
'InviteMembersModal|Create issues for your new team member to work on (optional)',
),
noProjects: s__(
'InviteMembersModal|To assign issues to a new team member, you need a project for the issues. %{linkStart}Create a project to get started.%{linkEnd}',
),
},
tasksProject: {
title: s__('InviteMembersModal|Choose a project for the issues'),
},
},
group: {
modalTitle: s__('InviteMembersModal|Invite a group'),
@ -476,6 +546,54 @@ export default {
data-testid="area-of-focus-checks"
/>
</div>
<div v-if="showTasksToBeDone" data-testid="invite-members-modal-tasks-to-be-done">
<label class="gl-mt-5">
{{ $options.labels.members.tasksToBeDone.title }}
</label>
<template v-if="projects.length">
<gl-form-checkbox-group
v-model="selectedTasksToBeDone"
:options="tasksToBeDoneOptions"
data-testid="invite-members-modal-tasks"
/>
<template v-if="showTaskProjects">
<label class="gl-mt-5 gl-display-block">
{{ $options.labels.members.tasksProject.title }}
</label>
<gl-dropdown
class="gl-w-half gl-xs-w-full"
:text="selectedTaskProject.title"
data-testid="invite-members-modal-project-select"
>
<template v-for="project in projects">
<gl-dropdown-item
:key="project.id"
active-class="is-active"
is-check-item
:is-checked="project.id === selectedTaskProject.id"
@click="changeSelectedTaskProject(project)"
>
{{ project.title }}
</gl-dropdown-item>
</template>
</gl-dropdown>
</template>
</template>
<gl-alert
v-else-if="tasksToBeDoneEnabled"
variant="tip"
:dismissible="false"
data-testid="invite-members-modal-no-projects-alert"
>
<gl-sprintf :message="$options.labels.members.tasksToBeDone.noProjects">
<template #link="{ content }">
<gl-link :href="newProjectPath" target="_blank" class="gl-label-link">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</gl-alert>
</div>
</div>
<template #modal-footer>

View File

@ -8,6 +8,12 @@ export const MEMBER_AREAS_OF_FOCUS = {
view: 'view',
submit: 'submit',
};
export const INVITE_MEMBERS_FOR_TASK = {
minimum_access_level: 30,
name: 'invite_members_for_task',
view: 'modal_opened_from_email',
submit: 'submit',
};
export const GROUP_FILTERS = {
ALL: 'all',

View File

@ -14,6 +14,9 @@ export default function initInviteMembersModal() {
return new Vue({
el,
provide: {
newProjectPath: el.dataset.newProjectPath,
},
render: (createElement) =>
createElement(InviteMembersModal, {
props: {
@ -24,6 +27,8 @@ export default function initInviteMembersModal() {
groupSelectFilter: el.dataset.groupsFilter,
groupSelectParentId: parseInt(el.dataset.parentId, 10),
areasOfFocusOptions: JSON.parse(el.dataset.areasOfFocusOptions),
tasksToBeDoneOptions: JSON.parse(el.dataset.tasksToBeDoneOptions || '[]'),
projects: JSON.parse(el.dataset.projects || '[]'),
noSelectionAreasOfFocus: JSON.parse(el.dataset.noSelectionAreasOfFocus),
usersFilter: el.dataset.usersFilter,
filterId: parseInt(el.dataset.filterId, 10),

View File

@ -1,4 +1,5 @@
<script>
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import 'mathjax/es5/tex-svg';
import Prompt from '../prompt.vue';
@ -7,6 +8,9 @@ export default {
components: {
Prompt,
},
directives: {
SafeHtml,
},
props: {
count: {
type: Number,
@ -33,13 +37,16 @@ export default {
return svg.outerHTML;
},
},
safeHtmlConfig: {
// to support SVGs and custom tags for mathjax
ADD_TAGS: ['use', 'mjx-container', 'mjx-tool', 'mjx-status', 'mjx-tip'],
},
};
</script>
<template>
<div class="output">
<prompt type="Out" :count="count" :show-output="index === 0" />
<!-- eslint-disable -->
<div ref="maths" v-html="code"></div>
<div ref="maths" v-safe-html:[$options.safeHtmlConfig]="code"></div>
</div>
</template>

View File

@ -21,6 +21,9 @@ export const i18n = {
`Pipeline|Pipeline %{idStart}#%{idEnd} %{statusStart}%{statusEnd} for %{commitStart}%{commitEnd}`,
),
viewBtn: s__('Pipeline|View pipeline'),
pipelineNotTriggeredMsg: s__(
'Pipeline|No pipeline was triggered for the latest changes due to the current CI/CD configuration.',
),
};
export default {
@ -73,16 +76,22 @@ export default {
result(res) {
if (res.data?.project?.pipeline) {
this.hasError = false;
} else {
this.hasError = true;
this.pipelineNotTriggered = true;
}
},
error() {
this.hasError = true;
this.networkError = true;
},
pollInterval: POLL_INTERVAL,
},
},
data() {
return {
networkError: false,
pipelineNotTriggered: false,
hasError: false,
};
},
@ -126,10 +135,16 @@ export default {
</div>
</template>
<template v-else-if="hasError">
<div>
<div v-if="networkError">
<gl-icon class="gl-mr-auto" name="warning-solid" />
<span data-testid="pipeline-error-msg">{{ $options.i18n.fetchError }}</span>
</div>
<div v-else>
<gl-icon class="gl-mr-auto" name="information-o" />
<span data-testid="pipeline-not-triggered-error-msg">
{{ $options.i18n.pipelineNotTriggeredMsg }}
</span>
</div>
</template>
<template v-else>
<div>

View File

@ -1,4 +1,5 @@
<script>
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { IssuableType } from '~/issue_show/constants';
import { __, sprintf } from '~/locale';
import AssigneeAvatarLink from './assignee_avatar_link.vue';
@ -11,6 +12,7 @@ export default {
AssigneeAvatarLink,
UserNameWithStatus,
},
mixins: [glFeatureFlagsMixin()],
props: {
users: {
type: Array,
@ -32,6 +34,10 @@ export default {
return this.users[0];
},
hasOneUser() {
if (this.showVerticalList) {
return false;
}
return this.users.length === 1;
},
hiddenAssigneesLabel() {
@ -45,6 +51,10 @@ export default {
return this.users.length - DEFAULT_RENDER_COUNT;
},
uncollapsedUsers() {
if (this.showVerticalList) {
return this.users;
}
const uncollapsedLength = this.showLess
? Math.min(this.users.length, DEFAULT_RENDER_COUNT)
: this.users.length;
@ -53,6 +63,12 @@ export default {
username() {
return `@${this.firstUser.username}`;
},
showVerticalList() {
return this.glFeatures.mrAttentionRequests && this.isMergeRequest;
},
isMergeRequest() {
return this.issuableType === IssuableType.MergeRequest;
},
},
methods: {
toggleShowLess() {
@ -84,11 +100,28 @@ export default {
<div v-else>
<div class="gl-display-flex gl-flex-wrap">
<div
v-for="user in uncollapsedUsers"
v-for="(user, index) in uncollapsedUsers"
:key="user.id"
class="user-item gl-display-inline-block"
:class="{
'user-item': !showVerticalList,
'gl-mb-3': index !== users.length - 1 && showVerticalList,
}"
class="gl-display-inline-block"
>
<assignee-avatar-link :user="user" :issuable-type="issuableType" />
<assignee-avatar-link
:user="user"
:issuable-type="issuableType"
:tooltip-has-name="!showVerticalList"
>
<div
v-if="showVerticalList"
class="gl-ml-3 gl-line-height-normal gl-display-grid"
data-testid="username"
>
<user-name-with-status :name="user.name" :availability="userAvailability(user)" />
<span>@{{ user.username }}</span>
</div>
</assignee-avatar-link>
</div>
</div>
<div v-if="renderShowMoreSection" class="user-list-more gl-hover-text-blue-800">

View File

@ -143,6 +143,10 @@ table.content {
line-height: 1.4;
padding: 15px 5px;
text-align: center;
ul.list-style-position-inside {
list-style-position: inside;
}
}
td.mailer-align-left {

View File

@ -92,6 +92,7 @@ class GroupsController < Groups::ApplicationController
if @group.import_state&.in_progress?
redirect_to group_import_path(@group)
else
publish_invite_members_for_task_experiment
render_show_html
end
end
@ -379,6 +380,13 @@ class GroupsController < Groups::ApplicationController
def captcha_required?
captcha_enabled? && !params[:parent_id]
end
def publish_invite_members_for_task_experiment
return unless params[:open_modal] == 'invite_members_for_task'
return unless current_user&.can?(:admin_group_member, @group)
experiment(:invite_members_for_task, namespace: @group).publish_to_client
end
end
GroupsController.prepend_mod_with('GroupsController')

View File

@ -41,6 +41,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:diffs_virtual_scrolling, project, default_enabled: :yaml)
push_frontend_feature_flag(:restructured_mr_widget, project, default_enabled: :yaml)
push_frontend_feature_flag(:mr_changes_fluid_layout, project, default_enabled: :yaml)
push_frontend_feature_flag(:mr_attention_requests, project, default_enabled: :yaml)
# Usage data feature flags
push_frontend_feature_flag(:users_expanding_widgets_usage_data, @project, default_enabled: :yaml)

View File

@ -16,6 +16,8 @@ module Registrations
result = ::Users::SignupService.new(current_user, update_params).execute
if result[:status] == :success
return redirect_to issues_dashboard_path(assignee_username: current_user.username) if show_tasks_to_be_done?
return redirect_to experiment(:combined_registration, user: current_user).redirect_path(trial_params) if show_signup_onboarding?
members = current_user.members
@ -68,6 +70,12 @@ module Registrations
false
end
def show_tasks_to_be_done?
return unless experiment(:invite_members_for_task).enabled?
MemberTask.for_members(current_user.members).exists?
end
def trial_params
nil
end

View File

@ -42,6 +42,14 @@ module InviteMembersHelper
e.candidate { dataset.merge!(areas_of_focus_options: member_areas_of_focus_options.to_json, no_selection_areas_of_focus: ['no_selection']) }
end
if show_invite_members_for_task?
dataset.merge!(
tasks_to_be_done_options: tasks_to_be_done_options.to_json,
projects: projects_for_source(source).to_json,
new_project_path: source.is_a?(Group) ? new_project_path(namespace_id: source.id) : ''
)
end
dataset
end
@ -71,4 +79,19 @@ module InviteMembersHelper
def users_filter_data(group)
{}
end
def show_invite_members_for_task?
return unless current_user && experiment(:invite_members_for_task).enabled?
params[:open_modal] == 'invite_members_for_task'
end
def tasks_to_be_done_options
::MemberTask::TASKS.keys.map { |task| { value: task, text: localized_tasks_to_be_done_choices[task] } }
end
def projects_for_source(source)
projects = source.is_a?(Project) ? [source] : source.projects
projects.map { |project| { id: project.id, title: project.title } }
end
end

View File

@ -56,6 +56,14 @@ module MembersHelper
end
end
def localized_tasks_to_be_done_choices
{
code: s_('TasksToBeDone|Create/import code into a project (repository)'),
ci: s_('TasksToBeDone|Set up CI/CD pipelines to build, test, deploy, and monitor code'),
issues: s_('TasksToBeDone|Create/import issues (tickets) to collaborate on ideas and plan work')
}.freeze
end
private
def source_text(member)

View File

@ -425,6 +425,14 @@ class Environment < ApplicationRecord
clear_reactive_cache!
end
def should_link_to_merge_requests?
unfoldered? || production? || staging?
end
def unfoldered?
environment_type.nil?
end
private
def rollout_status_available?

View File

@ -317,13 +317,15 @@ class Group < Namespace
owners.include?(user)
end
def add_users(users, access_level, current_user: nil, expires_at: nil)
def add_users(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
Members::Groups::BulkCreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
self,
users,
access_level,
current_user: current_user,
expires_at: expires_at
expires_at: expires_at,
tasks_to_be_done: tasks_to_be_done,
tasks_project_id: tasks_project_id
)
end

View File

@ -13,6 +13,7 @@ class Member < ApplicationRecord
include FromUnion
include UpdateHighestRole
include RestrictedSignup
include Gitlab::Experiment::Dsl
AVATAR_SIZE = 40
ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
@ -22,8 +23,10 @@ class Member < ApplicationRecord
belongs_to :created_by, class_name: "User"
belongs_to :user
belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
has_one :member_task
delegate :name, :username, :email, to: :user, prefix: true
delegate :tasks_to_be_done, to: :member_task, allow_nil: true
validates :expires_at, allow_blank: true, future_date: true
validates :user, presence: true, unless: :invite?
@ -413,6 +416,14 @@ class Member < ApplicationRecord
def after_accept_invite
post_create_hook
if experiment(:invite_members_for_task).enabled?
run_after_commit_or_now do
if member_task
TasksToBeDone::CreateWorker.perform_async(member_task.id, created_by_id, [user_id.to_i])
end
end
end
end
def after_decline_invite

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
class MemberTask < ApplicationRecord
TASKS = {
code: 0,
ci: 1,
issues: 2
}.freeze
belongs_to :member
belongs_to :project
validates :member, :project, presence: true
validates :tasks, inclusion: { in: TASKS.values }
validate :tasks_uniqueness
validate :project_in_member_source
scope :for_members, -> (members) { joins(:member).where(member: members) }
def tasks_to_be_done
Array(self[:tasks]).map { |task| TASKS.key(task) }
end
def tasks_to_be_done=(tasks)
self[:tasks] = Array(tasks).map do |task|
TASKS[task.to_sym]
end.uniq
end
private
def tasks_uniqueness
errors.add(:tasks, 'are not unique') unless Array(tasks).length == Array(tasks).uniq.length
end
def project_in_member_source
if member.is_a?(GroupMember)
errors.add(:project, _('is not in the member group')) unless project.namespace == member.source
elsif member.is_a?(ProjectMember)
errors.add(:project, _('is not the member project')) unless project == member.source
end
end
end

View File

@ -6,6 +6,11 @@ class MergeRequestDiffCommit < ApplicationRecord
include BulkInsertSafe
include ShaAttribute
include CachedCommit
include IgnorableColumns
ignore_column %i[author_name author_email committer_name committer_email],
remove_with: '14.6',
remove_after: '2021-11-22'
belongs_to :merge_request_diff
@ -51,9 +56,14 @@ class MergeRequestDiffCommit < ApplicationRecord
committer =
users[[commit_hash[:committer_name], commit_hash[:committer_email]]]
# These fields are only used to determine the author/committer IDs, we
# don't store them in the DB.
commit_hash = commit_hash
.except(:author_name, :author_email, :committer_name, :committer_email)
commit_hash.merge(
commit_author_id: author&.id,
committer_id: committer&.id,
commit_author_id: author.id,
committer_id: committer.id,
merge_request_diff_id: merge_request_diff_id,
relative_order: index,
sha: Gitlab::Database::ShaAttribute.serialize(sha), # rubocop:disable Cop/ActiveRecordSerialize
@ -104,18 +114,18 @@ class MergeRequestDiffCommit < ApplicationRecord
end
def author_name
commit_author_id ? commit_author.name : super
commit_author.name
end
def author_email
commit_author_id ? commit_author.email : super
commit_author.email
end
def committer_name
committer_id ? committer.name : super
committer.name
end
def committer_email
committer_id ? committer.email : super
committer.email
end
end

View File

@ -63,7 +63,11 @@ module Namespaces
# Make sure we drop the STI `type = 'Group'` condition for better performance.
# Logically equivalent so long as hierarchies remain homogeneous.
def without_sti_condition
unscope(where: :type)
if Feature.enabled?(:include_sti_condition, default_enabled: :yaml)
all
else
unscope(where: :type)
end
end
def order_by_depth(hierarchy_order)

View File

@ -41,13 +41,15 @@ class ProjectTeam
member
end
def add_users(users, access_level, current_user: nil, expires_at: nil)
def add_users(users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
Members::Projects::BulkCreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
project,
users,
access_level,
current_user: current_user,
expires_at: expires_at
expires_at: expires_at,
tasks_to_be_done: tasks_to_be_done,
tasks_project_id: tasks_project_id
)
end

View File

@ -6,7 +6,7 @@ module Members
included do
class << self
def add_users(source, users, access_level, current_user: nil, expires_at: nil)
def add_users(source, users, access_level, current_user: nil, expires_at: nil, tasks_to_be_done: [], tasks_project_id: nil)
return [] unless users.present?
emails, users, existing_members = parse_users_list(source, users)
@ -18,7 +18,9 @@ module Members
access_level,
existing_members: existing_members,
current_user: current_user,
expires_at: expires_at)
expires_at: expires_at,
tasks_to_be_done: tasks_to_be_done,
tasks_project_id: tasks_project_id)
.execute
end
end

View File

@ -16,7 +16,7 @@ module Deployments
# Review apps have the environment type set (e.g. to `review`, though the
# exact value may differ). We don't want to link merge requests to review
# app deployments, as this is not useful.
return if deployment.environment.environment_type
return unless deployment.environment.should_link_to_merge_requests?
# This service is triggered by a Sidekiq worker, which only runs when a
# deployment is successful. We add an extra check here in case we ever

View File

@ -63,10 +63,14 @@ module Members
invites,
params[:access_level],
expires_at: params[:expires_at],
current_user: current_user
current_user: current_user,
tasks_to_be_done: params[:tasks_to_be_done],
tasks_project_id: params[:tasks_project_id]
)
members.each { |member| process_result(member) }
create_tasks_to_be_done
end
def process_result(member)
@ -112,6 +116,19 @@ module Members
end
end
def create_tasks_to_be_done
return unless experiment(:invite_members_for_task).enabled?
return if params[:tasks_to_be_done].blank? || params[:tasks_project_id].blank?
valid_members = members.select { |member| member.valid? && member.member_task.valid? }
return unless valid_members.present?
# We can take the first `member_task` here, since all tasks will have the same attributes needed
# for the `TasksToBeDone::CreateWorker`, ie. `project` and `tasks_to_be_done`.
member_task = valid_members[0].member_task
TasksToBeDone::CreateWorker.perform_async(member_task.id, current_user.id, valid_members.map(&:user_id))
end
def areas_of_focus
params[:areas_of_focus] || []
end

View File

@ -4,6 +4,8 @@ module Members
# This class serves as more of an app-wide way we add/create members
# All roads to add members should take this path.
class CreatorService
include Gitlab::Experiment::Dsl
class << self
def parsed_access_level(access_level)
access_levels.fetch(access_level) { access_level.to_i }
@ -24,6 +26,7 @@ module Members
def execute
find_or_build_member
update_member
create_member_task
member
end
@ -61,6 +64,21 @@ module Members
}
end
def create_member_task
return unless experiment(:invite_members_for_task).enabled?
return unless member.persisted?
return if member_task_attributes.value?(nil)
member.create_member_task(member_task_attributes)
end
def member_task_attributes
{
tasks_to_be_done: args[:tasks_to_be_done],
project_id: args[:tasks_project_id]
}
end
def approve_request
::Members::ApproveAccessRequestService.new(current_user,
access_level: access_level)

View File

@ -39,6 +39,11 @@ module Members
errors[invite_email(member)] = member.errors.full_messages.to_sentence
end
override :create_tasks_to_be_done
def create_tasks_to_be_done
# Only create task issues for existing users. Tasks for new users are created when they signup.
end
def invite_email(member)
member.invite_email || member.user.email
end

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
module TasksToBeDone
class BaseService < ::IssuableBaseService
LABEL_PREFIX = 'tasks to be done'
def initialize(project:, current_user:, assignee_ids: [])
params = {
assignee_ids: assignee_ids,
title: title,
description: description,
add_labels: label_name
}
super(project: project, current_user: current_user, params: params)
end
def execute
if (issue = existing_task_issue)
update_service = Issues::UpdateService.new(project: project, current_user: current_user, params: { add_assignee_ids: params[:assignee_ids] })
update_service.execute(issue)
else
build_service = Issues::BuildService.new(project: project, current_user: current_user, params: params)
create(build_service.execute)
end
end
private
def existing_task_issue
IssuesFinder.new(
current_user,
project_id: project.id,
state: 'opened',
non_archived: true,
label_name: label_name
).execute.last
end
def title
raise NotImplementedError
end
def description
raise NotImplementedError
end
def label_suffix
raise NotImplementedError
end
def label_name
"#{LABEL_PREFIX}:#{label_suffix}"
end
end
end

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
module TasksToBeDone
class CreateCiTaskService < BaseService
protected
def title
'Set up CI/CD'
end
def description
<<~DESCRIPTION
GitLab CI/CD is a tool built into GitLab for software development through the [continuous methodologies](https://docs.gitlab.com/ee/ci/introduction/index.html#introduction-to-cicd-methodologies):
* Continuous Integration (CI)
* Continuous Delivery (CD)
* Continuous Deployment (CD)
Continuous Integration works by pushing small changes to your applications codebase hosted in a Git repository, and, to every push, run a pipeline of scripts to build, test, and validate the code changes before merging them into the main branch.
Continuous Delivery and Deployment consist of a step further CI, deploying your application to production at every push to the default branch of the repository.
These methodologies allow you to catch bugs and errors early in the development cycle, ensuring that all the code deployed to production complies with the code standards you established for your app.
* :book: [Read the documentation](https://docs.gitlab.com/ee/ci/introduction/index.html)
* :clapper: [Watch a Demo](https://www.youtube.com/watch?v=1iXFbchozdY)
## Next steps
* [ ] To start we recommend reviewing the following documentation:
* [ ] [How GitLab CI/CD works.](https://docs.gitlab.com/ee/ci/introduction/index.html#how-gitlab-cicd-works)
* [ ] [Fundamental pipeline architectures.](https://docs.gitlab.com/ee/ci/pipelines/pipeline_architectures.html)
* [ ] [GitLab CI/CD basic workflow.](https://docs.gitlab.com/ee/ci/introduction/index.html#basic-cicd-workflow)
* [ ] [Step-by-step guide for writing .gitlab-ci.yml for the first time.](https://docs.gitlab.com/ee/user/project/pages/getting_started_part_four.html)
* [ ] When you're ready select **Projects** (in the top navigation bar) > **Your projects** > select the Project you've already created.
* [ ] Select **CI / CD** in the left navigation to start setting up CI / CD in your project.
DESCRIPTION
end
def label_suffix
'ci'
end
end
end

View File

@ -0,0 +1,52 @@
# frozen_string_literal: true
module TasksToBeDone
class CreateCodeTaskService < BaseService
protected
def title
'Create or import your code into your Project (Repository)'
end
def description
<<~DESCRIPTION
You've already created your Group and Project within GitLab; we'll quickly review this hierarchy below. Once you're within your project you can easily create or import repositories.
**With GitLab Groups, you can:**
* Create one or multiple Projects for hosting your codebase (repositories).
* Assemble related projects together.
* Grant members access to several projects at once.
Groups can also be nested in subgroups.
Read more about groups in our [documentation](https://docs.gitlab.com/ee/user/group/).
**Within GitLab Projects, you can**
* Use it as an issue tracker.
* Collaborate on code.
* Continuously build, test, and deploy your app with built-in GitLab CI/CD.
You can also import an existing repository by providing the Git URL.
* :book: [Read the documentation](https://docs.gitlab.com/ee/user/project/index.html).
## Next steps
Create or import your first repository into the project you created:
* [ ] Click **Projects** in the top navigation bar, then click **Your projects**.
* [ ] Select the Project that you created, then select **Repository**.
* [ ] Once on the Repository page you can select the **+** icon to add or import files.
* [ ] You can review our full documentation on creating [repositories](https://docs.gitlab.com/ee/user/project/repository/) in GitLab.
:tada: All done, you can close this issue!
DESCRIPTION
end
def label_suffix
'code'
end
end
end

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
module TasksToBeDone
class CreateIssuesTaskService < BaseService
protected
def title
'Create/import issues (tickets) to collaborate on ideas and plan work'
end
def description
<<~DESCRIPTION
Issues allow you and your team to discuss proposals before, and during, their implementation. They can be used for a variety of other purposes, customized to your needs and workflow.
Issues are always associated with a specific project. If you have multiple projects in a group, you can view all the issues at the group level. [You can review our full Issue documentation here.](https://docs.gitlab.com/ee/user/project/issues/)
If you have existing issues or equivalent tickets you can import them as long as they are formatted as a CSV file, [the import process is covered here](https://docs.gitlab.com/ee/user/project/issues/csv_import.html).
**Common use cases include:**
* Discussing the implementation of a new idea
* Tracking tasks and work status
* Accepting feature proposals, questions, support requests, or bug reports
* Elaborating on new code implementations
## Next steps
* [ ] Select **Projects** in the top navigation > **Your Projects** > select the Project you've already created.
* [ ] Once you've selected that project, you can select **Issues** in the left navigation, then click **New issue**.
* [ ] Fill in the title and description in the **New issue** page.
* [ ] Click on **Create issue**.
Pro tip: When you're in a group or project you can always utilize the **+** icon in the top navigation (located to the left of the search bar) to quickly create new issues.
That's it! You can close this issue.
DESCRIPTION
end
def label_suffix
'issues'
end
end
end

View File

@ -210,6 +210,12 @@
%td{ style: "padding: 10px 20px 30px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; color:#000000; font-size: 18px; line-height: 24px;" }
%p{ style: "margin: 0 0 50px 0;" }
= @message.feedback_thanks
- if @message.invite_members?
%tr
%td{ align: "center", style: "padding: 0 20px 80px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" }
= @message.invite_text
%br
= @message.invite_link
%tr{ style: "background-color: #ffffff;" }
%td{ align: "center", style: "padding:75px 20px 25px;" }
= about_link('gitlab_logo.png', 80)

View File

@ -21,6 +21,10 @@
<%= @message.feedback_thanks %>
<% end %>
<% if @message.invite_members? %>
<%= @message.invite_text %>
<%= @message.invite_link %>
<% end %>

View File

@ -8,7 +8,11 @@
%td.text-content
%p
= _('You have been granted %{access_level} access to the %{source_link} %{source_type}.').html_safe % { access_level: access_level, source_link: source_link, source_type: source_type }
- if member.tasks_to_be_done.present?
= s_("InviteEmail|You were assigned the following tasks:")
%ul.list-style-position-inside
- member.tasks_to_be_done.each do |task|
%li= localized_tasks_to_be_done_choices[task]
%p
- leave_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: leave_link }
= _('If this was a mistake you can %{leave_link_start}leave the %{source_type}%{link_end}.').html_safe % { source_type: source_type, leave_link_start: leave_link_start, link_end: link_end }

View File

@ -24,6 +24,11 @@
%p
- if member.created_by
= html_escape(s_("InviteEmail|%{inviter} invited you to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} as a %{role}")) % placeholders.merge({ inviter: (link_to inviter_name, user_url(member.created_by)).html_safe })
- if member.tasks_to_be_done.present?
= s_("InviteEmail|and has assigned you the following tasks:")
%ul.list-style-position-inside
- member.tasks_to_be_done.each do |task|
%li= localized_tasks_to_be_done_choices[task]
- else
= html_escape(s_("InviteEmail|You are invited to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} as a %{role}")) % placeholders
%p.invite-actions

View File

@ -2808,6 +2808,15 @@
:weight: 1
:idempotent:
:tags: []
- :name: tasks_to_be_done_create
:worker_name: TasksToBeDone::CreateWorker
:feature_category: :onboarding
:has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 1
:idempotent: true
:tags: []
- :name: update_external_pull_requests
:worker_name: UpdateExternalPullRequestsWorker
:feature_category: :source_code_management

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module TasksToBeDone
class CreateWorker
include ApplicationWorker
data_consistency :always
idempotent!
feature_category :onboarding
urgency :low
worker_resource_boundary :cpu
def perform(member_task_id, current_user_id, assignee_ids = [])
member_task = MemberTask.find(member_task_id)
current_user = User.find(current_user_id)
project = member_task.project
member_task.tasks_to_be_done.each do |task|
service_class(task)
.new(project: project, current_user: current_user, assignee_ids: assignee_ids)
.execute
end
end
private
def service_class(task)
"TasksToBeDone::Create#{task.to_s.camelize}TaskService".constantize
end
end
end

View File

@ -0,0 +1,8 @@
---
name: include_sti_condition
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72119
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/343412
milestone: '14.5'
type: development
group: group::workspace
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: mr_attention_requests
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72773
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/343528
milestone: '14.4'
type: development
group: group::code review
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: invite_members_for_task
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69299
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339747
milestone: '14.5'
type: experiment
group: group::activation
default_enabled: false

View File

@ -393,6 +393,8 @@
- 1
- - system_hook_push
- 1
- - tasks_to_be_done_create
- 1
- - todos_destroyer
- 1
- - unassign_issuables

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class CreateMemberTasks < Gitlab::Database::Migration[1.0]
def change
create_table :member_tasks do |t|
t.references :member, index: true, null: false
t.references :project, index: true, null: false
t.timestamps_with_timezone null: false
t.integer :tasks, limit: 2, array: true, null: false, default: []
t.index [:member_id, :project_id], unique: true
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddMemberIdForeignKeyToMemberTasks < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
add_concurrent_foreign_key :member_tasks, :members, column: :member_id, on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key :member_tasks, column: :member_id
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddProjectIdForeignKeyToMemberTasks < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
add_concurrent_foreign_key :member_tasks, :projects, column: :project_id, on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key :member_tasks, column: :project_id
end
end
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
class CleanUpMigrateMergeRequestDiffCommitUsers < Gitlab::Database::Migration[1.0]
def up
jobs = Gitlab::Database::BackgroundMigrationJob
.for_migration_class('MigrateMergeRequestDiffCommitUsers')
.pending
.to_a
return if jobs.empty?
say("#{jobs.length} MigrateMergeRequestDiffCommitUsers are still pending")
# Normally we don't process background migrations in a regular migration, as
# this could take a while to complete and thus block a deployment.
#
# In this case the jobs have all been processed for GitLab.com at the time
# of writing. In addition, it's been a few releases since this migration was
# introduced. As a result, self-hosted instances should have their
# migrations finished a long time ago.
#
# For these reasons we clean up any pending jobs (just in case) before
# deploying the code. This also allows us to immediately start using the new
# setup only, instead of having to support both the old and new approach for
# at least one more release.
jobs.each do |job|
Gitlab::BackgroundMigration::MigrateMergeRequestDiffCommitUsers
.new
.perform(*job.arguments)
end
end
def down
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddGroupTraversalIdIndex < Gitlab::Database::Migration[1.0]
INDEX_NAME = 'index_namespaces_on_traversal_ids_for_groups'
disable_ddl_transaction!
def up
add_concurrent_index :namespaces, :traversal_ids, using: :gin, where: "type='Group'", name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :namespaces, INDEX_NAME
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class RemoveMergeRequestDiffCommitColumns < Gitlab::Database::Migration[1.0]
enable_lock_retries!
COLUMNS = %i[author_name author_email committer_name committer_email].freeze
def change
COLUMNS.each do |column|
remove_column(:merge_request_diff_commits, column, :text)
end
end
end

View File

@ -0,0 +1 @@
72358f01061f5296e21647d5da9bbb6a33e94055c9c9aded6088cfb9126564b2

View File

@ -0,0 +1 @@
f4fe6c4a2860dd35f767d98d5025326142cab7fc9c12b5efb1541e2604791691

View File

@ -0,0 +1 @@
59e5de7766dc55e820ec714fbb61b5db61a73959f1e877e66caf668f93d0d633

View File

@ -0,0 +1 @@
0f2578f0266154ad2790cc808233c71566b3a3ea87c40909feba9ccc5872927c

View File

@ -0,0 +1 @@
2685a534728ab1a50acb49a7a5ac7d9285fdc36ec3610b93a4219e6687c22b06

View File

@ -0,0 +1 @@
a62ac8920223469c6e4c5a7f67ce9eec972189c98a8c542b377afe4ab28ee25a

View File

@ -15671,6 +15671,24 @@ CREATE SEQUENCE lists_id_seq
ALTER SEQUENCE lists_id_seq OWNED BY lists.id;
CREATE TABLE member_tasks (
id bigint NOT NULL,
member_id bigint NOT NULL,
project_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
tasks smallint[] DEFAULT '{}'::smallint[] NOT NULL
);
CREATE SEQUENCE member_tasks_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE member_tasks_id_seq OWNED BY member_tasks.id;
CREATE TABLE members (
id integer NOT NULL,
access_level integer NOT NULL,
@ -15819,10 +15837,6 @@ CREATE TABLE merge_request_diff_commits (
merge_request_diff_id integer NOT NULL,
relative_order integer NOT NULL,
sha bytea NOT NULL,
author_name text,
author_email text,
committer_name text,
committer_email text,
message text,
trailers jsonb DEFAULT '{}'::jsonb NOT NULL,
commit_author_id bigint,
@ -21502,6 +21516,8 @@ ALTER TABLE ONLY lists ALTER COLUMN id SET DEFAULT nextval('lists_id_seq'::regcl
ALTER TABLE ONLY loose_foreign_keys_deleted_records ALTER COLUMN id SET DEFAULT nextval('loose_foreign_keys_deleted_records_id_seq'::regclass);
ALTER TABLE ONLY member_tasks ALTER COLUMN id SET DEFAULT nextval('member_tasks_id_seq'::regclass);
ALTER TABLE ONLY members ALTER COLUMN id SET DEFAULT nextval('members_id_seq'::regclass);
ALTER TABLE ONLY merge_request_assignees ALTER COLUMN id SET DEFAULT nextval('merge_request_assignees_id_seq'::regclass);
@ -23204,6 +23220,9 @@ ALTER TABLE ONLY list_user_preferences
ALTER TABLE ONLY lists
ADD CONSTRAINT lists_pkey PRIMARY KEY (id);
ALTER TABLE ONLY member_tasks
ADD CONSTRAINT member_tasks_pkey PRIMARY KEY (id);
ALTER TABLE ONLY members
ADD CONSTRAINT members_pkey PRIMARY KEY (id);
@ -25654,6 +25673,12 @@ CREATE INDEX index_lists_on_milestone_id ON lists USING btree (milestone_id);
CREATE INDEX index_lists_on_user_id ON lists USING btree (user_id);
CREATE INDEX index_member_tasks_on_member_id ON member_tasks USING btree (member_id);
CREATE UNIQUE INDEX index_member_tasks_on_member_id_and_project_id ON member_tasks USING btree (member_id, project_id);
CREATE INDEX index_member_tasks_on_project_id ON member_tasks USING btree (project_id);
CREATE INDEX index_members_on_access_level ON members USING btree (access_level);
CREATE INDEX index_members_on_expires_at ON members USING btree (expires_at);
@ -25872,6 +25897,8 @@ CREATE INDEX index_namespaces_on_shared_and_extra_runners_minutes_limit ON names
CREATE INDEX index_namespaces_on_traversal_ids ON namespaces USING gin (traversal_ids);
CREATE INDEX index_namespaces_on_traversal_ids_for_groups ON namespaces USING gin (traversal_ids) WHERE ((type)::text = 'Group'::text);
CREATE INDEX index_namespaces_on_type_and_id ON namespaces USING btree (type, id);
CREATE INDEX index_namespaces_public_groups_name_id ON namespaces USING btree (name, id) WHERE (((type)::text = 'Group'::text) AND (visibility_level = 20));
@ -27588,6 +27615,9 @@ ALTER TABLE ONLY project_pages_metadata
ALTER TABLE ONLY group_deletion_schedules
ADD CONSTRAINT fk_11e3ebfcdd FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY member_tasks
ADD CONSTRAINT fk_12816d4bbb FOREIGN KEY (member_id) REFERENCES members(id) ON DELETE CASCADE;
ALTER TABLE ONLY vulnerabilities
ADD CONSTRAINT fk_1302949740 FOREIGN KEY (last_edited_by_id) REFERENCES users(id) ON DELETE SET NULL;
@ -28068,6 +28098,9 @@ ALTER TABLE ONLY identities
ALTER TABLE ONLY boards
ADD CONSTRAINT fk_ab0a250ff6 FOREIGN KEY (iteration_cadence_id) REFERENCES iterations_cadences(id) ON DELETE CASCADE;
ALTER TABLE ONLY member_tasks
ADD CONSTRAINT fk_ab636303dd FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY dep_ci_build_trace_sections
ADD CONSTRAINT fk_ab7c104e26 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;

View File

@ -43,6 +43,8 @@ POST /projects/:id/invitations
| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
| `invite_source` | string | no | The source of the invitation that starts the member creation process. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/327120). |
| `areas_of_focus` | string | no | Areas the inviter wants the member to focus upon. |
| `tasks_to_be_done` | array of strings | no | Tasks the inviter wants the member to focus on. The tasks are added as issues to a specified project. The possible values are: `ci`, `code` and `issues`. If specified, requires `tasks_project_id`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69299) in GitLab 14.5 [with a flag](../administration/feature_flags.md) named `invite_members_for_task`. Disabled by default. |
| `tasks_project_id` | integer | no | The project ID in which to create the task issues. If specified, requires `tasks_to_be_done`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69299) in GitLab 14.5 [with a flag](../administration/feature_flags.md) named `invite_members_for_task`. Disabled by default. |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \

View File

@ -422,6 +422,8 @@ POST /projects/:id/members
| `expires_at` | string | no | A date string in the format `YEAR-MONTH-DAY` |
| `invite_source` | string | no | The source of the invitation that starts the member creation process. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/327120). |
| `areas_of_focus` | string | no | Areas the inviter wants the member to focus upon. |
| `tasks_to_be_done` | array of strings | no | Tasks the inviter wants the member to focus on. The tasks are added as issues to a specified project. The possible values are: `ci`, `code` and `issues`. If specified, requires `tasks_project_id`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69299) in GitLab 14.5 [with a flag](../administration/feature_flags.md) named `invite_members_for_task`. Disabled by default. |
| `tasks_project_id` | integer | no | The project ID in which to create the task issues. If specified, requires `tasks_to_be_done`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69299) in GitLab 14.5 [with a flag](../administration/feature_flags.md) named `invite_members_for_task`. Disabled by default. |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \

View File

@ -77,6 +77,26 @@ Data is shown for workflow items created during the selected date range. To filt
1. Optionally select a project.
1. Select a date range using the available date pickers.
### Upcoming date filter change
In the [epics](https://gitlab.com/groups/gitlab-org/-/epics/6046), we plan to alter
the date filter behavior to filter the end event time of the currently selected stage.
The change makes it possible to get a much better picture about the completed items within the
stage and helps uncover long-running items.
For example, an issue was created a year ago and the current stage was finished in the current month.
If you were to look at the metrics for the last three months, this issue would not be included in the calculation of
the stage metrics. With the new date filter, this item would be included.
DISCLAIMER:
This page contains information related to upcoming products, features, and functionality.
It is important to note that the information presented is for informational purposes only.
Please do not rely on this information for purchasing or planning purposes.
As with all projects, the items mentioned on this page are subject to change or delay.
The development, release, and timing of any products, features, or functionality remain at the
sole discretion of GitLab Inc.
## How metrics are measured
> DORA API-based deployment metrics [moved](https://gitlab.com/gitlab-org/gitlab/-/issues/337256)

View File

@ -25,6 +25,8 @@ module API
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
optional :invite_source, type: String, desc: 'Source that triggered the member creation process', default: 'invitations-api'
optional :areas_of_focus, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Areas the inviter wants the member to focus upon'
optional :tasks_to_be_done, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Tasks the inviter wants the member to do'
optional :tasks_project_id, type: Integer, desc: 'The project ID in which to create the task issues'
end
post ":id/invitations" do
params[:source] = find_source(source_type, params[:id])

View File

@ -95,6 +95,8 @@ module API
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
optional :invite_source, type: String, desc: 'Source that triggered the member creation process', default: 'members-api'
optional :areas_of_focus, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Areas the inviter wants the member to focus upon'
optional :tasks_to_be_done, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Tasks the inviter wants the member to do'
optional :tasks_project_id, type: Integer, desc: 'The project ID in which to create the task issues'
end
post ":id/members" do

View File

@ -36,6 +36,10 @@ module Gitlab
def progress
super(track_name: 'Admin')
end
def invite_members?
invite_members_for_task_experiment_enabled?
end
end
end
end

View File

@ -7,6 +7,7 @@ module Gitlab
class Base
include Gitlab::Email::Message::InProductMarketing::Helper
include Gitlab::Routing
include Gitlab::Experiment::Dsl
attr_accessor :format
@ -56,6 +57,18 @@ module Gitlab
end
end
def invite_members?
false
end
def invite_text
s_('InProductMarketing|Do you have a teammate who would be perfect for this task?')
end
def invite_link
action_link(s_('InProductMarketing|Invite them to help out.'), group_url(group, open_modal: 'invite_members_for_task'))
end
def unsubscribe
parts = Gitlab.com? ? unsubscribe_com : unsubscribe_self_managed(track, series)
@ -148,6 +161,16 @@ module Gitlab
link(s_('InProductMarketing|update your preferences'), preference_link)
end
def invite_members_for_task_experiment_enabled?
return unless user.can?(:admin_group_member, group)
experiment(:invite_members_for_task, namespace: group) do |e|
e.candidate { true }
e.record!
e.run
end
end
end
end
end

View File

@ -61,6 +61,10 @@ module Gitlab
][series]
end
def invite_members?
invite_members_for_task_experiment_enabled?
end
private
def project_link

View File

@ -36,6 +36,15 @@ module Gitlab
"#{text} (#{link})"
end
end
def action_link(text, link)
case format
when :html
ActionController::Base.helpers.link_to text, link, target: '_blank', rel: 'noopener noreferrer'
else
[text, link].join(' >> ')
end
end
end
end
end

View File

@ -65,6 +65,10 @@ module Gitlab
][series]
end
def invite_members?
invite_members_for_task_experiment_enabled?
end
private
def ci_link

View File

@ -399,6 +399,10 @@ excluded_attributes:
- :verification_checksum
- :verification_failure
merge_request_diff_commits:
- :author_name
- :author_email
- :committer_name
- :committer_email
- :merge_request_diff_id
- :commit_author_id
- :committer_id

View File

@ -17544,6 +17544,9 @@ msgstr ""
msgid "InProductMarketing|Do you have a minute?"
msgstr ""
msgid "InProductMarketing|Do you have a teammate who would be perfect for this task?"
msgstr ""
msgid "InProductMarketing|Easy"
msgstr ""
@ -17658,6 +17661,9 @@ msgstr ""
msgid "InProductMarketing|Increase Operational Efficiencies"
msgstr ""
msgid "InProductMarketing|Invite them to help out."
msgstr ""
msgid "InProductMarketing|Invite your colleagues and start shipping code faster."
msgstr ""
@ -18692,6 +18698,12 @@ msgstr ""
msgid "InviteEmail|You have been invited to join the %{project_or_group_name} %{project_or_group} as a %{role}"
msgstr ""
msgid "InviteEmail|You were assigned the following tasks:"
msgstr ""
msgid "InviteEmail|and has assigned you the following tasks:"
msgstr ""
msgid "InviteMembersBanner|Collaborate with your team"
msgstr ""
@ -18710,6 +18722,9 @@ msgstr ""
msgid "InviteMembersModal|Cancel"
msgstr ""
msgid "InviteMembersModal|Choose a project for the issues"
msgstr ""
msgid "InviteMembersModal|Close invite team members"
msgstr ""
@ -18725,6 +18740,9 @@ msgstr ""
msgid "InviteMembersModal|Contribute to the codebase"
msgstr ""
msgid "InviteMembersModal|Create issues for your new team member to work on (optional)"
msgstr ""
msgid "InviteMembersModal|GitLab member or email address"
msgstr ""
@ -18758,6 +18776,9 @@ msgstr ""
msgid "InviteMembersModal|Something went wrong"
msgstr ""
msgid "InviteMembersModal|To assign issues to a new team member, you need a project for the issues. %{linkStart}Create a project to get started.%{linkEnd}"
msgstr ""
msgid "InviteMembersModal|What would you like new member(s) to focus on? (optional)"
msgstr ""
@ -25384,6 +25405,9 @@ msgstr ""
msgid "Pipeline|Merged result pipeline"
msgstr ""
msgid "Pipeline|No pipeline was triggered for the latest changes due to the current CI/CD configuration."
msgstr ""
msgid "Pipeline|Passed"
msgstr ""
@ -33412,6 +33436,15 @@ msgstr ""
msgid "Task ID: %{elastic_task}"
msgstr ""
msgid "TasksToBeDone|Create/import code into a project (repository)"
msgstr ""
msgid "TasksToBeDone|Create/import issues (tickets) to collaborate on ideas and plan work"
msgstr ""
msgid "TasksToBeDone|Set up CI/CD pipelines to build, test, deploy, and monitor code"
msgstr ""
msgid "Team"
msgstr ""
@ -37555,6 +37588,9 @@ msgstr ""
msgid "ValueStreamAnalytics|Average number of deployments to production per day."
msgstr ""
msgid "ValueStreamAnalytics|Items in Value Stream Analytics are currently filtered by their creation time. There is an %{epic_link_start}epic%{epic_link_end} that will change the Value Stream Analytics date filter to use the end event time for the selected stage."
msgstr ""
msgid "ValueStreamAnalytics|Median time between merge request merge and deployment to a production environment for all MRs deployed in the given time period."
msgstr ""
@ -40555,6 +40591,12 @@ msgstr ""
msgid "is not in the group enforcing Group Managed Account"
msgstr ""
msgid "is not in the member group"
msgstr ""
msgid "is not the member project"
msgstr ""
msgid "is not valid. The iteration group has to match the iteration cadence group."
msgstr ""

View File

@ -82,6 +82,16 @@ RSpec.describe GroupsController, factory_default: :keep do
expect(subject).to redirect_to group_import_path(group)
end
end
context 'publishing the invite_members_for_task experiment' do
it 'publishes the experiment data to the client' do
wrapped_experiment(experiment(:invite_members_for_task)) do |e|
expect(e).to receive(:publish_to_client)
end
get :show, params: { id: group.to_param, open_modal: 'invite_members_for_task' }, format: format
end
end
end
describe 'GET #details' do

View File

@ -97,6 +97,16 @@ RSpec.describe Registrations::WelcomeController do
expect(subject).to redirect_to(dashboard_projects_path)
end
end
context 'when tasks to be done are assigned' do
let!(:member1) { create(:group_member, user: user, tasks_to_be_done: %w(ci code)) }
before do
stub_experiments(invite_members_for_task: true)
end
it { is_expected.to redirect_to(issues_dashboard_path(assignee_username: user.username)) }
end
end
end
end

View File

@ -34,5 +34,18 @@ FactoryBot.define do
access_level { GroupMember::MINIMAL_ACCESS }
end
transient do
tasks_to_be_done { [] }
end
after(:build) do |group_member, evaluator|
if evaluator.tasks_to_be_done.present?
build(:member_task,
member: group_member,
project: build(:project, namespace: group_member.source),
tasks_to_be_done: evaluator.tasks_to_be_done)
end
end
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
FactoryBot.define do
factory :member_task do
member { association(:group_member, :invited) }
project { association(:project, namespace: member.source) }
tasks_to_be_done { [:ci, :code] }
end
end

View File

@ -23,5 +23,15 @@ FactoryBot.define do
trait :blocked do
after(:build) { |project_member, _| project_member.user.block! }
end
transient do
tasks_to_be_done { [] }
end
after(:build) do |project_member, evaluator|
if evaluator.tasks_to_be_done.present?
build(:member_task, member: project_member, project: project_member.source, tasks_to_be_done: evaluator.tasks_to_be_done)
end
end
end
end

View File

@ -75,6 +75,7 @@ RSpec.describe 'factories' do
group_member
import_state
issue_customer_relations_contact
member_task
milestone_release
namespace
project_broken_repo

View File

@ -2795,11 +2795,7 @@
"sha": "bb5206fee213d983da88c47f9cf4cc6caf9c66dc",
"message": "Feature conflict added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-08-06T08:35:52.000+02:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-08-06T08:35:52.000+02:00",
"committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@ -2815,11 +2811,7 @@
"sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
"message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T10:01:38.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T10:01:38.000+01:00",
"committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@ -2835,11 +2827,7 @@
"sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
"message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:57:31.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:57:31.000+01:00",
"committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@ -2855,11 +2843,7 @@
"sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
"message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:54:21.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:54:21.000+01:00",
"committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@ -2875,11 +2859,7 @@
"sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
"message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:49:50.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:49:50.000+01:00",
"committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@ -2895,11 +2875,7 @@
"sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
"message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:48:32.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:48:32.000+01:00",
"committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@ -3291,11 +3267,7 @@
"relative_order": 0,
"message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:26:01.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:26:01.000+01:00",
"committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@ -3562,11 +3534,7 @@
"sha": "94b8d581c48d894b86661718582fecbc5e3ed2eb",
"message": "fixes #10\n",
"authored_date": "2016-01-19T13:22:56.000+01:00",
"author_name": "James Lopez",
"author_email": "james@jameslopez.es",
"committed_date": "2016-01-19T13:22:56.000+01:00",
"committer_name": "James Lopez",
"committer_email": "james@jameslopez.es",
"commit_author": {
"name": "James Lopez",
"email": "james@jameslopez.es"
@ -3833,11 +3801,7 @@
"sha": "ddd4ff416a931589c695eb4f5b23f844426f6928",
"message": "fixes #10\n",
"authored_date": "2016-01-19T14:14:43.000+01:00",
"author_name": "James Lopez",
"author_email": "james@jameslopez.es",
"committed_date": "2016-01-19T14:14:43.000+01:00",
"committer_name": "James Lopez",
"committer_email": "james@jameslopez.es",
"commit_author": {
"name": "James Lopez",
"email": "james@jameslopez.es"
@ -3853,11 +3817,7 @@
"sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
"message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
"authored_date": "2015-12-07T12:52:12.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "marin@gitlab.com",
"committed_date": "2015-12-07T12:52:12.000+01:00",
"committer_name": "Marin Jankovski",
"committer_email": "marin@gitlab.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "marin@gitlab.com"
@ -3873,11 +3833,7 @@
"sha": "048721d90c449b244b7b4c53a9186b04330174ec",
"message": "LFS object pointer.\n",
"authored_date": "2015-12-07T11:54:28.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "maxlazio@gmail.com",
"committed_date": "2015-12-07T11:54:28.000+01:00",
"committer_name": "Marin Jankovski",
"committer_email": "maxlazio@gmail.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "maxlazio@gmail.com"
@ -3893,11 +3849,7 @@
"sha": "5f923865dde3436854e9ceb9cdb7815618d4e849",
"message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
"authored_date": "2015-11-13T16:27:12.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T16:27:12.000+01:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@ -3913,11 +3865,7 @@
"sha": "d2d430676773caa88cdaf7c55944073b2fd5561a",
"message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
"authored_date": "2015-11-13T08:50:17.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:50:17.000+01:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@ -3933,11 +3881,7 @@
"sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
"message": "Add GitLab SVG\n",
"authored_date": "2015-11-13T08:39:43.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:39:43.000+01:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@ -3953,11 +3897,7 @@
"sha": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
"authored_date": "2015-11-13T07:21:40.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T07:21:40.000+01:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@ -3973,11 +3913,7 @@
"sha": "66eceea0db202bb39c4e445e8ca28689645366c5",
"message": "add spaces in whitespace file\n",
"authored_date": "2015-11-13T06:01:27.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:01:27.000+01:00",
"committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@ -3993,11 +3929,7 @@
"sha": "08f22f255f082689c0d7d39d19205085311542bc",
"message": "remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n",
"authored_date": "2015-11-13T06:00:16.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:00:16.000+01:00",
"committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@ -4013,11 +3945,7 @@
"sha": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
"authored_date": "2015-11-13T05:23:14.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T05:23:14.000+01:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@ -4033,11 +3961,7 @@
"sha": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
"message": "add whitespace in empty\n",
"authored_date": "2015-11-13T05:08:45.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:45.000+01:00",
"committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@ -4053,11 +3977,7 @@
"sha": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
"message": "add empty file\n",
"authored_date": "2015-11-13T05:08:04.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:04.000+01:00",
"committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@ -4073,11 +3993,7 @@
"sha": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
"message": "Add ISO-8859 test file\n",
"authored_date": "2015-08-25T17:53:12.000+02:00",
"author_name": "Stan Hu",
"author_email": "stanhu@packetzoom.com",
"committed_date": "2015-08-25T17:53:12.000+02:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@packetzoom.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@packetzoom.com"
@ -4093,11 +4009,7 @@
"sha": "e56497bb5f03a90a51293fc6d516788730953899",
"message": "Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/275#note_732774)\n\nSee merge request !2\n",
"authored_date": "2015-01-10T22:23:29.000+01:00",
"author_name": "Sytse Sijbrandij",
"author_email": "sytse@gitlab.com",
"committed_date": "2015-01-10T22:23:29.000+01:00",
"committer_name": "Sytse Sijbrandij",
"committer_email": "sytse@gitlab.com",
"commit_author": {
"name": "Sytse Sijbrandij",
"email": "sytse@gitlab.com"
@ -4113,11 +4025,7 @@
"sha": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
"message": "add directory structure for tree_helper spec\n",
"authored_date": "2015-01-10T21:28:18.000+01:00",
"author_name": "marmis85",
"author_email": "marmis85@gmail.com",
"committed_date": "2015-01-10T21:28:18.000+01:00",
"committer_name": "marmis85",
"committer_email": "marmis85@gmail.com",
"commit_author": {
"name": "marmis85",
"email": "marmis85@gmail.com"
@ -4133,11 +4041,7 @@
"sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
"message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T10:01:38.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T10:01:38.000+01:00",
"committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@ -4153,11 +4057,7 @@
"sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
"message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:57:31.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:57:31.000+01:00",
"committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@ -4173,11 +4073,7 @@
"sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
"message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:54:21.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:54:21.000+01:00",
"committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@ -4193,11 +4089,7 @@
"sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
"message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:49:50.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:49:50.000+01:00",
"committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@ -4213,11 +4105,7 @@
"sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
"message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:48:32.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:48:32.000+01:00",
"committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@ -4678,11 +4566,7 @@
"sha": "0bfedc29d30280c7e8564e19f654584b459e5868",
"message": "fixes #10\n",
"authored_date": "2016-01-19T15:25:23.000+01:00",
"author_name": "James Lopez",
"author_email": "james@jameslopez.es",
"committed_date": "2016-01-19T15:25:23.000+01:00",
"committer_name": "James Lopez",
"committer_email": "james@jameslopez.es",
"commit_author": {
"name": "James Lopez",
"email": "james@jameslopez.es"
@ -4698,11 +4582,7 @@
"sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
"message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
"authored_date": "2015-12-07T12:52:12.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "marin@gitlab.com",
"committed_date": "2015-12-07T12:52:12.000+01:00",
"committer_name": "Marin Jankovski",
"committer_email": "marin@gitlab.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "marin@gitlab.com"
@ -4718,11 +4598,7 @@
"sha": "048721d90c449b244b7b4c53a9186b04330174ec",
"message": "LFS object pointer.\n",
"authored_date": "2015-12-07T11:54:28.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "maxlazio@gmail.com",
"committed_date": "2015-12-07T11:54:28.000+01:00",
"committer_name": "Marin Jankovski",
"committer_email": "maxlazio@gmail.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "maxlazio@gmail.com"
@ -4738,11 +4614,7 @@
"sha": "5f923865dde3436854e9ceb9cdb7815618d4e849",
"message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
"authored_date": "2015-11-13T16:27:12.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T16:27:12.000+01:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@ -4758,11 +4630,7 @@
"sha": "d2d430676773caa88cdaf7c55944073b2fd5561a",
"message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
"authored_date": "2015-11-13T08:50:17.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:50:17.000+01:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@ -4778,11 +4646,7 @@
"sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
"message": "Add GitLab SVG\n",
"authored_date": "2015-11-13T08:39:43.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:39:43.000+01:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@ -4798,11 +4662,7 @@
"sha": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
"authored_date": "2015-11-13T07:21:40.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T07:21:40.000+01:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@ -4818,11 +4678,7 @@
"sha": "66eceea0db202bb39c4e445e8ca28689645366c5",
"message": "add spaces in whitespace file\n",
"authored_date": "2015-11-13T06:01:27.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:01:27.000+01:00",
"committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@ -4838,11 +4694,7 @@
"sha": "08f22f255f082689c0d7d39d19205085311542bc",
"message": "remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n",
"authored_date": "2015-11-13T06:00:16.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:00:16.000+01:00",
"committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@ -4858,11 +4710,7 @@
"sha": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
"authored_date": "2015-11-13T05:23:14.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T05:23:14.000+01:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@ -4878,11 +4726,7 @@
"sha": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
"message": "add whitespace in empty\n",
"authored_date": "2015-11-13T05:08:45.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:45.000+01:00",
"committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@ -4898,11 +4742,7 @@
"sha": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
"message": "add empty file\n",
"authored_date": "2015-11-13T05:08:04.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:04.000+01:00",
"committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@ -4918,11 +4758,7 @@
"sha": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
"message": "Add ISO-8859 test file\n",
"authored_date": "2015-08-25T17:53:12.000+02:00",
"author_name": "Stan Hu",
"author_email": "stanhu@packetzoom.com",
"committed_date": "2015-08-25T17:53:12.000+02:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@packetzoom.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@packetzoom.com"
@ -4938,11 +4774,7 @@
"sha": "e56497bb5f03a90a51293fc6d516788730953899",
"message": "Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/275#note_732774)\n\nSee merge request !2\n",
"authored_date": "2015-01-10T22:23:29.000+01:00",
"author_name": "Sytse Sijbrandij",
"author_email": "sytse@gitlab.com",
"committed_date": "2015-01-10T22:23:29.000+01:00",
"committer_name": "Sytse Sijbrandij",
"committer_email": "sytse@gitlab.com",
"commit_author": {
"name": "Sytse Sijbrandij",
"email": "sytse@gitlab.com"
@ -4958,11 +4790,7 @@
"sha": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
"message": "add directory structure for tree_helper spec\n",
"authored_date": "2015-01-10T21:28:18.000+01:00",
"author_name": "marmis85",
"author_email": "marmis85@gmail.com",
"committed_date": "2015-01-10T21:28:18.000+01:00",
"committer_name": "marmis85",
"committer_email": "marmis85@gmail.com",
"commit_author": {
"name": "marmis85",
"email": "marmis85@gmail.com"
@ -5307,11 +5135,7 @@
"sha": "97a0df9696e2aebf10c31b3016f40214e0e8f243",
"message": "fixes #10\n",
"authored_date": "2016-01-19T14:08:21.000+01:00",
"author_name": "James Lopez",
"author_email": "james@jameslopez.es",
"committed_date": "2016-01-19T14:08:21.000+01:00",
"committer_name": "James Lopez",
"committer_email": "james@jameslopez.es",
"commit_author": {
"name": "James Lopez",
"email": "james@jameslopez.es"
@ -5327,11 +5151,7 @@
"sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
"message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
"authored_date": "2015-12-07T12:52:12.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "marin@gitlab.com",
"committed_date": "2015-12-07T12:52:12.000+01:00",
"committer_name": "Marin Jankovski",
"committer_email": "marin@gitlab.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "marin@gitlab.com"
@ -5347,11 +5167,7 @@
"sha": "048721d90c449b244b7b4c53a9186b04330174ec",
"message": "LFS object pointer.\n",
"authored_date": "2015-12-07T11:54:28.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "maxlazio@gmail.com",
"committed_date": "2015-12-07T11:54:28.000+01:00",
"committer_name": "Marin Jankovski",
"committer_email": "maxlazio@gmail.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "maxlazio@gmail.com"
@ -5367,11 +5183,7 @@
"sha": "5f923865dde3436854e9ceb9cdb7815618d4e849",
"message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
"authored_date": "2015-11-13T16:27:12.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T16:27:12.000+01:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@ -5387,11 +5199,7 @@
"sha": "d2d430676773caa88cdaf7c55944073b2fd5561a",
"message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
"authored_date": "2015-11-13T08:50:17.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:50:17.000+01:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@ -5407,11 +5215,7 @@
"sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
"message": "Add GitLab SVG\n",
"authored_date": "2015-11-13T08:39:43.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:39:43.000+01:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@ -5427,11 +5231,7 @@
"sha": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
"authored_date": "2015-11-13T07:21:40.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T07:21:40.000+01:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@ -5447,11 +5247,7 @@
"sha": "66eceea0db202bb39c4e445e8ca28689645366c5",
"message": "add spaces in whitespace file\n",
"authored_date": "2015-11-13T06:01:27.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:01:27.000+01:00",
"committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@ -5467,11 +5263,7 @@
"sha": "08f22f255f082689c0d7d39d19205085311542bc",
"message": "remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n",
"authored_date": "2015-11-13T06:00:16.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:00:16.000+01:00",
"committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@ -5487,11 +5279,7 @@
"sha": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
"authored_date": "2015-11-13T05:23:14.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T05:23:14.000+01:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@ -5507,11 +5295,7 @@
"sha": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
"message": "add whitespace in empty\n",
"authored_date": "2015-11-13T05:08:45.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:45.000+01:00",
"committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@ -5527,11 +5311,7 @@
"sha": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
"message": "add empty file\n",
"authored_date": "2015-11-13T05:08:04.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:04.000+01:00",
"committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@ -5547,11 +5327,7 @@
"sha": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
"message": "Add ISO-8859 test file\n",
"authored_date": "2015-08-25T17:53:12.000+02:00",
"author_name": "Stan Hu",
"author_email": "stanhu@packetzoom.com",
"committed_date": "2015-08-25T17:53:12.000+02:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@packetzoom.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@packetzoom.com"
@ -6119,11 +5895,7 @@
"sha": "f998ac87ac9244f15e9c15109a6f4e62a54b779d",
"message": "fixes #10\n",
"authored_date": "2016-01-19T14:43:23.000+01:00",
"author_name": "James Lopez",
"author_email": "james@jameslopez.es",
"committed_date": "2016-01-19T14:43:23.000+01:00",
"committer_name": "James Lopez",
"committer_email": "james@jameslopez.es",
"commit_author": {
"name": "James Lopez",
"email": "james@jameslopez.es"
@ -6139,11 +5911,7 @@
"sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
"message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
"authored_date": "2015-12-07T12:52:12.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "marin@gitlab.com",
"committed_date": "2015-12-07T12:52:12.000+01:00",
"committer_name": "Marin Jankovski",
"committer_email": "marin@gitlab.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "marin@gitlab.com"
@ -6159,11 +5927,7 @@
"sha": "048721d90c449b244b7b4c53a9186b04330174ec",
"message": "LFS object pointer.\n",
"authored_date": "2015-12-07T11:54:28.000+01:00",
"author_name": "Marin Jankovski",
"author_email": "maxlazio@gmail.com",
"committed_date": "2015-12-07T11:54:28.000+01:00",
"committer_name": "Marin Jankovski",
"committer_email": "maxlazio@gmail.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "maxlazio@gmail.com"
@ -6179,11 +5943,7 @@
"sha": "5f923865dde3436854e9ceb9cdb7815618d4e849",
"message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
"authored_date": "2015-11-13T16:27:12.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T16:27:12.000+01:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@ -6199,11 +5959,7 @@
"sha": "d2d430676773caa88cdaf7c55944073b2fd5561a",
"message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
"authored_date": "2015-11-13T08:50:17.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:50:17.000+01:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@ -6219,11 +5975,7 @@
"sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
"message": "Add GitLab SVG\n",
"authored_date": "2015-11-13T08:39:43.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:39:43.000+01:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@ -6239,11 +5991,7 @@
"sha": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
"authored_date": "2015-11-13T07:21:40.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T07:21:40.000+01:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@ -6259,11 +6007,7 @@
"sha": "66eceea0db202bb39c4e445e8ca28689645366c5",
"message": "add spaces in whitespace file\n",
"authored_date": "2015-11-13T06:01:27.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:01:27.000+01:00",
"committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@ -6279,11 +6023,7 @@
"sha": "08f22f255f082689c0d7d39d19205085311542bc",
"message": "remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n",
"authored_date": "2015-11-13T06:00:16.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:00:16.000+01:00",
"committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@ -6299,11 +6039,7 @@
"sha": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
"message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
"authored_date": "2015-11-13T05:23:14.000+01:00",
"author_name": "Stan Hu",
"author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T05:23:14.000+01:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
@ -6319,11 +6055,7 @@
"sha": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
"message": "add whitespace in empty\n",
"authored_date": "2015-11-13T05:08:45.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:45.000+01:00",
"committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@ -6339,11 +6071,7 @@
"sha": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
"message": "add empty file\n",
"authored_date": "2015-11-13T05:08:04.000+01:00",
"author_name": "윤민식",
"author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:04.000+01:00",
"committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
@ -6359,11 +6087,7 @@
"sha": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
"message": "Add ISO-8859 test file\n",
"authored_date": "2015-08-25T17:53:12.000+02:00",
"author_name": "Stan Hu",
"author_email": "stanhu@packetzoom.com",
"committed_date": "2015-08-25T17:53:12.000+02:00",
"committer_name": "Stan Hu",
"committer_email": "stanhu@packetzoom.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@packetzoom.com"
@ -6379,11 +6103,7 @@
"sha": "e56497bb5f03a90a51293fc6d516788730953899",
"message": "Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/275#note_732774)\n\nSee merge request !2\n",
"authored_date": "2015-01-10T22:23:29.000+01:00",
"author_name": "Sytse Sijbrandij",
"author_email": "sytse@gitlab.com",
"committed_date": "2015-01-10T22:23:29.000+01:00",
"committer_name": "Sytse Sijbrandij",
"committer_email": "sytse@gitlab.com",
"commit_author": {
"name": "Sytse Sijbrandij",
"email": "sytse@gitlab.com"
@ -6399,11 +6119,7 @@
"sha": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
"message": "add directory structure for tree_helper spec\n",
"authored_date": "2015-01-10T21:28:18.000+01:00",
"author_name": "marmis85",
"author_email": "marmis85@gmail.com",
"committed_date": "2015-01-10T21:28:18.000+01:00",
"committer_name": "marmis85",
"committer_email": "marmis85@gmail.com",
"commit_author": {
"name": "marmis85",
"email": "marmis85@gmail.com"
@ -6419,11 +6135,7 @@
"sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
"message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T10:01:38.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T10:01:38.000+01:00",
"committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@ -6439,11 +6151,7 @@
"sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
"message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:57:31.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:57:31.000+01:00",
"committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@ -6459,11 +6167,7 @@
"sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
"message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:54:21.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:54:21.000+01:00",
"committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@ -6479,11 +6183,7 @@
"sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
"message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:49:50.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:49:50.000+01:00",
"committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@ -6499,11 +6199,7 @@
"sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
"message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:48:32.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:48:32.000+01:00",
"committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
@ -6952,11 +6648,7 @@
"sha": "a4e5dfebf42e34596526acb8611bc7ed80e4eb3f",
"message": "fixes #10\n",
"authored_date": "2016-01-19T15:44:02.000+01:00",
"author_name": "James Lopez",
"author_email": "james@jameslopez.es",
"committed_date": "2016-01-19T15:44:02.000+01:00",
"committer_name": "James Lopez",
"committer_email": "james@jameslopez.es",
"commit_author": {
"name": "James Lopez",
"email": "james@jameslopez.es"

File diff suppressed because one or more lines are too long

View File

@ -16,16 +16,25 @@ import Api from '~/api';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
import { INVITE_MEMBERS_IN_COMMENT, MEMBER_AREAS_OF_FOCUS } from '~/invite_members/constants';
import {
INVITE_MEMBERS_IN_COMMENT,
MEMBER_AREAS_OF_FOCUS,
INVITE_MEMBERS_FOR_TASK,
} from '~/invite_members/constants';
import eventHub from '~/invite_members/event_hub';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import { getParameterValues } from '~/lib/utils/url_utility';
import { apiPaths, membersApiResponse, invitationsApiResponse } from '../mock_data/api_responses';
let wrapper;
let mock;
jest.mock('~/experimentation/experiment_tracking');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
getParameterValues: jest.fn(() => []),
}));
const id = '1';
const name = 'test name';
@ -40,6 +49,15 @@ const areasOfFocusOptions = [
{ text: 'area1', value: 'area1' },
{ text: 'area2', value: 'area2' },
];
const tasksToBeDoneOptions = [
{ text: 'First task', value: 'first' },
{ text: 'Second task', value: 'second' },
];
const newProjectPath = 'projects/new';
const projects = [
{ text: 'First project', value: '1' },
{ text: 'Second project', value: '2' },
];
const user1 = { id: 1, name: 'Name One', username: 'one_1', avatar_url: '' };
const user2 = { id: 2, name: 'Name Two', username: 'one_2', avatar_url: '' };
@ -59,6 +77,9 @@ const sharedGroup = { id: '981' };
const createComponent = (data = {}, props = {}) => {
wrapper = shallowMountExtended(InviteMembersModal, {
provide: {
newProjectPath,
},
propsData: {
id,
name,
@ -68,6 +89,8 @@ const createComponent = (data = {}, props = {}) => {
areasOfFocusOptions,
defaultAccessLevel,
noSelectionAreasOfFocus,
tasksToBeDoneOptions,
projects,
helpLink,
...props,
},
@ -131,6 +154,10 @@ describe('InviteMembersModal', () => {
const membersFormGroupDescription = () => findMembersFormGroup().props('description');
const findMembersSelect = () => wrapper.findComponent(MembersTokenSelect);
const findAreaofFocusCheckBoxGroup = () => wrapper.findComponent(GlFormCheckboxGroup);
const findTasksToBeDone = () => wrapper.findByTestId('invite-members-modal-tasks-to-be-done');
const findTasks = () => wrapper.findByTestId('invite-members-modal-tasks');
const findProjectSelect = () => wrapper.findByTestId('invite-members-modal-project-select');
const findNoProjectsAlert = () => wrapper.findByTestId('invite-members-modal-no-projects-alert');
describe('rendering the modal', () => {
beforeEach(() => {
@ -191,6 +218,127 @@ describe('InviteMembersModal', () => {
});
});
describe('rendering the tasks to be done', () => {
const setupComponent = (
extraData = {},
props = {},
urlParameter = ['invite_members_for_task'],
) => {
const data = {
selectedAccessLevel: 30,
selectedTasksToBeDone: ['ci', 'code'],
...extraData,
};
getParameterValues.mockImplementation(() => urlParameter);
createComponent(data, props);
};
afterAll(() => {
getParameterValues.mockImplementation(() => []);
});
it('renders the tasks to be done', () => {
setupComponent();
expect(findTasksToBeDone().exists()).toBe(true);
});
describe('when the selected access level is lower than 30', () => {
it('does not render the tasks to be done', () => {
setupComponent({ selectedAccessLevel: 20 });
expect(findTasksToBeDone().exists()).toBe(false);
});
});
describe('when the url does not contain the parameter `open_modal=invite_members_for_task`', () => {
it('does not render the tasks to be done', () => {
setupComponent({}, {}, []);
expect(findTasksToBeDone().exists()).toBe(false);
});
});
describe('rendering the tasks', () => {
it('renders the tasks', () => {
setupComponent();
expect(findTasks().exists()).toBe(true);
});
it('does not render an alert', () => {
setupComponent();
expect(findNoProjectsAlert().exists()).toBe(false);
});
describe('when there are no projects passed in the data', () => {
it('does not render the tasks', () => {
setupComponent({}, { projects: [] });
expect(findTasks().exists()).toBe(false);
});
it('renders an alert with a link to the new projects path', () => {
setupComponent({}, { projects: [] });
expect(findNoProjectsAlert().exists()).toBe(true);
expect(findNoProjectsAlert().findComponent(GlLink).attributes('href')).toBe(
newProjectPath,
);
});
});
});
describe('rendering the project dropdown', () => {
it('renders the project select', () => {
setupComponent();
expect(findProjectSelect().exists()).toBe(true);
});
describe('when the modal is shown for a project', () => {
it('does not render the project select', () => {
setupComponent({}, { isProject: true });
expect(findProjectSelect().exists()).toBe(false);
});
});
describe('when no tasks are selected', () => {
it('does not render the project select', () => {
setupComponent({ selectedTasksToBeDone: [] });
expect(findProjectSelect().exists()).toBe(false);
});
});
});
describe('tracking events', () => {
it('tracks the view for invite_members_for_task', () => {
setupComponent();
expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name);
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
INVITE_MEMBERS_FOR_TASK.view,
);
});
it('tracks the submit for invite_members_for_task', () => {
setupComponent();
clickInviteButton();
expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name, {
label: 'selected_tasks_to_be_done',
property: 'ci,code',
});
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
INVITE_MEMBERS_FOR_TASK.submit,
);
});
});
});
describe('displaying the correct introText and form group description', () => {
describe('when inviting to a project', () => {
describe('when inviting members', () => {
@ -267,6 +415,8 @@ describe('InviteMembersModal', () => {
invite_source: inviteSource,
format: 'json',
areas_of_focus: noSelectionAreasOfFocus,
tasks_to_be_done: [],
tasks_project_id: '',
};
describe('when member is added successfully', () => {
@ -448,6 +598,8 @@ describe('InviteMembersModal', () => {
email: 'email@example.com',
invite_source: inviteSource,
areas_of_focus: noSelectionAreasOfFocus,
tasks_to_be_done: [],
tasks_project_id: '',
format: 'json',
};
@ -576,6 +728,8 @@ describe('InviteMembersModal', () => {
invite_source: inviteSource,
areas_of_focus: noSelectionAreasOfFocus,
format: 'json',
tasks_to_be_done: [],
tasks_project_id: '',
};
const emailPostData = { ...postData, email: 'email@example.com' };

View File

@ -40,6 +40,8 @@ describe('Pipeline Status', () => {
const findPipelineId = () => wrapper.find('[data-testid="pipeline-id"]');
const findPipelineCommit = () => wrapper.find('[data-testid="pipeline-commit"]');
const findPipelineErrorMsg = () => wrapper.find('[data-testid="pipeline-error-msg"]');
const findPipelineNotTriggeredErrorMsg = () =>
wrapper.find('[data-testid="pipeline-not-triggered-error-msg"]');
const findPipelineLoadingMsg = () => wrapper.find('[data-testid="pipeline-loading-msg"]');
const findPipelineViewBtn = () => wrapper.find('[data-testid="pipeline-view-btn"]');
const findStatusIcon = () => wrapper.find('[data-testid="pipeline-status-icon"]');
@ -117,7 +119,8 @@ describe('Pipeline Status', () => {
await waitForPromises();
});
it('renders error', () => {
it('renders api error', () => {
expect(findPipelineNotTriggeredErrorMsg().exists()).toBe(false);
expect(findIcon().attributes('name')).toBe('warning-solid');
expect(findPipelineErrorMsg().text()).toBe(i18n.fetchError);
});
@ -129,6 +132,23 @@ describe('Pipeline Status', () => {
expect(findPipelineViewBtn().exists()).toBe(false);
});
});
describe('when pipeline is null', () => {
beforeEach(() => {
mockPipelineQuery.mockResolvedValue({
data: { project: { pipeline: null } },
});
createComponentWithApollo();
waitForPromises();
});
it('renders pipeline not triggered error', () => {
expect(findPipelineErrorMsg().exists()).toBe(false);
expect(findIcon().attributes('name')).toBe('information-o');
expect(findPipelineNotTriggeredErrorMsg().text()).toBe(i18n.pipelineNotTriggeredMsg);
});
});
});
describe('when feature flag for pipeline mini graph is enabled', () => {

View File

@ -10,7 +10,7 @@ const DEFAULT_RENDER_COUNT = 5;
describe('UncollapsedAssigneeList component', () => {
let wrapper;
function createComponent(props = {}) {
function createComponent(props = {}, glFeatures = {}) {
const propsData = {
users: [],
rootPath: TEST_HOST,
@ -19,6 +19,7 @@ describe('UncollapsedAssigneeList component', () => {
wrapper = mount(UncollapsedAssigneeList, {
propsData,
provide: { glFeatures },
});
}
@ -99,4 +100,22 @@ describe('UncollapsedAssigneeList component', () => {
});
});
});
describe('merge requests', () => {
it.each`
numberOfUsers
${1}
${5}
`('displays as a vertical list for $numberOfUsers of users', ({ numberOfUsers }) => {
createComponent(
{
users: UsersMockHelper.createNumberRandomUsers(numberOfUsers),
issuableType: 'merge_request',
},
{ mrAttentionRequests: true },
);
expect(wrapper.findAll('[data-testid="username"]').length).toBe(numberOfUsers);
});
});
});

View File

@ -59,7 +59,84 @@ RSpec.describe InviteMembersHelper do
no_selection_areas_of_focus: []
}
expect(helper.common_invite_modal_dataset(project)).to match(attributes)
expect(helper.common_invite_modal_dataset(project)).to include(attributes)
end
end
context 'tasks_to_be_done' do
subject(:output) { helper.common_invite_modal_dataset(source) }
let_it_be(:source) { project }
before do
stub_experiments(invite_members_for_task: true)
end
context 'when not logged in' do
before do
allow(helper).to receive(:params).and_return({ open_modal: 'invite_members_for_task' })
end
it "doesn't have the tasks to be done attributes" do
expect(output[:tasks_to_be_done_options]).to be_nil
expect(output[:projects]).to be_nil
expect(output[:new_project_path]).to be_nil
end
end
context 'when logged in but the open_modal param is not present' do
before do
allow(helper).to receive(:current_user).and_return(developer)
end
it "doesn't have the tasks to be done attributes" do
expect(output[:tasks_to_be_done_options]).to be_nil
expect(output[:projects]).to be_nil
expect(output[:new_project_path]).to be_nil
end
end
context 'when logged in and the open_modal param is present' do
before do
allow(helper).to receive(:current_user).and_return(developer)
allow(helper).to receive(:params).and_return({ open_modal: 'invite_members_for_task' })
end
context 'for a group' do
let_it_be(:source) { create(:group, projects: [project]) }
it 'has the expected attributes', :aggregate_failures do
expect(output[:tasks_to_be_done_options]).to eq(
[
{ value: :code, text: 'Create/import code into a project (repository)' },
{ value: :ci, text: 'Set up CI/CD pipelines to build, test, deploy, and monitor code' },
{ value: :issues, text: 'Create/import issues (tickets) to collaborate on ideas and plan work' }
].to_json
)
expect(output[:projects]).to eq(
[{ id: project.id, title: project.title }].to_json
)
expect(output[:new_project_path]).to eq(
new_project_path(namespace_id: source.id)
)
end
end
context 'for a project' do
it 'has the expected attributes', :aggregate_failures do
expect(output[:tasks_to_be_done_options]).to eq(
[
{ value: :code, text: 'Create/import code into a project (repository)' },
{ value: :ci, text: 'Set up CI/CD pipelines to build, test, deploy, and monitor code' },
{ value: :issues, text: 'Create/import issues (tickets) to collaborate on ideas and plan work' }
].to_json
)
expect(output[:projects]).to eq(
[{ id: project.id, title: project.title }].to_json
)
expect(output[:new_project_path]).to eq('')
end
end
end
end
end

View File

@ -68,4 +68,10 @@ RSpec.describe MembersHelper do
it { expect(leave_confirmation_message(project)).to eq "Are you sure you want to leave the \"#{project.full_name}\" project?" }
it { expect(leave_confirmation_message(group)).to eq "Are you sure you want to leave the \"#{group.name}\" group?" }
end
describe '#localized_tasks_to_be_done_choices' do
it 'has a translation for all `TASKS_TO_BE_DONE` keys' do
expect(localized_tasks_to_be_done_choices).to include(*MemberTask::TASKS.keys)
end
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::MigrateMergeRequestDiffCommitUsers do
RSpec.describe Gitlab::BackgroundMigration::MigrateMergeRequestDiffCommitUsers, schema: 20211012134316 do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:users) { table(:users) }

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::StealMigrateMergeRequestDiffCommitUsers do
RSpec.describe Gitlab::BackgroundMigration::StealMigrateMergeRequestDiffCommitUsers, schema: 20211012134316 do
let(:migration) { described_class.new }
describe '#perform' do

View File

@ -133,6 +133,7 @@ project_members:
- user
- source
- project
- member_task
merge_requests:
- status_check_responses
- subscriptions

View File

@ -23,7 +23,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
]
RSpec::Mocks.with_temporary_scope do
@project = create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project')
@project = create(:project, :repository, :builds_enabled, :issues_disabled, name: 'project', path: 'project')
@shared = @project.import_export_shared
stub_all_feature_flags
@ -36,7 +36,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
allow_any_instance_of(Gitlab::Git::Repository).to receive(:branch_exists?).and_return(false)
expect(@shared).not_to receive(:error)
expect_any_instance_of(Gitlab::Git::Repository).to receive(:create_branch).with('feature', 'DCBA')
allow_any_instance_of(Gitlab::Git::Repository).to receive(:create_branch)
project_tree_restorer = described_class.new(user: @user, shared: @shared, project: @project)

View File

@ -47,22 +47,30 @@ RSpec.describe Emails::InProductMarketing do
end
where(:track, :series) do
:create | 0
:create | 1
:create | 2
:verify | 0
:verify | 1
:verify | 2
:trial | 0
:trial | 1
:trial | 2
:team | 0
:team | 1
:team | 2
:experience | 0
:create | 0
:create | 1
:create | 2
:verify | 0
:verify | 1
:verify | 2
:trial | 0
:trial | 1
:trial | 2
:team | 0
:team | 1
:team | 2
:experience | 0
:team_short | 0
:trial_short | 0
:admin_verify | 0
end
with_them do
before do
stub_experiments(invite_members_for_task: :candidate)
group.add_owner(user)
end
it 'has the correct subject and content' do
message = Gitlab::Email::Message::InProductMarketing.for(track).new(group: group, user: user, series: series)
@ -76,6 +84,14 @@ RSpec.describe Emails::InProductMarketing do
else
is_expected.to have_body_text(CGI.unescapeHTML(message.cta_link))
end
if track =~ /(create|verify)/
is_expected.to have_body_text(message.invite_text)
is_expected.to have_body_text(CGI.unescapeHTML(message.invite_link))
else
is_expected.not_to have_body_text(message.invite_text)
is_expected.not_to have_body_text(CGI.unescapeHTML(message.invite_link))
end
end
end
end

View File

@ -8,6 +8,7 @@ RSpec.describe Notify do
include EmailSpec::Matchers
include EmailHelpers
include RepoHelpers
include MembersHelper
include_context 'gitlab email notification'
@ -761,10 +762,21 @@ RSpec.describe Notify do
is_expected.to have_body_text project_member.human_access
is_expected.to have_body_text 'leave the project'
is_expected.to have_body_text project_url(project, leave: 1)
is_expected.not_to have_body_text 'You were assigned the following tasks:'
end
context 'with tasks to be done present' do
let(:project_member) { create(:project_member, project: project, user: user, tasks_to_be_done: [:ci, :code]) }
it 'contains the assigned tasks to be done' do
is_expected.to have_body_text 'You were assigned the following tasks:'
is_expected.to have_body_text localized_tasks_to_be_done_choices[:ci]
is_expected.to have_body_text localized_tasks_to_be_done_choices[:code]
end
end
end
def invite_to_project(project, inviter:, user: nil)
def invite_to_project(project, inviter:, user: nil, tasks_to_be_done: [])
create(
:project_member,
:developer,
@ -772,7 +784,8 @@ RSpec.describe Notify do
invite_token: '1234',
invite_email: 'toto@example.com',
user: user,
created_by: inviter
created_by: inviter,
tasks_to_be_done: tasks_to_be_done
)
end
@ -804,6 +817,7 @@ RSpec.describe Notify do
is_expected.to have_content("#{inviter.name} invited you to join the")
is_expected.to have_content('Project details')
is_expected.to have_content("What's it about?")
is_expected.not_to have_body_text 'and has assigned you the following tasks:'
end
end
@ -890,6 +904,16 @@ RSpec.describe Notify do
end
end
end
context 'with tasks to be done present', :aggregate_failures do
let(:project_member) { invite_to_project(project, inviter: inviter, tasks_to_be_done: [:ci, :code]) }
it 'contains the assigned tasks to be done' do
is_expected.to have_body_text 'and has assigned you the following tasks:'
is_expected.to have_body_text localized_tasks_to_be_done_choices[:ci]
is_expected.to have_body_text localized_tasks_to_be_done_choices[:code]
end
end
end
describe 'project invitation accepted' do
@ -1398,7 +1422,7 @@ RSpec.describe Notify do
end
end
def invite_to_group(group, inviter:, user: nil)
def invite_to_group(group, inviter:, user: nil, tasks_to_be_done: [])
create(
:group_member,
:developer,
@ -1406,7 +1430,8 @@ RSpec.describe Notify do
invite_token: '1234',
invite_email: 'toto@example.com',
user: user,
created_by: inviter
created_by: inviter,
tasks_to_be_done: tasks_to_be_done
)
end
@ -1431,6 +1456,7 @@ RSpec.describe Notify do
is_expected.to have_body_text group.name
is_expected.to have_body_text group_member.human_access.downcase
is_expected.to have_body_text group_member.invite_token
is_expected.not_to have_body_text 'and has assigned you the following tasks:'
end
end
@ -1444,6 +1470,24 @@ RSpec.describe Notify do
is_expected.to have_body_text group_member.invite_token
end
end
context 'with tasks to be done present', :aggregate_failures do
let(:group_member) { invite_to_group(group, inviter: inviter, tasks_to_be_done: [:ci, :code]) }
it 'contains the assigned tasks to be done' do
is_expected.to have_body_text 'and has assigned you the following tasks:'
is_expected.to have_body_text localized_tasks_to_be_done_choices[:ci]
is_expected.to have_body_text localized_tasks_to_be_done_choices[:code]
end
context 'when there is no inviter' do
let(:inviter) { nil }
it 'does not contain the assigned tasks to be done' do
is_expected.not_to have_body_text 'and has assigned you the following tasks:'
end
end
end
end
describe 'group invitation reminders' do

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration! 'clean_up_migrate_merge_request_diff_commit_users'
RSpec.describe CleanUpMigrateMergeRequestDiffCommitUsers, :migration do
describe '#up' do
context 'when there are pending jobs' do
it 'processes the jobs immediately' do
Gitlab::Database::BackgroundMigrationJob.create!(
class_name: 'MigrateMergeRequestDiffCommitUsers',
status: :pending,
arguments: [10, 20]
)
spy = Gitlab::BackgroundMigration::MigrateMergeRequestDiffCommitUsers
migration = described_class.new
allow(Gitlab::BackgroundMigration::MigrateMergeRequestDiffCommitUsers)
.to receive(:new)
.and_return(spy)
expect(migration).to receive(:say)
expect(spy).to receive(:perform).with(10, 20)
migration.up
end
end
context 'when all jobs are completed' do
it 'does nothing' do
Gitlab::Database::BackgroundMigrationJob.create!(
class_name: 'MigrateMergeRequestDiffCommitUsers',
status: :succeeded,
arguments: [10, 20]
)
migration = described_class.new
expect(migration).not_to receive(:say)
expect(Gitlab::BackgroundMigration::MigrateMergeRequestDiffCommitUsers)
.not_to receive(:new)
migration.up
end
end
end
end

View File

@ -1710,4 +1710,36 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
subject
end
end
describe '#should_link_to_merge_requests?' do
subject { environment.should_link_to_merge_requests? }
context 'when environment is foldered' do
context 'when environment is production tier' do
let(:environment) { create(:environment, project: project, name: 'production/aws') }
it { is_expected.to eq(true) }
end
context 'when environment is development tier' do
let(:environment) { create(:environment, project: project, name: 'review/feature') }
it { is_expected.to eq(false) }
end
end
context 'when environment is unfoldered' do
context 'when environment is production tier' do
let(:environment) { create(:environment, project: project, name: 'production') }
it { is_expected.to eq(true) }
end
context 'when environment is development tier' do
let(:environment) { create(:environment, project: project, name: 'development') }
it { is_expected.to eq(true) }
end
end
end
end

View File

@ -563,6 +563,25 @@ RSpec.describe Group do
it { expect(group.ancestors.to_sql).not_to include 'traversal_ids <@' }
end
end
context 'when project namespace exists in the group' do
let!(:project) { create(:project, group: group) }
let!(:project_namespace) { create(:project_namespace, project: project) }
it 'filters out project namespace' do
expect(group.descendants.find_by_id(project_namespace.id)).to be_nil
end
context 'when include_sti_condition is disabled' do
before do
stub_feature_flags(include_sti_condition: false)
end
it 'raises an exception' do
expect { group.descendants.find_by_id(project_namespace.id)}.to raise_error(ActiveRecord::SubclassNotFound)
end
end
end
end
end
@ -718,6 +737,22 @@ RSpec.describe Group do
expect(group.group_members.developers.map(&:user)).to include(user)
expect(group.group_members.guests.map(&:user)).not_to include(user)
end
context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do
let!(:project) { create(:project, group: group) }
before do
stub_experiments(invite_members_for_task: true)
group.add_users([create(:user)], :developer, tasks_to_be_done: %w(ci code), tasks_project_id: project.id)
end
it 'creates a member_task with the correct attributes', :aggregate_failures do
member = group.group_members.last
expect(member.tasks_to_be_done).to match_array([:ci, :code])
expect(member.member_task.project).to eq(project)
end
end
end
describe '#avatar_type' do

View File

@ -9,6 +9,7 @@ RSpec.describe Member do
describe 'Associations' do
it { is_expected.to belong_to(:user) }
it { is_expected.to have_one(:member_task) }
end
describe 'Validation' do
@ -678,6 +679,19 @@ RSpec.describe Member do
expect(member.invite_token).not_to be_nil
expect_any_instance_of(Member).not_to receive(:after_accept_invite)
end
it 'schedules a TasksToBeDone::CreateWorker task' do
stub_experiments(invite_members_for_task: true)
member_task = create(:member_task, member: member, project: member.project)
expect(TasksToBeDone::CreateWorker)
.to receive(:perform_async)
.with(member_task.id, member.created_by_id, [user.id])
.once
member.accept_invite!(user)
end
end
describe '#decline_invite!' do

View File

@ -0,0 +1,124 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe MemberTask do
describe 'Associations' do
it { is_expected.to belong_to(:member) }
it { is_expected.to belong_to(:project) }
end
describe 'Validations' do
it { is_expected.to validate_presence_of(:member) }
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_inclusion_of(:tasks).in_array(MemberTask::TASKS.values) }
describe 'unique tasks validation' do
subject do
build(:member_task, tasks: [0, 0])
end
it 'expects the task values to be unique' do
expect(subject).to be_invalid
expect(subject.errors[:tasks]).to include('are not unique')
end
end
describe 'project validations' do
let_it_be(:project) { create(:project) }
subject do
build(:member_task, member: member, project: project, tasks_to_be_done: [:ci, :code])
end
context 'when the member source is a group' do
let_it_be(:member) { create(:group_member) }
it "expects the project to be part of the member's group projects" do
expect(subject).to be_invalid
expect(subject.errors[:project]).to include('is not in the member group')
end
context "when the project is part of the member's group projects" do
let_it_be(:project) { create(:project, namespace: member.source) }
it { is_expected.to be_valid }
end
end
context 'when the member source is a project' do
let_it_be(:member) { create(:project_member) }
it "expects the project to be the member's project" do
expect(subject).to be_invalid
expect(subject.errors[:project]).to include('is not the member project')
end
context "when the project is the member's project" do
let_it_be(:project) { member.source }
it { is_expected.to be_valid }
end
end
end
end
describe '.for_members' do
it 'returns the member_tasks for multiple members' do
member1 = create(:group_member)
member_task1 = create(:member_task, member: member1)
create(:member_task)
expect(described_class.for_members([member1])).to match_array([member_task1])
end
end
describe '#tasks_to_be_done' do
subject { member_task.tasks_to_be_done }
let_it_be(:member_task) { build(:member_task) }
before do
member_task[:tasks] = [0, 1]
end
it 'returns an array of symbols for the corresponding integers' do
expect(subject).to match_array([:ci, :code])
end
end
describe '#tasks_to_be_done=' do
let_it_be(:member_task) { build(:member_task) }
context 'when passing valid values' do
subject { member_task[:tasks] }
before do
member_task.tasks_to_be_done = tasks
end
context 'when passing tasks as strings' do
let_it_be(:tasks) { %w(ci code) }
it 'sets an array of integers for the corresponding tasks' do
expect(subject).to match_array([0, 1])
end
end
context 'when passing a single task' do
let_it_be(:tasks) { :ci }
it 'sets an array of integers for the corresponding tasks' do
expect(subject).to match_array([1])
end
end
context 'when passing a task twice' do
let_it_be(:tasks) { %w(ci ci) }
it 'is set only once' do
expect(subject).to match_array([1])
end
end
end
end
end

View File

@ -46,11 +46,7 @@ RSpec.describe MergeRequestDiffCommit do
{
"message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
"authored_date": "2014-02-27T10:01:38.000+01:00".to_time,
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T10:01:38.000+01:00".to_time,
"committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author_id": an_instance_of(Integer),
"committer_id": an_instance_of(Integer),
"merge_request_diff_id": merge_request_diff_id,
@ -61,11 +57,7 @@ RSpec.describe MergeRequestDiffCommit do
{
"message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
"authored_date": "2014-02-27T09:57:31.000+01:00".to_time,
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:57:31.000+01:00".to_time,
"committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author_id": an_instance_of(Integer),
"committer_id": an_instance_of(Integer),
"merge_request_diff_id": merge_request_diff_id,
@ -111,11 +103,7 @@ RSpec.describe MergeRequestDiffCommit do
[{
"message": "Weird commit date\n",
"authored_date": timestamp,
"author_name": "Alejandro Rodríguez",
"author_email": "alejorro70@gmail.com",
"committed_date": timestamp,
"committer_name": "Alejandro Rodríguez",
"committer_email": "alejorro70@gmail.com",
"commit_author_id": an_instance_of(Integer),
"committer_id": an_instance_of(Integer),
"merge_request_diff_id": merge_request_diff_id,

View File

@ -234,6 +234,20 @@ RSpec.describe ProjectTeam do
expect(project.team.reporter?(user1)).to be(true)
expect(project.team.reporter?(user2)).to be(true)
end
context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do
before do
stub_experiments(invite_members_for_task: true)
project.team.add_users([user1], :developer, tasks_to_be_done: %w(ci code), tasks_project_id: project.id)
end
it 'creates a member_task with the correct attributes', :aggregate_failures do
member = project.project_members.last
expect(member.tasks_to_be_done).to match_array([:ci, :code])
expect(member.member_task.project).to eq(project)
end
end
end
describe '#add_user' do

View File

@ -166,6 +166,38 @@ RSpec.describe API::Invitations do
end
end
context 'with tasks_to_be_done and tasks_project_id in the params' do
before do
stub_experiments(invite_members_for_task: true)
end
let(:project_id) { source_type == 'project' ? source.id : create(:project, namespace: source).id }
context 'when there is 1 invitation' do
it 'creates a member_task with the tasks_to_be_done and the project' do
post invitations_url(source, maintainer),
params: { email: email, access_level: Member::DEVELOPER, tasks_to_be_done: %w(code ci), tasks_project_id: project_id }
member = source.members.find_by(invite_email: email)
expect(member.tasks_to_be_done).to match_array([:code, :ci])
expect(member.member_task.project_id).to eq(project_id)
end
end
context 'when there are multiple invitations' do
it 'creates a member_task with the tasks_to_be_done and the project' do
post invitations_url(source, maintainer),
params: { email: [email, email2].join(','), access_level: Member::DEVELOPER, tasks_to_be_done: %w(code ci), tasks_project_id: project_id }
members = source.members.where(invite_email: [email, email2])
members.each do |member|
expect(member.tasks_to_be_done).to match_array([:code, :ci])
expect(member.member_task.project_id).to eq(project_id)
end
end
end
end
context 'with invite_source considerations', :snowplow do
let(:params) { { email: email, access_level: Member::DEVELOPER } }

View File

@ -81,14 +81,22 @@ RSpec.describe API::Members do
expect(json_response.map { |u| u['id'] }).to match_array [maintainer.id, developer.id]
end
it 'finds members with query string' do
get api(members_url, developer), params: { query: maintainer.username }
context 'with cross db check disabled' do
around do |example|
allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/343305') do
example.run
end
end
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.count).to eq(1)
expect(json_response.first['username']).to eq(maintainer.username)
it 'finds members with query string' do
get api(members_url, developer), params: { query: maintainer.username }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.count).to eq(1)
expect(json_response.first['username']).to eq(maintainer.username)
end
end
it 'finds members with the given user_ids' do
@ -406,6 +414,38 @@ RSpec.describe API::Members do
end
end
context 'with tasks_to_be_done and tasks_project_id in the params' do
before do
stub_experiments(invite_members_for_task: true)
end
let(:project_id) { source_type == 'project' ? source.id : create(:project, namespace: source).id }
context 'when there is 1 user to add' do
it 'creates a member_task with the correct attributes' do
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
params: { user_id: stranger.id, access_level: Member::DEVELOPER, tasks_to_be_done: %w(code ci), tasks_project_id: project_id }
member = source.members.find_by(user_id: stranger.id)
expect(member.tasks_to_be_done).to match_array([:code, :ci])
expect(member.member_task.project_id).to eq(project_id)
end
end
context 'when there are multiple users to add' do
it 'creates a member_task with the correct attributes' do
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
params: { user_id: [developer.id, stranger.id].join(','), access_level: Member::DEVELOPER, tasks_to_be_done: %w(code ci), tasks_project_id: project_id }
members = source.members.where(user_id: [developer.id, stranger.id])
members.each do |member|
expect(member.tasks_to_be_done).to match_array([:code, :ci])
expect(member.member_task.project_id).to eq(project_id)
end
end
end
end
it "returns 409 if member already exists" do
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
params: { user_id: maintainer.id, access_level: Member::MAINTAINER }

View File

@ -32,6 +32,19 @@ RSpec.describe Deployments::LinkMergeRequestsService do
end
end
context 'when the deployment is for one of the production environments' do
it 'links merge requests' do
environment =
create(:environment, environment_type: 'production', name: 'production/gcp')
deploy = create(:deployment, :success, environment: environment)
expect(deploy).to receive(:link_merge_requests).once
described_class.new(deploy).execute
end
end
context 'when the deployment failed' do
it 'does nothing' do
environment = create(:environment, name: 'foo')

View File

@ -196,4 +196,110 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
end
end
end
context 'when assigning tasks to be done' do
let(:additional_params) do
{ invite_source: '_invite_source_', tasks_to_be_done: %w(ci code), tasks_project_id: source.id }
end
before do
stub_experiments(invite_members_for_task: true)
end
it 'creates 2 task issues', :aggregate_failures do
expect(TasksToBeDone::CreateWorker)
.to receive(:perform_async)
.with(anything, user.id, [member.id])
.once
.and_call_original
expect { execute_service }.to change { source.issues.count }.by(2)
expect(source.issues).to all have_attributes(
project: source,
author: user,
assignees: array_including(member)
)
end
context 'when passing many user ids' do
before do
stub_licensed_features(multiple_issue_assignees: false)
end
let(:another_user) { create(:user) }
let(:user_ids) { [member.id, another_user.id].join(',') }
it 'still creates 2 task issues', :aggregate_failures do
expect(TasksToBeDone::CreateWorker)
.to receive(:perform_async)
.with(anything, user.id, array_including(member.id, another_user.id))
.once
.and_call_original
expect { execute_service }.to change { source.issues.count }.by(2)
expect(source.issues).to all have_attributes(
project: source,
author: user,
assignees: array_including(member)
)
end
end
context 'when a `tasks_project_id` is missing' do
let(:additional_params) do
{ invite_source: '_invite_source_', tasks_to_be_done: %w(ci code) }
end
it 'does not create task issues' do
expect(TasksToBeDone::CreateWorker).not_to receive(:perform_async)
execute_service
end
end
context 'when `tasks_to_be_done` are missing' do
let(:additional_params) do
{ invite_source: '_invite_source_', tasks_project_id: source.id }
end
it 'does not create task issues' do
expect(TasksToBeDone::CreateWorker).not_to receive(:perform_async)
execute_service
end
end
context 'when invalid `tasks_to_be_done` are passed' do
let(:additional_params) do
{ invite_source: '_invite_source_', tasks_project_id: source.id, tasks_to_be_done: %w(invalid_task) }
end
it 'does not create task issues' do
expect(TasksToBeDone::CreateWorker).not_to receive(:perform_async)
execute_service
end
end
context 'when invalid `tasks_project_id` is passed' do
let(:another_project) { create(:project) }
let(:additional_params) do
{ invite_source: '_invite_source_', tasks_project_id: another_project.id, tasks_to_be_done: %w(ci code) }
end
it 'does not create task issues' do
expect(TasksToBeDone::CreateWorker).not_to receive(:perform_async)
execute_service
end
end
context 'when a member was already invited' do
let(:user_ids) { create(:project_member, :invited, project: source).invite_email }
let(:additional_params) do
{ invite_source: '_invite_source_', tasks_project_id: source.id, tasks_to_be_done: %w(ci code) }
end
it 'does not create task issues' do
expect(TasksToBeDone::CreateWorker).not_to receive(:perform_async)
execute_service
end
end
end
end

View File

@ -22,6 +22,11 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_
end
it_behaves_like 'records an onboarding progress action', :user_added
it 'does not create task issues' do
expect(TasksToBeDone::CreateWorker).not_to receive(:perform_async)
expect { result }.not_to change { project.issues.count }
end
end
context 'when email belongs to an existing user as a secondary email' do

View File

@ -0,0 +1,69 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe TasksToBeDone::BaseService do
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
let_it_be(:assignee_one) { create(:user) }
let_it_be(:assignee_two) { create(:user) }
let_it_be(:assignee_ids) { [assignee_one.id] }
let_it_be(:label) { create(:label, title: 'tasks to be done:ci', project: project) }
before do
project.add_maintainer(current_user)
project.add_developer(assignee_one)
project.add_developer(assignee_two)
end
subject(:service) do
TasksToBeDone::CreateCiTaskService.new(
project: project,
current_user: current_user,
assignee_ids: assignee_ids
)
end
context 'no existing task issue', :aggregate_failures do
it 'creates an issue' do
params = {
assignee_ids: assignee_ids,
title: 'Set up CI/CD',
description: anything,
add_labels: label.title
}
expect(Issues::BuildService)
.to receive(:new)
.with(project: project, current_user: current_user, params: params)
.and_call_original
expect { service.execute }.to change(Issue, :count).by(1)
expect(project.issues.last).to have_attributes(
author: current_user,
title: params[:title],
assignees: [assignee_one],
labels: [label]
)
end
end
context 'an open issue with the same label already exists', :aggregate_failures do
let_it_be(:assignee_ids) { [assignee_two.id] }
it 'assigns the user to the existing issue' do
issue = create(:labeled_issue, project: project, labels: [label], assignees: [assignee_one])
params = { add_assignee_ids: assignee_ids }
expect(Issues::UpdateService)
.to receive(:new)
.with(project: project, current_user: current_user, params: params)
.and_call_original
expect { service.execute }.not_to change(Issue, :count)
expect(issue.reload.assignees).to match_array([assignee_one, assignee_two])
end
end
end

View File

@ -286,6 +286,7 @@ licenses: :gitlab_main
lists: :gitlab_main
list_user_preferences: :gitlab_main
loose_foreign_keys_deleted_records: :gitlab_main
member_tasks: :gitlab_main
members: :gitlab_main
merge_request_assignees: :gitlab_main
merge_request_blocks: :gitlab_main

Some files were not shown because too many files have changed in this diff Show More