Commit ffd55e74 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 0d864083 a1082b70
...@@ -29,6 +29,7 @@ export default { ...@@ -29,6 +29,7 @@ export default {
issuableAttributesQueries, issuableAttributesQueries,
i18n: { i18n: {
[IssuableAttributeType.Milestone]: __('Milestone'), [IssuableAttributeType.Milestone]: __('Milestone'),
expired: __('(expired)'),
none: __('None'), none: __('None'),
}, },
directives: { directives: {
...@@ -74,9 +75,14 @@ export default { ...@@ -74,9 +75,14 @@ export default {
type: String, type: String,
required: true, required: true,
validator(value) { validator(value) {
return value === IssuableType.Issue; return [IssuableType.Issue, IssuableType.MergeRequest].includes(value);
}, },
}, },
icon: {
type: String,
required: false,
default: undefined,
},
}, },
apollo: { apollo: {
currentAttribute: { currentAttribute: {
...@@ -172,6 +178,9 @@ export default { ...@@ -172,6 +178,9 @@ export default {
attributeTypeTitle() { attributeTypeTitle() {
return this.$options.i18n[this.issuableAttribute]; return this.$options.i18n[this.issuableAttribute];
}, },
attributeTypeIcon() {
return this.icon || this.issuableAttribute;
},
i18n() { i18n() {
return { return {
noAttribute: sprintf(s__('DropdownWidget|No %{issuableAttribute}'), { noAttribute: sprintf(s__('DropdownWidget|No %{issuableAttribute}'), {
...@@ -224,7 +233,8 @@ export default { ...@@ -224,7 +233,8 @@ export default {
variables: { variables: {
fullPath: this.workspacePath, fullPath: this.workspacePath,
attributeId: attributeId:
this.issuableAttribute === IssuableAttributeType.Milestone this.issuableAttribute === IssuableAttributeType.Milestone &&
this.issuableType === IssuableType.Issue
? getIdFromGraphQLId(attributeId) ? getIdFromGraphQLId(attributeId)
: attributeId, : attributeId,
iid: this.iid, iid: this.iid,
...@@ -255,6 +265,11 @@ export default { ...@@ -255,6 +265,11 @@ export default {
attributeId === this.currentAttribute?.id || (!this.currentAttribute?.id && !attributeId) attributeId === this.currentAttribute?.id || (!this.currentAttribute?.id && !attributeId)
); );
}, },
isAttributeOverdue(attribute) {
return this.issuableAttribute === IssuableAttributeType.Milestone
? attribute?.expired
: false;
},
showDropdown() { showDropdown() {
this.$refs.newDropdown.show(); this.$refs.newDropdown.show();
}, },
...@@ -284,8 +299,10 @@ export default { ...@@ -284,8 +299,10 @@ export default {
> >
<template #collapsed> <template #collapsed>
<div v-if="isClassicSidebar" v-gl-tooltip class="sidebar-collapsed-icon"> <div v-if="isClassicSidebar" v-gl-tooltip class="sidebar-collapsed-icon">
<gl-icon :size="16" :aria-label="attributeTypeTitle" :name="issuableAttribute" /> <gl-icon :size="16" :aria-label="attributeTypeTitle" :name="attributeTypeIcon" />
<span class="collapse-truncated-title">{{ attributeTitle }}</span> <span class="collapse-truncated-title">
{{ attributeTitle }}
</span>
</div> </div>
<div <div
:data-testid="`select-${issuableAttribute}`" :data-testid="`select-${issuableAttribute}`"
...@@ -308,6 +325,7 @@ export default { ...@@ -308,6 +325,7 @@ export default {
:data-qa-selector="`${issuableAttribute}_link`" :data-qa-selector="`${issuableAttribute}_link`"
> >
{{ attributeTitle }} {{ attributeTitle }}
<span v-if="isAttributeOverdue(currentAttribute)">{{ $options.i18n.expired }}</span>
</gl-link> </gl-link>
</slot> </slot>
</div> </div>
...@@ -358,6 +376,7 @@ export default { ...@@ -358,6 +376,7 @@ export default {
@click="updateAttribute(attrItem.id)" @click="updateAttribute(attrItem.id)"
> >
{{ attrItem.title }} {{ attrItem.title }}
<span v-if="isAttributeOverdue(attrItem)">{{ $options.i18n.expired }}</span>
</gl-dropdown-item> </gl-dropdown-item>
</slot> </slot>
</template> </template>
......
...@@ -12,6 +12,7 @@ import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql'; ...@@ -12,6 +12,7 @@ import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql'; import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql'; import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql';
import issueTimeTrackingQuery from '~/sidebar/queries/issue_time_tracking.query.graphql'; import issueTimeTrackingQuery from '~/sidebar/queries/issue_time_tracking.query.graphql';
import mergeRequestMilestone from '~/sidebar/queries/merge_request_milestone.query.graphql';
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql'; import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
import mergeRequestSubscribed from '~/sidebar/queries/merge_request_subscribed.query.graphql'; import mergeRequestSubscribed from '~/sidebar/queries/merge_request_subscribed.query.graphql';
import mergeRequestTimeTrackingQuery from '~/sidebar/queries/merge_request_time_tracking.query.graphql'; import mergeRequestTimeTrackingQuery from '~/sidebar/queries/merge_request_time_tracking.query.graphql';
...@@ -24,6 +25,7 @@ import updateEpicSubscriptionMutation from '~/sidebar/queries/update_epic_subscr ...@@ -24,6 +25,7 @@ import updateEpicSubscriptionMutation from '~/sidebar/queries/update_epic_subscr
import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_confidential.mutation.graphql'; import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_confidential.mutation.graphql';
import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql'; import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql';
import updateIssueSubscriptionMutation from '~/sidebar/queries/update_issue_subscription.mutation.graphql'; import updateIssueSubscriptionMutation from '~/sidebar/queries/update_issue_subscription.mutation.graphql';
import mergeRequestMilestoneMutation from '~/sidebar/queries/update_merge_request_milestone.mutation.graphql';
import updateMergeRequestSubscriptionMutation from '~/sidebar/queries/update_merge_request_subscription.mutation.graphql'; import updateMergeRequestSubscriptionMutation from '~/sidebar/queries/update_merge_request_subscription.mutation.graphql';
import updateAlertAssigneesMutation from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql'; import updateAlertAssigneesMutation from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
import getAlertAssignees from '~/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql'; import getAlertAssignees from '~/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql';
...@@ -171,12 +173,19 @@ export const issuableMilestoneQueries = { ...@@ -171,12 +173,19 @@ export const issuableMilestoneQueries = {
query: projectIssueMilestoneQuery, query: projectIssueMilestoneQuery,
mutation: projectIssueMilestoneMutation, mutation: projectIssueMilestoneMutation,
}, },
[IssuableType.MergeRequest]: {
query: mergeRequestMilestone,
mutation: mergeRequestMilestoneMutation,
},
}; };
export const milestonesQueries = { export const milestonesQueries = {
[IssuableType.Issue]: { [IssuableType.Issue]: {
query: projectMilestonesQuery, query: projectMilestonesQuery,
}, },
[IssuableType.MergeRequest]: {
query: projectMilestonesQuery,
},
}; };
export const IssuableAttributeType = { export const IssuableAttributeType = {
......
...@@ -18,6 +18,7 @@ import SidebarConfidentialityWidget from '~/sidebar/components/confidential/side ...@@ -18,6 +18,7 @@ import SidebarConfidentialityWidget from '~/sidebar/components/confidential/side
import SidebarDueDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue'; import SidebarDueDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue'; import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue';
import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue'; import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue';
import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue';
import { apolloProvider } from '~/sidebar/graphql'; import { apolloProvider } from '~/sidebar/graphql';
import trackShowInviteMemberLink from '~/sidebar/track_invite_members'; import trackShowInviteMemberLink from '~/sidebar/track_invite_members';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
...@@ -29,6 +30,7 @@ import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue'; ...@@ -29,6 +30,7 @@ import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue';
import SidebarSeverity from './components/severity/sidebar_severity.vue'; import SidebarSeverity from './components/severity/sidebar_severity.vue';
import SidebarSubscriptionsWidget from './components/subscriptions/sidebar_subscriptions_widget.vue'; import SidebarSubscriptionsWidget from './components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue'; import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import { IssuableAttributeType } from './constants';
import SidebarMoveIssue from './lib/sidebar_move_issue'; import SidebarMoveIssue from './lib/sidebar_move_issue';
Vue.use(Translate); Vue.use(Translate);
...@@ -154,7 +156,8 @@ function mountReviewersComponent(mediator) { ...@@ -154,7 +156,8 @@ function mountReviewersComponent(mediator) {
issuableIid: String(iid), issuableIid: String(iid),
projectPath: fullPath, projectPath: fullPath,
field: el.dataset.field, field: el.dataset.field,
issuableType: isInIssuePage() || isInDesignPage() ? 'issue' : 'merge_request', issuableType:
isInIssuePage() || isInDesignPage() ? IssuableType.Issue : IssuableType.MergeRequest,
}, },
}), }),
}); });
...@@ -166,6 +169,40 @@ function mountReviewersComponent(mediator) { ...@@ -166,6 +169,40 @@ function mountReviewersComponent(mediator) {
} }
} }
function mountMilestoneSelect() {
const el = document.querySelector('.js-milestone-select');
if (!el) {
return false;
}
const { canEdit, projectPath, issueIid } = el.dataset;
return new Vue({
el,
apolloProvider,
components: {
SidebarDropdownWidget,
},
provide: {
canUpdate: parseBoolean(canEdit),
isClassicSidebar: true,
},
render: (createElement) =>
createElement('sidebar-dropdown-widget', {
props: {
attrWorkspacePath: projectPath,
workspacePath: projectPath,
iid: issueIid,
issuableType:
isInIssuePage() || isInDesignPage() ? IssuableType.Issue : IssuableType.MergeRequest,
issuableAttribute: IssuableAttributeType.Milestone,
icon: 'clock',
},
}),
});
}
export function mountSidebarLabels() { export function mountSidebarLabels() {
const el = document.querySelector('.js-sidebar-labels'); const el = document.querySelector('.js-sidebar-labels');
...@@ -466,6 +503,7 @@ export function mountSidebar(mediator) { ...@@ -466,6 +503,7 @@ export function mountSidebar(mediator) {
mountAssigneesComponentDeprecated(mediator); mountAssigneesComponentDeprecated(mediator);
} }
mountReviewersComponent(mediator); mountReviewersComponent(mediator);
mountMilestoneSelect();
mountConfidentialComponent(mediator); mountConfidentialComponent(mediator);
mountDueDateComponent(mediator); mountDueDateComponent(mediator);
mountReferenceComponent(mediator); mountReferenceComponent(mediator);
......
#import "./milestone.fragment.graphql"
query mergeRequestMilestone($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
__typename
issuable: mergeRequest(iid: $iid) {
__typename
id
attribute: milestone {
...MilestoneFragment
}
}
}
}
...@@ -2,4 +2,5 @@ fragment MilestoneFragment on Milestone { ...@@ -2,4 +2,5 @@ fragment MilestoneFragment on Milestone {
id id
title title
webUrl: webPath webUrl: webPath
expired
} }
...@@ -11,6 +11,7 @@ mutation projectIssueMilestoneMutation($fullPath: ID!, $iid: String!, $attribute ...@@ -11,6 +11,7 @@ mutation projectIssueMilestoneMutation($fullPath: ID!, $iid: String!, $attribute
title title
id id
state state
expired
} }
} }
} }
......
...@@ -3,7 +3,13 @@ ...@@ -3,7 +3,13 @@
query projectMilestones($fullPath: ID!, $title: String, $state: MilestoneStateEnum) { query projectMilestones($fullPath: ID!, $title: String, $state: MilestoneStateEnum) {
workspace: project(fullPath: $fullPath) { workspace: project(fullPath: $fullPath) {
__typename __typename
attributes: milestones(searchTitle: $title, state: $state) { attributes: milestones(
searchTitle: $title
state: $state
sort: EXPIRED_LAST_DUE_DATE_ASC
first: 20
includeAncestors: true
) {
nodes { nodes {
...MilestoneFragment ...MilestoneFragment
state state
......
mutation mergeRequestSetMilestone($fullPath: ID!, $iid: String!, $attributeId: ID) {
issuableSetAttribute: mergeRequestSetMilestone(
input: { projectPath: $fullPath, iid: $iid, milestoneId: $attributeId }
) {
__typename
errors
issuable: mergeRequest {
__typename
id
attribute: milestone {
title
id
state
}
}
}
}
...@@ -360,7 +360,7 @@ class MergeRequest < ApplicationRecord ...@@ -360,7 +360,7 @@ class MergeRequest < ApplicationRecord
scope :preload_approved_by_users, -> { preload(:approved_by_users) } scope :preload_approved_by_users, -> { preload(:approved_by_users) }
scope :preload_metrics, -> (relation) { preload(metrics: relation) } scope :preload_metrics, -> (relation) { preload(metrics: relation) }
scope :preload_project_and_latest_diff, -> { preload(:source_project, :latest_merge_request_diff) } scope :preload_project_and_latest_diff, -> { preload(:source_project, :latest_merge_request_diff) }
scope :preload_latest_diff_commit, -> { preload(latest_merge_request_diff: :merge_request_diff_commits) } scope :preload_latest_diff_commit, -> { preload(latest_merge_request_diff: { merge_request_diff_commits: [:commit_author, :committer] }) }
scope :preload_milestoneish_associations, -> { preload_routables.preload(:assignees, :labels) } scope :preload_milestoneish_associations, -> { preload_routables.preload(:assignees, :labels) }
scope :with_web_entity_associations, -> { preload(:author, target_project: [:project_feature, group: [:route, :parent], namespace: :route]) } scope :with_web_entity_associations, -> { preload(:author, target_project: [:project_feature, group: [:route, :parent], namespace: :route]) }
......
# frozen_string_literal: true
class MergeRequest::DiffCommitUser < ApplicationRecord
validates :name, length: { maximum: 512 }
validates :email, length: { maximum: 512 }
validates :name, presence: true, unless: :email
validates :email, presence: true, unless: :name
# Prepares a value to be inserted into a column in the table
# `merge_request_diff_commit_users`. Values in this table are limited to
# 512 characters.
#
# We treat empty strings as NULL values, as there's no point in (for
# example) storing a row where both the name and Email are an empty
# string. In addition, if we treated them differently we could end up with
# two rows: one where field X is NULL, and one where field X is an empty
# string. This is redundant, so we avoid storing such data.
def self.prepare(value)
value.present? ? value[0..511] : nil
end
# Creates a new row, or returns an existing one if a row already exists.
def self.find_or_create(name, email)
find_or_create_by!(name: name, email: email)
rescue ActiveRecord::RecordNotUnique
retry
end
# Finds many (name, email) pairs in bulk.
def self.bulk_find(pairs)
queries = {}
rows = []
pairs.each do |(name, email)|
queries[[name, email]] = where(name: name, email: email).to_sql
end
# We may end up having to query many users. To ensure we don't hit any
# query size limits, we get a fixed number of users at a time.
queries.values.each_slice(1_000).map do |slice|
rows.concat(from("(#{slice.join("\nUNION ALL\n")}) #{table_name}").to_a)
end
rows
end
# Finds or creates rows for the given pairs of names and Emails.
#
# The `names_and_emails` argument must be an Array/Set of tuples like so:
#
# [
# [name, email],
# [name, email],
# ...
# ]
#
# This method expects that the names and Emails have already been trimmed to
# at most 512 characters.
#
# The return value is a Hash that maps these tuples to instances of this
# model.
def self.bulk_find_or_create(pairs)
mapping = {}
create = []
# Over time, fewer new rows need to be created. We take advantage of that
# here by first finding all rows that already exist, using a limited number
# of queries (in most cases only one query will be needed).
bulk_find(pairs).each do |row|
mapping[[row.name, row.email]] = row
end
pairs.each do |(name, email)|
create << { name: name, email: email } unless mapping[[name, email]]
end
return mapping if create.empty?
# Sometimes we may need to insert new users into the table. We do this in
# bulk, so we only need one INSERT for all missing users.
insert_all(create, returning: %w[id name email]).each do |row|
mapping[[row['name'], row['email']]] =
new(id: row['id'], name: row['name'], email: row['email'])
end
# It's possible for (name, email) pairs to be inserted concurrently,
# resulting in the above insert not returning anything. Here we get any
# remaining users that were created concurrently.
bulk_find(pairs.reject { |pair| mapping.key?(pair) }).each do |row|
mapping[[row.name, row.email]] = row
end
mapping
end
end
...@@ -701,7 +701,7 @@ class MergeRequestDiff < ApplicationRecord ...@@ -701,7 +701,7 @@ class MergeRequestDiff < ApplicationRecord
end end
def load_commits(limit: nil) def load_commits(limit: nil)
commits = merge_request_diff_commits.limit(limit) commits = merge_request_diff_commits.with_users.limit(limit)
.map { |commit| Commit.from_hash(commit.to_hash, project) } .map { |commit| Commit.from_hash(commit.to_hash, project) }
CommitCollection CommitCollection
......
...@@ -9,21 +9,51 @@ class MergeRequestDiffCommit < ApplicationRecord ...@@ -9,21 +9,51 @@ class MergeRequestDiffCommit < ApplicationRecord
belongs_to :merge_request_diff belongs_to :merge_request_diff
# This relation is called `commit_author` and not `author`, as the project
# import/export logic treats relations named `author` as instances of the
# `User` class.
#
# NOTE: these columns are _not_ indexed, nor do they use foreign keys.
#
# This is deliberate, as creating these indexes on GitLab.com takes a _very_
# long time. In addition, there's no real need for them either based on how
# this data is used.
#
# For more information, refer to the following:
#
# - https://gitlab.com/gitlab-com/gl-infra/production/-/issues/5038#note_614592881
# - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63669
belongs_to :commit_author, class_name: 'MergeRequest::DiffCommitUser'
belongs_to :committer, class_name: 'MergeRequest::DiffCommitUser'
sha_attribute :sha sha_attribute :sha
alias_attribute :id, :sha alias_attribute :id, :sha
serialize :trailers, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize serialize :trailers, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize
validates :trailers, json_schema: { filename: 'git_trailers' } validates :trailers, json_schema: { filename: 'git_trailers' }
scope :with_users, -> { preload(:commit_author, :committer) }
# A list of keys of which their values need to be trimmed before they can be
# inserted into the merge_request_diff_commit_users table.
TRIM_USER_KEYS =
%i[author_name author_email committer_name committer_email].freeze
# Deprecated; use `bulk_insert!` from `BulkInsertSafe` mixin instead. # Deprecated; use `bulk_insert!` from `BulkInsertSafe` mixin instead.
# cf. https://gitlab.com/gitlab-org/gitlab/issues/207989 for progress # cf. https://gitlab.com/gitlab-org/gitlab/issues/207989 for progress
def self.create_bulk(merge_request_diff_id, commits) def self.create_bulk(merge_request_diff_id, commits)
rows = commits.map.with_index do |commit, index| commit_hashes, user_tuples = prepare_commits_for_bulk_insert(commits)
# See #parent_ids. users = MergeRequest::DiffCommitUser.bulk_find_or_create(user_tuples)
commit_hash = commit.to_hash.except(:parent_ids)
rows = commit_hashes.map.with_index do |commit_hash, index|
sha = commit_hash.delete(:id) sha = commit_hash.delete(:id)
author = users[[commit_hash[:author_name], commit_hash[:author_email]]]
committer =
users[[commit_hash[:committer_name], commit_hash[:committer_email]]]
commit_hash.merge( commit_hash.merge(
commit_author_id: author&.id,
committer_id: committer&.id,
merge_request_diff_id: merge_request_diff_id, merge_request_diff_id: merge_request_diff_id,
relative_order: index, relative_order: index,
sha: Gitlab::Database::ShaAttribute.serialize(sha), # rubocop:disable Cop/ActiveRecordSerialize sha: Gitlab::Database::ShaAttribute.serialize(sha), # rubocop:disable Cop/ActiveRecordSerialize
...@@ -36,6 +66,24 @@ class MergeRequestDiffCommit < ApplicationRecord ...@@ -36,6 +66,24 @@ class MergeRequestDiffCommit < ApplicationRecord
Gitlab::Database.bulk_insert(self.table_name, rows) # rubocop:disable Gitlab/BulkInsert Gitlab::Database.bulk_insert(self.table_name, rows) # rubocop:disable Gitlab/BulkInsert
end end
def self.prepare_commits_for_bulk_insert(commits)
user_tuples = Set.new
hashes = commits.map do |commit|
hash = commit.to_hash.except(:parent_ids)
TRIM_USER_KEYS.each do |key|
hash[key] = MergeRequest::DiffCommitUser.prepare(hash[key])
end
user_tuples << [hash[:author_name], hash[:author_email]]
user_tuples << [hash[:committer_name], hash[:committer_email]]
hash
end
[hashes, user_tuples]
end
def self.oldest_merge_request_id_per_commit(project_id, shas) def self.oldest_merge_request_id_per_commit(project_id, shas)
# This method is defined here and not on MergeRequest, otherwise the SHA # This method is defined here and not on MergeRequest, otherwise the SHA
# values used in the WHERE below won't be encoded correctly. # values used in the WHERE below won't be encoded correctly.
...@@ -54,4 +102,20 @@ class MergeRequestDiffCommit < ApplicationRecord ...@@ -54,4 +102,20 @@ class MergeRequestDiffCommit < ApplicationRecord
) )
.group(:sha) .group(:sha)
end end
def author_name
commit_author_id ? commit_author.name : super
end
def author_email
commit_author_id ? commit_author.email : super
end
def committer_name
committer_id ? committer.name : super
end
def committer_email
committer_id ? committer.email : super
end
end end
...@@ -34,31 +34,8 @@ ...@@ -34,31 +34,8 @@
= render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type = render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type
- if issuable_sidebar[:supports_milestone] - if issuable_sidebar[:supports_milestone]
- milestone = issuable_sidebar[:milestone] || {}
.block.milestone{ :class => ("gl-border-b-0!" if issuable_sidebar[:supports_iterations]), data: { qa_selector: 'milestone_block' } } .block.milestone{ :class => ("gl-border-b-0!" if issuable_sidebar[:supports_iterations]), data: { qa_selector: 'milestone_block' } }
.sidebar-collapsed-icon.has-tooltip{ title: sidebar_milestone_tooltip_label(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } .js-milestone-select{ data: { can_edit: can_edit_issuable.to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid] } }
= sprite_icon('clock')
%span.milestone-title.collapse-truncated-title
- if milestone.present?
= milestone[:title]
- else
= _('None')
.hide-collapsed.gl-line-height-20.gl-mb-2.gl-text-gray-900{ data: { testid: "milestone_title" } }
= _('Milestone')
= loading_icon(css_class: 'gl-vertical-align-text-bottom hidden block-loading')
- if can_edit_issuable
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { qa_selector: "edit_milestone_link", track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" }
.value.hide-collapsed
- if milestone.present?
- milestone_title = milestone[:expired] ? _("%{milestone_name} (Past due)").html_safe % { milestone_name: milestone[:title] } : milestone[:title]
= link_to milestone_title, milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link', qa_title: milestone[:title] }
- else
%span.no-value
= _('None')
.selectbox.hide-collapsed
= f.hidden_field 'milestone_id', value: milestone[:id], id: nil
= dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable_type}[milestone_id]", project_id: issuable_sidebar[:project_id], issuable_id: issuable_sidebar[:id], ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], use_id: true, default_no: true, selected: milestone[:title], null_default: true, display: 'static' }})
- if @project.group.present? && issuable_sidebar[:supports_iterations] - if @project.group.present? && issuable_sidebar[:supports_iterations]
.block{ class: 'gl-pt-0!', data: { qa_selector: 'iteration_container' } } .block{ class: 'gl-pt-0!', data: { qa_selector: 'iteration_container' } }
......
# frozen_string_literal: true
class AddMergeRequestDiffCommitUsers < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
create_table_with_constraints :merge_request_diff_commit_users, id: :bigint do |t|
t.text :name
t.text :email
t.text_limit :name, 512
t.text_limit :email, 512
t.index [:name, :email], unique: true
end
# Names or Emails can be optional, so in some cases one of these may be
# null. But if both are NULL/empty, no row should exist in this table.
add_check_constraint(
:merge_request_diff_commit_users,
"(COALESCE(name, '') != '') OR (COALESCE(email, '') != '')",
:merge_request_diff_commit_users_name_or_email_existence
)
end
def down
drop_table :merge_request_diff_commit_users
end
end
# frozen_string_literal: true
class AddMergeRequestDiffCommitUserColumns < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
def up
# NOTE: these columns are _not_ indexed, nor do they use foreign keys.
#
# This is deliberate, as creating these indexes on GitLab.com takes a _very_
# long time. In addition, there's no real need for them either based on how
# this data is used.
#
# For more information, refer to the following:
#
# - https://gitlab.com/gitlab-com/gl-infra/production/-/issues/5038#note_614592881
# - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63669
add_column(:merge_request_diff_commits, :commit_author_id, :bigint)
add_column(:merge_request_diff_commits, :committer_id, :bigint)
end
def down
remove_column(:merge_request_diff_commits, :commit_author_id)
remove_column(:merge_request_diff_commits, :committer_id)
end
end
# frozen_string_literal: true
class ScheduleMergeRequestDiffUsersBackgroundMigration < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
# The number of rows to process in a single migration job.
#
# The minimum interval for background migrations is two minutes. On staging we
# observed we can process roughly 20 000 rows in a minute. Based on the total
# number of rows on staging, this translates to a total processing time of
# roughly 14 days.
#
# By using a batch size of 40 000, we maintain a rate of roughly 20 000 rows
# per minute, hopefully keeping the total migration time under two weeks;
# instead of four weeks.
BATCH_SIZE = 40_000
MIGRATION_NAME = 'MigrateMergeRequestDiffCommitUsers'
class MergeRequestDiff < ActiveRecord::Base
self.table_name = 'merge_request_diffs'
end
def up
start = MergeRequestDiff.minimum(:id).to_i
max = MergeRequestDiff.maximum(:id).to_i
delay = BackgroundMigrationWorker.minimum_interval
# The table merge_request_diff_commits contains _a lot_ of rows (roughly 400
# 000 000 on staging). Iterating a table that large to determine job ranges
# would take a while.
#
# To avoid that overhead, we simply schedule fixed ranges according to the
# minimum and maximum IDs. The background migration in turn only processes
# rows that actually exist.
while start < max
stop = start + BATCH_SIZE
migrate_in(delay, MIGRATION_NAME, [start, stop])
Gitlab::Database::BackgroundMigrationJob
.create!(class_name: MIGRATION_NAME, arguments: [start, stop])
delay += BackgroundMigrationWorker.minimum_interval
start += BATCH_SIZE
end
end
def down
# no-op
end
end
42b3090efee66f5a7a5c06d8768d1417892c5d6745f60163a09f58e6e3722761
\ No newline at end of file
aa04d433e400ed3ec11e5d40ada72f122b1d8b7a82f8803d9206da5c94ec5ef9
\ No newline at end of file
0c01bb41113c468a602649b591e1fd2959a6e3190c835ef2e27351cf69f50fd5
\ No newline at end of file
...@@ -14760,6 +14760,24 @@ CREATE SEQUENCE merge_request_context_commits_id_seq ...@@ -14760,6 +14760,24 @@ CREATE SEQUENCE merge_request_context_commits_id_seq
ALTER SEQUENCE merge_request_context_commits_id_seq OWNED BY merge_request_context_commits.id; ALTER SEQUENCE merge_request_context_commits_id_seq OWNED BY merge_request_context_commits.id;
CREATE TABLE merge_request_diff_commit_users (
id bigint NOT NULL,
name text,
email text,
CONSTRAINT check_147358fc42 CHECK ((char_length(name) <= 512)),
CONSTRAINT check_f5fa206cf7 CHECK ((char_length(email) <= 512)),
CONSTRAINT merge_request_diff_commit_users_name_or_email_existence CHECK (((COALESCE(name, ''::text) <> ''::text) OR (COALESCE(email, ''::text) <> ''::text)))
);
CREATE SEQUENCE merge_request_diff_commit_users_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE merge_request_diff_commit_users_id_seq OWNED BY merge_request_diff_commit_users.id;
CREATE TABLE merge_request_diff_commits ( CREATE TABLE merge_request_diff_commits (
authored_date timestamp without time zone, authored_date timestamp without time zone,
committed_date timestamp without time zone, committed_date timestamp without time zone,
...@@ -14771,7 +14789,9 @@ CREATE TABLE merge_request_diff_commits ( ...@@ -14771,7 +14789,9 @@ CREATE TABLE merge_request_diff_commits (
committer_name text, committer_name text,
committer_email text, committer_email text,
message text, message text,
trailers jsonb DEFAULT '{}'::jsonb NOT NULL trailers jsonb DEFAULT '{}'::jsonb NOT NULL,
commit_author_id bigint,
committer_id bigint
); );
CREATE TABLE merge_request_diff_details ( CREATE TABLE merge_request_diff_details (
...@@ -20161,6 +20181,8 @@ ALTER TABLE ONLY merge_request_cleanup_schedules ALTER COLUMN merge_request_id S ...@@ -20161,6 +20181,8 @@ ALTER TABLE ONLY merge_request_cleanup_schedules ALTER COLUMN merge_request_id S
ALTER TABLE ONLY merge_request_context_commits ALTER COLUMN id SET DEFAULT nextval('merge_request_context_commits_id_seq'::regclass); ALTER TABLE ONLY merge_request_context_commits ALTER COLUMN id SET DEFAULT nextval('merge_request_context_commits_id_seq'::regclass);
ALTER TABLE ONLY merge_request_diff_commit_users ALTER COLUMN id SET DEFAULT nextval('merge_request_diff_commit_users_id_seq'::regclass);
ALTER TABLE ONLY merge_request_diff_details ALTER COLUMN merge_request_diff_id SET DEFAULT nextval('merge_request_diff_details_merge_request_diff_id_seq'::regclass); ALTER TABLE ONLY merge_request_diff_details ALTER COLUMN merge_request_diff_id SET DEFAULT nextval('merge_request_diff_details_merge_request_diff_id_seq'::regclass);
ALTER TABLE ONLY merge_request_diffs ALTER COLUMN id SET DEFAULT nextval('merge_request_diffs_id_seq'::regclass); ALTER TABLE ONLY merge_request_diffs ALTER COLUMN id SET DEFAULT nextval('merge_request_diffs_id_seq'::regclass);
...@@ -21601,6 +21623,9 @@ ALTER TABLE ONLY merge_request_context_commit_diff_files ...@@ -21601,6 +21623,9 @@ ALTER TABLE ONLY merge_request_context_commit_diff_files
ALTER TABLE ONLY merge_request_context_commits ALTER TABLE ONLY merge_request_context_commits
ADD CONSTRAINT merge_request_context_commits_pkey PRIMARY KEY (id); ADD CONSTRAINT merge_request_context_commits_pkey PRIMARY KEY (id);
ALTER TABLE ONLY merge_request_diff_commit_users
ADD CONSTRAINT merge_request_diff_commit_users_pkey PRIMARY KEY (id);
ALTER TABLE ONLY merge_request_diff_commits ALTER TABLE ONLY merge_request_diff_commits
ADD CONSTRAINT merge_request_diff_commits_pkey PRIMARY KEY (merge_request_diff_id, relative_order); ADD CONSTRAINT merge_request_diff_commits_pkey PRIMARY KEY (merge_request_diff_id, relative_order);
...@@ -23904,6 +23929,8 @@ CREATE INDEX index_merge_request_blocks_on_blocked_merge_request_id ON merge_req ...@@ -23904,6 +23929,8 @@ CREATE INDEX index_merge_request_blocks_on_blocked_merge_request_id ON merge_req
CREATE UNIQUE INDEX index_merge_request_cleanup_schedules_on_merge_request_id ON merge_request_cleanup_schedules USING btree (merge_request_id); CREATE UNIQUE INDEX index_merge_request_cleanup_schedules_on_merge_request_id ON merge_request_cleanup_schedules USING btree (merge_request_id);
CREATE UNIQUE INDEX index_merge_request_diff_commit_users_on_name_and_email ON merge_request_diff_commit_users USING btree (name, email);
CREATE INDEX index_merge_request_diff_commits_on_sha ON merge_request_diff_commits USING btree (sha); CREATE INDEX index_merge_request_diff_commits_on_sha ON merge_request_diff_commits USING btree (sha);
CREATE INDEX index_merge_request_diff_details_failed_verification ON merge_request_diff_details USING btree (verification_retry_at NULLS FIRST) WHERE (verification_state = 3); CREATE INDEX index_merge_request_diff_details_failed_verification ON merge_request_diff_details USING btree (verification_retry_at NULLS FIRST) WHERE (verification_state = 3);
...@@ -116,6 +116,9 @@ export const buildNullSeriesForLeadTimeChart = (seriesData) => { ...@@ -116,6 +116,9 @@ export const buildNullSeriesForLeadTimeChart = (seriesData) => {
}); });
merge(nonNullSeries, { merge(nonNullSeries, {
showSymbol: true,
showAllSymbol: true,
symbolSize: 8,
lineStyle: { lineStyle: {
color: dataVizBlue500, color: dataVizBlue500,
}, },
......
...@@ -86,6 +86,9 @@ Array [ ...@@ -86,6 +86,9 @@ Array [
"color": "#5772ff", "color": "#5772ff",
}, },
"name": "Lead time", "name": "Lead time",
"showAllSymbol": true,
"showSymbol": true,
"symbolSize": 8,
}, },
] ]
`; `;
...@@ -66,6 +66,9 @@ describe('ee/dora/components/util.js', () => { ...@@ -66,6 +66,9 @@ describe('ee/dora/components/util.js', () => {
}, },
{ {
name: 'Chart title', name: 'Chart title',
showAllSymbol: true,
showSymbol: true,
symbolSize: 8,
data: expect.any(Array), data: expect.any(Array),
lineStyle: { lineStyle: {
color: expect.any(String), color: expect.any(String),
......
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# Migrates author and committer names and emails from
# merge_request_diff_commits to two columns that point to
# merge_request_diff_commit_users.
#
# rubocop: disable Metrics/ClassLength
class MigrateMergeRequestDiffCommitUsers
# The number of user rows in merge_request_diff_commit_users to get in a
# single query.
USER_ROWS_PER_QUERY = 1_000
# The number of rows in merge_request_diff_commits to get in a single
# query.
COMMIT_ROWS_PER_QUERY = 10_000
# The number of rows in merge_request_diff_commits to update in a single
# query.
#
# Tests in staging revealed that increasing the number of updates per
# query translates to a longer total runtime for a migration. For example,
# given the same range of rows to migrate, 1000 updates per query required
# a total of roughly 15 seconds. On the other hand, 5000 updates per query
# required a total of roughly 25 seconds. For this reason, we use a value
# of 1000 rows per update.
UPDATES_PER_QUERY = 1_000
# rubocop: disable Style/Documentation
class MergeRequestDiffCommit < ActiveRecord::Base
include FromUnion
extend ::SuppressCompositePrimaryKeyWarning
self.table_name = 'merge_request_diff_commits'
# Yields each row to migrate in the given range.
#
# This method uses keyset pagination to ensure we don't retrieve
# potentially tens of thousands (or even hundreds of thousands) of rows
# in a single query. Such queries could time out, or increase the amount
# of memory needed to process the data.
#
# We can't use `EachBatch` and similar approaches, as
# merge_request_diff_commits doesn't have a single monotonically
# increasing primary key.
def self.each_row_to_migrate(start_id, stop_id, &block)
order = Pagination::Keyset::Order.build(
%w[merge_request_diff_id relative_order].map do |col|
Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: col,
order_expression: self.arel_table[col.to_sym].asc,
nullable: :not_nullable,
distinct: false
)
end
)
scope = MergeRequestDiffCommit
.where(merge_request_diff_id: start_id...stop_id)
.order(order)
Pagination::Keyset::Iterator
.new(scope: scope, use_union_optimization: true)
.each_batch(of: COMMIT_ROWS_PER_QUERY) { |rows| rows.each(&block) }
end
end
# rubocop: enable Style/Documentation
# rubocop: disable Style/Documentation
class MergeRequestDiffCommitUser < ActiveRecord::Base
self.table_name = 'merge_request_diff_commit_users'
def self.union(queries)
from("(#{queries.join("\nUNION ALL\n")}) #{table_name}")
end
end
# rubocop: enable Style/Documentation
def perform(start_id, stop_id)
# This Hash maps user names + emails to their corresponding rows in
# merge_request_diff_commit_users.
user_mapping = {}
user_details, diff_rows_to_update = get_data_to_update(start_id, stop_id)
get_user_rows_in_batches(user_details, user_mapping)
create_missing_users(user_details, user_mapping)
update_commit_rows(diff_rows_to_update, user_mapping)
Database::BackgroundMigrationJob.mark_all_as_succeeded(
'MigrateMergeRequestDiffCommitUsers',
[start_id, stop_id]
)
end
# Returns the data we'll use to determine what merge_request_diff_commits
# rows to update, and what data to use for populating their
# commit_author_id and committer_id columns.
def get_data_to_update(start_id, stop_id)
# This Set is used to retrieve users that already exist in
# merge_request_diff_commit_users.
users = Set.new
# This Hash maps the primary key of every row in
# merge_request_diff_commits to the (trimmed) author and committer
# details to use for updating the row.
to_update = {}
MergeRequestDiffCommit.each_row_to_migrate(start_id, stop_id) do |row|
author = [prepare(row.author_name), prepare(row.author_email)]
committer = [prepare(row.committer_name), prepare(row.committer_email)]
to_update[[row.merge_request_diff_id, row.relative_order]] =
[author, committer]
users << author if author[0] || author[1]
users << committer if committer[0] || committer[1]
end
[users, to_update]
end
# Gets any existing rows in merge_request_diff_commit_users in batches.
#
# This method may end up having to retrieve lots of rows. To reduce the
# overhead, we batch queries into a UNION query. We limit the number of
# queries per UNION so we don't end up sending a single query containing
# too many SELECT statements.
def get_user_rows_in_batches(users, user_mapping)
users.each_slice(USER_ROWS_PER_QUERY) do |pairs|
queries = pairs.map do |(name, email)|
MergeRequestDiffCommitUser.where(name: name, email: email).to_sql
end
MergeRequestDiffCommitUser.union(queries).each do |row|
user_mapping[[row.name.to_s, row.email.to_s]] = row
end
end
end
# Creates any users for which no row exists in
# merge_request_diff_commit_users.
#
# Not all users queried may exist yet, so we need to create any missing
# ones; making sure we handle concurrent creations of the same user
def create_missing_users(users, mapping)
create = []
users.each do |(name, email)|
create << { name: name, email: email } unless mapping[[name, email]]
end
return if create.empty?
MergeRequestDiffCommitUser
.insert_all(create, returning: %w[id name email])
.each do |row|
mapping[[row['name'], row['email']]] = MergeRequestDiffCommitUser
.new(id: row['id'], name: row['name'], email: row['email'])
end
# It's possible for (name, email) pairs to be inserted concurrently,
# resulting in the above insert not returning anything. Here we get any
# remaining users that were created concurrently.
get_user_rows_in_batches(
users.reject { |pair| mapping.key?(pair) },
mapping
)
end
# Updates rows in merge_request_diff_commits with their new
# commit_author_id and committer_id values.
def update_commit_rows(to_update, user_mapping)
MergeRequestDiffCommitUser.transaction do
to_update.each_slice(UPDATES_PER_QUERY) do |slice|
updates = {}
slice.each do |(diff_id, order), (author, committer)|
author_id = user_mapping[author]&.id
committer_id = user_mapping[committer]&.id
updates[[diff_id, order]] = [author_id, committer_id]
end
bulk_update_commit_rows(updates)
end
end
end
# Bulk updates rows in the merge_request_diff_commits table with their new
# author and/or committer ID values.
#
# Updates are batched together to reduce the overhead of having to produce
# a single UPDATE for every row, as we may end up having to update
# thousands of rows at once.
#
# The query produced by this method is along the lines of the following:
#
# UPDATE merge_request_diff_commits
# SET commit_author_id =
# CASE
# WHEN (merge_request_diff_id, relative_order) = (x, y) THEN X
# WHEN ...
# END,
# committer_id =
# CASE
# WHEN (merge_request_diff_id, relative_order) = (x, y) THEN Y
# WHEN ...
# END
# WHERE (merge_request_diff_id, relative_order) IN ( (x, y), ... )
#
# The `mapping` argument is a Hash in the following format:
#
# { [merge_request_diff_id, relative_order] => [author_id, committer_id] }
#
# rubocop: disable Metrics/AbcSize
def bulk_update_commit_rows(mapping)
author_case = Arel::Nodes::Case.new
committer_case = Arel::Nodes::Case.new
primary_values = []
mapping.each do |diff_id_and_order, (author_id, committer_id)|
primary_value = Arel::Nodes::Grouping.new(diff_id_and_order)
primary_values << primary_value
if author_id
author_case.when(primary_key.eq(primary_value)).then(author_id)
end
if committer_id
committer_case.when(primary_key.eq(primary_value)).then(committer_id)
end
end
if author_case.conditions.empty? && committer_case.conditions.empty?
return
end
fields = []
# Statements such as `SET x = CASE END` are not valid SQL statements, so
# we omit setting an ID field if there are no values to populate it
# with.
if author_case.conditions.any?
fields << [arel_table[:commit_author_id], author_case]
end
if committer_case.conditions.any?
fields << [arel_table[:committer_id], committer_case]
end
query = Arel::UpdateManager.new
.table(arel_table)
.where(primary_key.in(primary_values))
.set(fields)
.to_sql
MergeRequestDiffCommit.connection.execute(query)
end
# rubocop: enable Metrics/AbcSize
def primary_key
Arel::Nodes::Grouping.new(
[arel_table[:merge_request_diff_id], arel_table[:relative_order]]
)
end
def arel_table
MergeRequestDiffCommit.arel_table
end
# Prepares a value to be inserted into a column in the table
# `merge_request_diff_commit_users`. Values in this table are limited to
# 512 characters.
#
# We treat empty strings as NULL values, as there's no point in (for
# example) storing a row where both the name and Email are an empty
# string. In addition, if we treated them differently we could end up with
# two rows: one where field X is NULL, and one where field X is an empty
# string. This is redundant, so we avoid storing such data.
def prepare(value)
value.present? ? value[0..511] : nil
end
end
# rubocop: enable Metrics/ClassLength
end
end
...@@ -61,7 +61,9 @@ tree: ...@@ -61,7 +61,9 @@ tree:
- :push_event_payload - :push_event_payload
- :suggestions - :suggestions
- merge_request_diff: - merge_request_diff:
- :merge_request_diff_commits - merge_request_diff_commits:
- :commit_author
- :committer
- :merge_request_diff_files - :merge_request_diff_files
- events: - events:
- :push_event_payload - :push_event_payload
...@@ -201,6 +203,10 @@ excluded_attributes: ...@@ -201,6 +203,10 @@ excluded_attributes:
- :verification_failure - :verification_failure
merge_request_diff_commits: merge_request_diff_commits:
- :merge_request_diff_id - :merge_request_diff_id
- :commit_author_id
- :committer_id
merge_request_diff_commit_user:
- :id
merge_request_diff_detail: merge_request_diff_detail:
- :merge_request_diff_id - :merge_request_diff_id
- :verification_retry_at - :verification_retry_at
......
...@@ -28,6 +28,7 @@ module Gitlab ...@@ -28,6 +28,7 @@ module Gitlab
def find def find
return if epic? && group.nil? return if epic? && group.nil?
return find_diff_commit_user if diff_commit_user?
super super
end end
...@@ -81,6 +82,13 @@ module Gitlab ...@@ -81,6 +82,13 @@ module Gitlab
end end
end end
def find_diff_commit_user
find_with_cache do
MergeRequest::DiffCommitUser
.find_or_create(@attributes['name'], @attributes['email'])
end
end
def label? def label?
klass == Label klass == Label
end end
...@@ -101,6 +109,10 @@ module Gitlab ...@@ -101,6 +109,10 @@ module Gitlab
klass == DesignManagement::Design klass == DesignManagement::Design
end end
def diff_commit_user?
klass == MergeRequest::DiffCommitUser
end
# If an existing group milestone used the IID # If an existing group milestone used the IID
# claim the IID back and set the group milestone to use one available # claim the IID back and set the group milestone to use one available
# This is necessary to fix situations like the following: # This is necessary to fix situations like the following:
......
...@@ -31,7 +31,9 @@ module Gitlab ...@@ -31,7 +31,9 @@ module Gitlab
ci_cd_settings: 'ProjectCiCdSetting', ci_cd_settings: 'ProjectCiCdSetting',
error_tracking_setting: 'ErrorTracking::ProjectErrorTrackingSetting', error_tracking_setting: 'ErrorTracking::ProjectErrorTrackingSetting',
links: 'Releases::Link', links: 'Releases::Link',
metrics_setting: 'ProjectMetricsSetting' }.freeze metrics_setting: 'ProjectMetricsSetting',
commit_author: 'MergeRequest::DiffCommitUser',
committer: 'MergeRequest::DiffCommitUser' }.freeze
BUILD_MODELS = %i[Ci::Build commit_status].freeze BUILD_MODELS = %i[Ci::Build commit_status].freeze
...@@ -56,6 +58,7 @@ module Gitlab ...@@ -56,6 +58,7 @@ module Gitlab
external_pull_request external_pull_request
external_pull_requests external_pull_requests
DesignManagement::Design DesignManagement::Design
MergeRequest::DiffCommitUser
].freeze ].freeze
def create def create
......
...@@ -708,9 +708,6 @@ msgstr "" ...@@ -708,9 +708,6 @@ msgstr ""
msgid "%{message} showing first %{warnings_displayed}" msgid "%{message} showing first %{warnings_displayed}"
msgstr "" msgstr ""
msgid "%{milestone_name} (Past due)"
msgstr ""
msgid "%{milestone} (expired)" msgid "%{milestone} (expired)"
msgstr "" msgstr ""
...@@ -1097,6 +1094,9 @@ msgstr "" ...@@ -1097,6 +1094,9 @@ msgstr ""
msgid "(deleted)" msgid "(deleted)"
msgstr "" msgstr ""
msgid "(expired)"
msgstr ""
msgid "(leave blank if you don't want to change it)" msgid "(leave blank if you don't want to change it)"
msgstr "" msgstr ""
......
...@@ -40,16 +40,22 @@ module QA ...@@ -40,16 +40,22 @@ module QA
base.view 'app/views/shared/issuable/_sidebar.html.haml' do base.view 'app/views/shared/issuable/_sidebar.html.haml' do
element :assignee_block element :assignee_block
element :edit_milestone_link
element :milestone_block element :milestone_block
element :milestone_link end
base.view 'app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue' do
element :milestone_link, 'data-qa-selector="`${issuableAttribute}_link`"' # rubocop:disable QA/ElementWithPattern
end
base.view 'app/assets/javascripts/sidebar/components/sidebar_editable_item.vue' do
element :edit_link
end end
end end
def assign_milestone(milestone) def assign_milestone(milestone)
click_element(:edit_milestone_link)
within_element(:milestone_block) do within_element(:milestone_block) do
click_link("#{milestone.title}") click_element(:edit_link)
click_on(milestone.title)
end end
wait_until(reload: false) do wait_until(reload: false) do
...@@ -89,7 +95,7 @@ module QA ...@@ -89,7 +95,7 @@ module QA
def has_milestone?(milestone_title) def has_milestone?(milestone_title)
wait_milestone_block_finish_loading do wait_milestone_block_finish_loading do
has_element?(:milestone_link, title: milestone_title) has_element?(:milestone_link, text: milestone_title)
end end
end end
......
...@@ -56,6 +56,7 @@ RSpec.describe 'Database schema' do ...@@ -56,6 +56,7 @@ RSpec.describe 'Database schema' do
ldap_group_links: %w[group_id], ldap_group_links: %w[group_id],
members: %w[source_id created_by_id], members: %w[source_id created_by_id],
merge_requests: %w[last_edited_by_id state_id], merge_requests: %w[last_edited_by_id state_id],
merge_request_diff_commits: %w[commit_author_id committer_id],
namespaces: %w[owner_id parent_id], namespaces: %w[owner_id parent_id],
notes: %w[author_id commit_id noteable_id updated_by_id resolved_by_id confirmed_by_id discussion_id], notes: %w[author_id commit_id noteable_id updated_by_id resolved_by_id confirmed_by_id discussion_id],
notification_settings: %w[source_id], notification_settings: %w[source_id],
......
# frozen_string_literal: true
FactoryBot.define do
factory :merge_request_diff_commit_user, class: 'MergeRequest::DiffCommitUser' do
name { generate(:name) }
email { generate(:email) }
end
end
...@@ -259,37 +259,35 @@ RSpec.describe 'Issue Sidebar' do ...@@ -259,37 +259,35 @@ RSpec.describe 'Issue Sidebar' do
end end
context 'editing issue milestone', :js do context 'editing issue milestone', :js do
let_it_be(:milestone_expired) { create(:milestone, project: project, due_date: 5.days.ago) } let_it_be(:milestone_expired) { create(:milestone, project: project, title: 'Foo - expired', due_date: 5.days.ago) }
let_it_be(:milestone_no_duedate) { create(:milestone, project: project, title: 'Foo - No due date') } let_it_be(:milestone_no_duedate) { create(:milestone, project: project, title: 'Foo - No due date') }
let_it_be(:milestone1) { create(:milestone, project: project, title: 'Milestone-1', due_date: 20.days.from_now) } let_it_be(:milestone1) { create(:milestone, project: project, title: 'Milestone-1', due_date: 20.days.from_now) }
let_it_be(:milestone2) { create(:milestone, project: project, title: 'Milestone-2', due_date: 15.days.from_now) } let_it_be(:milestone2) { create(:milestone, project: project, title: 'Milestone-2', due_date: 15.days.from_now) }
let_it_be(:milestone3) { create(:milestone, project: project, title: 'Milestone-3', due_date: 10.days.from_now) } let_it_be(:milestone3) { create(:milestone, project: project, title: 'Milestone-3', due_date: 10.days.from_now) }
before do before do
page.within('[data-testid="milestone_title"]') do page.within('.block.milestone') do
click_on 'Edit' click_button 'Edit'
end end
wait_for_all_requests
end end
it 'shows milestons list in the dropdown' do it 'shows milestones list in the dropdown' do
page.within('.block.milestone .dropdown-content') do page.within('.block.milestone') do
# 5 milestones + "No milestone" = 6 items # 5 milestones + "No milestone" = 6 items
expect(page.find('ul')).to have_selector('li[data-milestone-id]', count: 6) expect(page.find('.gl-new-dropdown-contents')).to have_selector('li.gl-new-dropdown-item', count: 6)
end end
end end
it 'shows expired milestone at the bottom of the list' do it 'shows expired milestone at the bottom of the list and milestone due earliest at the top of the list', :aggregate_failures do
page.within('.block.milestone .dropdown-content ul') do page.within('.block.milestone .gl-new-dropdown-contents') do
expect(page.find('li:last-child')).to have_content milestone_expired.title expect(page.find('li:last-child')).to have_content milestone_expired.title
end
end
it 'shows milestone due earliest at the top of the list' do expect(page.all('li.gl-new-dropdown-item')[1]).to have_content milestone3.title
page.within('.block.milestone .dropdown-content ul') do expect(page.all('li.gl-new-dropdown-item')[2]).to have_content milestone2.title
expect(page.all('li[data-milestone-id]')[1]).to have_content milestone3.title expect(page.all('li.gl-new-dropdown-item')[3]).to have_content milestone1.title
expect(page.all('li[data-milestone-id]')[2]).to have_content milestone2.title expect(page.all('li.gl-new-dropdown-item')[4]).to have_content milestone_no_duedate.title
expect(page.all('li[data-milestone-id]')[3]).to have_content milestone1.title
expect(page.all('li[data-milestone-id]')[4]).to have_content milestone_no_duedate.title
end end
end end
end end
......
...@@ -333,37 +333,40 @@ RSpec.describe "Issues > User edits issue", :js do ...@@ -333,37 +333,40 @@ RSpec.describe "Issues > User edits issue", :js do
describe 'update milestone' do describe 'update milestone' do
context 'by authorized user' do context 'by authorized user' do
it 'allows user to select unassigned' do it 'allows user to select no milestone' do
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
wait_for_requests
page.within('.milestone') do page.within('.block.milestone') do
expect(page).to have_content "None" expect(page).to have_content 'None'
end
click_button 'Edit'
wait_for_requests
click_button 'No milestone'
wait_for_requests
find('.block.milestone .edit-link').click
sleep 2 # wait for ajax stuff to complete
first('.dropdown-content li').click
sleep 2
page.within('.milestone') do
expect(page).to have_content 'None' expect(page).to have_content 'None'
end end
end end
it 'allows user to de-select milestone' do it 'allows user to de-select milestone' do
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
wait_for_requests
page.within('.milestone') do page.within('.milestone') do
click_link 'Edit' click_button 'Edit'
click_link milestone.title wait_for_requests
click_button milestone.title
page.within '.value' do page.within '[data-testid="select-milestone"]' do
expect(page).to have_content milestone.title expect(page).to have_content milestone.title
end end
click_link 'Edit' click_button 'Edit'
click_link milestone.title wait_for_requests
click_button 'No milestone'
page.within '.value' do page.within '[data-testid="select-milestone"]' do
expect(page).to have_content 'None' expect(page).to have_content 'None'
end end
end end
...@@ -371,16 +374,17 @@ RSpec.describe "Issues > User edits issue", :js do ...@@ -371,16 +374,17 @@ RSpec.describe "Issues > User edits issue", :js do
it 'allows user to search milestone' do it 'allows user to search milestone' do
visit project_issue_path(project_with_milestones, issue_with_milestones) visit project_issue_path(project_with_milestones, issue_with_milestones)
wait_for_requests
page.within('.milestone') do page.within('.milestone') do
click_link 'Edit' click_button 'Edit'
wait_for_requests wait_for_requests
# We need to enclose search string in quotes for exact match as all the milestone titles # We need to enclose search string in quotes for exact match as all the milestone titles
# within tests are prefixed with `My title`. # within tests are prefixed with `My title`.
find('.dropdown-input-field', visible: true).send_keys "\"#{milestones[0].title}\"" find('.gl-form-input', visible: true).send_keys "\"#{milestones[0].title}\""
wait_for_requests wait_for_requests
page.within '.dropdown-content' do page.within '.gl-new-dropdown-contents' do
expect(page).to have_content milestones[0].title expect(page).to have_content milestones[0].title
end end
end end
......
...@@ -2799,7 +2799,15 @@ ...@@ -2799,7 +2799,15 @@
"author_email": "dmitriy.zaporozhets@gmail.com", "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-08-06T08:35:52.000+02:00", "committed_date": "2014-08-06T08:35:52.000+02:00",
"committer_name": "Dmitriy Zaporozhets", "committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com" "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
},
"committer": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 27, "merge_request_diff_id": 27,
...@@ -2811,7 +2819,15 @@ ...@@ -2811,7 +2819,15 @@
"author_email": "dmitriy.zaporozhets@gmail.com", "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T10:01:38.000+01:00", "committed_date": "2014-02-27T10:01:38.000+01:00",
"committer_name": "Dmitriy Zaporozhets", "committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com" "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
},
"committer": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 27, "merge_request_diff_id": 27,
...@@ -2823,7 +2839,15 @@ ...@@ -2823,7 +2839,15 @@
"author_email": "dmitriy.zaporozhets@gmail.com", "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:57:31.000+01:00", "committed_date": "2014-02-27T09:57:31.000+01:00",
"committer_name": "Dmitriy Zaporozhets", "committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com" "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
},
"committer": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 27, "merge_request_diff_id": 27,
...@@ -2835,7 +2859,15 @@ ...@@ -2835,7 +2859,15 @@
"author_email": "dmitriy.zaporozhets@gmail.com", "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:54:21.000+01:00", "committed_date": "2014-02-27T09:54:21.000+01:00",
"committer_name": "Dmitriy Zaporozhets", "committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com" "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
},
"committer": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 27, "merge_request_diff_id": 27,
...@@ -2847,7 +2879,15 @@ ...@@ -2847,7 +2879,15 @@
"author_email": "dmitriy.zaporozhets@gmail.com", "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:49:50.000+01:00", "committed_date": "2014-02-27T09:49:50.000+01:00",
"committer_name": "Dmitriy Zaporozhets", "committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com" "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
},
"committer": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 27, "merge_request_diff_id": 27,
...@@ -2859,7 +2899,15 @@ ...@@ -2859,7 +2899,15 @@
"author_email": "dmitriy.zaporozhets@gmail.com", "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:48:32.000+01:00", "committed_date": "2014-02-27T09:48:32.000+01:00",
"committer_name": "Dmitriy Zaporozhets", "committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com" "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
},
"committer": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
} }
], ],
"merge_request_diff_files": [ "merge_request_diff_files": [
...@@ -3247,7 +3295,15 @@ ...@@ -3247,7 +3295,15 @@
"author_email": "dmitriy.zaporozhets@gmail.com", "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:26:01.000+01:00", "committed_date": "2014-02-27T09:26:01.000+01:00",
"committer_name": "Dmitriy Zaporozhets", "committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com" "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
},
"committer": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
} }
], ],
"merge_request_diff_files": [ "merge_request_diff_files": [
...@@ -3510,7 +3566,15 @@ ...@@ -3510,7 +3566,15 @@
"author_email": "james@jameslopez.es", "author_email": "james@jameslopez.es",
"committed_date": "2016-01-19T13:22:56.000+01:00", "committed_date": "2016-01-19T13:22:56.000+01:00",
"committer_name": "James Lopez", "committer_name": "James Lopez",
"committer_email": "james@jameslopez.es" "committer_email": "james@jameslopez.es",
"commit_author": {
"name": "James Lopez",
"email": "james@jameslopez.es"
},
"committer": {
"name": "James Lopez",
"email": "james@jameslopez.es"
}
} }
], ],
"merge_request_diff_files": [ "merge_request_diff_files": [
...@@ -3773,7 +3837,15 @@ ...@@ -3773,7 +3837,15 @@
"author_email": "james@jameslopez.es", "author_email": "james@jameslopez.es",
"committed_date": "2016-01-19T14:14:43.000+01:00", "committed_date": "2016-01-19T14:14:43.000+01:00",
"committer_name": "James Lopez", "committer_name": "James Lopez",
"committer_email": "james@jameslopez.es" "committer_email": "james@jameslopez.es",
"commit_author": {
"name": "James Lopez",
"email": "james@jameslopez.es"
},
"committer": {
"name": "James Lopez",
"email": "james@jameslopez.es"
}
}, },
{ {
"merge_request_diff_id": 14, "merge_request_diff_id": 14,
...@@ -3785,7 +3857,15 @@ ...@@ -3785,7 +3857,15 @@
"author_email": "marin@gitlab.com", "author_email": "marin@gitlab.com",
"committed_date": "2015-12-07T12:52:12.000+01:00", "committed_date": "2015-12-07T12:52:12.000+01:00",
"committer_name": "Marin Jankovski", "committer_name": "Marin Jankovski",
"committer_email": "marin@gitlab.com" "committer_email": "marin@gitlab.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "marin@gitlab.com"
},
"committer": {
"name": "Marin Jankovski",
"email": "marin@gitlab.com"
}
}, },
{ {
"merge_request_diff_id": 14, "merge_request_diff_id": 14,
...@@ -3797,7 +3877,15 @@ ...@@ -3797,7 +3877,15 @@
"author_email": "maxlazio@gmail.com", "author_email": "maxlazio@gmail.com",
"committed_date": "2015-12-07T11:54:28.000+01:00", "committed_date": "2015-12-07T11:54:28.000+01:00",
"committer_name": "Marin Jankovski", "committer_name": "Marin Jankovski",
"committer_email": "maxlazio@gmail.com" "committer_email": "maxlazio@gmail.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "maxlazio@gmail.com"
},
"committer": {
"name": "Marin Jankovski",
"email": "maxlazio@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 14, "merge_request_diff_id": 14,
...@@ -3809,7 +3897,15 @@ ...@@ -3809,7 +3897,15 @@
"author_email": "stanhu@gmail.com", "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T16:27:12.000+01:00", "committed_date": "2015-11-13T16:27:12.000+01:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com" "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 14, "merge_request_diff_id": 14,
...@@ -3821,7 +3917,15 @@ ...@@ -3821,7 +3917,15 @@
"author_email": "stanhu@gmail.com", "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:50:17.000+01:00", "committed_date": "2015-11-13T08:50:17.000+01:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com" "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 14, "merge_request_diff_id": 14,
...@@ -3833,7 +3937,15 @@ ...@@ -3833,7 +3937,15 @@
"author_email": "stanhu@gmail.com", "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:39:43.000+01:00", "committed_date": "2015-11-13T08:39:43.000+01:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com" "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 14, "merge_request_diff_id": 14,
...@@ -3845,7 +3957,15 @@ ...@@ -3845,7 +3957,15 @@
"author_email": "stanhu@gmail.com", "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T07:21:40.000+01:00", "committed_date": "2015-11-13T07:21:40.000+01:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com" "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 14, "merge_request_diff_id": 14,
...@@ -3857,7 +3977,15 @@ ...@@ -3857,7 +3977,15 @@
"author_email": "minsik.yoon@samsung.com", "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:01:27.000+01:00", "committed_date": "2015-11-13T06:01:27.000+01:00",
"committer_name": "윤민식", "committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com" "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
},
"committer": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
}
}, },
{ {
"merge_request_diff_id": 14, "merge_request_diff_id": 14,
...@@ -3869,7 +3997,15 @@ ...@@ -3869,7 +3997,15 @@
"author_email": "minsik.yoon@samsung.com", "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:00:16.000+01:00", "committed_date": "2015-11-13T06:00:16.000+01:00",
"committer_name": "윤민식", "committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com" "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
},
"committer": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
}
}, },
{ {
"merge_request_diff_id": 14, "merge_request_diff_id": 14,
...@@ -3881,7 +4017,15 @@ ...@@ -3881,7 +4017,15 @@
"author_email": "stanhu@gmail.com", "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T05:23:14.000+01:00", "committed_date": "2015-11-13T05:23:14.000+01:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com" "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 14, "merge_request_diff_id": 14,
...@@ -3893,7 +4037,15 @@ ...@@ -3893,7 +4037,15 @@
"author_email": "minsik.yoon@samsung.com", "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:45.000+01:00", "committed_date": "2015-11-13T05:08:45.000+01:00",
"committer_name": "윤민식", "committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com" "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
},
"committer": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
}
}, },
{ {
"merge_request_diff_id": 14, "merge_request_diff_id": 14,
...@@ -3905,7 +4057,15 @@ ...@@ -3905,7 +4057,15 @@
"author_email": "minsik.yoon@samsung.com", "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:04.000+01:00", "committed_date": "2015-11-13T05:08:04.000+01:00",
"committer_name": "윤민식", "committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com" "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
},
"committer": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
}
}, },
{ {
"merge_request_diff_id": 14, "merge_request_diff_id": 14,
...@@ -3917,7 +4077,15 @@ ...@@ -3917,7 +4077,15 @@
"author_email": "stanhu@packetzoom.com", "author_email": "stanhu@packetzoom.com",
"committed_date": "2015-08-25T17:53:12.000+02:00", "committed_date": "2015-08-25T17:53:12.000+02:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@packetzoom.com" "committer_email": "stanhu@packetzoom.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@packetzoom.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@packetzoom.com"
}
}, },
{ {
"merge_request_diff_id": 14, "merge_request_diff_id": 14,
...@@ -3929,7 +4097,15 @@ ...@@ -3929,7 +4097,15 @@
"author_email": "sytse@gitlab.com", "author_email": "sytse@gitlab.com",
"committed_date": "2015-01-10T22:23:29.000+01:00", "committed_date": "2015-01-10T22:23:29.000+01:00",
"committer_name": "Sytse Sijbrandij", "committer_name": "Sytse Sijbrandij",
"committer_email": "sytse@gitlab.com" "committer_email": "sytse@gitlab.com",
"commit_author": {
"name": "Sytse Sijbrandij",
"email": "sytse@gitlab.com"
},
"committer": {
"name": "Sytse Sijbrandij",
"email": "sytse@gitlab.com"
}
}, },
{ {
"merge_request_diff_id": 14, "merge_request_diff_id": 14,
...@@ -3941,7 +4117,15 @@ ...@@ -3941,7 +4117,15 @@
"author_email": "marmis85@gmail.com", "author_email": "marmis85@gmail.com",
"committed_date": "2015-01-10T21:28:18.000+01:00", "committed_date": "2015-01-10T21:28:18.000+01:00",
"committer_name": "marmis85", "committer_name": "marmis85",
"committer_email": "marmis85@gmail.com" "committer_email": "marmis85@gmail.com",
"commit_author": {
"name": "marmis85",
"email": "marmis85@gmail.com"
},
"committer": {
"name": "marmis85",
"email": "marmis85@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 14, "merge_request_diff_id": 14,
...@@ -3953,7 +4137,15 @@ ...@@ -3953,7 +4137,15 @@
"author_email": "dmitriy.zaporozhets@gmail.com", "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T10:01:38.000+01:00", "committed_date": "2014-02-27T10:01:38.000+01:00",
"committer_name": "Dmitriy Zaporozhets", "committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com" "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
},
"committer": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 14, "merge_request_diff_id": 14,
...@@ -3965,7 +4157,15 @@ ...@@ -3965,7 +4157,15 @@
"author_email": "dmitriy.zaporozhets@gmail.com", "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:57:31.000+01:00", "committed_date": "2014-02-27T09:57:31.000+01:00",
"committer_name": "Dmitriy Zaporozhets", "committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com" "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
},
"committer": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 14, "merge_request_diff_id": 14,
...@@ -3977,7 +4177,15 @@ ...@@ -3977,7 +4177,15 @@
"author_email": "dmitriy.zaporozhets@gmail.com", "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:54:21.000+01:00", "committed_date": "2014-02-27T09:54:21.000+01:00",
"committer_name": "Dmitriy Zaporozhets", "committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com" "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
},
"committer": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 14, "merge_request_diff_id": 14,
...@@ -3989,7 +4197,15 @@ ...@@ -3989,7 +4197,15 @@
"author_email": "dmitriy.zaporozhets@gmail.com", "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:49:50.000+01:00", "committed_date": "2014-02-27T09:49:50.000+01:00",
"committer_name": "Dmitriy Zaporozhets", "committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com" "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
},
"committer": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 14, "merge_request_diff_id": 14,
...@@ -4001,7 +4217,15 @@ ...@@ -4001,7 +4217,15 @@
"author_email": "dmitriy.zaporozhets@gmail.com", "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:48:32.000+01:00", "committed_date": "2014-02-27T09:48:32.000+01:00",
"committer_name": "Dmitriy Zaporozhets", "committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com" "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
},
"committer": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
} }
], ],
"merge_request_diff_files": [ "merge_request_diff_files": [
...@@ -4458,7 +4682,15 @@ ...@@ -4458,7 +4682,15 @@
"author_email": "james@jameslopez.es", "author_email": "james@jameslopez.es",
"committed_date": "2016-01-19T15:25:23.000+01:00", "committed_date": "2016-01-19T15:25:23.000+01:00",
"committer_name": "James Lopez", "committer_name": "James Lopez",
"committer_email": "james@jameslopez.es" "committer_email": "james@jameslopez.es",
"commit_author": {
"name": "James Lopez",
"email": "james@jameslopez.es"
},
"committer": {
"name": "James Lopez",
"email": "james@jameslopez.es"
}
}, },
{ {
"merge_request_diff_id": 13, "merge_request_diff_id": 13,
...@@ -4470,7 +4702,15 @@ ...@@ -4470,7 +4702,15 @@
"author_email": "marin@gitlab.com", "author_email": "marin@gitlab.com",
"committed_date": "2015-12-07T12:52:12.000+01:00", "committed_date": "2015-12-07T12:52:12.000+01:00",
"committer_name": "Marin Jankovski", "committer_name": "Marin Jankovski",
"committer_email": "marin@gitlab.com" "committer_email": "marin@gitlab.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "marin@gitlab.com"
},
"committer": {
"name": "Marin Jankovski",
"email": "marin@gitlab.com"
}
}, },
{ {
"merge_request_diff_id": 13, "merge_request_diff_id": 13,
...@@ -4482,7 +4722,15 @@ ...@@ -4482,7 +4722,15 @@
"author_email": "maxlazio@gmail.com", "author_email": "maxlazio@gmail.com",
"committed_date": "2015-12-07T11:54:28.000+01:00", "committed_date": "2015-12-07T11:54:28.000+01:00",
"committer_name": "Marin Jankovski", "committer_name": "Marin Jankovski",
"committer_email": "maxlazio@gmail.com" "committer_email": "maxlazio@gmail.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "maxlazio@gmail.com"
},
"committer": {
"name": "Marin Jankovski",
"email": "maxlazio@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 13, "merge_request_diff_id": 13,
...@@ -4494,7 +4742,15 @@ ...@@ -4494,7 +4742,15 @@
"author_email": "stanhu@gmail.com", "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T16:27:12.000+01:00", "committed_date": "2015-11-13T16:27:12.000+01:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com" "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 13, "merge_request_diff_id": 13,
...@@ -4506,7 +4762,15 @@ ...@@ -4506,7 +4762,15 @@
"author_email": "stanhu@gmail.com", "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:50:17.000+01:00", "committed_date": "2015-11-13T08:50:17.000+01:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com" "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 13, "merge_request_diff_id": 13,
...@@ -4518,7 +4782,15 @@ ...@@ -4518,7 +4782,15 @@
"author_email": "stanhu@gmail.com", "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:39:43.000+01:00", "committed_date": "2015-11-13T08:39:43.000+01:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com" "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 13, "merge_request_diff_id": 13,
...@@ -4530,7 +4802,15 @@ ...@@ -4530,7 +4802,15 @@
"author_email": "stanhu@gmail.com", "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T07:21:40.000+01:00", "committed_date": "2015-11-13T07:21:40.000+01:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com" "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 13, "merge_request_diff_id": 13,
...@@ -4542,7 +4822,15 @@ ...@@ -4542,7 +4822,15 @@
"author_email": "minsik.yoon@samsung.com", "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:01:27.000+01:00", "committed_date": "2015-11-13T06:01:27.000+01:00",
"committer_name": "윤민식", "committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com" "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
},
"committer": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
}
}, },
{ {
"merge_request_diff_id": 13, "merge_request_diff_id": 13,
...@@ -4554,7 +4842,15 @@ ...@@ -4554,7 +4842,15 @@
"author_email": "minsik.yoon@samsung.com", "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:00:16.000+01:00", "committed_date": "2015-11-13T06:00:16.000+01:00",
"committer_name": "윤민식", "committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com" "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
},
"committer": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
}
}, },
{ {
"merge_request_diff_id": 13, "merge_request_diff_id": 13,
...@@ -4566,7 +4862,15 @@ ...@@ -4566,7 +4862,15 @@
"author_email": "stanhu@gmail.com", "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T05:23:14.000+01:00", "committed_date": "2015-11-13T05:23:14.000+01:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com" "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 13, "merge_request_diff_id": 13,
...@@ -4578,7 +4882,15 @@ ...@@ -4578,7 +4882,15 @@
"author_email": "minsik.yoon@samsung.com", "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:45.000+01:00", "committed_date": "2015-11-13T05:08:45.000+01:00",
"committer_name": "윤민식", "committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com" "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
},
"committer": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
}
}, },
{ {
"merge_request_diff_id": 13, "merge_request_diff_id": 13,
...@@ -4590,7 +4902,15 @@ ...@@ -4590,7 +4902,15 @@
"author_email": "minsik.yoon@samsung.com", "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:04.000+01:00", "committed_date": "2015-11-13T05:08:04.000+01:00",
"committer_name": "윤민식", "committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com" "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
},
"committer": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
}
}, },
{ {
"merge_request_diff_id": 13, "merge_request_diff_id": 13,
...@@ -4602,7 +4922,15 @@ ...@@ -4602,7 +4922,15 @@
"author_email": "stanhu@packetzoom.com", "author_email": "stanhu@packetzoom.com",
"committed_date": "2015-08-25T17:53:12.000+02:00", "committed_date": "2015-08-25T17:53:12.000+02:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@packetzoom.com" "committer_email": "stanhu@packetzoom.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@packetzoom.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@packetzoom.com"
}
}, },
{ {
"merge_request_diff_id": 13, "merge_request_diff_id": 13,
...@@ -4614,7 +4942,15 @@ ...@@ -4614,7 +4942,15 @@
"author_email": "sytse@gitlab.com", "author_email": "sytse@gitlab.com",
"committed_date": "2015-01-10T22:23:29.000+01:00", "committed_date": "2015-01-10T22:23:29.000+01:00",
"committer_name": "Sytse Sijbrandij", "committer_name": "Sytse Sijbrandij",
"committer_email": "sytse@gitlab.com" "committer_email": "sytse@gitlab.com",
"commit_author": {
"name": "Sytse Sijbrandij",
"email": "sytse@gitlab.com"
},
"committer": {
"name": "Sytse Sijbrandij",
"email": "sytse@gitlab.com"
}
}, },
{ {
"merge_request_diff_id": 13, "merge_request_diff_id": 13,
...@@ -4626,7 +4962,15 @@ ...@@ -4626,7 +4962,15 @@
"author_email": "marmis85@gmail.com", "author_email": "marmis85@gmail.com",
"committed_date": "2015-01-10T21:28:18.000+01:00", "committed_date": "2015-01-10T21:28:18.000+01:00",
"committer_name": "marmis85", "committer_name": "marmis85",
"committer_email": "marmis85@gmail.com" "committer_email": "marmis85@gmail.com",
"commit_author": {
"name": "marmis85",
"email": "marmis85@gmail.com"
},
"committer": {
"name": "marmis85",
"email": "marmis85@gmail.com"
}
} }
], ],
"merge_request_diff_files": [ "merge_request_diff_files": [
...@@ -4967,7 +5311,15 @@ ...@@ -4967,7 +5311,15 @@
"author_email": "james@jameslopez.es", "author_email": "james@jameslopez.es",
"committed_date": "2016-01-19T14:08:21.000+01:00", "committed_date": "2016-01-19T14:08:21.000+01:00",
"committer_name": "James Lopez", "committer_name": "James Lopez",
"committer_email": "james@jameslopez.es" "committer_email": "james@jameslopez.es",
"commit_author": {
"name": "James Lopez",
"email": "james@jameslopez.es"
},
"committer": {
"name": "James Lopez",
"email": "james@jameslopez.es"
}
}, },
{ {
"merge_request_diff_id": 12, "merge_request_diff_id": 12,
...@@ -4979,7 +5331,15 @@ ...@@ -4979,7 +5331,15 @@
"author_email": "marin@gitlab.com", "author_email": "marin@gitlab.com",
"committed_date": "2015-12-07T12:52:12.000+01:00", "committed_date": "2015-12-07T12:52:12.000+01:00",
"committer_name": "Marin Jankovski", "committer_name": "Marin Jankovski",
"committer_email": "marin@gitlab.com" "committer_email": "marin@gitlab.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "marin@gitlab.com"
},
"committer": {
"name": "Marin Jankovski",
"email": "marin@gitlab.com"
}
}, },
{ {
"merge_request_diff_id": 12, "merge_request_diff_id": 12,
...@@ -4991,7 +5351,15 @@ ...@@ -4991,7 +5351,15 @@
"author_email": "maxlazio@gmail.com", "author_email": "maxlazio@gmail.com",
"committed_date": "2015-12-07T11:54:28.000+01:00", "committed_date": "2015-12-07T11:54:28.000+01:00",
"committer_name": "Marin Jankovski", "committer_name": "Marin Jankovski",
"committer_email": "maxlazio@gmail.com" "committer_email": "maxlazio@gmail.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "maxlazio@gmail.com"
},
"committer": {
"name": "Marin Jankovski",
"email": "maxlazio@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 12, "merge_request_diff_id": 12,
...@@ -5003,7 +5371,15 @@ ...@@ -5003,7 +5371,15 @@
"author_email": "stanhu@gmail.com", "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T16:27:12.000+01:00", "committed_date": "2015-11-13T16:27:12.000+01:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com" "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 12, "merge_request_diff_id": 12,
...@@ -5015,7 +5391,15 @@ ...@@ -5015,7 +5391,15 @@
"author_email": "stanhu@gmail.com", "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:50:17.000+01:00", "committed_date": "2015-11-13T08:50:17.000+01:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com" "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 12, "merge_request_diff_id": 12,
...@@ -5027,7 +5411,15 @@ ...@@ -5027,7 +5411,15 @@
"author_email": "stanhu@gmail.com", "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:39:43.000+01:00", "committed_date": "2015-11-13T08:39:43.000+01:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com" "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 12, "merge_request_diff_id": 12,
...@@ -5039,7 +5431,15 @@ ...@@ -5039,7 +5431,15 @@
"author_email": "stanhu@gmail.com", "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T07:21:40.000+01:00", "committed_date": "2015-11-13T07:21:40.000+01:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com" "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 12, "merge_request_diff_id": 12,
...@@ -5051,7 +5451,15 @@ ...@@ -5051,7 +5451,15 @@
"author_email": "minsik.yoon@samsung.com", "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:01:27.000+01:00", "committed_date": "2015-11-13T06:01:27.000+01:00",
"committer_name": "윤민식", "committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com" "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
},
"committer": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
}
}, },
{ {
"merge_request_diff_id": 12, "merge_request_diff_id": 12,
...@@ -5063,7 +5471,15 @@ ...@@ -5063,7 +5471,15 @@
"author_email": "minsik.yoon@samsung.com", "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:00:16.000+01:00", "committed_date": "2015-11-13T06:00:16.000+01:00",
"committer_name": "윤민식", "committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com" "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
},
"committer": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
}
}, },
{ {
"merge_request_diff_id": 12, "merge_request_diff_id": 12,
...@@ -5075,7 +5491,15 @@ ...@@ -5075,7 +5491,15 @@
"author_email": "stanhu@gmail.com", "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T05:23:14.000+01:00", "committed_date": "2015-11-13T05:23:14.000+01:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com" "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 12, "merge_request_diff_id": 12,
...@@ -5087,7 +5511,15 @@ ...@@ -5087,7 +5511,15 @@
"author_email": "minsik.yoon@samsung.com", "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:45.000+01:00", "committed_date": "2015-11-13T05:08:45.000+01:00",
"committer_name": "윤민식", "committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com" "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
},
"committer": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
}
}, },
{ {
"merge_request_diff_id": 12, "merge_request_diff_id": 12,
...@@ -5099,7 +5531,15 @@ ...@@ -5099,7 +5531,15 @@
"author_email": "minsik.yoon@samsung.com", "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:04.000+01:00", "committed_date": "2015-11-13T05:08:04.000+01:00",
"committer_name": "윤민식", "committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com" "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
},
"committer": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
}
}, },
{ {
"merge_request_diff_id": 12, "merge_request_diff_id": 12,
...@@ -5111,7 +5551,15 @@ ...@@ -5111,7 +5551,15 @@
"author_email": "stanhu@packetzoom.com", "author_email": "stanhu@packetzoom.com",
"committed_date": "2015-08-25T17:53:12.000+02:00", "committed_date": "2015-08-25T17:53:12.000+02:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@packetzoom.com" "committer_email": "stanhu@packetzoom.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@packetzoom.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@packetzoom.com"
}
} }
], ],
"merge_request_diff_files": [ "merge_request_diff_files": [
...@@ -5675,7 +6123,15 @@ ...@@ -5675,7 +6123,15 @@
"author_email": "james@jameslopez.es", "author_email": "james@jameslopez.es",
"committed_date": "2016-01-19T14:43:23.000+01:00", "committed_date": "2016-01-19T14:43:23.000+01:00",
"committer_name": "James Lopez", "committer_name": "James Lopez",
"committer_email": "james@jameslopez.es" "committer_email": "james@jameslopez.es",
"commit_author": {
"name": "James Lopez",
"email": "james@jameslopez.es"
},
"committer": {
"name": "James Lopez",
"email": "james@jameslopez.es"
}
}, },
{ {
"merge_request_diff_id": 10, "merge_request_diff_id": 10,
...@@ -5687,7 +6143,15 @@ ...@@ -5687,7 +6143,15 @@
"author_email": "marin@gitlab.com", "author_email": "marin@gitlab.com",
"committed_date": "2015-12-07T12:52:12.000+01:00", "committed_date": "2015-12-07T12:52:12.000+01:00",
"committer_name": "Marin Jankovski", "committer_name": "Marin Jankovski",
"committer_email": "marin@gitlab.com" "committer_email": "marin@gitlab.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "marin@gitlab.com"
},
"committer": {
"name": "Marin Jankovski",
"email": "marin@gitlab.com"
}
}, },
{ {
"merge_request_diff_id": 10, "merge_request_diff_id": 10,
...@@ -5699,7 +6163,15 @@ ...@@ -5699,7 +6163,15 @@
"author_email": "maxlazio@gmail.com", "author_email": "maxlazio@gmail.com",
"committed_date": "2015-12-07T11:54:28.000+01:00", "committed_date": "2015-12-07T11:54:28.000+01:00",
"committer_name": "Marin Jankovski", "committer_name": "Marin Jankovski",
"committer_email": "maxlazio@gmail.com" "committer_email": "maxlazio@gmail.com",
"commit_author": {
"name": "Marin Jankovski",
"email": "maxlazio@gmail.com"
},
"committer": {
"name": "Marin Jankovski",
"email": "maxlazio@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 10, "merge_request_diff_id": 10,
...@@ -5711,7 +6183,15 @@ ...@@ -5711,7 +6183,15 @@
"author_email": "stanhu@gmail.com", "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T16:27:12.000+01:00", "committed_date": "2015-11-13T16:27:12.000+01:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com" "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 10, "merge_request_diff_id": 10,
...@@ -5723,7 +6203,15 @@ ...@@ -5723,7 +6203,15 @@
"author_email": "stanhu@gmail.com", "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:50:17.000+01:00", "committed_date": "2015-11-13T08:50:17.000+01:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com" "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 10, "merge_request_diff_id": 10,
...@@ -5735,7 +6223,15 @@ ...@@ -5735,7 +6223,15 @@
"author_email": "stanhu@gmail.com", "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T08:39:43.000+01:00", "committed_date": "2015-11-13T08:39:43.000+01:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com" "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 10, "merge_request_diff_id": 10,
...@@ -5747,7 +6243,15 @@ ...@@ -5747,7 +6243,15 @@
"author_email": "stanhu@gmail.com", "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T07:21:40.000+01:00", "committed_date": "2015-11-13T07:21:40.000+01:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com" "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 10, "merge_request_diff_id": 10,
...@@ -5759,7 +6263,15 @@ ...@@ -5759,7 +6263,15 @@
"author_email": "minsik.yoon@samsung.com", "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:01:27.000+01:00", "committed_date": "2015-11-13T06:01:27.000+01:00",
"committer_name": "윤민식", "committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com" "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
},
"committer": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
}
}, },
{ {
"merge_request_diff_id": 10, "merge_request_diff_id": 10,
...@@ -5771,7 +6283,15 @@ ...@@ -5771,7 +6283,15 @@
"author_email": "minsik.yoon@samsung.com", "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T06:00:16.000+01:00", "committed_date": "2015-11-13T06:00:16.000+01:00",
"committer_name": "윤민식", "committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com" "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
},
"committer": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
}
}, },
{ {
"merge_request_diff_id": 10, "merge_request_diff_id": 10,
...@@ -5783,7 +6303,15 @@ ...@@ -5783,7 +6303,15 @@
"author_email": "stanhu@gmail.com", "author_email": "stanhu@gmail.com",
"committed_date": "2015-11-13T05:23:14.000+01:00", "committed_date": "2015-11-13T05:23:14.000+01:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@gmail.com" "committer_email": "stanhu@gmail.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 10, "merge_request_diff_id": 10,
...@@ -5795,7 +6323,15 @@ ...@@ -5795,7 +6323,15 @@
"author_email": "minsik.yoon@samsung.com", "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:45.000+01:00", "committed_date": "2015-11-13T05:08:45.000+01:00",
"committer_name": "윤민식", "committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com" "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
},
"committer": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
}
}, },
{ {
"merge_request_diff_id": 10, "merge_request_diff_id": 10,
...@@ -5807,7 +6343,15 @@ ...@@ -5807,7 +6343,15 @@
"author_email": "minsik.yoon@samsung.com", "author_email": "minsik.yoon@samsung.com",
"committed_date": "2015-11-13T05:08:04.000+01:00", "committed_date": "2015-11-13T05:08:04.000+01:00",
"committer_name": "윤민식", "committer_name": "윤민식",
"committer_email": "minsik.yoon@samsung.com" "committer_email": "minsik.yoon@samsung.com",
"commit_author": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
},
"committer": {
"name": "윤민식",
"email": "minsik.yoon@samsung.com"
}
}, },
{ {
"merge_request_diff_id": 10, "merge_request_diff_id": 10,
...@@ -5819,7 +6363,15 @@ ...@@ -5819,7 +6363,15 @@
"author_email": "stanhu@packetzoom.com", "author_email": "stanhu@packetzoom.com",
"committed_date": "2015-08-25T17:53:12.000+02:00", "committed_date": "2015-08-25T17:53:12.000+02:00",
"committer_name": "Stan Hu", "committer_name": "Stan Hu",
"committer_email": "stanhu@packetzoom.com" "committer_email": "stanhu@packetzoom.com",
"commit_author": {
"name": "Stan Hu",
"email": "stanhu@packetzoom.com"
},
"committer": {
"name": "Stan Hu",
"email": "stanhu@packetzoom.com"
}
}, },
{ {
"merge_request_diff_id": 10, "merge_request_diff_id": 10,
...@@ -5831,7 +6383,15 @@ ...@@ -5831,7 +6383,15 @@
"author_email": "sytse@gitlab.com", "author_email": "sytse@gitlab.com",
"committed_date": "2015-01-10T22:23:29.000+01:00", "committed_date": "2015-01-10T22:23:29.000+01:00",
"committer_name": "Sytse Sijbrandij", "committer_name": "Sytse Sijbrandij",
"committer_email": "sytse@gitlab.com" "committer_email": "sytse@gitlab.com",
"commit_author": {
"name": "Sytse Sijbrandij",
"email": "sytse@gitlab.com"
},
"committer": {
"name": "Sytse Sijbrandij",
"email": "sytse@gitlab.com"
}
}, },
{ {
"merge_request_diff_id": 10, "merge_request_diff_id": 10,
...@@ -5843,7 +6403,15 @@ ...@@ -5843,7 +6403,15 @@
"author_email": "marmis85@gmail.com", "author_email": "marmis85@gmail.com",
"committed_date": "2015-01-10T21:28:18.000+01:00", "committed_date": "2015-01-10T21:28:18.000+01:00",
"committer_name": "marmis85", "committer_name": "marmis85",
"committer_email": "marmis85@gmail.com" "committer_email": "marmis85@gmail.com",
"commit_author": {
"name": "marmis85",
"email": "marmis85@gmail.com"
},
"committer": {
"name": "marmis85",
"email": "marmis85@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 10, "merge_request_diff_id": 10,
...@@ -5855,7 +6423,15 @@ ...@@ -5855,7 +6423,15 @@
"author_email": "dmitriy.zaporozhets@gmail.com", "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T10:01:38.000+01:00", "committed_date": "2014-02-27T10:01:38.000+01:00",
"committer_name": "Dmitriy Zaporozhets", "committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com" "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
},
"committer": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 10, "merge_request_diff_id": 10,
...@@ -5867,7 +6443,15 @@ ...@@ -5867,7 +6443,15 @@
"author_email": "dmitriy.zaporozhets@gmail.com", "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:57:31.000+01:00", "committed_date": "2014-02-27T09:57:31.000+01:00",
"committer_name": "Dmitriy Zaporozhets", "committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com" "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
},
"committer": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 10, "merge_request_diff_id": 10,
...@@ -5879,7 +6463,15 @@ ...@@ -5879,7 +6463,15 @@
"author_email": "dmitriy.zaporozhets@gmail.com", "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:54:21.000+01:00", "committed_date": "2014-02-27T09:54:21.000+01:00",
"committer_name": "Dmitriy Zaporozhets", "committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com" "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
},
"committer": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 10, "merge_request_diff_id": 10,
...@@ -5891,7 +6483,15 @@ ...@@ -5891,7 +6483,15 @@
"author_email": "dmitriy.zaporozhets@gmail.com", "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:49:50.000+01:00", "committed_date": "2014-02-27T09:49:50.000+01:00",
"committer_name": "Dmitriy Zaporozhets", "committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com" "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
},
"committer": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
}, },
{ {
"merge_request_diff_id": 10, "merge_request_diff_id": 10,
...@@ -5903,7 +6503,15 @@ ...@@ -5903,7 +6503,15 @@
"author_email": "dmitriy.zaporozhets@gmail.com", "author_email": "dmitriy.zaporozhets@gmail.com",
"committed_date": "2014-02-27T09:48:32.000+01:00", "committed_date": "2014-02-27T09:48:32.000+01:00",
"committer_name": "Dmitriy Zaporozhets", "committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com" "committer_email": "dmitriy.zaporozhets@gmail.com",
"commit_author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
},
"committer": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
} }
], ],
"merge_request_diff_files": [ "merge_request_diff_files": [
...@@ -6348,7 +6956,15 @@ ...@@ -6348,7 +6956,15 @@
"author_email": "james@jameslopez.es", "author_email": "james@jameslopez.es",
"committed_date": "2016-01-19T15:44:02.000+01:00", "committed_date": "2016-01-19T15:44:02.000+01:00",
"committer_name": "James Lopez", "committer_name": "James Lopez",
"committer_email": "james@jameslopez.es" "committer_email": "james@jameslopez.es",
"commit_author": {
"name": "James Lopez",
"email": "james@jameslopez.es"
},
"committer": {
"name": "James Lopez",
"email": "james@jameslopez.es"
}
} }
], ],
"merge_request_diff_files": [ "merge_request_diff_files": [
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -530,6 +530,7 @@ export const mockMilestone1 = { ...@@ -530,6 +530,7 @@ export const mockMilestone1 = {
title: 'Foobar Milestone', title: 'Foobar Milestone',
webUrl: 'http://gdk.test:3000/groups/gitlab-org/-/milestones/1', webUrl: 'http://gdk.test:3000/groups/gitlab-org/-/milestones/1',
state: 'active', state: 'active',
expired: false,
}; };
export const mockMilestone2 = { export const mockMilestone2 = {
...@@ -538,6 +539,7 @@ export const mockMilestone2 = { ...@@ -538,6 +539,7 @@ export const mockMilestone2 = {
title: 'Awesome Milestone', title: 'Awesome Milestone',
webUrl: 'http://gdk.test:3000/groups/gitlab-org/-/milestones/2', webUrl: 'http://gdk.test:3000/groups/gitlab-org/-/milestones/2',
state: 'active', state: 'active',
expired: false,
}; };
export const mockProjectMilestonesResponse = { export const mockProjectMilestonesResponse = {
...@@ -571,6 +573,7 @@ export const mockMilestoneMutationResponse = { ...@@ -571,6 +573,7 @@ export const mockMilestoneMutationResponse = {
id: 'gid://gitlab/Milestone/2', id: 'gid://gitlab/Milestone/2',
title: 'Awesome Milestone', title: 'Awesome Milestone',
state: 'active', state: 'active',
expired: false,
__typename: 'Milestone', __typename: 'Milestone',
}, },
__typename: 'Issue', __typename: 'Issue',
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::MigrateMergeRequestDiffCommitUsers do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:users) { table(:users) }
let(:merge_requests) { table(:merge_requests) }
let(:diffs) { table(:merge_request_diffs) }
let(:commits) do
table(:merge_request_diff_commits).tap do |t|
t.extend(SuppressCompositePrimaryKeyWarning)
end
end
let(:commit_users) { described_class::MergeRequestDiffCommitUser }
let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
let(:project) { projects.create!(namespace_id: namespace.id) }
let(:merge_request) do
merge_requests.create!(
source_branch: 'x',
target_branch: 'master',
target_project_id: project.id
)
end
let(:diff) { diffs.create!(merge_request_id: merge_request.id) }
let(:migration) { described_class.new }
describe 'MergeRequestDiffCommit' do
describe '.each_row_to_migrate' do
it 'yields the rows to migrate for a given range' do
commit1 = commits.create!(
merge_request_diff_id: diff.id,
relative_order: 0,
sha: Gitlab::Database::ShaAttribute.serialize('123abc'),
author_name: 'bob',
author_email: 'bob@example.com',
committer_name: 'bob',
committer_email: 'bob@example.com'
)
commit2 = commits.create!(
merge_request_diff_id: diff.id,
relative_order: 1,
sha: Gitlab::Database::ShaAttribute.serialize('123abc'),
author_name: 'Alice',
author_email: 'alice@example.com',
committer_name: 'Alice',
committer_email: 'alice@example.com'
)
# We stub this constant to make sure we run at least two pagination
# queries for getting the data. This way we can test if the pagination
# is actually working properly.
stub_const(
'Gitlab::BackgroundMigration::MigrateMergeRequestDiffCommitUsers::COMMIT_ROWS_PER_QUERY',
1
)
rows = []
described_class::MergeRequestDiffCommit.each_row_to_migrate(diff.id, diff.id + 1) do |row|
rows << row
end
expect(rows.length).to eq(2)
expect(rows[0].author_name).to eq(commit1.author_name)
expect(rows[1].author_name).to eq(commit2.author_name)
end
end
end
describe 'MergeRequestDiffCommitUser' do
describe '.union' do
it 'produces a union of the given queries' do
alice = commit_users.create!(name: 'Alice', email: 'alice@example.com')
bob = commit_users.create!(name: 'Bob', email: 'bob@example.com')
users = commit_users.union([
commit_users.where(name: 'Alice').to_sql,
commit_users.where(name: 'Bob').to_sql
])
expect(users).to include(alice)
expect(users).to include(bob)
end
end
end
describe '#perform' do
it 'migrates the data in the range' do
commits.create!(
merge_request_diff_id: diff.id,
relative_order: 0,
sha: Gitlab::Database::ShaAttribute.serialize('123abc'),
author_name: 'bob',
author_email: 'bob@example.com',
committer_name: 'bob',
committer_email: 'bob@example.com'
)
migration.perform(diff.id, diff.id + 1)
bob = commit_users.find_by(name: 'bob')
commit = commits.first
expect(commit.commit_author_id).to eq(bob.id)
expect(commit.committer_id).to eq(bob.id)
end
it 'treats empty names and Emails the same as NULL values' do
commits.create!(
merge_request_diff_id: diff.id,
relative_order: 0,
sha: Gitlab::Database::ShaAttribute.serialize('123abc'),
author_name: 'bob',
author_email: 'bob@example.com',
committer_name: '',
committer_email: ''
)
migration.perform(diff.id, diff.id + 1)
bob = commit_users.find_by(name: 'bob')
commit = commits.first
expect(commit.commit_author_id).to eq(bob.id)
expect(commit.committer_id).to be_nil
end
it 'does not update rows without a committer and author' do
commits.create!(
merge_request_diff_id: diff.id,
relative_order: 0,
sha: Gitlab::Database::ShaAttribute.serialize('123abc')
)
migration.perform(diff.id, diff.id + 1)
commit = commits.first
expect(commit_users.count).to eq(0)
expect(commit.commit_author_id).to be_nil
expect(commit.committer_id).to be_nil
end
it 'marks the background job as done' do
Gitlab::Database::BackgroundMigrationJob.create!(
class_name: 'MigrateMergeRequestDiffCommitUsers',
arguments: [diff.id, diff.id + 1]
)
migration.perform(diff.id, diff.id + 1)
job = Gitlab::Database::BackgroundMigrationJob.first
expect(job.status).to eq('succeeded')
end
end
describe '#get_data_to_update' do
it 'returns the users and commit rows to update' do
commits.create!(
merge_request_diff_id: diff.id,
relative_order: 0,
sha: Gitlab::Database::ShaAttribute.serialize('123abc'),
author_name: 'bob' + ('a' * 510),
author_email: 'bob@example.com',
committer_name: 'bob' + ('a' * 510),
committer_email: 'bob@example.com'
)
commits.create!(
merge_request_diff_id: diff.id,
relative_order: 1,
sha: Gitlab::Database::ShaAttribute.serialize('456abc'),
author_name: 'alice',
author_email: 'alice@example.com',
committer_name: 'alice',
committer_email: 'alice@example.com'
)
users, to_update = migration.get_data_to_update(diff.id, diff.id + 1)
bob_name = 'bob' + ('a' * 509)
expect(users).to include(%w[alice alice@example.com])
expect(users).to include([bob_name, 'bob@example.com'])
expect(to_update[[diff.id, 0]])
.to eq([[bob_name, 'bob@example.com'], [bob_name, 'bob@example.com']])
expect(to_update[[diff.id, 1]])
.to eq([%w[alice alice@example.com], %w[alice alice@example.com]])
end
it 'does not include a user if both the name and Email are missing' do
commits.create!(
merge_request_diff_id: diff.id,
relative_order: 0,
sha: Gitlab::Database::ShaAttribute.serialize('123abc'),
author_name: nil,
author_email: nil,
committer_name: 'bob',
committer_email: 'bob@example.com'
)
users, _ = migration.get_data_to_update(diff.id, diff.id + 1)
expect(users).to eq([%w[bob bob@example.com]].to_set)
end
end
describe '#get_user_rows_in_batches' do
it 'retrieves all existing users' do
alice = commit_users.create!(name: 'alice', email: 'alice@example.com')
bob = commit_users.create!(name: 'bob', email: 'bob@example.com')
users = [[alice.name, alice.email], [bob.name, bob.email]]
mapping = {}
migration.get_user_rows_in_batches(users, mapping)
expect(mapping[%w[alice alice@example.com]]).to eq(alice)
expect(mapping[%w[bob bob@example.com]]).to eq(bob)
end
end
describe '#create_missing_users' do
it 'creates merge request diff commit users that are missing' do
alice = commit_users.create!(name: 'alice', email: 'alice@example.com')
users = [%w[alice alice@example.com], %w[bob bob@example.com]]
mapping = { %w[alice alice@example.com] => alice }
migration.create_missing_users(users, mapping)
expect(mapping[%w[alice alice@example.com]]).to eq(alice)
expect(mapping[%w[bob bob@example.com]].name).to eq('bob')
expect(mapping[%w[bob bob@example.com]].email).to eq('bob@example.com')
end
end
describe '#update_commit_rows' do
it 'updates the merge request diff commit rows' do
to_update = { [42, 0] => [%w[alice alice@example.com], []] }
user_mapping = { %w[alice alice@example.com] => double(:user, id: 1) }
expect(migration)
.to receive(:bulk_update_commit_rows)
.with({ [42, 0] => [1, nil] })
migration.update_commit_rows(to_update, user_mapping)
end
end
describe '#bulk_update_commit_rows' do
context 'when there are no authors and committers' do
it 'does not update any rows' do
migration.bulk_update_commit_rows({ [1, 0] => [] })
expect(described_class::MergeRequestDiffCommit.connection)
.not_to receive(:execute)
end
end
context 'when there are only authors' do
it 'only updates the author IDs' do
author = commit_users.create!(name: 'Alice', email: 'alice@example.com')
commit = commits.create!(
merge_request_diff_id: diff.id,
relative_order: 0,
sha: Gitlab::Database::ShaAttribute.serialize('123abc')
)
mapping = {
[commit.merge_request_diff_id, commit.relative_order] =>
[author.id, nil]
}
migration.bulk_update_commit_rows(mapping)
commit = commits.first
expect(commit.commit_author_id).to eq(author.id)
expect(commit.committer_id).to be_nil
end
end
context 'when there are only committers' do
it 'only updates the committer IDs' do
committer =
commit_users.create!(name: 'Alice', email: 'alice@example.com')
commit = commits.create!(
merge_request_diff_id: diff.id,
relative_order: 0,
sha: Gitlab::Database::ShaAttribute.serialize('123abc')
)
mapping = {
[commit.merge_request_diff_id, commit.relative_order] =>
[nil, committer.id]
}
migration.bulk_update_commit_rows(mapping)
commit = commits.first
expect(commit.committer_id).to eq(committer.id)
expect(commit.commit_author_id).to be_nil
end
end
context 'when there are both authors and committers' do
it 'updates both the author and committer IDs' do
author = commit_users.create!(name: 'Bob', email: 'bob@example.com')
committer =
commit_users.create!(name: 'Alice', email: 'alice@example.com')
commit = commits.create!(
merge_request_diff_id: diff.id,
relative_order: 0,
sha: Gitlab::Database::ShaAttribute.serialize('123abc')
)
mapping = {
[commit.merge_request_diff_id, commit.relative_order] =>
[author.id, committer.id]
}
migration.bulk_update_commit_rows(mapping)
commit = commits.first
expect(commit.commit_author_id).to eq(author.id)
expect(commit.committer_id).to eq(committer.id)
end
end
context 'when there are multiple commit rows to update' do
it 'updates all the rows' do
author = commit_users.create!(name: 'Bob', email: 'bob@example.com')
committer =
commit_users.create!(name: 'Alice', email: 'alice@example.com')
commit1 = commits.create!(
merge_request_diff_id: diff.id,
relative_order: 0,
sha: Gitlab::Database::ShaAttribute.serialize('123abc')
)
commit2 = commits.create!(
merge_request_diff_id: diff.id,
relative_order: 1,
sha: Gitlab::Database::ShaAttribute.serialize('456abc')
)
mapping = {
[commit1.merge_request_diff_id, commit1.relative_order] =>
[author.id, committer.id],
[commit2.merge_request_diff_id, commit2.relative_order] =>
[author.id, nil]
}
migration.bulk_update_commit_rows(mapping)
commit1 = commits.find_by(relative_order: 0)
commit2 = commits.find_by(relative_order: 1)
expect(commit1.commit_author_id).to eq(author.id)
expect(commit1.committer_id).to eq(committer.id)
expect(commit2.commit_author_id).to eq(author.id)
expect(commit2.committer_id).to be_nil
end
end
end
describe '#primary_key' do
it 'returns the primary key for the commits table' do
key = migration.primary_key
expect(key.to_sql).to eq('("merge_request_diff_commits"."merge_request_diff_id", "merge_request_diff_commits"."relative_order")')
end
end
describe '#prepare' do
it 'trims a value to at most 512 characters' do
expect(migration.prepare('€' * 1_000)).to eq('€' * 512)
end
it 'returns nil if the value is an empty string' do
expect(migration.prepare('')).to be_nil
end
end
end
...@@ -198,6 +198,8 @@ merge_request_diff: ...@@ -198,6 +198,8 @@ merge_request_diff:
- merge_request_diff_files - merge_request_diff_files
merge_request_diff_commits: merge_request_diff_commits:
- merge_request_diff - merge_request_diff
- commit_author
- committer
merge_request_diff_detail: merge_request_diff_detail:
- merge_request_diff - merge_request_diff
merge_request_diff_files: merge_request_diff_files:
......
...@@ -109,14 +109,14 @@ RSpec.describe 'Test coverage of the Project Import' do ...@@ -109,14 +109,14 @@ RSpec.describe 'Test coverage of the Project Import' do
def failure_message(not_tested_relations) def failure_message(not_tested_relations)
<<~MSG <<~MSG
These relations seem to be added recenty and These relations seem to be added recently and
they expected to be covered in our Import specs: #{not_tested_relations}. they expected to be covered in our Import specs: #{not_tested_relations}.
To do that, expand one of the files listed in `project_json_fixtures` To do that, expand one of the files listed in `project_json_fixtures`
(or expand the list if you consider adding a new fixture file). (or expand the list if you consider adding a new fixture file).
After that, add a new spec into After that, add a new spec into
`spec/lib/gitlab/import_export/project_tree_restorer_spec.rb` `spec/lib/gitlab/import_export/project/tree_restorer_spec.rb`
to check that the relation is being imported correctly. to check that the relation is being imported correctly.
In case the spec breaks the master or there is a sense of urgency, In case the spec breaks the master or there is a sense of urgency,
......
...@@ -150,4 +150,30 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do ...@@ -150,4 +150,30 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do
expect(merge_request.persisted?).to be true expect(merge_request.persisted?).to be true
end end
end end
context 'merge request diff commit users' do
it 'finds the existing user' do
user = MergeRequest::DiffCommitUser
.find_or_create('Alice', 'alice@example.com')
found = described_class.build(
MergeRequest::DiffCommitUser,
'name' => 'Alice',
'email' => 'alice@example.com'
)
expect(found).to eq(user)
end
it 'creates a new user' do
found = described_class.build(
MergeRequest::DiffCommitUser,
'name' => 'Alice',
'email' => 'alice@example.com'
)
expect(found.name).to eq('Alice')
expect(found.email).to eq('alice@example.com')
end
end
end end
...@@ -224,6 +224,27 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do ...@@ -224,6 +224,27 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
expect(MergeRequestDiffCommit.count).to eq(77) expect(MergeRequestDiffCommit.count).to eq(77)
end end
it 'assigns committer and author details to all diff commits' do
MergeRequestDiffCommit.all.each do |commit|
expect(commit.commit_author_id).not_to be_nil
expect(commit.committer_id).not_to be_nil
end
end
it 'assigns the correct commit users to different diff commits' do
commit1 = MergeRequestDiffCommit
.find_by(sha: '0b4bc9a49b562e85de7cc9e834518ea6828729b9')
commit2 = MergeRequestDiffCommit
.find_by(sha: 'a4e5dfebf42e34596526acb8611bc7ed80e4eb3f')
expect(commit1.commit_author.name).to eq('Dmitriy Zaporozhets')
expect(commit1.commit_author.email).to eq('dmitriy.zaporozhets@gmail.com')
expect(commit2.commit_author.name).to eq('James Lopez')
expect(commit2.commit_author.email).to eq('james@jameslopez.es')
end
it 'has the correct data for merge request latest_merge_request_diff' do it 'has the correct data for merge request latest_merge_request_diff' do
MergeRequest.find_each do |merge_request| MergeRequest.find_each do |merge_request|
expect(merge_request.latest_merge_request_diff_id).to eq(merge_request.merge_request_diffs.maximum(:id)) expect(merge_request.latest_merge_request_diff_id).to eq(merge_request.merge_request_diffs.maximum(:id))
......
...@@ -235,6 +235,10 @@ MergeRequestDiffCommit: ...@@ -235,6 +235,10 @@ MergeRequestDiffCommit:
- committer_email - committer_email
- message - message
- trailers - trailers
MergeRequest::DiffCommitUser:
- id
- name
- email
MergeRequestDiffFile: MergeRequestDiffFile:
- merge_request_diff_id - merge_request_diff_id
- relative_order - relative_order
......
# frozen_string_literal: true
require 'spec_helper'
require_migration! 'schedule_merge_request_diff_users_background_migration'
RSpec.describe ScheduleMergeRequestDiffUsersBackgroundMigration, :migration do
let(:migration) { described_class.new }
describe '#up' do
before do
allow(described_class::MergeRequestDiff)
.to receive(:minimum)
.with(:id)
.and_return(42)
allow(described_class::MergeRequestDiff)
.to receive(:maximum)
.with(:id)
.and_return(85_123)
end
it 'schedules the migrations in batches' do
expect(migration)
.to receive(:migrate_in)
.ordered
.with(2.minutes.to_i, described_class::MIGRATION_NAME, [42, 40_042])
expect(migration)
.to receive(:migrate_in)
.ordered
.with(4.minutes.to_i, described_class::MIGRATION_NAME, [40_042, 80_042])
expect(migration)
.to receive(:migrate_in)
.ordered
.with(6.minutes.to_i, described_class::MIGRATION_NAME, [80_042, 120_042])
migration.up
end
it 'creates rows to track the background migration jobs' do
expect(Gitlab::Database::BackgroundMigrationJob)
.to receive(:create!)
.ordered
.with(class_name: described_class::MIGRATION_NAME, arguments: [42, 40_042])
expect(Gitlab::Database::BackgroundMigrationJob)
.to receive(:create!)
.ordered
.with(class_name: described_class::MIGRATION_NAME, arguments: [40_042, 80_042])
expect(Gitlab::Database::BackgroundMigrationJob)
.to receive(:create!)
.ordered
.with(class_name: described_class::MIGRATION_NAME, arguments: [80_042, 120_042])
migration.up
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe MergeRequest::DiffCommitUser do
describe 'validations' do
it 'requires that names are less than 512 characters long' do
expect(described_class.new(name: 'a' * 1000)).not_to be_valid
end
it 'requires that Emails are less than 512 characters long' do
expect(described_class.new(email: 'a' * 1000)).not_to be_valid
end
it 'requires either a name or Email' do
expect(described_class.new).not_to be_valid
end
it 'allows setting of just a name' do
expect(described_class.new(name: 'Alice')).to be_valid
end
it 'allows setting of just an Email' do
expect(described_class.new(email: 'alice@example.com')).to be_valid
end
it 'allows setting of both a name and Email' do
expect(described_class.new(name: 'Alice', email: 'alice@example.com'))
.to be_valid
end
end
describe '.prepare' do
it 'trims a value to at most 512 characters' do
expect(described_class.prepare('€' * 1_000)).to eq('€' * 512)
end
it 'returns nil if the value is an empty string' do
expect(described_class.prepare('')).to be_nil
end
end
describe '.find_or_create' do
it 'creates a new row if none exist' do
alice = described_class.find_or_create('Alice', 'alice@example.com')
expect(alice.name).to eq('Alice')
expect(alice.email).to eq('alice@example.com')
end
it 'returns an existing row if one exists' do
user1 = create(:merge_request_diff_commit_user)
user2 = described_class.find_or_create(user1.name, user1.email)
expect(user1).to eq(user2)
end
it 'handles concurrent inserts' do
user = create(:merge_request_diff_commit_user)
expect(described_class)
.to receive(:find_or_create_by!)
.ordered
.with(name: user.name, email: user.email)
.and_raise(ActiveRecord::RecordNotUnique)
expect(described_class)
.to receive(:find_or_create_by!)
.ordered
.with(name: user.name, email: user.email)
.and_return(user)
expect(described_class.find_or_create(user.name, user.email)).to eq(user)
end
end
describe '.bulk_find_or_create' do
it 'bulk creates missing rows and reuses existing rows' do
bob = create(
:merge_request_diff_commit_user,
name: 'Bob',
email: 'bob@example.com'
)
users = described_class.bulk_find_or_create(
[%w[Alice alice@example.com], %w[Bob bob@example.com]]
)
alice = described_class.find_by(name: 'Alice')
expect(users[%w[Alice alice@example.com]]).to eq(alice)
expect(users[%w[Bob bob@example.com]]).to eq(bob)
end
it 'does not insert any data when all users exist' do
bob = create(
:merge_request_diff_commit_user,
name: 'Bob',
email: 'bob@example.com'
)
users = described_class.bulk_find_or_create([%w[Bob bob@example.com]])
expect(described_class).not_to receive(:insert_all)
expect(users[%w[Bob bob@example.com]]).to eq(bob)
end
it 'handles concurrently inserted rows' do
bob = create(
:merge_request_diff_commit_user,
name: 'Bob',
email: 'bob@example.com'
)
input = [%w[Bob bob@example.com]]
expect(described_class)
.to receive(:bulk_find)
.twice
.with(input)
.and_return([], [bob])
users = described_class.bulk_find_or_create(input)
expect(users[%w[Bob bob@example.com]]).to eq(bob)
end
end
end
...@@ -16,6 +16,11 @@ RSpec.describe MergeRequestDiffCommit do ...@@ -16,6 +16,11 @@ RSpec.describe MergeRequestDiffCommit do
let(:invalid_items_for_bulk_insertion) { [] } # class does not have any validations defined let(:invalid_items_for_bulk_insertion) { [] } # class does not have any validations defined
end end
describe 'associations' do
it { is_expected.to belong_to(:commit_author) }
it { is_expected.to belong_to(:committer) }
end
describe '#to_hash' do describe '#to_hash' do
subject { merge_request.commits.first } subject { merge_request.commits.first }
...@@ -46,6 +51,8 @@ RSpec.describe MergeRequestDiffCommit do ...@@ -46,6 +51,8 @@ RSpec.describe MergeRequestDiffCommit do
"committed_date": "2014-02-27T10:01:38.000+01:00".to_time, "committed_date": "2014-02-27T10:01:38.000+01:00".to_time,
"committer_name": "Dmitriy Zaporozhets", "committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com", "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, "merge_request_diff_id": merge_request_diff_id,
"relative_order": 0, "relative_order": 0,
"sha": Gitlab::Database::ShaAttribute.serialize("5937ac0a7beb003549fc5fd26fc247adbce4a52e"), "sha": Gitlab::Database::ShaAttribute.serialize("5937ac0a7beb003549fc5fd26fc247adbce4a52e"),
...@@ -59,6 +66,8 @@ RSpec.describe MergeRequestDiffCommit do ...@@ -59,6 +66,8 @@ RSpec.describe MergeRequestDiffCommit do
"committed_date": "2014-02-27T09:57:31.000+01:00".to_time, "committed_date": "2014-02-27T09:57:31.000+01:00".to_time,
"committer_name": "Dmitriy Zaporozhets", "committer_name": "Dmitriy Zaporozhets",
"committer_email": "dmitriy.zaporozhets@gmail.com", "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, "merge_request_diff_id": merge_request_diff_id,
"relative_order": 1, "relative_order": 1,
"sha": Gitlab::Database::ShaAttribute.serialize("570e7b2abdd848b95f2f578043fc23bd6f6fd24d"), "sha": Gitlab::Database::ShaAttribute.serialize("570e7b2abdd848b95f2f578043fc23bd6f6fd24d"),
...@@ -76,6 +85,21 @@ RSpec.describe MergeRequestDiffCommit do ...@@ -76,6 +85,21 @@ RSpec.describe MergeRequestDiffCommit do
subject subject
end end
it 'creates diff commit users' do
diff = create(:merge_request_diff, merge_request: merge_request)
described_class.create_bulk(diff.id, [commits.first])
commit_row = MergeRequestDiffCommit
.find_by(merge_request_diff_id: diff.id, relative_order: 0)
commit_user_row =
MergeRequest::DiffCommitUser.find_by(name: 'Dmitriy Zaporozhets')
expect(commit_row.commit_author).to eq(commit_user_row)
expect(commit_row.committer).to eq(commit_user_row)
end
context 'with dates larger than the DB limit' do context 'with dates larger than the DB limit' do
let(:commits) do let(:commits) do
# This commit's date is "Sun Aug 17 07:12:55 292278994 +0000" # This commit's date is "Sun Aug 17 07:12:55 292278994 +0000"
...@@ -92,6 +116,8 @@ RSpec.describe MergeRequestDiffCommit do ...@@ -92,6 +116,8 @@ RSpec.describe MergeRequestDiffCommit do
"committed_date": timestamp, "committed_date": timestamp,
"committer_name": "Alejandro Rodríguez", "committer_name": "Alejandro Rodríguez",
"committer_email": "alejorro70@gmail.com", "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, "merge_request_diff_id": merge_request_diff_id,
"relative_order": 0, "relative_order": 0,
"sha": Gitlab::Database::ShaAttribute.serialize("ba3343bc4fa403a8dfbfcab7fc1a8c29ee34bd69"), "sha": Gitlab::Database::ShaAttribute.serialize("ba3343bc4fa403a8dfbfcab7fc1a8c29ee34bd69"),
...@@ -107,4 +133,28 @@ RSpec.describe MergeRequestDiffCommit do ...@@ -107,4 +133,28 @@ RSpec.describe MergeRequestDiffCommit do
end end
end end
end end
describe '.prepare_commits_for_bulk_insert' do
it 'returns the commit hashes and unique user tuples' do
commit = double(:commit, to_hash: {
parent_ids: %w[foo bar],
author_name: 'a' * 1000,
author_email: 'a' * 1000,
committer_name: 'Alice',
committer_email: 'alice@example.com'
})
hashes, tuples = described_class.prepare_commits_for_bulk_insert([commit])
expect(hashes).to eq([{
author_name: 'a' * 512,
author_email: 'a' * 512,
committer_name: 'Alice',
committer_email: 'alice@example.com'
}])
expect(tuples)
.to include(['a' * 512, 'a' * 512], %w[Alice alice@example.com])
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment